feat(home): 라이브 섹션 전체 아이템을 추가한다

This commit is contained in:
2026-06-02 17:04:53 +09:00
parent 0e50d7f8d5
commit 9b29623f6f
4 changed files with 136 additions and 13 deletions

View File

@@ -1,7 +1,10 @@
package kr.co.vividnext.sodalive.v2.main.home.ui package kr.co.vividnext.sodalive.v2.main.home.ui
import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.extensions.loadUrl import kr.co.vividnext.sodalive.extensions.loadUrl
@@ -11,10 +14,12 @@ import kr.co.vividnext.sodalive.v2.widget.livethumbnail.LiveThumbnailSimpleView
class HomeLiveAdapter : RecyclerView.Adapter<HomeLiveAdapter.LiveViewHolder>() { class HomeLiveAdapter : RecyclerView.Adapter<HomeLiveAdapter.LiveViewHolder>() {
private var items: List<HomeRecommendationLiveUiModel> = emptyList() private var items: List<HomeRecommendationLiveUiModel> = emptyList()
private var hasMore: Boolean = false
private var onClick: ((HomeRecommendationLiveUiModel) -> Unit)? = null private var onClick: ((HomeRecommendationLiveUiModel) -> Unit)? = null
fun submitItems(items: List<HomeRecommendationLiveUiModel>) { fun submitItems(items: List<HomeRecommendationLiveUiModel>) {
this.items = items this.items = items.take(MAX_VISIBLE_LIVE_COUNT)
hasMore = items.size > MAX_VISIBLE_LIVE_COUNT
notifyDataSetChanged() notifyDataSetChanged()
} }
@@ -23,20 +28,54 @@ class HomeLiveAdapter : RecyclerView.Adapter<HomeLiveAdapter.LiveViewHolder>() {
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LiveViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LiveViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.view_live_thumbnail_simple, parent, false) return if (viewType == VIEW_TYPE_MORE) {
view.layoutParams = recyclerItemLayoutParams(parent) MoreViewHolder(createMoreView(parent))
return LiveViewHolder(view as LiveThumbnailSimpleView) } else {
val view = LayoutInflater.from(parent.context).inflate(R.layout.view_live_thumbnail_simple, parent, false)
view.layoutParams = liveItemLayoutParams(parent)
LiveItemViewHolder(view as LiveThumbnailSimpleView)
}
} }
override fun onBindViewHolder(holder: LiveViewHolder, position: Int) { override fun onBindViewHolder(holder: LiveViewHolder, position: Int) {
holder.bind(items[position], onClick) when (holder) {
is LiveItemViewHolder -> holder.bind(items[position], onClick)
is MoreViewHolder -> holder.bind()
}
} }
override fun getItemCount(): Int = items.size override fun getItemCount(): Int = items.size + if (hasMore) 1 else 0
class LiveViewHolder( override fun getItemViewType(position: Int): Int {
return if (hasMore && position == itemCount - 1) VIEW_TYPE_MORE else VIEW_TYPE_LIVE
}
private fun liveItemLayoutParams(parent: ViewGroup): RecyclerView.LayoutParams {
return RecyclerView.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply { marginEnd = parent.resources.getDimensionPixelSize(R.dimen.spacing_14) }
}
private fun createMoreView(parent: ViewGroup): TextView {
return TextView(parent.context).apply {
layoutParams = RecyclerView.LayoutParams(
parent.resources.getDimensionPixelSize(R.dimen.home_live_more_width),
parent.resources.getDimensionPixelSize(R.dimen.home_live_row_height)
)
gravity = Gravity.CENTER
setBackgroundResource(R.color.black)
setText(R.string.screen_home_theme_all)
setTextAppearance(R.style.Typography_Body5)
setTextColor(parent.context.getColor(R.color.soda_400))
}
}
sealed class LiveViewHolder(view: View) : RecyclerView.ViewHolder(view)
class LiveItemViewHolder(
private val view: LiveThumbnailSimpleView private val view: LiveThumbnailSimpleView
) : RecyclerView.ViewHolder(view) { ) : LiveViewHolder(view) {
fun bind( fun bind(
item: HomeRecommendationLiveUiModel, item: HomeRecommendationLiveUiModel,
onClick: ((HomeRecommendationLiveUiModel) -> Unit)? onClick: ((HomeRecommendationLiveUiModel) -> Unit)?
@@ -54,4 +93,18 @@ class HomeLiveAdapter : RecyclerView.Adapter<HomeLiveAdapter.LiveViewHolder>() {
view.setOnLiveThumbnailClick(if (onClick == null) null else { _: LiveThumbnailItem -> onClick.invoke(item) }) view.setOnLiveThumbnailClick(if (onClick == null) null else { _: LiveThumbnailItem -> onClick.invoke(item) })
} }
} }
class MoreViewHolder(
private val view: TextView
) : LiveViewHolder(view) {
fun bind() {
view.setOnClickListener(null)
}
}
companion object {
private const val MAX_VISIBLE_LIVE_COUNT = 20
private const val VIEW_TYPE_LIVE = 0
private const val VIEW_TYPE_MORE = 1
}
} }

View File

@@ -39,19 +39,20 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:paddingBottom="@dimen/spacing_32"> android:paddingBottom="@dimen/spacing_28">
<LinearLayout <LinearLayout
android:id="@+id/ll_home_live_section" android:id="@+id/ll_home_live_section"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:layout_marginTop="@dimen/spacing_12"
android:paddingTop="@dimen/spacing_24"> android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_home_lives" android:id="@+id/rv_home_lives"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="120dp" android:layout_height="wrap_content"
android:clipToPadding="false"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingHorizontal="@dimen/spacing_20" android:paddingHorizontal="@dimen/spacing_20"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
@@ -63,7 +64,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:paddingTop="@dimen/spacing_24"> android:paddingTop="@dimen/spacing_28">
<kr.co.vividnext.sodalive.v2.widget.banner.BannerView <kr.co.vividnext.sodalive.v2.widget.banner.BannerView
android:id="@+id/rv_home_banners" android:id="@+id/rv_home_banners"

View File

@@ -12,6 +12,9 @@
<dimen name="spacing_32">32dp</dimen> <dimen name="spacing_32">32dp</dimen>
<dimen name="spacing_48">48dp</dimen> <dimen name="spacing_48">48dp</dimen>
<dimen name="home_live_more_width">58dp</dimen>
<dimen name="home_live_row_height">102dp</dimen>
<dimen name="radius_4">4dp</dimen> <dimen name="radius_4">4dp</dimen>
<dimen name="radius_8">8dp</dimen> <dimen name="radius_8">8dp</dimen>
<dimen name="radius_14">14dp</dimen> <dimen name="radius_14">14dp</dimen>

View File

@@ -2,15 +2,20 @@ package kr.co.vividnext.sodalive.v2.main.home
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.widget.NestedScrollView import androidx.core.widget.NestedScrollView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationLiveUiModel
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeLiveAdapter
import kr.co.vividnext.sodalive.v2.widget.TextTabBarView import kr.co.vividnext.sodalive.v2.widget.TextTabBarView
import kr.co.vividnext.sodalive.v2.widget.banner.BannerView import kr.co.vividnext.sodalive.v2.widget.banner.BannerView
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
@@ -119,6 +124,56 @@ class HomeMainFragmentLayoutTest {
assertEquals(View.GONE, popularCommunitySection.visibility) assertEquals(View.GONE, popularCommunitySection.visibility)
} }
@Test
fun `home live section matches figma row dimensions`() {
val root = inflateView(R.layout.fragment_v2_main_home)
val liveList = root.findViewById<RecyclerView>(R.id.rv_home_lives)
assertNotNull(liveList)
assertEquals(102.dpToPx(), liveList.layoutParams.height)
assertEquals(20.dpToPx(), liveList.paddingStart)
}
@Test
fun `home live adapter uses figma item gap`() {
val context = ApplicationProvider.getApplicationContext<Context>()
val parent = RecyclerView(context)
parent.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
val viewHolder = HomeLiveAdapter().onCreateViewHolder(parent, 0)
val layoutParams = viewHolder.itemView.layoutParams as ViewGroup.MarginLayoutParams
assertEquals(14.dpToPx(), layoutParams.marginEnd)
}
@Test
fun `home live adapter appends all item after twenty lives`() {
val context = ApplicationProvider.getApplicationContext<Context>()
val parent = RecyclerView(context)
parent.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
val adapter = HomeLiveAdapter()
adapter.submitItems((1L..21L).map(::liveItem))
val viewHolder = adapter.onCreateViewHolder(parent, adapter.getItemViewType(20))
adapter.onBindViewHolder(viewHolder, 20)
val moreText = viewHolder.itemView as TextView
assertEquals(21, adapter.itemCount)
assertEquals(context.getString(R.string.screen_home_theme_all), moreText.text.toString())
assertEquals(58.dpToPx(), moreText.layoutParams.width)
assertEquals(102.dpToPx(), moreText.layoutParams.height)
assertEquals(context.getColor(R.color.soda_400), moreText.currentTextColor)
assertEquals(context.getColor(R.color.black), (moreText.background as ColorDrawable).color)
}
@Test
fun `home live adapter caps lives before all item`() {
val adapter = HomeLiveAdapter()
adapter.submitItems((1L..22L).map(::liveItem))
assertEquals(21, adapter.itemCount)
}
@Test @Test
fun `home layout uses section title components and custom genre title row`() { fun `home layout uses section title components and custom genre title row`() {
val root = inflateView(R.layout.fragment_v2_main_home) val root = inflateView(R.layout.fragment_v2_main_home)
@@ -193,4 +248,15 @@ class HomeMainFragmentLayoutTest {
val context = ApplicationProvider.getApplicationContext<Context>() val context = ApplicationProvider.getApplicationContext<Context>()
return (this * context.resources.displayMetrics.density).toInt() return (this * context.resources.displayMetrics.density).toInt()
} }
private fun liveItem(id: Long): HomeRecommendationLiveUiModel {
return HomeRecommendationLiveUiModel(
liveId = id,
creatorId = id,
imageUrl = null,
title = "title$id",
creatorNickname = "creator$id",
beginDateTime = null
)
}
} }