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 cb132d52..e5f393f9 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 @@ -8,6 +8,7 @@ import android.view.ViewGroup import android.widget.LinearLayout import android.widget.TextView import androidx.core.content.ContextCompat +import androidx.core.view.doOnLayout import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -45,6 +46,7 @@ import kr.co.vividnext.sodalive.v2.main.content.model.toContentBannerIntent import kr.co.vividnext.sodalive.v2.main.content.model.toContentBannerRoute import kr.co.vividnext.sodalive.v2.main.content.model.usesDayOfWeekQuery import kr.co.vividnext.sodalive.v2.main.content.model.usesSeriesItems +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_SPAN_COUNT import kr.co.vividnext.sodalive.v2.main.content.ui.ContentAllAudioCardAdapter import kr.co.vividnext.sodalive.v2.main.content.ui.ContentAllSeriesCardAdapter @@ -55,6 +57,7 @@ import kr.co.vividnext.sodalive.v2.main.content.ui.ContentNewAndHotAdapter import kr.co.vividnext.sodalive.v2.main.content.ui.ContentOriginalSeriesAdapter import kr.co.vividnext.sodalive.v2.main.content.ui.addContentGridItemSpacing import kr.co.vividnext.sodalive.v2.main.content.ui.addContentHorizontalItemSpacing +import kr.co.vividnext.sodalive.v2.main.content.ui.calculateContentGridItemWidthPx import kr.co.vividnext.sodalive.v2.widget.AudioContentCardSize import kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingAdapter import kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingItem @@ -281,6 +284,7 @@ class ContentMainFragment : BaseFragment( layoutManager = contentAllGridLayoutManager adapter = contentAllAudioCardAdapter addContentGridItemSpacing(CONTENT_ALL_GRID_SPAN_COUNT) + doOnLayout { updateAllTabGridItemWidth() } addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) @@ -357,6 +361,7 @@ class ContentMainFragment : BaseFragment( bindAllTabControls(state) binding.layoutContentAllSurface.visibility = View.VISIBLE hideAllTabEmptyError() + updateAllTabGridItemWidth() if (state.selectedType.usesSeriesItems()) { binding.rvContentAllItems.adapter = contentAllSeriesCardAdapter contentAllAudioCardAdapter.submitItems(emptyList()) @@ -372,6 +377,12 @@ class ContentMainFragment : BaseFragment( } } + private fun updateAllTabGridItemWidth() { + val widthPx = binding.rvContentAllItems.calculateContentGridItemWidthPx(CONTENT_ALL_GRID_SPAN_COUNT) + contentAllAudioCardAdapter.setGridItemWidthPx(widthPx) + contentAllSeriesCardAdapter.setGridItemWidthPx(widthPx) + } + private fun bindAllTabEmpty(state: MainContentAllTabUiState.Empty) { bindAllTabControls(state) binding.layoutContentAllSurface.visibility = View.VISIBLE @@ -589,6 +600,5 @@ class ContentMainFragment : BaseFragment( private const val CONTENT_TAB_RECOMMENDATION = 0 private const val CONTENT_TAB_RANKING = 1 private const val CONTENT_TAB_ALL = 2 - private const val CONTENT_ALL_GRID_SPAN_COUNT = 3 } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentAllAudioCardAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentAllAudioCardAdapter.kt index 42792086..cb08ce5f 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentAllAudioCardAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentAllAudioCardAdapter.kt @@ -6,13 +6,19 @@ import androidx.recyclerview.widget.RecyclerView import kr.co.vividnext.sodalive.databinding.ItemContentAudioCardBinding import kr.co.vividnext.sodalive.extensions.loadUrl import kr.co.vividnext.sodalive.v2.main.content.model.MainContentAllAudioUiModel -import kr.co.vividnext.sodalive.v2.widget.AudioContentCardSize class ContentAllAudioCardAdapter( private val onAudioClick: (Long) -> Unit = {} ) : RecyclerView.Adapter() { private var items: List = emptyList() + private var gridItemWidthPx: Int = 0 + + fun setGridItemWidthPx(widthPx: Int) { + if (widthPx <= 0 || gridItemWidthPx == widthPx) return + gridItemWidthPx = widthPx + notifyDataSetChanged() + } fun submitItems(items: List) { this.items = items @@ -27,7 +33,10 @@ class ContentAllAudioCardAdapter( } override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.bind(items[position]) + holder.bind( + item = items[position], + gridItemWidthPx = gridItemWidthPx + ) } override fun getItemCount(): Int = items.size @@ -36,8 +45,8 @@ class ContentAllAudioCardAdapter( private val binding: ItemContentAudioCardBinding, private val onAudioClick: (Long) -> Unit ) : RecyclerView.ViewHolder(binding.root) { - fun bind(item: MainContentAllAudioUiModel) = with(binding.audioContentCard.root) { - setSize(AudioContentCardSize.Small) + fun bind(item: MainContentAllAudioUiModel, gridItemWidthPx: Int) = with(binding.audioContentCard.root) { + setGridItemWidthPx(gridItemWidthPx) 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/ContentAllSeriesCardAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentAllSeriesCardAdapter.kt index 9ab620f1..8ca3983d 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentAllSeriesCardAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentAllSeriesCardAdapter.kt @@ -6,13 +6,19 @@ import androidx.recyclerview.widget.RecyclerView import kr.co.vividnext.sodalive.databinding.ItemContentAllSeriesCardBinding import kr.co.vividnext.sodalive.extensions.loadUrl import kr.co.vividnext.sodalive.v2.main.content.model.MainContentAllSeriesUiModel -import kr.co.vividnext.sodalive.v2.widget.SeriesContentCardSize class ContentAllSeriesCardAdapter( private val onSeriesClick: (Long) -> Unit = {} ) : RecyclerView.Adapter() { private var items: List = emptyList() + private var gridItemWidthPx: Int = 0 + + fun setGridItemWidthPx(widthPx: Int) { + if (widthPx <= 0 || gridItemWidthPx == widthPx) return + gridItemWidthPx = widthPx + notifyDataSetChanged() + } fun submitItems(items: List) { this.items = items @@ -27,7 +33,10 @@ class ContentAllSeriesCardAdapter( } override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.bind(items[position]) + holder.bind( + item = items[position], + gridItemWidthPx = gridItemWidthPx + ) } override fun getItemCount(): Int = items.size @@ -36,8 +45,8 @@ class ContentAllSeriesCardAdapter( private val binding: ItemContentAllSeriesCardBinding, private val onSeriesClick: (Long) -> Unit ) : RecyclerView.ViewHolder(binding.root) { - fun bind(item: MainContentAllSeriesUiModel) = with(binding.seriesContentCard.root) { - setSize(SeriesContentCardSize.Small) + fun bind(item: MainContentAllSeriesUiModel, gridItemWidthPx: Int) = with(binding.seriesContentCard.root) { + setGridItemWidthPx(gridItemWidthPx) setContent(item.title, item.creatorNickname) setOriginalVisible(item.showOriginalTag) 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 7be7e613..654581ce 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 @@ -14,6 +14,13 @@ fun RecyclerView.addContentGridItemSpacing(spanCount: Int = CONTENT_RECOMMENDED_ if (itemDecorationCount == 0) addItemDecoration(ContentGridItemDecoration(spanCount)) } +fun RecyclerView.calculateContentGridItemWidthPx(spanCount: Int): Int { + val availableWidth = measuredWidth - paddingLeft - paddingRight + if (availableWidth <= 0 || spanCount <= 0) return 0 + val totalGap = GRID_ITEM_GAP_DP.dpToPx() * (spanCount - 1) + return ((availableWidth - totalGap) / spanCount).roundToInt() +} + private class ContentHorizontalItemDecoration : RecyclerView.ItemDecoration() { override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { val position = parent.getChildAdapterPosition(view) @@ -41,3 +48,4 @@ private const val HORIZONTAL_ITEM_GAP_DP = 8 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 +const val CONTENT_ALL_GRID_SPAN_COUNT = 3 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 d6ee1f24..f9606f53 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 @@ -119,6 +119,10 @@ class ContentMainFragmentSourceTest { assertTrue(source.contains("CONTENT_ALL_GRID_SPAN_COUNT")) assertTrue(source.contains("GridLayoutManager(requireContext(), CONTENT_ALL_GRID_SPAN_COUNT)")) assertTrue(source.contains("addContentGridItemSpacing(CONTENT_ALL_GRID_SPAN_COUNT)")) + assertTrue(source.contains("import androidx.core.view.doOnLayout")) + assertTrue(source.contains("doOnLayout { updateAllTabGridItemWidth() }")) + assertTrue(source.contains("private fun updateAllTabGridItemWidth()")) + assertTrue(source.contains("binding.rvContentAllItems.calculateContentGridItemWidthPx(CONTENT_ALL_GRID_SPAN_COUNT)")) assertTrue(source.contains("MainContentAllType.AUDIO")) assertTrue(source.contains("MainContentAllType.SERIES")) assertTrue(source.contains("MainContentAllType.ORIGINAL")) @@ -358,6 +362,56 @@ class ContentMainFragmentSourceTest { assertEquals(null, banner(link = "mailto:test@example.com").toContentBannerRoute()) } + @Test + fun `content 전체 탭 adapter는 fixed Small card width 대신 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/ContentAllAudioCardAdapter.kt" + ).readText() + val seriesAdapter = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentAllSeriesCardAdapter.kt" + ).readText() + val layoutParams = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentRecyclerItemLayoutParams.kt" + ).readText() + val audioCard = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardView.kt" + ).readText() + val seriesCard = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/widget/SeriesContentCardView.kt" + ).readText() + + assertFalse(audioAdapter.contains("setSize(AudioContentCardSize.Small)")) + assertFalse(seriesAdapter.contains("setSize(SeriesContentCardSize.Small)")) + assertFalse(audioAdapter.contains("holder.itemView.parent as? RecyclerView")) + assertFalse(seriesAdapter.contains("holder.itemView.parent as? RecyclerView")) + assertSourceContains(audioAdapter, "private var gridItemWidthPx: Int = 0") + assertSourceContains(seriesAdapter, "private var gridItemWidthPx: Int = 0") + assertSourceContains(audioAdapter, "fun setGridItemWidthPx(widthPx: Int)") + assertSourceContains(seriesAdapter, "fun setGridItemWidthPx(widthPx: Int)") + assertSourceContains(audioAdapter, "if (widthPx <= 0 || gridItemWidthPx == widthPx) return") + assertSourceContains(seriesAdapter, "if (widthPx <= 0 || gridItemWidthPx == widthPx) return") + assertSourceContains(fragment, "private fun updateAllTabGridItemWidth()") + assertSourceContains(fragment, "binding.rvContentAllItems.calculateContentGridItemWidthPx(CONTENT_ALL_GRID_SPAN_COUNT)") + assertSourceContains(fragment, "contentAllAudioCardAdapter.setGridItemWidthPx(widthPx)") + assertSourceContains(fragment, "contentAllSeriesCardAdapter.setGridItemWidthPx(widthPx)") + assertSourceContains(fragment, "doOnLayout { updateAllTabGridItemWidth() }") + assertTrue( + "전체 탭 content bind는 item submit 전에 최신 grid width를 adapter에 주입해야 한다.", + fragment.indexOf("updateAllTabGridItemWidth()") < + fragment.indexOf("contentAllSeriesCardAdapter.submitItems(state.seriesItems)") + ) + assertSourceContains(audioAdapter, "setGridItemWidthPx(gridItemWidthPx)") + assertSourceContains(seriesAdapter, "setGridItemWidthPx(gridItemWidthPx)") + assertSourceContains(layoutParams, "measuredWidth - paddingLeft - paddingRight") + assertSourceContains(layoutParams, "GRID_ITEM_GAP_DP.dpToPx() * (spanCount - 1)") + assertSourceContains(audioCard, "fun setGridItemWidthPx(widthPx: Int)") + assertSourceContains(seriesCard, "fun setGridItemWidthPx(widthPx: Int)") + assertSourceContains(seriesCard, "172f / 122f") + } + private fun banner( eventItem: EventItem? = null, creatorId: Long? = null,