fix(content): 추천 오디오 카드 폭을 보정한다

This commit is contained in:
2026-07-09 14:56:19 +09:00
parent f3d610633c
commit 379263ec87
4 changed files with 91 additions and 12 deletions

View File

@@ -54,6 +54,7 @@ import kr.co.vividnext.sodalive.v2.main.content.model.usesSeriesItems
import kr.co.vividnext.sodalive.v2.main.content.overview.ContentOverviewActivity import kr.co.vividnext.sodalive.v2.main.content.overview.ContentOverviewActivity
import kr.co.vividnext.sodalive.v2.main.content.overview.data.ContentOverviewType import kr.co.vividnext.sodalive.v2.main.content.overview.data.ContentOverviewType
import kr.co.vividnext.sodalive.v2.main.content.ui.CONTENT_ALL_GRID_SPAN_COUNT import kr.co.vividnext.sodalive.v2.main.content.ui.CONTENT_ALL_GRID_SPAN_COUNT
import kr.co.vividnext.sodalive.v2.main.content.ui.CONTENT_RECOMMENDED_GRID_GAP_DP
import kr.co.vividnext.sodalive.v2.main.content.ui.CONTENT_RECOMMENDED_GRID_SPAN_COUNT import kr.co.vividnext.sodalive.v2.main.content.ui.CONTENT_RECOMMENDED_GRID_SPAN_COUNT
import kr.co.vividnext.sodalive.v2.main.content.ui.ContentAllAudioCardAdapter import kr.co.vividnext.sodalive.v2.main.content.ui.ContentAllAudioCardAdapter
import kr.co.vividnext.sodalive.v2.main.content.ui.ContentAllSeriesCardAdapter import kr.co.vividnext.sodalive.v2.main.content.ui.ContentAllSeriesCardAdapter
@@ -318,7 +319,8 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
binding.rvContentRecommendedAudios.apply { binding.rvContentRecommendedAudios.apply {
layoutManager = GridLayoutManager(requireContext(), CONTENT_RECOMMENDED_GRID_SPAN_COUNT) layoutManager = GridLayoutManager(requireContext(), CONTENT_RECOMMENDED_GRID_SPAN_COUNT)
adapter = recommendedAudioAdapter adapter = recommendedAudioAdapter
addContentGridItemSpacing() addContentGridItemSpacing(gapDp = CONTENT_RECOMMENDED_GRID_GAP_DP)
doOnLayout { updateRecommendedAudioGridItemWidth() }
} }
binding.rvContentRankings.apply { binding.rvContentRankings.apply {
layoutManager = ContentRankingAdapter.createGridLayoutManager(requireContext()) layoutManager = ContentRankingAdapter.createGridLayoutManager(requireContext())
@@ -423,6 +425,14 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
} }
} }
private fun updateRecommendedAudioGridItemWidth() {
val widthPx = binding.rvContentRecommendedAudios.calculateContentGridItemWidthPx(
CONTENT_RECOMMENDED_GRID_SPAN_COUNT,
gapDp = CONTENT_RECOMMENDED_GRID_GAP_DP
)
recommendedAudioAdapter.setGridItemWidthPx(widthPx)
}
private fun updateAllTabGridItemWidth() { private fun updateAllTabGridItemWidth() {
val widthPx = binding.rvContentAllItems.calculateContentGridItemWidthPx(CONTENT_ALL_GRID_SPAN_COUNT) val widthPx = binding.rvContentAllItems.calculateContentGridItemWidthPx(CONTENT_ALL_GRID_SPAN_COUNT)
contentAllAudioCardAdapter.setGridItemWidthPx(widthPx) contentAllAudioCardAdapter.setGridItemWidthPx(widthPx)
@@ -546,6 +556,7 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
private fun bindRecommendedAudioSection(section: ContentAudioCardSection) { private fun bindRecommendedAudioSection(section: ContentAudioCardSection) {
binding.llContentRecommendedAudioSection.visibility = section.items.toSectionVisibility() binding.llContentRecommendedAudioSection.visibility = section.items.toSectionVisibility()
updateRecommendedAudioGridItemWidth()
recommendedAudioAdapter.submitItems(section.items) recommendedAudioAdapter.submitItems(section.items)
} }

View File

@@ -14,10 +14,35 @@ class ContentAudioCardAdapter(
) : RecyclerView.Adapter<ContentAudioCardAdapter.ViewHolder>() { ) : RecyclerView.Adapter<ContentAudioCardAdapter.ViewHolder>() {
private var items: List<ContentAudioCardUiModel> = emptyList() private var items: List<ContentAudioCardUiModel> = emptyList()
private var gridItemWidthPx: Int = 0
fun setGridItemWidthPx(widthPx: Int) {
if (widthPx <= 0 || gridItemWidthPx == widthPx) return
gridItemWidthPx = widthPx
if (items.isNotEmpty()) notifyItemRangeChanged(0, items.size)
}
fun submitItems(items: List<ContentAudioCardUiModel>) { fun submitItems(items: List<ContentAudioCardUiModel>) {
val previousItemCount = this.items.size
this.items = items this.items = items
notifyDataSetChanged() notifyItemChanges(previousItemCount, items.size)
}
private fun notifyItemChanges(previousItemCount: Int, currentItemCount: Int) {
when {
previousItemCount == 0 && currentItemCount > 0 -> notifyItemRangeInserted(0, currentItemCount)
previousItemCount > 0 && currentItemCount == 0 -> notifyItemRangeRemoved(0, previousItemCount)
previousItemCount == currentItemCount -> notifyItemRangeChanged(0, currentItemCount)
else -> {
val changedItemCount = minOf(previousItemCount, currentItemCount)
if (changedItemCount > 0) notifyItemRangeChanged(0, changedItemCount)
if (currentItemCount > previousItemCount) {
notifyItemRangeInserted(previousItemCount, currentItemCount - previousItemCount)
} else {
notifyItemRangeRemoved(currentItemCount, previousItemCount - currentItemCount)
}
}
}
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
@@ -29,7 +54,10 @@ class ContentAudioCardAdapter(
} }
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position]) holder.bind(
item = items[position],
gridItemWidthPx = gridItemWidthPx
)
} }
override fun getItemCount(): Int = items.size override fun getItemCount(): Int = items.size
@@ -39,8 +67,12 @@ class ContentAudioCardAdapter(
private val cardSize: AudioContentCardSize, private val cardSize: AudioContentCardSize,
private val onAudioClick: (ContentAudioCardUiModel) -> Unit private val onAudioClick: (ContentAudioCardUiModel) -> Unit
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: ContentAudioCardUiModel) = with(binding.audioContentCard.root) { fun bind(item: ContentAudioCardUiModel, gridItemWidthPx: Int) = with(binding.audioContentCard.root) {
if (gridItemWidthPx > 0) {
setGridItemWidthPx(gridItemWidthPx)
} else {
setSize(cardSize) setSize(cardSize)
}
setContent(item.title, item.creatorNickname) setContent(item.title, item.creatorNickname)
setTags(item.tags) setTags(item.tags)
setAdultVisible(item.showAdultBadge) setAdultVisible(item.showAdultBadge)

View File

@@ -10,14 +10,20 @@ fun RecyclerView.addContentHorizontalItemSpacing() {
if (itemDecorationCount == 0) addItemDecoration(ContentHorizontalItemDecoration()) if (itemDecorationCount == 0) addItemDecoration(ContentHorizontalItemDecoration())
} }
fun RecyclerView.addContentGridItemSpacing(spanCount: Int = CONTENT_RECOMMENDED_GRID_SPAN_COUNT) { fun RecyclerView.addContentGridItemSpacing(
if (itemDecorationCount == 0) addItemDecoration(ContentGridItemDecoration(spanCount)) spanCount: Int = CONTENT_RECOMMENDED_GRID_SPAN_COUNT,
gapDp: Int = GRID_ITEM_GAP_DP
) {
if (itemDecorationCount == 0) addItemDecoration(ContentGridItemDecoration(spanCount, gapDp))
} }
fun RecyclerView.calculateContentGridItemWidthPx(spanCount: Int): Int { fun RecyclerView.calculateContentGridItemWidthPx(
spanCount: Int,
gapDp: Int = GRID_ITEM_GAP_DP
): Int {
val availableWidth = measuredWidth - paddingLeft - paddingRight val availableWidth = measuredWidth - paddingLeft - paddingRight
if (availableWidth <= 0 || spanCount <= 0) return 0 if (availableWidth <= 0 || spanCount <= 0) return 0
val totalGap = GRID_ITEM_GAP_DP.dpToPx() * (spanCount - 1) val totalGap = gapDp.dpToPx() * (spanCount - 1)
return ((availableWidth - totalGap) / spanCount).roundToInt() return ((availableWidth - totalGap) / spanCount).roundToInt()
} }
@@ -31,13 +37,14 @@ private class ContentHorizontalItemDecoration : RecyclerView.ItemDecoration() {
} }
private class ContentGridItemDecoration( private class ContentGridItemDecoration(
private val spanCount: Int private val spanCount: Int,
private val gapDp: Int
) : RecyclerView.ItemDecoration() { ) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
val position = parent.getChildAdapterPosition(view) val position = parent.getChildAdapterPosition(view)
if (position == RecyclerView.NO_POSITION) return if (position == RecyclerView.NO_POSITION) return
val columnIndex = position % spanCount val columnIndex = position % spanCount
val gap = GRID_ITEM_GAP_DP.dpToPx() val gap = gapDp.dpToPx()
outRect.left = (columnIndex * gap / spanCount).roundToInt() outRect.left = (columnIndex * gap / spanCount).roundToInt()
outRect.right = (gap - (columnIndex + 1) * gap / spanCount).roundToInt() outRect.right = (gap - (columnIndex + 1) * gap / spanCount).roundToInt()
if (position >= spanCount) outRect.top = GRID_ITEM_VERTICAL_GAP_DP.dpToPx().toInt() if (position >= spanCount) outRect.top = GRID_ITEM_VERTICAL_GAP_DP.dpToPx().toInt()
@@ -45,6 +52,7 @@ private class ContentGridItemDecoration(
} }
private const val HORIZONTAL_ITEM_GAP_DP = 8 private const val HORIZONTAL_ITEM_GAP_DP = 8
const val CONTENT_RECOMMENDED_GRID_GAP_DP = 4
private const val GRID_ITEM_GAP_DP = 8 private const val GRID_ITEM_GAP_DP = 8
private const val GRID_ITEM_VERTICAL_GAP_DP = 28 private const val GRID_ITEM_VERTICAL_GAP_DP = 28
const val CONTENT_RECOMMENDED_GRID_SPAN_COUNT = 2 const val CONTENT_RECOMMENDED_GRID_SPAN_COUNT = 2

View File

@@ -494,6 +494,34 @@ class ContentMainFragmentSourceTest {
assertEquals(null, banner(link = "mailto:test@example.com").toContentBannerRoute()) assertEquals(null, banner(link = "mailto:test@example.com").toContentBannerRoute())
} }
@Test
fun `content 추천 오디오 adapter는 fixed Large card width 대신 2열 grid item width를 사용한다`() {
val fragment = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt"
).readText()
val audioAdapter = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentAudioCardAdapter.kt"
).readText()
assertSourceContains(audioAdapter, "private var gridItemWidthPx: Int = 0")
assertSourceContains(audioAdapter, "fun setGridItemWidthPx(widthPx: Int)")
assertSourceContains(audioAdapter, "if (widthPx <= 0 || gridItemWidthPx == widthPx) return")
assertFalse(audioAdapter.contains("notifyDataSetChanged()"))
assertSourceContains(audioAdapter, "notifyItemRangeChanged(0, items.size)")
assertSourceContains(audioAdapter, "notifyItemRangeInserted(previousItemCount, currentItemCount - previousItemCount)")
assertSourceContains(audioAdapter, "notifyItemRangeRemoved(currentItemCount, previousItemCount - currentItemCount)")
assertSourceContains(fragment, "private fun updateRecommendedAudioGridItemWidth()")
assertSourceContains(
fragment,
"binding.rvContentRecommendedAudios.calculateContentGridItemWidthPx("
)
assertSourceContains(fragment, "CONTENT_RECOMMENDED_GRID_SPAN_COUNT")
assertSourceContains(fragment, "gapDp = CONTENT_RECOMMENDED_GRID_GAP_DP")
assertSourceContains(fragment, "recommendedAudioAdapter.setGridItemWidthPx(widthPx)")
assertSourceContains(fragment, "doOnLayout { updateRecommendedAudioGridItemWidth() }")
assertSourceContains(audioAdapter, "setGridItemWidthPx(gridItemWidthPx)")
}
@Test @Test
fun `content 전체 탭 adapter는 fixed Small card width 대신 grid item width를 사용한다`() { fun `content 전체 탭 adapter는 fixed Small card width 대신 grid item width를 사용한다`() {
val fragment = projectFile( val fragment = projectFile(
@@ -538,7 +566,7 @@ class ContentMainFragmentSourceTest {
assertSourceContains(audioAdapter, "setGridItemWidthPx(gridItemWidthPx)") assertSourceContains(audioAdapter, "setGridItemWidthPx(gridItemWidthPx)")
assertSourceContains(seriesAdapter, "setGridItemWidthPx(gridItemWidthPx)") assertSourceContains(seriesAdapter, "setGridItemWidthPx(gridItemWidthPx)")
assertSourceContains(layoutParams, "measuredWidth - paddingLeft - paddingRight") assertSourceContains(layoutParams, "measuredWidth - paddingLeft - paddingRight")
assertSourceContains(layoutParams, "GRID_ITEM_GAP_DP.dpToPx() * (spanCount - 1)") assertSourceContains(layoutParams, "gapDp.dpToPx() * (spanCount - 1)")
assertSourceContains(audioCard, "fun setGridItemWidthPx(widthPx: Int)") assertSourceContains(audioCard, "fun setGridItemWidthPx(widthPx: Int)")
assertSourceContains(seriesCard, "fun setGridItemWidthPx(widthPx: Int)") assertSourceContains(seriesCard, "fun setGridItemWidthPx(widthPx: Int)")
assertSourceContains(seriesCard, "172f / 122f") assertSourceContains(seriesCard, "172f / 122f")