fix(content): 전체 탭 grid 폭을 주입한다

This commit is contained in:
2026-06-25 18:31:39 +09:00
parent 78e0a53018
commit 136fdced17
5 changed files with 99 additions and 9 deletions

View File

@@ -8,6 +8,7 @@ import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.doOnLayout
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView 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.toContentBannerRoute
import kr.co.vividnext.sodalive.v2.main.content.model.usesDayOfWeekQuery 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.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.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
@@ -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.ContentOriginalSeriesAdapter
import kr.co.vividnext.sodalive.v2.main.content.ui.addContentGridItemSpacing 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.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.AudioContentCardSize
import kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingAdapter import kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingAdapter
import kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingItem import kr.co.vividnext.sodalive.v2.widget.contentranking.ContentRankingItem
@@ -281,6 +284,7 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
layoutManager = contentAllGridLayoutManager layoutManager = contentAllGridLayoutManager
adapter = contentAllAudioCardAdapter adapter = contentAllAudioCardAdapter
addContentGridItemSpacing(CONTENT_ALL_GRID_SPAN_COUNT) addContentGridItemSpacing(CONTENT_ALL_GRID_SPAN_COUNT)
doOnLayout { updateAllTabGridItemWidth() }
addOnScrollListener(object : RecyclerView.OnScrollListener() { addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy) super.onScrolled(recyclerView, dx, dy)
@@ -357,6 +361,7 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
bindAllTabControls(state) bindAllTabControls(state)
binding.layoutContentAllSurface.visibility = View.VISIBLE binding.layoutContentAllSurface.visibility = View.VISIBLE
hideAllTabEmptyError() hideAllTabEmptyError()
updateAllTabGridItemWidth()
if (state.selectedType.usesSeriesItems()) { if (state.selectedType.usesSeriesItems()) {
binding.rvContentAllItems.adapter = contentAllSeriesCardAdapter binding.rvContentAllItems.adapter = contentAllSeriesCardAdapter
contentAllAudioCardAdapter.submitItems(emptyList()) contentAllAudioCardAdapter.submitItems(emptyList())
@@ -372,6 +377,12 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
} }
} }
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) { private fun bindAllTabEmpty(state: MainContentAllTabUiState.Empty) {
bindAllTabControls(state) bindAllTabControls(state)
binding.layoutContentAllSurface.visibility = View.VISIBLE binding.layoutContentAllSurface.visibility = View.VISIBLE
@@ -589,6 +600,5 @@ class ContentMainFragment : BaseFragment<FragmentV2MainContentBinding>(
private const val CONTENT_TAB_RECOMMENDATION = 0 private const val CONTENT_TAB_RECOMMENDATION = 0
private const val CONTENT_TAB_RANKING = 1 private const val CONTENT_TAB_RANKING = 1
private const val CONTENT_TAB_ALL = 2 private const val CONTENT_TAB_ALL = 2
private const val CONTENT_ALL_GRID_SPAN_COUNT = 3
} }
} }

View File

@@ -6,13 +6,19 @@ import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.databinding.ItemContentAudioCardBinding import kr.co.vividnext.sodalive.databinding.ItemContentAudioCardBinding
import kr.co.vividnext.sodalive.extensions.loadUrl import kr.co.vividnext.sodalive.extensions.loadUrl
import kr.co.vividnext.sodalive.v2.main.content.model.MainContentAllAudioUiModel import kr.co.vividnext.sodalive.v2.main.content.model.MainContentAllAudioUiModel
import kr.co.vividnext.sodalive.v2.widget.AudioContentCardSize
class ContentAllAudioCardAdapter( class ContentAllAudioCardAdapter(
private val onAudioClick: (Long) -> Unit = {} private val onAudioClick: (Long) -> Unit = {}
) : RecyclerView.Adapter<ContentAllAudioCardAdapter.ViewHolder>() { ) : RecyclerView.Adapter<ContentAllAudioCardAdapter.ViewHolder>() {
private var items: List<MainContentAllAudioUiModel> = emptyList() private var items: List<MainContentAllAudioUiModel> = emptyList()
private var gridItemWidthPx: Int = 0
fun setGridItemWidthPx(widthPx: Int) {
if (widthPx <= 0 || gridItemWidthPx == widthPx) return
gridItemWidthPx = widthPx
notifyDataSetChanged()
}
fun submitItems(items: List<MainContentAllAudioUiModel>) { fun submitItems(items: List<MainContentAllAudioUiModel>) {
this.items = items this.items = items
@@ -27,7 +33,10 @@ class ContentAllAudioCardAdapter(
} }
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
@@ -36,8 +45,8 @@ class ContentAllAudioCardAdapter(
private val binding: ItemContentAudioCardBinding, private val binding: ItemContentAudioCardBinding,
private val onAudioClick: (Long) -> Unit private val onAudioClick: (Long) -> Unit
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: MainContentAllAudioUiModel) = with(binding.audioContentCard.root) { fun bind(item: MainContentAllAudioUiModel, gridItemWidthPx: Int) = with(binding.audioContentCard.root) {
setSize(AudioContentCardSize.Small) setGridItemWidthPx(gridItemWidthPx)
setContent(item.title, item.creatorNickname) setContent(item.title, item.creatorNickname)
setTags(item.tags) setTags(item.tags)
setAdultVisible(item.showAdultBadge) setAdultVisible(item.showAdultBadge)

View File

@@ -6,13 +6,19 @@ import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.databinding.ItemContentAllSeriesCardBinding import kr.co.vividnext.sodalive.databinding.ItemContentAllSeriesCardBinding
import kr.co.vividnext.sodalive.extensions.loadUrl import kr.co.vividnext.sodalive.extensions.loadUrl
import kr.co.vividnext.sodalive.v2.main.content.model.MainContentAllSeriesUiModel import kr.co.vividnext.sodalive.v2.main.content.model.MainContentAllSeriesUiModel
import kr.co.vividnext.sodalive.v2.widget.SeriesContentCardSize
class ContentAllSeriesCardAdapter( class ContentAllSeriesCardAdapter(
private val onSeriesClick: (Long) -> Unit = {} private val onSeriesClick: (Long) -> Unit = {}
) : RecyclerView.Adapter<ContentAllSeriesCardAdapter.ViewHolder>() { ) : RecyclerView.Adapter<ContentAllSeriesCardAdapter.ViewHolder>() {
private var items: List<MainContentAllSeriesUiModel> = emptyList() private var items: List<MainContentAllSeriesUiModel> = emptyList()
private var gridItemWidthPx: Int = 0
fun setGridItemWidthPx(widthPx: Int) {
if (widthPx <= 0 || gridItemWidthPx == widthPx) return
gridItemWidthPx = widthPx
notifyDataSetChanged()
}
fun submitItems(items: List<MainContentAllSeriesUiModel>) { fun submitItems(items: List<MainContentAllSeriesUiModel>) {
this.items = items this.items = items
@@ -27,7 +33,10 @@ class ContentAllSeriesCardAdapter(
} }
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
@@ -36,8 +45,8 @@ class ContentAllSeriesCardAdapter(
private val binding: ItemContentAllSeriesCardBinding, private val binding: ItemContentAllSeriesCardBinding,
private val onSeriesClick: (Long) -> Unit private val onSeriesClick: (Long) -> Unit
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: MainContentAllSeriesUiModel) = with(binding.seriesContentCard.root) { fun bind(item: MainContentAllSeriesUiModel, gridItemWidthPx: Int) = with(binding.seriesContentCard.root) {
setSize(SeriesContentCardSize.Small) setGridItemWidthPx(gridItemWidthPx)
setContent(item.title, item.creatorNickname) setContent(item.title, item.creatorNickname)
setOriginalVisible(item.showOriginalTag) setOriginalVisible(item.showOriginalTag)
setAdultVisible(item.showAdultBadge) setAdultVisible(item.showAdultBadge)

View File

@@ -14,6 +14,13 @@ fun RecyclerView.addContentGridItemSpacing(spanCount: Int = CONTENT_RECOMMENDED_
if (itemDecorationCount == 0) addItemDecoration(ContentGridItemDecoration(spanCount)) 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() { private class ContentHorizontalItemDecoration : 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)
@@ -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_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
const val CONTENT_ALL_GRID_SPAN_COUNT = 3

View File

@@ -119,6 +119,10 @@ class ContentMainFragmentSourceTest {
assertTrue(source.contains("CONTENT_ALL_GRID_SPAN_COUNT")) assertTrue(source.contains("CONTENT_ALL_GRID_SPAN_COUNT"))
assertTrue(source.contains("GridLayoutManager(requireContext(), 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("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.AUDIO"))
assertTrue(source.contains("MainContentAllType.SERIES")) assertTrue(source.contains("MainContentAllType.SERIES"))
assertTrue(source.contains("MainContentAllType.ORIGINAL")) assertTrue(source.contains("MainContentAllType.ORIGINAL"))
@@ -358,6 +362,56 @@ 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 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( private fun banner(
eventItem: EventItem? = null, eventItem: EventItem? = null,
creatorId: Long? = null, creatorId: Long? = null,