feat(home): 홈 추천 상단 UI shell을 추가한다

This commit is contained in:
2026-06-02 14:51:01 +09:00
parent d07a2837d9
commit 68f8d869cc
4 changed files with 476 additions and 5 deletions

View File

@@ -1,8 +1,59 @@
package kr.co.vividnext.sodalive.v2.main
import android.os.Bundle
import android.view.View
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseFragment
import kr.co.vividnext.sodalive.databinding.FragmentV2MainHomeBinding
import kr.co.vividnext.sodalive.databinding.ViewSectionTitleBinding
class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
FragmentV2MainHomeBinding::inflate
) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.textTabBarHome.root.setMenus(
listOf(
getString(R.string.screen_home_tab_recommendation),
getString(R.string.screen_home_tab_ranking),
getString(R.string.screen_home_tab_following)
),
selectedIndex = 0
)
binding.textTabBarHome.root.setOnTabSelectedListener { }
setUpSectionTitles()
}
private fun setUpSectionTitles() {
binding.viewHomeRecentActivityTitle.setTitle(
R.string.home_recommendation_section_recently_active_creators
)
binding.viewHomeRecentDebutTitle.setTitle(
R.string.home_recommendation_section_recent_debut_creators,
showMore = true
)
binding.viewHomeFirstAudioTitle.setTitle(
R.string.home_recommendation_section_first_audio_contents,
showMore = true
)
binding.viewHomeAiCharacterTitle.setTitle(
R.string.home_recommendation_section_ai_characters,
showMore = true
)
binding.viewHomeCheerCreatorTitle.setTitle(
R.string.home_recommendation_section_cheer_creators
)
binding.viewHomePopularCommunityTitle.setTitle(
R.string.home_recommendation_section_popular_community_posts
)
}
private fun ViewSectionTitleBinding.setTitle(
titleResId: Int,
showMore: Boolean = false
) {
tvSectionTitle.setText(titleResId)
ivSectionTitleChevron.visibility = if (showMore) View.VISIBLE else View.GONE
}
}

View File

@@ -1,5 +1,257 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black" />
android:background="@color/black">
<include
android:id="@+id/view_home_title_bar"
layout="@layout/view_title_bar_home"
android:layout_width="0dp"
android:layout_height="60dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<include
android:id="@+id/text_tab_bar_home"
layout="@layout/view_text_tab_bar"
android:layout_width="0dp"
android:layout_height="52dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/view_home_title_bar" />
<androidx.core.widget.NestedScrollView
android:id="@+id/nsv_home_recommendation_content"
android:layout_width="0dp"
android:layout_height="0dp"
android:fillViewport="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_tab_bar_home">
<LinearLayout
android:id="@+id/ll_home_recommendation_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="@dimen/spacing_32">
<LinearLayout
android:id="@+id/ll_home_live_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="@dimen/spacing_24">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_home_lives"
android:layout_width="match_parent"
android:layout_height="120dp"
android:orientation="horizontal"
android:paddingHorizontal="@dimen/spacing_20"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/view_live_thumbnail_simple" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_home_banner_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="@dimen/spacing_24">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_home_banners"
android:layout_width="match_parent"
android:layout_height="160dp"
android:orientation="horizontal"
android:paddingHorizontal="@dimen/spacing_20"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_home_recent_activity_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="@dimen/spacing_24">
<include
android:id="@+id/view_home_recent_activity_title"
layout="@layout/view_section_title" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_home_recent_activity_creators"
android:layout_width="match_parent"
android:layout_height="160dp"
android:orientation="horizontal"
android:paddingHorizontal="@dimen/spacing_20"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_home_recent_debut_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="@dimen/spacing_24">
<include
android:id="@+id/view_home_recent_debut_title"
layout="@layout/view_section_title" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_home_recent_debut_creators"
android:layout_width="match_parent"
android:layout_height="160dp"
android:orientation="horizontal"
android:paddingHorizontal="@dimen/spacing_20"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_home_first_audio_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="@dimen/spacing_24">
<include
android:id="@+id/view_home_first_audio_title"
layout="@layout/view_section_title" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_home_first_audio_contents"
android:layout_width="match_parent"
android:layout_height="180dp"
android:orientation="horizontal"
android:paddingHorizontal="@dimen/spacing_20"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_home_ai_character_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="@dimen/spacing_24">
<include
android:id="@+id/view_home_ai_character_title"
layout="@layout/view_section_title" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_home_ai_characters"
android:layout_width="match_parent"
android:layout_height="180dp"
android:orientation="horizontal"
android:paddingHorizontal="@dimen/spacing_20"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_home_genre_creator_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="@dimen/spacing_24">
<LinearLayout
android:id="@+id/ll_home_genre_creator_title"
android:layout_width="match_parent"
android:layout_height="42dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="@dimen/spacing_20">
<TextView
android:id="@+id/tv_home_genre_creator_title_genre"
style="@style/Typography.Heading3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/color_3bb9f1"
tools:text="로맨스" />
<TextView
android:id="@+id/tv_home_genre_creator_title_suffix"
style="@style/Typography.Heading3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/home_recommendation_section_genre_creator_suffix"
android:textColor="@color/white" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_home_genre_creators"
android:layout_width="match_parent"
android:layout_height="160dp"
android:orientation="horizontal"
android:paddingHorizontal="@dimen/spacing_20"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_home_cheer_creator_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="@dimen/spacing_24">
<include
android:id="@+id/view_home_cheer_creator_title"
layout="@layout/view_section_title" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_home_cheer_creators"
android:layout_width="match_parent"
android:layout_height="160dp"
android:orientation="horizontal"
android:paddingHorizontal="@dimen/spacing_20"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_home_popular_community_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="@dimen/spacing_24">
<include
android:id="@+id/view_home_popular_community_title"
layout="@layout/view_section_title" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_home_popular_community_posts"
android:layout_width="match_parent"
android:layout_height="180dp"
android:orientation="horizontal"
android:paddingHorizontal="@dimen/spacing_20"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_home_business_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="@dimen/spacing_20"
android:paddingTop="@dimen/spacing_24"
tools:ignore="UseCompoundDrawables">
<TextView
android:id="@+id/tv_home_business_info"
style="@style/Typography.Body2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/home_recommendation_business_info"
android:textColor="@color/gray_500" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -21,8 +21,25 @@
android:layout_weight="1" />
<ImageView
android:id="@+id/iv_title_bar_menu"
android:id="@+id/iv_title_bar_cash"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null" />
android:contentDescription="@null"
android:src="@drawable/ic_bar_cash" />
<ImageView
android:id="@+id/iv_title_bar_search"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_16"
android:contentDescription="@null"
android:src="@drawable/ic_bar_search" />
<ImageView
android:id="@+id/iv_title_bar_bell"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_16"
android:contentDescription="@null"
android:src="@drawable/ic_bar_bell" />
</LinearLayout>

View File

@@ -0,0 +1,151 @@
package kr.co.vividnext.sodalive.v2.main.home
import android.app.Application
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.widget.NestedScrollView
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ApplicationProvider
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.v2.widget.TextTabBarView
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertSame
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28], application = Application::class)
class HomeMainFragmentLayoutTest {
@Test
fun `home layout keeps title and tabs outside recommendation scroll content`() {
val root = inflateView(R.layout.fragment_v2_main_home)
val titleBar = root.findViewById<View>(R.id.view_home_title_bar)
val tabBar = root.findViewById<TextTabBarView>(R.id.text_tab_bar_home)
val scrollView = root.findViewById<NestedScrollView>(R.id.nsv_home_recommendation_content)
assertNotNull(titleBar)
assertNotNull(tabBar)
assertNotNull(scrollView)
assertSame(root, titleBar.parent)
assertSame(root, tabBar.parent)
assertSame(root, scrollView.parent)
assertEquals(60.dpToPx(), titleBar.layoutParams.height)
assertEquals(52.dpToPx(), tabBar.layoutParams.height)
assertFalse(root.containsClassName("androidx.viewpager2.widget.ViewPager2"))
}
@Test
fun `home layout contains recommendation section containers with horizontal lists`() {
val root = inflateView(R.layout.fragment_v2_main_home)
val sectionIds = listOf(
R.id.ll_home_live_section,
R.id.ll_home_banner_section,
R.id.ll_home_recent_activity_section,
R.id.ll_home_recent_debut_section,
R.id.ll_home_first_audio_section,
R.id.ll_home_ai_character_section,
R.id.ll_home_genre_creator_section,
R.id.ll_home_cheer_creator_section,
R.id.ll_home_popular_community_section
)
val listIds = listOf(
R.id.rv_home_lives,
R.id.rv_home_banners,
R.id.rv_home_recent_activity_creators,
R.id.rv_home_recent_debut_creators,
R.id.rv_home_first_audio_contents,
R.id.rv_home_ai_characters,
R.id.rv_home_genre_creators,
R.id.rv_home_cheer_creators,
R.id.rv_home_popular_community_posts
)
sectionIds.forEach { sectionId -> assertNotNull(root.findViewById<LinearLayout>(sectionId)) }
listIds.forEach { listId ->
val recyclerView = root.findViewById<RecyclerView>(listId)
assertNotNull(recyclerView)
assertEquals(RecyclerView.HORIZONTAL, recyclerView.layoutManager?.canScrollHorizontally()?.toOrientation())
}
assertNotNull(root.findViewById<View>(R.id.ll_home_business_info))
}
@Test
fun `home layout uses section title components and custom genre title row`() {
val root = inflateView(R.layout.fragment_v2_main_home)
val sectionTitleIds = listOf(
R.id.view_home_recent_activity_title,
R.id.view_home_recent_debut_title,
R.id.view_home_first_audio_title,
R.id.view_home_ai_character_title,
R.id.view_home_cheer_creator_title,
R.id.view_home_popular_community_title
)
val moreTitleIds = listOf(
R.id.view_home_recent_debut_title,
R.id.view_home_first_audio_title,
R.id.view_home_ai_character_title
)
sectionTitleIds.forEach { titleId -> assertNotNull(root.findViewById<View>(titleId)) }
moreTitleIds.forEach { titleId ->
val titleView = root.findViewById<View>(titleId)
val chevron = titleView.findViewById<ImageView>(R.id.iv_section_title_chevron)
assertNotNull(chevron)
}
assertNotNull(root.findViewById<LinearLayout>(R.id.ll_home_genre_creator_title))
assertNotNull(root.findViewById<TextView>(R.id.tv_home_genre_creator_title_genre))
assertNotNull(root.findViewById<TextView>(R.id.tv_home_genre_creator_title_suffix))
}
@Test
fun `home title bar exposes right icons in cash search bell order`() {
val titleBar = inflateView(R.layout.view_title_bar_home)
val iconIds = listOf(
R.id.iv_title_bar_cash,
R.id.iv_title_bar_search,
R.id.iv_title_bar_bell
)
val drawableIds = listOf(
R.drawable.ic_bar_cash,
R.drawable.ic_bar_search,
R.drawable.ic_bar_bell
)
iconIds.zip(drawableIds).forEach { (iconId, drawableId) ->
val imageView = titleBar.findViewById<ImageView>(iconId)
assertNotNull(imageView)
assertEquals(drawableId, shadowOf(imageView.drawable).createdFromResId)
}
}
private fun inflateView(layoutResId: Int): View {
val context = ApplicationProvider.getApplicationContext<Context>()
return LayoutInflater.from(context).inflate(layoutResId, null, false)
}
private fun View.containsClassName(className: String): Boolean {
if (javaClass.name == className) return true
if (this !is android.view.ViewGroup) return false
return (0 until childCount).any { getChildAt(it).containsClassName(className) }
}
private fun Boolean.toOrientation(): Int = if (this) RecyclerView.HORIZONTAL else RecyclerView.VERTICAL
private fun Int.dpToPx(): Int {
val context = ApplicationProvider.getApplicationContext<Context>()
return (this * context.resources.displayMetrics.density).toInt()
}
}