diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/HomeMainFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/HomeMainFragment.kt index ab6a5a37..d18dab87 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/HomeMainFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/HomeMainFragment.kt @@ -30,7 +30,6 @@ import kr.co.vividnext.sodalive.v2.main.home.ui.HomeAiCharacterAdapter import kr.co.vividnext.sodalive.v2.main.home.ui.HomeBannerBinder import kr.co.vividnext.sodalive.v2.main.home.ui.HomeCheerCreatorAdapter import kr.co.vividnext.sodalive.v2.main.home.ui.HomeFirstAudioAdapter -import kr.co.vividnext.sodalive.v2.main.home.ui.HomeFollowAllButtonBinder import kr.co.vividnext.sodalive.v2.main.home.ui.HomeGenreCreatorAdapter import kr.co.vividnext.sodalive.v2.main.home.ui.HomeLiveAdapter import kr.co.vividnext.sodalive.v2.main.home.ui.HomeRecentActivityCreatorAdapter @@ -47,7 +46,7 @@ class HomeMainFragment : BaseFragment( private val firstAudioAdapter = HomeFirstAudioAdapter() private val aiCharacterAdapter = HomeAiCharacterAdapter() private val genreCreatorAdapter = HomeGenreCreatorAdapter { creatorIds -> onGenreFollowAllClick(creatorIds) } - private val cheerCreatorAdapter = HomeCheerCreatorAdapter() + private val cheerCreatorAdapter = HomeCheerCreatorAdapter { creatorIds -> onCheerFollowAllClick(creatorIds) } private var bannerBinder: HomeBannerBinder? = null private var onGenreFollowAllClick: (List) -> Unit = {} private var onCheerFollowAllClick: (List) -> Unit = {} @@ -135,13 +134,7 @@ class HomeMainFragment : BaseFragment( private fun bindCheerCreatorSection(section: HomeRecommendationCheerCreatorSection) { binding.llHomeCheerCreatorSection.visibility = section.items.toSectionVisibility() - cheerCreatorAdapter.submitItems(section.items) - HomeFollowAllButtonBinder.bind( - view = binding.viewHomeCheerFollowAll.root, - creatorIds = section.items.map { it.creatorId }, - isFollowCompleted = section.isFollowCompleted, - onClick = onCheerFollowAllClick - ) + cheerCreatorAdapter.submitSection(section.items, section.isFollowCompleted) } private fun bindPopularCommunitySection(section: HomeRecommendationPopularCommunityPostSection) { @@ -247,7 +240,7 @@ class HomeMainFragment : BaseFragment( HomeRecommendationGenreCreatorGroupUiModel("판타지", sampleCreators(701L, count = 8)) ) ), - cheerCreators = HomeRecommendationCheerCreatorSection(sampleCreators(701L)), + cheerCreators = HomeRecommendationCheerCreatorSection(sampleCreators(701L, count = 8)), popularCommunityPosts = HomeRecommendationPopularCommunityPostSection(emptyList()) ) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeCheerCreatorAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeCheerCreatorAdapter.kt index a9f57b16..8632879a 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeCheerCreatorAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeCheerCreatorAdapter.kt @@ -1,3 +1,136 @@ package kr.co.vividnext.sodalive.v2.main.home.ui -class HomeCheerCreatorAdapter : HomeCreatorProfileAdapter() +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.GridLayout +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.extensions.loadUrl +import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationCreatorUiModel + +class HomeCheerCreatorAdapter( + private val onFollowAllClick: (List) -> Unit +) : RecyclerView.Adapter() { + private var creators: List = emptyList() + private var isFollowCompleted: Boolean = false + + fun submitSection( + creators: List, + isFollowCompleted: Boolean + ) { + this.creators = creators + this.isFollowCompleted = isFollowCompleted + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CheerCreatorGroupViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.item_home_cheer_creator_group, parent, false) + view.layoutParams = cheerGroupLayoutParams(parent) + return CheerCreatorGroupViewHolder(view, parent, onFollowAllClick) + } + + override fun onBindViewHolder(holder: CheerCreatorGroupViewHolder, position: Int) { + holder.bind(creators, isFollowCompleted) + } + + override fun getItemCount(): Int = if (creators.isEmpty()) 0 else 1 + + class CheerCreatorGroupViewHolder( + itemView: View, + private val parent: ViewGroup, + private val onFollowAllClick: (List) -> Unit + ) : RecyclerView.ViewHolder(itemView) { + private val creatorGrid = itemView.findViewById(R.id.gl_home_cheer_creator_profiles) + private val followAllButton = itemView.findViewById(R.id.view_home_cheer_group_follow_all) + + fun bind(creators: List, isFollowCompleted: Boolean) { + val visibleCreators = creators.take(CREATOR_COUNT_PER_GROUP) + bindCreators(visibleCreators, calculateProfileSize()) + creatorGrid.post { + bindCreators(visibleCreators, calculateProfileSize()) + } + HomeFollowAllButtonBinder.bind( + view = followAllButton, + creatorIds = creators.map { it.creatorId }, + isFollowCompleted = isFollowCompleted, + onClick = onFollowAllClick + ) + } + + private fun calculateProfileSize(): Int { + val gridWidth = creatorGrid.width.takeIf { it > 0 } + ?: (resolvedCardWidth() - itemView.paddingStart - itemView.paddingEnd) + val totalColumnGap = itemView.resources.getDimensionPixelSize(R.dimen.spacing_14) * (GRID_COLUMN_COUNT - 1) + return (gridWidth - totalColumnGap) / GRID_COLUMN_COUNT + } + + private fun resolvedCardWidth(): Int { + val parentContentWidth = parentContentWidth() + itemView.width.takeIf { it > 0 }?.let { return minOf(it, parentContentWidth) } + itemView.layoutParams.width.takeIf { it > 0 }?.let { return minOf(it, parentContentWidth) } + + parentContentWidth.takeIf { it > 0 }?.let { return it } + + return itemView.resources.displayMetrics.widthPixels - parentHorizontalInset() + } + + private fun parentContentWidth(): Int { + return parent.width - parentHorizontalInset() + } + + private fun parentHorizontalInset(): Int { + val paddingInset = parent.paddingStart + parent.paddingEnd + return paddingInset.takeIf { it > 0 } ?: itemView.resources.getDimensionPixelSize(R.dimen.spacing_28) + } + + private fun bindCreators(creators: List, profileSize: Int) { + creatorGrid.removeAllViews() + creators.forEachIndexed { index, creator -> + val profileView = LayoutInflater.from(creatorGrid.context).inflate( + R.layout.item_home_genre_creator_profile, + creatorGrid, + false + ) + val row = index / GRID_COLUMN_COUNT + val column = index % GRID_COLUMN_COUNT + profileView.layoutParams = GridLayout.LayoutParams( + GridLayout.spec(row), + GridLayout.spec(column) + ).apply { + width = profileSize + height = ViewGroup.LayoutParams.WRAP_CONTENT + if (column < GRID_COLUMN_COUNT - 1) { + marginEnd = profileView.resources.getDimensionPixelSize(R.dimen.spacing_14) + } + if (row > 0) { + topMargin = profileView.resources.getDimensionPixelSize(R.dimen.spacing_14) + } + } + profileView.findViewById(R.id.iv_home_genre_creator_profile).apply { + if (creator.profileImage == null) { + setImageDrawable(null) + } else { + loadUrl(creator.profileImage) + } + } + profileView.findViewById(R.id.tv_home_genre_creator_profile_nickname).text = creator.nickname + creatorGrid.addView(profileView) + } + } + } + + companion object { + private const val CREATOR_COUNT_PER_GROUP = 8 + private const val GRID_COLUMN_COUNT = 4 + } +} + +private fun cheerGroupLayoutParams(parent: ViewGroup): RecyclerView.LayoutParams { + val paddingInset = parent.paddingStart + parent.paddingEnd + val horizontalInset = paddingInset.takeIf { it > 0 } ?: parent.resources.getDimensionPixelSize(R.dimen.spacing_28) + val width = (parent.width - horizontalInset).takeIf { it > 0 } ?: ViewGroup.LayoutParams.MATCH_PARENT + return RecyclerView.LayoutParams(width, ViewGroup.LayoutParams.WRAP_CONTENT) +} diff --git a/app/src/main/res/layout/fragment_v2_main_home.xml b/app/src/main/res/layout/fragment_v2_main_home.xml index 83f03dfc..e97d1d10 100644 --- a/app/src/main/res/layout/fragment_v2_main_home.xml +++ b/app/src/main/res/layout/fragment_v2_main_home.xml @@ -189,14 +189,10 @@ - - (R.id.gl_home_cheer_creator_profiles) + val followAllButton = group.findViewById(R.id.view_home_cheer_group_follow_all) + + assertEquals(ViewGroup.LayoutParams.MATCH_PARENT, group.layoutParams.width) + assertEquals(ViewGroup.LayoutParams.WRAP_CONTENT, group.layoutParams.height) + assertEquals(R.drawable.bg_home_cheer_creator_group, shadowOf(group.background).createdFromResId) + assertEquals(14.dpToPx(), group.paddingStart) + assertEquals(14.dpToPx(), group.paddingTop) + assertEquals(14.dpToPx(), group.paddingEnd) + assertEquals(14.dpToPx(), group.paddingBottom) + assertEquals(ViewGroup.LayoutParams.MATCH_PARENT, creatorGrid.layoutParams.width) + assertEquals(4, creatorGrid.columnCount) + assertEquals(2, creatorGrid.rowCount) + assertNotNull(followAllButton) + } + + @Test + fun `home cheer creator section uses full width card with horizontal padding`() { + val root = inflateView(R.layout.fragment_v2_main_home) + val cheerList = root.findViewById(R.id.rv_home_cheer_creators) + + assertEquals(ViewGroup.LayoutParams.WRAP_CONTENT, cheerList.layoutParams.height) + assertEquals(14.dpToPx(), cheerList.paddingStart) + assertEquals(14.dpToPx(), cheerList.paddingEnd) + } + + @Test + fun `cheer creator adapter sizes card to parent width minus horizontal padding`() { + val context = ApplicationProvider.getApplicationContext() + val parent = RecyclerView(context) + parent.setPadding(14.dpToPx(), 0, 14.dpToPx(), 0) + parent.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false) + parent.layout(0, 0, 402.dpToPx(), 600.dpToPx()) + + val viewHolder = HomeCheerCreatorAdapter(onFollowAllClick = {}).onCreateViewHolder(parent, 0) + + assertEquals(374.dpToPx(), viewHolder.itemView.layoutParams.width) + } + + @Test + fun `cheer creator adapter renders max eight creators`() { + val context = ApplicationProvider.getApplicationContext() + val parent = RecyclerView(context) + parent.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false) + val adapter = HomeCheerCreatorAdapter(onFollowAllClick = {}) + adapter.submitSection(genreCreators(1L) + genreCreators(11L), isFollowCompleted = false) + val viewHolder = adapter.onCreateViewHolder(parent, 0) + + adapter.onBindViewHolder(viewHolder, 0) + + assertEquals(1, adapter.itemCount) + assertEquals(8, viewHolder.itemView.findViewById(R.id.gl_home_cheer_creator_profiles).childCount) + } + + @Test + fun `cheer creator adapter follow all uses all cheer creator ids`() { + val context = ApplicationProvider.getApplicationContext() + val parent = RecyclerView(context) + parent.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false) + val clickedCreatorIds = mutableListOf() + val adapter = HomeCheerCreatorAdapter(onFollowAllClick = { clickedCreatorIds.addAll(it) }) + adapter.submitSection(genreCreators(10L), isFollowCompleted = false) + val viewHolder = adapter.onCreateViewHolder(parent, 0) + + adapter.onBindViewHolder(viewHolder, 0) + viewHolder.itemView.findViewById(R.id.view_home_cheer_group_follow_all).performClick() + + assertEquals((10L..17L).toList(), clickedCreatorIds) + } + @Test fun `genre creator visible groups hide all empty groups before fragment visibility`() { val section = HomeRecommendationGenreCreatorSection(