feat(home): 응원 크리에이터 카드 바인딩을 추가한다
This commit is contained in:
@@ -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.HomeBannerBinder
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeCheerCreatorAdapter
|
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.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.HomeGenreCreatorAdapter
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeLiveAdapter
|
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeLiveAdapter
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeRecentActivityCreatorAdapter
|
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeRecentActivityCreatorAdapter
|
||||||
@@ -47,7 +46,7 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
|||||||
private val firstAudioAdapter = HomeFirstAudioAdapter()
|
private val firstAudioAdapter = HomeFirstAudioAdapter()
|
||||||
private val aiCharacterAdapter = HomeAiCharacterAdapter()
|
private val aiCharacterAdapter = HomeAiCharacterAdapter()
|
||||||
private val genreCreatorAdapter = HomeGenreCreatorAdapter { creatorIds -> onGenreFollowAllClick(creatorIds) }
|
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 bannerBinder: HomeBannerBinder? = null
|
||||||
private var onGenreFollowAllClick: (List<Long>) -> Unit = {}
|
private var onGenreFollowAllClick: (List<Long>) -> Unit = {}
|
||||||
private var onCheerFollowAllClick: (List<Long>) -> Unit = {}
|
private var onCheerFollowAllClick: (List<Long>) -> Unit = {}
|
||||||
@@ -135,13 +134,7 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
|||||||
|
|
||||||
private fun bindCheerCreatorSection(section: HomeRecommendationCheerCreatorSection) {
|
private fun bindCheerCreatorSection(section: HomeRecommendationCheerCreatorSection) {
|
||||||
binding.llHomeCheerCreatorSection.visibility = section.items.toSectionVisibility()
|
binding.llHomeCheerCreatorSection.visibility = section.items.toSectionVisibility()
|
||||||
cheerCreatorAdapter.submitItems(section.items)
|
cheerCreatorAdapter.submitSection(section.items, section.isFollowCompleted)
|
||||||
HomeFollowAllButtonBinder.bind(
|
|
||||||
view = binding.viewHomeCheerFollowAll.root,
|
|
||||||
creatorIds = section.items.map { it.creatorId },
|
|
||||||
isFollowCompleted = section.isFollowCompleted,
|
|
||||||
onClick = onCheerFollowAllClick
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindPopularCommunitySection(section: HomeRecommendationPopularCommunityPostSection) {
|
private fun bindPopularCommunitySection(section: HomeRecommendationPopularCommunityPostSection) {
|
||||||
@@ -247,7 +240,7 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
|||||||
HomeRecommendationGenreCreatorGroupUiModel("판타지", sampleCreators(701L, count = 8))
|
HomeRecommendationGenreCreatorGroupUiModel("판타지", sampleCreators(701L, count = 8))
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
cheerCreators = HomeRecommendationCheerCreatorSection(sampleCreators(701L)),
|
cheerCreators = HomeRecommendationCheerCreatorSection(sampleCreators(701L, count = 8)),
|
||||||
popularCommunityPosts = HomeRecommendationPopularCommunityPostSection(emptyList())
|
popularCommunityPosts = HomeRecommendationPopularCommunityPostSection(emptyList())
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,136 @@
|
|||||||
package kr.co.vividnext.sodalive.v2.main.home.ui
|
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<Long>) -> Unit
|
||||||
|
) : RecyclerView.Adapter<HomeCheerCreatorAdapter.CheerCreatorGroupViewHolder>() {
|
||||||
|
private var creators: List<HomeRecommendationCreatorUiModel> = emptyList()
|
||||||
|
private var isFollowCompleted: Boolean = false
|
||||||
|
|
||||||
|
fun submitSection(
|
||||||
|
creators: List<HomeRecommendationCreatorUiModel>,
|
||||||
|
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<Long>) -> Unit
|
||||||
|
) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
private val creatorGrid = itemView.findViewById<GridLayout>(R.id.gl_home_cheer_creator_profiles)
|
||||||
|
private val followAllButton = itemView.findViewById<View>(R.id.view_home_cheer_group_follow_all)
|
||||||
|
|
||||||
|
fun bind(creators: List<HomeRecommendationCreatorUiModel>, 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<HomeRecommendationCreatorUiModel>, 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<ImageView>(R.id.iv_home_genre_creator_profile).apply {
|
||||||
|
if (creator.profileImage == null) {
|
||||||
|
setImageDrawable(null)
|
||||||
|
} else {
|
||||||
|
loadUrl(creator.profileImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
profileView.findViewById<TextView>(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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -189,14 +189,10 @@
|
|||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/rv_home_cheer_creators"
|
android:id="@+id/rv_home_cheer_creators"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="160dp"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:paddingHorizontal="@dimen/spacing_20"
|
android:paddingHorizontal="@dimen/spacing_14"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/view_home_cheer_follow_all"
|
|
||||||
layout="@layout/view_home_follow_all_button" />
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationGenreCreato
|
|||||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationLiveUiModel
|
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationLiveUiModel
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.visibleHomeGenreCreatorGroups
|
import kr.co.vividnext.sodalive.v2.main.home.model.visibleHomeGenreCreatorGroups
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeAiCharacterAdapter
|
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeAiCharacterAdapter
|
||||||
|
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.HomeFirstAudioAdapter
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeFollowAllButtonBinder
|
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.HomeGenreCreatorAdapter
|
||||||
@@ -293,6 +294,10 @@ class HomeMainFragmentLayoutTest {
|
|||||||
assertEquals(4, creatorGrid.columnCount)
|
assertEquals(4, creatorGrid.columnCount)
|
||||||
assertEquals(2, creatorGrid.rowCount)
|
assertEquals(2, creatorGrid.rowCount)
|
||||||
assertNotNull(followAllButton)
|
assertNotNull(followAllButton)
|
||||||
|
val followAllLayoutParams = followAllButton.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
assertEquals(0, followAllLayoutParams.marginStart)
|
||||||
|
assertEquals(14.dpToPx(), followAllLayoutParams.topMargin)
|
||||||
|
assertEquals(0, followAllLayoutParams.marginEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -473,6 +478,79 @@ class HomeMainFragmentLayoutTest {
|
|||||||
assertEquals((10L..17L).toList(), clickedCreatorIds)
|
assertEquals((10L..17L).toList(), clickedCreatorIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `cheer creator group item matches figma card structure`() {
|
||||||
|
val group = inflateViewWithParent(R.layout.item_home_cheer_creator_group)
|
||||||
|
val creatorGrid = group.findViewById<GridLayout>(R.id.gl_home_cheer_creator_profiles)
|
||||||
|
val followAllButton = group.findViewById<View>(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<RecyclerView>(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<Context>()
|
||||||
|
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<Context>()
|
||||||
|
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<GridLayout>(R.id.gl_home_cheer_creator_profiles).childCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `cheer creator adapter follow all uses all cheer creator ids`() {
|
||||||
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
|
val parent = RecyclerView(context)
|
||||||
|
parent.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
|
||||||
|
val clickedCreatorIds = mutableListOf<Long>()
|
||||||
|
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<View>(R.id.view_home_cheer_group_follow_all).performClick()
|
||||||
|
|
||||||
|
assertEquals((10L..17L).toList(), clickedCreatorIds)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `genre creator visible groups hide all empty groups before fragment visibility`() {
|
fun `genre creator visible groups hide all empty groups before fragment visibility`() {
|
||||||
val section = HomeRecommendationGenreCreatorSection(
|
val section = HomeRecommendationGenreCreatorSection(
|
||||||
|
|||||||
Reference in New Issue
Block a user