diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt index 6e95b54e..70eb0ad8 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt @@ -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.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_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.ContentAllAudioCardAdapter import kr.co.vividnext.sodalive.v2.main.content.ui.ContentAllSeriesCardAdapter @@ -318,7 +319,8 @@ class ContentMainFragment : BaseFragment( binding.rvContentRecommendedAudios.apply { layoutManager = GridLayoutManager(requireContext(), CONTENT_RECOMMENDED_GRID_SPAN_COUNT) adapter = recommendedAudioAdapter - addContentGridItemSpacing() + addContentGridItemSpacing(gapDp = CONTENT_RECOMMENDED_GRID_GAP_DP) + doOnLayout { updateRecommendedAudioGridItemWidth() } } binding.rvContentRankings.apply { layoutManager = ContentRankingAdapter.createGridLayoutManager(requireContext()) @@ -423,6 +425,14 @@ class ContentMainFragment : BaseFragment( } } + 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() { val widthPx = binding.rvContentAllItems.calculateContentGridItemWidthPx(CONTENT_ALL_GRID_SPAN_COUNT) contentAllAudioCardAdapter.setGridItemWidthPx(widthPx) @@ -546,6 +556,7 @@ class ContentMainFragment : BaseFragment( private fun bindRecommendedAudioSection(section: ContentAudioCardSection) { binding.llContentRecommendedAudioSection.visibility = section.items.toSectionVisibility() + updateRecommendedAudioGridItemWidth() recommendedAudioAdapter.submitItems(section.items) } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentAudioCardAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentAudioCardAdapter.kt index 9e06c629..f2a21776 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentAudioCardAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentAudioCardAdapter.kt @@ -14,10 +14,35 @@ class ContentAudioCardAdapter( ) : RecyclerView.Adapter() { private var items: List = 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) { + val previousItemCount = this.items.size 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 { @@ -29,7 +54,10 @@ class ContentAudioCardAdapter( } override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.bind(items[position]) + holder.bind( + item = items[position], + gridItemWidthPx = gridItemWidthPx + ) } override fun getItemCount(): Int = items.size @@ -39,8 +67,12 @@ class ContentAudioCardAdapter( private val cardSize: AudioContentCardSize, private val onAudioClick: (ContentAudioCardUiModel) -> Unit ) : RecyclerView.ViewHolder(binding.root) { - fun bind(item: ContentAudioCardUiModel) = with(binding.audioContentCard.root) { - setSize(cardSize) + fun bind(item: ContentAudioCardUiModel, gridItemWidthPx: Int) = with(binding.audioContentCard.root) { + if (gridItemWidthPx > 0) { + setGridItemWidthPx(gridItemWidthPx) + } else { + setSize(cardSize) + } setContent(item.title, item.creatorNickname) setTags(item.tags) setAdultVisible(item.showAdultBadge) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentRecyclerItemLayoutParams.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentRecyclerItemLayoutParams.kt index 654581ce..a4ce0950 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentRecyclerItemLayoutParams.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentRecyclerItemLayoutParams.kt @@ -10,14 +10,20 @@ fun RecyclerView.addContentHorizontalItemSpacing() { if (itemDecorationCount == 0) addItemDecoration(ContentHorizontalItemDecoration()) } -fun RecyclerView.addContentGridItemSpacing(spanCount: Int = CONTENT_RECOMMENDED_GRID_SPAN_COUNT) { - if (itemDecorationCount == 0) addItemDecoration(ContentGridItemDecoration(spanCount)) +fun RecyclerView.addContentGridItemSpacing( + 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 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() } @@ -31,13 +37,14 @@ private class ContentHorizontalItemDecoration : RecyclerView.ItemDecoration() { } private class ContentGridItemDecoration( - private val spanCount: Int + private val spanCount: Int, + private val gapDp: Int ) : RecyclerView.ItemDecoration() { override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { val position = parent.getChildAdapterPosition(view) if (position == RecyclerView.NO_POSITION) return val columnIndex = position % spanCount - val gap = GRID_ITEM_GAP_DP.dpToPx() + val gap = gapDp.dpToPx() outRect.left = (columnIndex * gap / spanCount).roundToInt() outRect.right = (gap - (columnIndex + 1) * gap / spanCount).roundToInt() 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 +const val CONTENT_RECOMMENDED_GRID_GAP_DP = 4 private const val GRID_ITEM_GAP_DP = 8 private const val GRID_ITEM_VERTICAL_GAP_DP = 28 const val CONTENT_RECOMMENDED_GRID_SPAN_COUNT = 2 diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragmentSourceTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragmentSourceTest.kt index dbd7d926..a14a195a 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragmentSourceTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragmentSourceTest.kt @@ -494,6 +494,34 @@ class ContentMainFragmentSourceTest { 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 fun `content 전체 탭 adapter는 fixed Small card width 대신 grid item width를 사용한다`() { val fragment = projectFile( @@ -538,7 +566,7 @@ class ContentMainFragmentSourceTest { assertSourceContains(audioAdapter, "setGridItemWidthPx(gridItemWidthPx)") assertSourceContains(seriesAdapter, "setGridItemWidthPx(gridItemWidthPx)") 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(seriesCard, "fun setGridItemWidthPx(widthPx: Int)") assertSourceContains(seriesCard, "172f / 122f")