feat(widget): 오디오 콘텐츠 카드 배지를 정리한다

This commit is contained in:
2026-06-02 19:00:13 +09:00
parent bd475d1c87
commit b402025ca5
4 changed files with 185 additions and 28 deletions

View File

@@ -115,6 +115,11 @@ class AudioContentCardView @JvmOverloads constructor(
private fun createIconTag(drawableResId: Int): ImageView { private fun createIconTag(drawableResId: Int): ImageView {
return ImageView(context).apply { return ImageView(context).apply {
id = when (drawableResId) {
R.drawable.ic_content_tag_original -> R.id.iv_audio_content_tag_original
R.drawable.ic_content_tag_point -> R.id.iv_audio_content_tag_point
else -> View.NO_ID
}
setImageResource(drawableResId) setImageResource(drawableResId)
contentDescription = null contentDescription = null
importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO
@@ -124,9 +129,20 @@ class AudioContentCardView @JvmOverloads constructor(
private fun createFirstTag(): LinearLayout { private fun createFirstTag(): LinearLayout {
return LinearLayout(context).apply { return LinearLayout(context).apply {
id = R.id.ll_audio_content_tag_first
orientation = HORIZONTAL orientation = HORIZONTAL
background = ContextCompat.getDrawable(context, R.drawable.bg_audio_content_tag_first) background = ContextCompat.getDrawable(context, R.drawable.bg_audio_content_tag_first)
layoutParams = LinearLayout.LayoutParams(FIRST_TAG_WIDTH_DP.dpToPx(), TAG_HEIGHT_DP.dpToPx()) gravity = Gravity.CENTER
setPadding(
FIRST_TAG_PADDING_DP.dpToPx(),
FIRST_TAG_PADDING_DP.dpToPx(),
FIRST_TAG_PADDING_DP.dpToPx(),
FIRST_TAG_PADDING_DP.dpToPx()
)
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
TAG_HEIGHT_DP.dpToPx()
)
addView(createFirstStarView()) addView(createFirstStarView())
addView(createFirstTextView()) addView(createFirstTextView())
} }
@@ -137,10 +153,7 @@ class AudioContentCardView @JvmOverloads constructor(
setImageResource(R.drawable.ic_content_tag_first_star) setImageResource(R.drawable.ic_content_tag_first_star)
contentDescription = null contentDescription = null
importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO
layoutParams = LinearLayout.LayoutParams(FIRST_STAR_SIZE_DP.dpToPx(), FIRST_STAR_SIZE_DP.dpToPx()).apply { layoutParams = LinearLayout.LayoutParams(FIRST_STAR_SIZE_DP.dpToPx(), FIRST_STAR_SIZE_DP.dpToPx())
marginStart = 2.dpToPx()
topMargin = 4.dpToPx()
}
} }
} }
@@ -151,29 +164,27 @@ class AudioContentCardView @JvmOverloads constructor(
setTextColor(ContextCompat.getColor(context, R.color.white)) setTextColor(ContextCompat.getColor(context, R.color.white))
textSize = 16f textSize = 16f
isSingleLine = true isSingleLine = true
includeFontPadding = false includeFontPadding = true
gravity = Gravity.CENTER_VERTICAL gravity = Gravity.CENTER_VERTICAL
layoutParams = LinearLayout.LayoutParams( layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT ViewGroup.LayoutParams.WRAP_CONTENT
).apply { ).apply {
marginStart = 1.dpToPx() marginStart = FIRST_TEXT_MARGIN_START_DP.dpToPx()
topMargin = 2.dpToPx()
} }
} }
} }
private fun createFreeTag(): TextView { private fun createFreeTag(): TextView {
return TextView(context).apply { return TextView(context).apply {
id = R.id.tv_audio_content_tag_free
text = context.getString(R.string.audio_content_tag_free) text = context.getString(R.string.audio_content_tag_free)
background = ContextCompat.getDrawable(context, R.drawable.bg_audio_content_tag_free) background = ContextCompat.getDrawable(context, R.drawable.bg_audio_content_tag_free)
setTextColor(ContextCompat.getColor(context, R.color.white)) setTextColor(ContextCompat.getColor(context, R.color.white))
textSize = 14f setTextAppearance(R.style.Typography_Body4)
gravity = Gravity.CENTER gravity = Gravity.CENTER
isSingleLine = true isSingleLine = true
includeFontPadding = false includeFontPadding = false
minWidth = FREE_TAG_MIN_WIDTH_DP.dpToPx()
setPadding(FREE_TAG_HORIZONTAL_PADDING_DP.dpToPx(), 0, FREE_TAG_HORIZONTAL_PADDING_DP.dpToPx(), 0)
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, TAG_HEIGHT_DP.dpToPx()) layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, TAG_HEIGHT_DP.dpToPx())
} }
} }
@@ -216,10 +227,9 @@ class AudioContentCardView @JvmOverloads constructor(
private companion object { private companion object {
const val TITLE_CREATOR_GAP_DP = 2 const val TITLE_CREATOR_GAP_DP = 2
const val TAG_HEIGHT_DP = 24 const val TAG_HEIGHT_DP = 24
const val FIRST_TAG_WIDTH_DP = 62
const val FIRST_STAR_SIZE_DP = 17 const val FIRST_STAR_SIZE_DP = 17
const val FREE_TAG_MIN_WIDTH_DP = 34 const val FIRST_TAG_PADDING_DP = 4
const val FREE_TAG_HORIZONTAL_PADDING_DP = 6 const val FIRST_TEXT_MARGIN_START_DP = 2
const val FIRST_TEXT = "FIRST" const val FIRST_TEXT = "FIRST"
} }
} }

View File

@@ -1,4 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"> <shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#052742" /> <solid android:color="#052742" />
<padding
android:left="4dp"
android:right="4dp" />
</shape> </shape>

View File

@@ -21,39 +21,40 @@
<LinearLayout <LinearLayout
android:id="@+id/ll_audio_content_tag_top" android:id="@+id/ll_audio_content_tag_top"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="@dimen/spacing_24" android:layout_height="wrap_content"
android:layout_gravity="top|start" android:layout_gravity="top|start"
android:orientation="horizontal" android:orientation="horizontal"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible"> tools:visibility="visible">
<ImageView <ImageView
android:id="@+id/iv_audio_content_tag_original"
android:layout_width="@dimen/spacing_24" android:layout_width="@dimen/spacing_24"
android:layout_height="@dimen/spacing_24" android:layout_height="@dimen/spacing_24"
android:contentDescription="@null" android:contentDescription="@null"
android:src="@drawable/ic_content_tag_original" /> android:src="@drawable/ic_content_tag_original" />
<LinearLayout <LinearLayout
android:layout_width="62dp" android:id="@+id/ll_audio_content_tag_first"
android:layout_width="wrap_content"
android:layout_height="@dimen/spacing_24" android:layout_height="@dimen/spacing_24"
android:background="@drawable/bg_audio_content_tag_first" android:background="@drawable/bg_audio_content_tag_first"
android:orientation="horizontal"> android:gravity="center"
android:orientation="horizontal"
android:paddingHorizontal="4dp">
<ImageView <ImageView
android:layout_width="17dp" android:layout_width="17dp"
android:layout_height="17dp" android:layout_height="17dp"
android:layout_marginStart="2dp"
android:layout_marginTop="4dp"
android:contentDescription="@null" android:contentDescription="@null"
android:src="@drawable/ic_content_tag_first_star" /> android:src="@drawable/ic_content_tag_first_star" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="1dp" android:layout_marginStart="2dp"
android:layout_marginTop="2dp"
android:fontFamily="@font/phosphate_solid" android:fontFamily="@font/phosphate_solid"
android:includeFontPadding="false" android:includeFontPadding="true"
android:singleLine="true" android:singleLine="true"
android:text="FIRST" android:text="FIRST"
android:textColor="@color/white" android:textColor="@color/white"
@@ -65,30 +66,30 @@
<LinearLayout <LinearLayout
android:id="@+id/ll_audio_content_tag_bottom" android:id="@+id/ll_audio_content_tag_bottom"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="@dimen/spacing_24" android:layout_height="wrap_content"
android:layout_gravity="bottom|start" android:layout_gravity="bottom|start"
android:orientation="horizontal" android:orientation="horizontal"
android:visibility="gone" android:visibility="gone"
tools:ignore="UseCompoundDrawables"
tools:visibility="visible"> tools:visibility="visible">
<ImageView <ImageView
android:id="@+id/iv_audio_content_tag_point"
android:layout_width="@dimen/spacing_24" android:layout_width="@dimen/spacing_24"
android:layout_height="@dimen/spacing_24" android:layout_height="@dimen/spacing_24"
android:contentDescription="@null" android:contentDescription="@null"
android:src="@drawable/ic_content_tag_point" /> android:src="@drawable/ic_content_tag_point" />
<TextView <TextView
android:id="@+id/tv_audio_content_tag_free"
style="@style/Typography.Body4"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="@dimen/spacing_24" android:layout_height="@dimen/spacing_24"
android:background="@drawable/bg_audio_content_tag_free" android:background="@drawable/bg_audio_content_tag_free"
android:gravity="center" android:gravity="center"
android:includeFontPadding="false"
android:minWidth="34dp"
android:paddingHorizontal="6dp"
android:singleLine="true" android:singleLine="true"
android:text="@string/audio_content_tag_free" android:text="@string/audio_content_tag_free"
android:textColor="@color/white" android:textColor="@color/white" />
android:textSize="14sp" />
</LinearLayout> </LinearLayout>
</FrameLayout> </FrameLayout>

View File

@@ -15,9 +15,13 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationFirstAudioContentUiModel
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationLiveUiModel import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationLiveUiModel
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeFirstAudioAdapter
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeLiveAdapter import kr.co.vividnext.sodalive.v2.main.home.ui.HomeLiveAdapter
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeRecentDebutCreatorAdapter import kr.co.vividnext.sodalive.v2.main.home.ui.HomeRecentDebutCreatorAdapter
import kr.co.vividnext.sodalive.v2.widget.AudioContentCardView
import kr.co.vividnext.sodalive.v2.widget.AudioContentTag
import kr.co.vividnext.sodalive.v2.widget.TextTabBarView import kr.co.vividnext.sodalive.v2.widget.TextTabBarView
import kr.co.vividnext.sodalive.v2.widget.banner.BannerView import kr.co.vividnext.sodalive.v2.widget.banner.BannerView
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
@@ -146,6 +150,131 @@ class HomeMainFragmentLayoutTest {
assertEquals(4.dpToPx(), layoutParams.marginEnd) assertEquals(4.dpToPx(), layoutParams.marginEnd)
} }
@Test
fun `first audio item matches figma card dimensions`() {
val firstAudio = inflateViewWithParent(R.layout.item_home_first_audio_content)
val thumbnailContainer = firstAudio.findViewById<View>(R.id.fl_home_first_audio_thumbnail_container)
val thumbnail = firstAudio.findViewById<ImageView>(R.id.iv_home_first_audio_thumbnail)
val creatorProfile = firstAudio.findViewById<ImageView>(R.id.iv_home_first_audio_creator_profile)
val creatorName = firstAudio.findViewById<TextView>(R.id.tv_home_first_audio_creator_nickname)
assertEquals(185.dpToPx(), firstAudio.layoutParams.width)
assertEquals(ViewGroup.LayoutParams.WRAP_CONTENT, firstAudio.layoutParams.height)
assertEquals(185.dpToPx(), thumbnail.layoutParams.width)
assertEquals(185.dpToPx(), thumbnail.layoutParams.height)
assertNotNull(thumbnailContainer)
assertEquals(42.dpToPx(), creatorProfile.layoutParams.width)
assertEquals(42.dpToPx(), creatorProfile.layoutParams.height)
assertEquals(14f, creatorName.textSize / creatorName.resources.displayMetrics.scaledDensity)
}
@Test
fun `home first audio section matches figma list spacing`() {
val root = inflateView(R.layout.fragment_v2_main_home)
val firstAudioList = root.findViewById<RecyclerView>(R.id.rv_home_first_audio_contents)
val context = ApplicationProvider.getApplicationContext<Context>()
val parent = RecyclerView(context)
parent.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
val viewHolder = HomeFirstAudioAdapter().onCreateViewHolder(parent, 0)
val layoutParams = viewHolder.itemView.layoutParams as ViewGroup.MarginLayoutParams
assertEquals(14.dpToPx(), firstAudioList.paddingStart)
assertEquals(14.dpToPx(), (firstAudioList.layoutParams as ViewGroup.MarginLayoutParams).topMargin)
assertEquals(4.dpToPx(), layoutParams.marginEnd)
}
@Test
fun `first audio adapter clips thumbnail container`() {
val context = ApplicationProvider.getApplicationContext<Context>()
val parent = RecyclerView(context)
parent.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
val viewHolder = HomeFirstAudioAdapter().onCreateViewHolder(parent, 0)
val thumbnailContainer = viewHolder.itemView.findViewById<View>(R.id.fl_home_first_audio_thumbnail_container)
assertEquals(true, thumbnailContainer.clipToOutline)
assertNotNull(thumbnailContainer.outlineProvider)
}
@Test
fun `first audio adapter binds tag visibility`() {
val context = ApplicationProvider.getApplicationContext<Context>()
val parent = RecyclerView(context)
parent.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
val adapter = HomeFirstAudioAdapter()
adapter.submitItems(listOf(firstAudioItem()))
val viewHolder = adapter.onCreateViewHolder(parent, 0)
adapter.onBindViewHolder(viewHolder, 0)
assertEquals(View.VISIBLE, viewHolder.itemView.findViewById<View>(R.id.ll_home_first_audio_tag_top).visibility)
assertEquals(View.VISIBLE, viewHolder.itemView.findViewById<View>(R.id.ll_home_first_audio_tag_bottom).visibility)
assertEquals(View.VISIBLE, viewHolder.itemView.findViewById<View>(R.id.ll_home_first_audio_tag_first).visibility)
assertEquals(View.VISIBLE, viewHolder.itemView.findViewById<View>(R.id.iv_home_first_audio_tag_point).visibility)
assertEquals(View.VISIBLE, viewHolder.itemView.findViewById<View>(R.id.tv_home_first_audio_tag_free).visibility)
}
@Test
fun `first audio adapter clears nullable images`() {
val context = ApplicationProvider.getApplicationContext<Context>()
val parent = RecyclerView(context)
parent.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
val adapter = HomeFirstAudioAdapter()
adapter.submitItems(listOf(firstAudioItem()))
val viewHolder = adapter.onCreateViewHolder(parent, 0)
val thumbnail = viewHolder.itemView.findViewById<ImageView>(R.id.iv_home_first_audio_thumbnail)
val creatorProfile = viewHolder.itemView.findViewById<ImageView>(R.id.iv_home_first_audio_creator_profile)
thumbnail.setImageResource(R.drawable.ic_launcher_background)
creatorProfile.setImageResource(R.drawable.ic_launcher_background)
adapter.onBindViewHolder(viewHolder, 0)
assertEquals(null, thumbnail.drawable)
assertEquals(null, creatorProfile.drawable)
}
@Test
fun `audio content card clips image area and overlay tags together`() {
val card = inflateView(R.layout.view_audio_content_card) as AudioContentCardView
val thumbnailContainer = card.findViewById<View>(R.id.fl_audio_content_thumbnail_container)
assertEquals(true, thumbnailContainer.clipToOutline)
assertNotNull(thumbnailContainer.outlineProvider)
}
@Test
fun `audio content card tag attributes match first audio item`() {
val card = inflateView(R.layout.view_audio_content_card) as AudioContentCardView
val firstAudio = inflateView(R.layout.item_home_first_audio_content)
card.setTags(setOf(AudioContentTag.Original, AudioContentTag.First, AudioContentTag.Point, AudioContentTag.Free))
val topTagContainer = card.findViewById<LinearLayout>(R.id.ll_audio_content_tag_top)
val bottomTagContainer = card.findViewById<LinearLayout>(R.id.ll_audio_content_tag_bottom)
val expectedFirstTag = firstAudio.findViewById<LinearLayout>(R.id.ll_home_first_audio_tag_first)
val expectedFreeTag = firstAudio.findViewById<TextView>(R.id.tv_home_first_audio_tag_free)
val originalTag = card.findViewById<ImageView>(R.id.iv_audio_content_tag_original)
val firstTag = card.findViewById<LinearLayout>(R.id.ll_audio_content_tag_first)
val firstIcon = firstTag.getChildAt(0) as ImageView
val firstText = firstTag.getChildAt(1) as TextView
val pointTag = card.findViewById<ImageView>(R.id.iv_audio_content_tag_point)
val freeTag = card.findViewById<TextView>(R.id.tv_audio_content_tag_free)
assertEquals(ViewGroup.LayoutParams.WRAP_CONTENT, topTagContainer.layoutParams.height)
assertEquals(ViewGroup.LayoutParams.WRAP_CONTENT, bottomTagContainer.layoutParams.height)
assertEquals(24.dpToPx(), originalTag.layoutParams.width)
assertEquals(24.dpToPx(), originalTag.layoutParams.height)
assertEquals(ViewGroup.LayoutParams.WRAP_CONTENT, firstTag.layoutParams.width)
assertEquals(expectedFirstTag.layoutParams.height, firstTag.layoutParams.height)
assertEquals(expectedFirstTag.paddingStart, firstTag.paddingStart)
assertEquals(expectedFirstTag.paddingTop, firstTag.paddingTop)
assertEquals(17.dpToPx(), firstIcon.layoutParams.width)
assertEquals(17.dpToPx(), firstIcon.layoutParams.height)
assertEquals(2.dpToPx(), (firstText.layoutParams as ViewGroup.MarginLayoutParams).marginStart)
assertEquals(true, firstText.includeFontPadding)
assertEquals(24.dpToPx(), pointTag.layoutParams.width)
assertEquals(24.dpToPx(), pointTag.layoutParams.height)
assertEquals(expectedFreeTag.layoutParams.height, freeTag.layoutParams.height)
assertEquals(expectedFreeTag.paddingStart, freeTag.paddingStart)
}
@Test @Test
fun `popular community section is hidden until phase7 binding is implemented`() { fun `popular community section is hidden until phase7 binding is implemented`() {
val root = inflateView(R.layout.fragment_v2_main_home) val root = inflateView(R.layout.fragment_v2_main_home)
@@ -289,4 +418,18 @@ class HomeMainFragmentLayoutTest {
beginDateTime = null beginDateTime = null
) )
} }
private fun firstAudioItem(): HomeRecommendationFirstAudioContentUiModel {
return HomeRecommendationFirstAudioContentUiModel(
contentId = 1L,
creatorId = 1L,
creatorNickname = "크리에이터 이름",
creatorProfileImage = null,
title = "콘텐츠 제목",
price = 0,
coverImage = null,
releaseDate = "",
tags = setOf(AudioContentTag.First, AudioContentTag.Point, AudioContentTag.Free)
)
}
} }