From abd084cff8ff44691aec2ae90cf8a14ed0b860b0 Mon Sep 17 00:00:00 2001 From: klaus Date: Fri, 10 Jul 2026 10:16:36 +0900 Subject: [PATCH] =?UTF-8?q?fix(content-ranking):=20=EC=B9=B4=EB=93=9C=20UI?= =?UTF-8?q?=20=ED=81=AC=EA=B8=B0=EB=A5=BC=20=EB=B0=98=EC=9D=91=ED=98=95=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ContentRankingGridCardView.kt | 61 +++++++++--- .../ContentRankingHorizontalCardView.kt | 39 ++++++-- .../ContentRankingLargeCardView.kt | 38 +++++-- .../ContentRankingCardViewTest.kt | 99 ++++++++++++++++++- 4 files changed, 204 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingGridCardView.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingGridCardView.kt index a5d95f13..ca1039b4 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingGridCardView.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingGridCardView.kt @@ -6,6 +6,7 @@ import android.graphics.RenderEffect import android.graphics.Shader import android.os.Build import android.util.AttributeSet +import android.util.TypedValue import android.view.View import android.view.ViewGroup import android.view.ViewOutlineProvider @@ -13,6 +14,7 @@ import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.v2.widget.ranking.RankingResponsiveScale import kotlin.math.roundToInt open class ContentRankingGridCardView @JvmOverloads constructor( @@ -29,6 +31,7 @@ open class ContentRankingGridCardView @JvmOverloads constructor( private var titleText: TextView? = null private var creatorText: TextView? = null private var currentItem: ContentRankingItem? = null + private var currentTextStyle: ContentRankingTextStyle? = null private var clickListener: ((ContentRankingItem) -> Unit)? = null override fun onFinishInflate() { @@ -48,11 +51,13 @@ open class ContentRankingGridCardView @JvmOverloads constructor( fun bind(item: ContentRankingItem) { currentItem = item + val textStyle = currentTextStyle ?: defaultTextStyle() + applyTextStyle(textStyle) requireNotNull(rankText).apply { text = item.rank.toString() applyContentRankingRankGradient() } - bindDelta(item) + bindDelta(item, textStyle) requireNotNull(titleText).apply { text = item.displayContentName(context.getString(R.string.content_ranking_inaccessible_info)) visibility = if (text.isNullOrBlank()) View.INVISIBLE else View.VISIBLE @@ -79,34 +84,58 @@ open class ContentRankingGridCardView @JvmOverloads constructor( currentItem?.let(::applyAccessState) } - private fun bindDelta(item: ContentRankingItem) { + private fun bindDelta( + item: ContentRankingItem, + textStyle: ContentRankingTextStyle + ) { requireNotNull(deltaGroup).visibility = if (item.showRankChange) View.VISIBLE else View.GONE if (!item.showRankChange) return val presentation = ContentRankingDeltaPresentation.from(item.rankChangeType, item.rankChangeAmount) - applyDeltaContainer(presentation) + val scale = RankingResponsiveScale.fromResources(resources) + applyDeltaContainer(presentation, scale) requireNotNull(deltaIcon).apply { setImageResource(presentation.iconRes) layoutParams = (layoutParams as ViewGroup.MarginLayoutParams).apply { - width = presentation.iconWidthDp.dpToPx() - height = presentation.iconHeightDp.dpToPx() - marginStart = presentation.iconMarginStartDp.dpToPx() + width = presentation.iconWidthDp.dpToPx(scale) + height = presentation.iconHeightDp.dpToPx(scale) + marginStart = presentation.iconMarginStartDp.dpToPx(scale) } } requireNotNull(deltaAmountText).apply { text = presentation.amountText + setTextSize(TypedValue.COMPLEX_UNIT_SP, textStyle.deltaTextSizeSp) visibility = if (presentation.showAmount) View.VISIBLE else View.GONE } } - private fun applyDeltaContainer(presentation: ContentRankingDeltaPresentation) { + private fun applyDeltaContainer( + presentation: ContentRankingDeltaPresentation, + scale: Float + ) { requireNotNull(deltaGroup).apply { setBackgroundResource(if (presentation.showPillBackground) R.drawable.bg_creator_ranking_delta else 0) - val horizontalPadding = if (presentation.showPillBackground) 4.dpToPx() else 0 + val horizontalPadding = if (presentation.showPillBackground) 4.dpToPx(scale) else 0 setPadding(horizontalPadding, paddingTop, horizontalPadding, paddingBottom) } } + private fun applyTextStyle(style: ContentRankingTextStyle) { + requireNotNull(rankText).setTextSize(TypedValue.COMPLEX_UNIT_SP, style.rankTextSizeSp) + requireNotNull(titleText).setTextSize(TypedValue.COMPLEX_UNIT_SP, style.titleTextSizeSp) + requireNotNull(creatorText).setTextSize(TypedValue.COMPLEX_UNIT_SP, style.creatorTextSizeSp) + requireNotNull(deltaAmountText).setTextSize(TypedValue.COMPLEX_UNIT_SP, style.deltaTextSizeSp) + } + + private fun defaultTextStyle(): ContentRankingTextStyle { + val scale = RankingResponsiveScale.fromResources(resources) + return if (this is ContentRankingSmallGridCardView) { + ContentRankingTextStyle.smallGrid(scale) + } else { + ContentRankingTextStyle.mediumGrid(scale) + } + } + private fun applyAccessState(item: ContentRankingItem) { applyBlur(item.isInaccessible) isClickable = item.isTouchable && clickListener != null @@ -120,26 +149,32 @@ open class ContentRankingGridCardView @JvmOverloads constructor( private fun positionMedium(size: ContentRankingCardSize) { val scale = size.widthPx / 185f + val textStyle = ContentRankingTextStyle.mediumGrid(RankingResponsiveScale.fromResources(resources)) + currentTextStyle = textStyle requireNotNull(rankText).apply { - textSize = 54f + setTextSize(TypedValue.COMPLEX_UNIT_SP, textStyle.rankTextSizeSp) layoutParams = LayoutParams((55 * scale).roundToInt(), (75 * scale).roundToInt()) setPadding(0, 0, 0, (6 * scale).roundToInt()) } - requireNotNull(titleText).textSize = 22f + requireNotNull(titleText).setTextSize(TypedValue.COMPLEX_UNIT_SP, textStyle.titleTextSizeSp) + requireNotNull(creatorText).setTextSize(TypedValue.COMPLEX_UNIT_SP, textStyle.creatorTextSizeSp) placeDelta(left = 10, top = 70, scale = scale) placeLabel(width = 165, top = 136, scale = scale, size = size) } private fun positionSmall(size: ContentRankingCardSize) { val scale = size.widthPx / 122f + val textStyle = ContentRankingTextStyle.smallGrid(RankingResponsiveScale.fromResources(resources)) + currentTextStyle = textStyle requireNotNull(rankText).apply { - textSize = 40f + setTextSize(TypedValue.COMPLEX_UNIT_SP, textStyle.rankTextSizeSp) layoutParams = LayoutParams((42 * scale).roundToInt(), (56 * scale).roundToInt()).apply { leftMargin = (8 * scale).roundToInt() } setPadding(0, 0, 0, (5 * scale).roundToInt()) } - requireNotNull(titleText).textSize = 14f + requireNotNull(titleText).setTextSize(TypedValue.COMPLEX_UNIT_SP, textStyle.titleTextSizeSp) + requireNotNull(creatorText).setTextSize(TypedValue.COMPLEX_UNIT_SP, textStyle.creatorTextSizeSp) placeDelta(left = 10, top = 49, scale = scale) placeLabel(width = 102, top = 81, scale = scale, size = size) } @@ -178,7 +213,7 @@ open class ContentRankingGridCardView @JvmOverloads constructor( } } - private fun Int.dpToPx(): Int = (this * resources.displayMetrics.density).roundToInt() + private fun Int.dpToPx(scale: Float = 1f): Int = (this * scale * resources.displayMetrics.density).roundToInt() private companion object { const val SMALL_THRESHOLD_PX = 140 diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingHorizontalCardView.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingHorizontalCardView.kt index 2f42afe9..1baf9bc4 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingHorizontalCardView.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingHorizontalCardView.kt @@ -6,6 +6,7 @@ import android.graphics.RenderEffect import android.graphics.Shader import android.os.Build import android.util.AttributeSet +import android.util.TypedValue import android.view.View import android.view.ViewGroup import android.view.ViewOutlineProvider @@ -13,6 +14,7 @@ import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.v2.widget.ranking.RankingResponsiveScale import kotlin.math.roundToInt class ContentRankingHorizontalCardView @JvmOverloads constructor( @@ -48,11 +50,13 @@ class ContentRankingHorizontalCardView @JvmOverloads constructor( fun bind(item: ContentRankingItem) { currentItem = item + val textStyle = ContentRankingTextStyle.horizontal(RankingResponsiveScale.fromResources(resources)) + applyTextStyle(textStyle) requireNotNull(rankText).apply { text = item.rank.toString() applyContentRankingRankGradient() } - bindDelta(item) + bindDelta(item, textStyle) val inaccessibleMessage = context.getString(R.string.content_ranking_inaccessible_info) requireNotNull(titleText).text = item.displayContentName(inaccessibleMessage) requireNotNull(creatorText).text = item.displayCreatorName() @@ -69,6 +73,7 @@ class ContentRankingHorizontalCardView @JvmOverloads constructor( height = size.heightPx } positionViews(size) + applyTextStyle(ContentRankingTextStyle.horizontal(RankingResponsiveScale.fromResources(resources))) } fun imageView(): ImageView = requireNotNull(image) @@ -78,35 +83,51 @@ class ContentRankingHorizontalCardView @JvmOverloads constructor( currentItem?.let(::applyAccessState) } - private fun bindDelta(item: ContentRankingItem) { + private fun bindDelta( + item: ContentRankingItem, + textStyle: ContentRankingTextStyle + ) { val deltaGroup = findViewById(R.id.ll_content_ranking_delta) deltaGroup.visibility = if (item.showRankChange) View.VISIBLE else View.GONE if (!item.showRankChange) return val presentation = ContentRankingDeltaPresentation.from(item.rankChangeType, item.rankChangeAmount) - applyDeltaContainer(presentation) + val scale = RankingResponsiveScale.fromResources(resources) + applyDeltaContainer(presentation, scale) requireNotNull(deltaIcon).apply { setImageResource(presentation.iconRes) layoutParams = (layoutParams as ViewGroup.MarginLayoutParams).apply { - width = presentation.iconWidthDp.dpToPx() - height = presentation.iconHeightDp.dpToPx() - marginStart = presentation.iconMarginStartDp.dpToPx() + width = presentation.iconWidthDp.dpToPx(scale) + height = presentation.iconHeightDp.dpToPx(scale) + marginStart = presentation.iconMarginStartDp.dpToPx(scale) } } requireNotNull(deltaAmountText).apply { text = presentation.amountText + setTextSize(TypedValue.COMPLEX_UNIT_SP, textStyle.deltaTextSizeSp) visibility = if (presentation.showAmount) View.VISIBLE else View.GONE } } - private fun applyDeltaContainer(presentation: ContentRankingDeltaPresentation) { + private fun applyDeltaContainer( + presentation: ContentRankingDeltaPresentation, + scale: Float + ) { findViewById(R.id.ll_content_ranking_delta).apply { setBackgroundResource(if (presentation.showPillBackground) R.drawable.bg_creator_ranking_delta else 0) - val horizontalPadding = if (presentation.showPillBackground) 4.dpToPx() else 0 + val horizontalPadding = if (presentation.showPillBackground) 4.dpToPx(scale) else 0 setPadding(horizontalPadding, paddingTop, horizontalPadding, paddingBottom) } } + private fun applyTextStyle(style: ContentRankingTextStyle) { + requireNotNull(rankText).setTextSize(TypedValue.COMPLEX_UNIT_SP, style.rankTextSizeSp) + requireNotNull(titleText).setTextSize(TypedValue.COMPLEX_UNIT_SP, style.titleTextSizeSp) + requireNotNull(creatorText).setTextSize(TypedValue.COMPLEX_UNIT_SP, style.creatorTextSizeSp) + requireNotNull(inaccessibleText).setTextSize(TypedValue.COMPLEX_UNIT_SP, style.titleTextSizeSp) + requireNotNull(deltaAmountText).setTextSize(TypedValue.COMPLEX_UNIT_SP, style.deltaTextSizeSp) + } + private fun applyAccessState(item: ContentRankingItem) { applyBlur(item.isInaccessible) isClickable = item.isTouchable && clickListener != null @@ -157,5 +178,5 @@ class ContentRankingHorizontalCardView @JvmOverloads constructor( } } - private fun Int.dpToPx(): Int = (this * resources.displayMetrics.density).roundToInt() + private fun Int.dpToPx(scale: Float = 1f): Int = (this * scale * resources.displayMetrics.density).roundToInt() } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingLargeCardView.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingLargeCardView.kt index ccd62faa..f267a5e6 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingLargeCardView.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingLargeCardView.kt @@ -6,6 +6,7 @@ import android.graphics.RenderEffect import android.graphics.Shader import android.os.Build import android.util.AttributeSet +import android.util.TypedValue import android.view.View import android.view.ViewGroup import android.view.ViewOutlineProvider @@ -13,6 +14,7 @@ import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.v2.widget.ranking.RankingResponsiveScale import kotlin.math.roundToInt class ContentRankingLargeCardView @JvmOverloads constructor( @@ -50,11 +52,13 @@ class ContentRankingLargeCardView @JvmOverloads constructor( fun bind(item: ContentRankingItem) { currentItem = item + val textStyle = ContentRankingTextStyle.large(RankingResponsiveScale.fromResources(resources)) + applyTextStyle(textStyle) requireNotNull(rankText).apply { text = item.rank.toString() applyContentRankingRankGradient() } - bindDelta(item) + bindDelta(item, textStyle) requireNotNull(titleText).apply { text = item.displayContentName(context.getString(R.string.content_ranking_inaccessible_info)) visibility = if (text.isNullOrBlank()) View.INVISIBLE else View.VISIBLE @@ -72,6 +76,7 @@ class ContentRankingLargeCardView @JvmOverloads constructor( height = size.heightPx } positionViews(size) + applyTextStyle(ContentRankingTextStyle.large(RankingResponsiveScale.fromResources(resources))) } fun imageView(): ImageView = contentImageView() @@ -83,34 +88,49 @@ class ContentRankingLargeCardView @JvmOverloads constructor( currentItem?.let(::applyAccessState) } - private fun bindDelta(item: ContentRankingItem) { + private fun bindDelta( + item: ContentRankingItem, + textStyle: ContentRankingTextStyle + ) { requireNotNull(deltaGroup).visibility = if (item.showRankChange) View.VISIBLE else View.GONE if (!item.showRankChange) return val presentation = ContentRankingDeltaPresentation.from(item.rankChangeType, item.rankChangeAmount) - applyDeltaContainer(presentation) + val scale = RankingResponsiveScale.fromResources(resources) + applyDeltaContainer(presentation, scale) requireNotNull(deltaIcon).apply { setImageResource(presentation.iconRes) layoutParams = (layoutParams as ViewGroup.MarginLayoutParams).apply { - width = presentation.iconWidthDp.dpToPx() - height = presentation.iconHeightDp.dpToPx() - marginStart = presentation.iconMarginStartDp.dpToPx() + width = presentation.iconWidthDp.dpToPx(scale) + height = presentation.iconHeightDp.dpToPx(scale) + marginStart = presentation.iconMarginStartDp.dpToPx(scale) } } requireNotNull(deltaAmountText).apply { text = presentation.amountText + setTextSize(TypedValue.COMPLEX_UNIT_SP, textStyle.deltaTextSizeSp) visibility = if (presentation.showAmount) View.VISIBLE else View.GONE } } - private fun applyDeltaContainer(presentation: ContentRankingDeltaPresentation) { + private fun applyDeltaContainer( + presentation: ContentRankingDeltaPresentation, + scale: Float + ) { requireNotNull(deltaGroup).apply { setBackgroundResource(if (presentation.showPillBackground) R.drawable.bg_creator_ranking_delta else 0) - val horizontalPadding = if (presentation.showPillBackground) 4.dpToPx() else 0 + val horizontalPadding = if (presentation.showPillBackground) 4.dpToPx(scale) else 0 setPadding(horizontalPadding, paddingTop, horizontalPadding, paddingBottom) } } + private fun applyTextStyle(style: ContentRankingTextStyle) { + requireNotNull(rankText).setTextSize(TypedValue.COMPLEX_UNIT_SP, style.rankTextSizeSp) + requireNotNull(titleText).setTextSize(TypedValue.COMPLEX_UNIT_SP, style.titleTextSizeSp) + requireNotNull(creatorText).setTextSize(TypedValue.COMPLEX_UNIT_SP, style.creatorTextSizeSp) + requireNotNull(deltaAmountText).setTextSize(TypedValue.COMPLEX_UNIT_SP, style.deltaTextSizeSp) + } + private fun applyAccessState(item: ContentRankingItem) { applyBlur(item.isInaccessible) isClickable = item.isTouchable && clickListener != null @@ -159,7 +179,7 @@ class ContentRankingLargeCardView @JvmOverloads constructor( } } - private fun Int.dpToPx(): Int = (this * resources.displayMetrics.density).roundToInt() + private fun Int.dpToPx(scale: Float = 1f): Int = (this * scale * resources.displayMetrics.density).roundToInt() private companion object { const val FIGMA_WIDTH = 374 diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingCardViewTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingCardViewTest.kt index 5c42337c..9d836460 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingCardViewTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/widget/contentranking/ContentRankingCardViewTest.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.widget.contentranking import android.app.Application import android.content.Context +import android.content.res.Configuration import android.view.Gravity import android.view.LayoutInflater import android.view.View @@ -144,6 +145,83 @@ class ContentRankingCardViewTest { assertEquals(View.VISIBLE, view.findViewById(R.id.iv_content_ranking_background).visibility) } + @Test + fun `large card는 402dp 미만에서 실제 text size를 축소한다`() { + val view = inflateView( + layoutResId = R.layout.view_content_ranking_large_card, + screenWidthDp = 360 + ) + + view.bind(sampleItem(rank = 1)) + + assertTextSizeSp(view.findViewById(R.id.tv_content_ranking_rank), 96f * 360f / 402f) + assertTextSizeSp(view.findViewById(R.id.tv_content_ranking_title), 22f * 360f / 402f) + assertTextSizeSp(view.findViewById(R.id.tv_content_ranking_creator), 12f * 360f / 402f) + assertTextSizeSp(view.findViewById(R.id.tv_content_ranking_delta_amount), 16f * 360f / 402f) + } + + @Test + fun `medium grid card는 402dp 미만에서 실제 text size를 축소한다`() { + val view = inflateView( + layoutResId = R.layout.view_content_ranking_medium_grid_card, + screenWidthDp = 360 + ) + + view.setCardSize(ContentRankingCardSize(widthPx = 185, heightPx = 185)) + view.bind(sampleItem(rank = 2)) + + assertTextSizeSp(view.findViewById(R.id.tv_content_ranking_rank), 54f * 360f / 402f) + assertTextSizeSp(view.findViewById(R.id.tv_content_ranking_title), 22f * 360f / 402f) + assertTextSizeSp(view.findViewById(R.id.tv_content_ranking_creator), 12f * 360f / 402f) + assertTextSizeSp(view.findViewById(R.id.tv_content_ranking_delta_amount), 16f * 360f / 402f) + } + + @Test + fun `small grid card는 402dp 미만에서 조건부 baseline text size를 축소한다`() { + val view = inflateView( + layoutResId = R.layout.view_content_ranking_small_grid_card, + screenWidthDp = 360 + ) + + view.setCardSize(ContentRankingCardSize(widthPx = 122, heightPx = 122)) + view.bind(sampleItem(rank = 8)) + + assertTextSizeSp(view.findViewById(R.id.tv_content_ranking_rank), 36f * 360f / 402f) + assertTextSizeSp(view.findViewById(R.id.tv_content_ranking_title), 14f * 360f / 402f) + assertTextSizeSp(view.findViewById(R.id.tv_content_ranking_creator), 12f * 360f / 402f) + assertTextSizeSp(view.findViewById(R.id.tv_content_ranking_delta_amount), 16f * 360f / 402f) + } + + @Test + fun `small grid card는 card size 적용 전 bind에서도 402dp 미만 text size를 축소한다`() { + val view = inflateView( + layoutResId = R.layout.view_content_ranking_small_grid_card, + screenWidthDp = 360 + ) + + view.bind(sampleItem(rank = 8)) + + assertTextSizeSp(view.findViewById(R.id.tv_content_ranking_rank), 36f * 360f / 402f) + assertTextSizeSp(view.findViewById(R.id.tv_content_ranking_title), 14f * 360f / 402f) + assertTextSizeSp(view.findViewById(R.id.tv_content_ranking_creator), 12f * 360f / 402f) + assertTextSizeSp(view.findViewById(R.id.tv_content_ranking_delta_amount), 16f * 360f / 402f) + } + + @Test + fun `horizontal card는 402dp 미만에서 실제 text size를 축소한다`() { + val view = inflateView( + layoutResId = R.layout.view_content_ranking_horizontal_card, + screenWidthDp = 360 + ) + + view.bind(sampleItem(rank = 11)) + + assertTextSizeSp(view.findViewById(R.id.tv_content_ranking_rank), 40f * 360f / 402f) + assertTextSizeSp(view.findViewById(R.id.tv_content_ranking_title), 18f * 360f / 402f) + assertTextSizeSp(view.findViewById(R.id.tv_content_ranking_creator), 14f * 360f / 402f) + assertTextSizeSp(view.findViewById(R.id.tv_content_ranking_delta_amount), 16f * 360f / 402f) + } + @Test fun `large view holder는 배경 blur와 전경 non blur coverImage를 모두 로드한다`() { val source = projectFile( @@ -161,11 +239,28 @@ class ContentRankingCardViewTest { ) } - private inline fun inflateView(layoutResId: Int): T { - val context = ApplicationProvider.getApplicationContext() + private inline fun inflateView( + layoutResId: Int, + screenWidthDp: Int? = null + ): T { + val baseContext = ApplicationProvider.getApplicationContext() + val context = if (screenWidthDp == null) { + baseContext + } else { + baseContext.createConfigurationContext( + Configuration(baseContext.resources.configuration).apply { this.screenWidthDp = screenWidthDp } + ) + } return LayoutInflater.from(context).inflate(layoutResId, null, false) as T } + private fun assertTextSizeSp( + view: TextView, + expectedSp: Float + ) { + assertEquals(expectedSp, view.textSize / view.resources.displayMetrics.scaledDensity, 0.0001f) + } + private fun assertRankTextBox( view: TextView, expectedWidth: Int,