feat(creator): 채널 홈 탭 전환을 연결한다

This commit is contained in:
2026-06-16 14:35:39 +09:00
parent 984aa13edf
commit f3c19ed8ba
5 changed files with 245 additions and 171 deletions

View File

@@ -3,11 +3,9 @@ package kr.co.vividnext.sodalive.v2.creator.channel
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.Typeface
import android.view.Gravity
import android.view.View
import android.view.View.MeasureSpec
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
@@ -15,7 +13,9 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayoutMediator
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
import kr.co.vividnext.sodalive.base.BaseActivity
@@ -31,25 +31,22 @@ import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelAudioContentResponse
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelScheduleResponse
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHeaderUiModel
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelHomeUiState
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelScrollState
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelTab
import kr.co.vividnext.sodalive.v2.creator.channel.model.CreatorChannelTitleBarState
import kr.co.vividnext.sodalive.v2.creator.channel.ui.CreatorChannelHomeSectionAdapter
import kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivity
import org.koin.androidx.viewmodel.ext.android.viewModel
class CreatorChannelActivity : BaseActivity<ActivityCreatorChannelBinding>(
ActivityCreatorChannelBinding::inflate
) {
class CreatorChannelActivity :
BaseActivity<ActivityCreatorChannelBinding>(ActivityCreatorChannelBinding::inflate),
CreatorChannelHomeFragment.Host {
private val viewModel: CreatorChannelHomeViewModel by viewModel()
private val sectionAdapter = CreatorChannelHomeSectionAdapter(::onScheduleClicked, ::onAudioContentClicked)
private var creatorId: Long = 0L
private var currentHeader: CreatorChannelHeaderUiModel? = null
private var selectedTab: CreatorChannelTab = CreatorChannelTab.Home
private var homeActionDelegate: CreatorChannelHomeFragment.HomeActionDelegate? = null
private var isFollowInProgress: Boolean = false
private var statusBarHeight: Int = 0
private var tabLayoutMediator: TabLayoutMediator? = null
private var pageChangeCallback: ViewPager2.OnPageChangeCallback? = null
private val baseTitleBarHeight: Int by lazy { 60.dpToPx().toInt() }
override val shouldApplySystemBarTopInset: Boolean = false
@@ -61,18 +58,11 @@ class CreatorChannelActivity : BaseActivity<ActivityCreatorChannelBinding>(
return
}
setupRecyclerView()
setupTabsAndPager()
setStatusBarIconAppearance()
setTitleBarTopInset()
setupScrollListener()
setupClickListeners()
observeViewModel()
viewModel.loadHome(creatorId)
}
private fun setupRecyclerView() {
binding.rvHomeSections.layoutManager = LinearLayoutManager(this)
binding.rvHomeSections.adapter = sectionAdapter
}
private fun setupClickListeners() {
@@ -81,47 +71,13 @@ class CreatorChannelActivity : BaseActivity<ActivityCreatorChannelBinding>(
binding.layoutFollowCapsule.setOnClickListener { onFollowActionClicked() }
binding.ivBell.setOnClickListener { onFollowActionClicked() }
binding.tvChatButton.setOnClickListener {
currentHeader?.characterId?.let { characterId -> viewModel.createChatRoom(characterId) }
currentHeader?.characterId?.let { characterId -> homeActionDelegate?.createChatRoom(characterId) }
}
binding.tvDmButton.setOnClickListener {
startActivity(DmChatRoomActivity.newIntentByCreatorId(this, creatorId))
}
}
private fun observeViewModel() {
viewModel.homeStateLiveData.observe(this) { state ->
when (state) {
is CreatorChannelHomeUiState.Content -> bindContent(state)
is CreatorChannelHomeUiState.Error -> Unit
CreatorChannelHomeUiState.Empty -> Unit
CreatorChannelHomeUiState.Loading -> Unit
}
}
viewModel.chatRoomIdLiveData.observe(this) { event ->
event.consume()?.let { chatRoomId ->
startActivity(ChatRoomActivity.newIntent(this, chatRoomId))
}
}
viewModel.toastLiveData.observe(this) { event ->
event.consume()?.let {
val message = it.message ?: it.resId?.let(::getString)
message?.let { text -> Toast.makeText(applicationContext, text, Toast.LENGTH_LONG).show() }
}
}
viewModel.isFollowInProgressLiveData.observe(this) { inProgress ->
isFollowInProgress = inProgress
currentHeader?.let(::bindTitleBar)
}
}
private fun bindContent(content: CreatorChannelHomeUiState.Content) {
currentHeader = content.header
bindHeader(content.header)
bindTitleBar(content.header)
bindTabs(content.tabs)
sectionAdapter.submitItems(content.sections)
}
private fun bindHeader(header: CreatorChannelHeaderUiModel) {
binding.tvNickname.text = header.nickname
binding.tvFollowerCount.text = getString(
@@ -166,54 +122,11 @@ class CreatorChannelActivity : BaseActivity<ActivityCreatorChannelBinding>(
}
}
private fun bindTabs(tabs: List<CreatorChannelTab>) {
binding.tabContainer.removeAllViews()
tabs.forEach { tab ->
binding.tabContainer.addView(createTabView(tab, isSelected = tab == selectedTab))
}
}
private fun createTabView(tab: CreatorChannelTab, isSelected: Boolean): LinearLayout {
val tabText = TextView(this).apply {
text = getString(tab.labelResId)
gravity = Gravity.CENTER
setTextColor(getColor(if (isSelected) R.color.white else R.color.gray_500))
setTypeface(null, Typeface.NORMAL)
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
0,
1f
)
}
tabText.textSize = 16f
val indicator = View(this).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
3.dpToPx().toInt()
)
}
indicator.setBackgroundColor(getColor(R.color.soda_400))
indicator.isVisible = isSelected
return LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.MATCH_PARENT
).apply {
width = 110.dpToPx().toInt()
}
addView(tabText)
addView(indicator)
setOnClickListener { onTabClicked(tab) }
}
}
private fun onFollowActionClicked() {
if (isFollowInProgress) return
val header = currentHeader ?: return
if (!header.isFollow) {
viewModel.follow(follow = true, notify = true)
homeActionDelegate?.follow(follow = true, notify = true)
return
}
@@ -222,20 +135,13 @@ class CreatorChannelActivity : BaseActivity<ActivityCreatorChannelBinding>(
private fun showFollowNotifyFragment() {
val notifyFragment = CreatorFollowNotifyFragment(
onClickNotifyAll = { viewModel.follow(follow = true, notify = true) },
onClickNotifyNone = { viewModel.follow(follow = true, notify = false) },
onClickUnFollow = { viewModel.follow(follow = false, notify = false) }
onClickNotifyAll = { homeActionDelegate?.follow(follow = true, notify = true) },
onClickNotifyNone = { homeActionDelegate?.follow(follow = true, notify = false) },
onClickUnFollow = { homeActionDelegate?.follow(follow = false, notify = false) }
)
notifyFragment.show(supportFragmentManager, CreatorFollowNotifyFragment::class.java.simpleName)
}
private fun onTabClicked(tab: CreatorChannelTab) {
if (tab != CreatorChannelTab.Home) return
selectedTab = CreatorChannelTab.Home
val content = viewModel.homeStateLiveData.value as? CreatorChannelHomeUiState.Content ?: return
bindTabs(content.tabs)
}
private fun setTitleBarTopInset() {
ViewCompat.setOnApplyWindowInsetsListener(binding.titleBarContainer) { view, insets ->
val topInset = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top
@@ -262,7 +168,7 @@ class CreatorChannelActivity : BaseActivity<ActivityCreatorChannelBinding>(
if (headerHeight <= 0) return
val tabTranslationY = (scrollY - (headerHeight - stickyTop)).coerceAtLeast(0)
binding.horizontalTabScrollView.translationY = tabTranslationY.toFloat()
binding.tabLayout.translationY = tabTranslationY.toFloat()
val tabBarTop = headerHeight - scrollY + tabTranslationY
val profileVisibleHeight = (headerHeight - scrollY).coerceIn(0, headerHeight)
@@ -293,6 +199,73 @@ class CreatorChannelActivity : BaseActivity<ActivityCreatorChannelBinding>(
Toast.makeText(applicationContext, getString(R.string.creator_channel_more_ready), Toast.LENGTH_SHORT).show()
}
private fun setupTabsAndPager() {
binding.viewPager.adapter = CreatorChannelPagerAdapter(this, creatorId)
binding.viewPager.isUserInputEnabled = true
binding.viewPager.offscreenPageLimit = CreatorChannelTab.entries.size - 1
tabLayoutMediator = TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position ->
tab.text = getString(CreatorChannelTab.entries[position].labelResId)
}.also {
it.attach()
}
val callback = object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
updateViewPagerHeight()
}
}
pageChangeCallback = callback
binding.viewPager.registerOnPageChangeCallback(callback)
}
override fun onCreatorChannelHeaderChanged(header: CreatorChannelHeaderUiModel) {
currentHeader = header
bindHeader(header)
bindTitleBar(header)
}
override fun onCreatorChannelFollowProgressChanged(inProgress: Boolean) {
isFollowInProgress = inProgress
currentHeader?.let(::bindTitleBar)
}
override fun onCreatorChannelChatRoomCreated(chatRoomId: Long) {
startActivity(ChatRoomActivity.newIntent(this, chatRoomId))
}
override fun onCreatorChannelScheduleClicked(schedule: CreatorChannelScheduleResponse) {
onScheduleClicked(schedule)
}
override fun onCreatorChannelAudioContentClicked(audioContent: CreatorChannelAudioContentResponse) {
onAudioContentClicked(audioContent)
}
override fun onCreatorChannelHomeActionDelegateReady(
delegate: CreatorChannelHomeFragment.HomeActionDelegate?
) {
homeActionDelegate = delegate
}
override fun onCreatorChannelHomeContentChanged() {
updateViewPagerHeight()
}
private fun updateViewPagerHeight() {
binding.viewPager.post {
val recyclerView = binding.viewPager.getChildAt(0) as? RecyclerView ?: return@post
val currentPage = recyclerView.layoutManager?.findViewByPosition(binding.viewPager.currentItem) ?: return@post
val widthSpec = MeasureSpec.makeMeasureSpec(binding.viewPager.width, MeasureSpec.EXACTLY)
val heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
currentPage.measure(widthSpec, heightSpec)
val measuredHeight = currentPage.measuredHeight
if (measuredHeight <= 0 || binding.viewPager.layoutParams.height == measuredHeight) return@post
binding.viewPager.updateLayoutParams {
height = measuredHeight
}
}
}
private fun onScheduleClicked(schedule: CreatorChannelScheduleResponse) {
when (schedule.type) {
CreatorActivityType.Audio,
@@ -330,6 +303,15 @@ class CreatorChannelActivity : BaseActivity<ActivityCreatorChannelBinding>(
detailFragment.show(supportFragmentManager, detailFragment.tag)
}
override fun onDestroy() {
tabLayoutMediator?.detach()
pageChangeCallback?.let { callback ->
binding.viewPager.unregisterOnPageChangeCallback(callback)
}
pageChangeCallback = null
super.onDestroy()
}
companion object {
const val EXTRA_CREATOR_ID: String = "extra_creator_id"

View File

@@ -45,6 +45,7 @@ class CreatorChannelHomeFragment : BaseFragment<FragmentCreatorChannelHomeBindin
}
override fun onDestroyView() {
binding.rvHomeSections.adapter = null
host.onCreatorChannelHomeActionDelegateReady(null)
super.onDestroyView()
}
@@ -55,6 +56,7 @@ class CreatorChannelHomeFragment : BaseFragment<FragmentCreatorChannelHomeBindin
is CreatorChannelHomeUiState.Content -> {
host.onCreatorChannelHeaderChanged(state.header)
sectionAdapter.submitItems(state.sections)
host.onCreatorChannelHomeContentChanged()
}
is CreatorChannelHomeUiState.Error -> Unit
CreatorChannelHomeUiState.Empty -> Unit
@@ -90,6 +92,7 @@ class CreatorChannelHomeFragment : BaseFragment<FragmentCreatorChannelHomeBindin
fun onCreatorChannelScheduleClicked(schedule: CreatorChannelScheduleResponse)
fun onCreatorChannelAudioContentClicked(audioContent: CreatorChannelAudioContentResponse)
fun onCreatorChannelHomeActionDelegateReady(delegate: HomeActionDelegate?)
fun onCreatorChannelHomeContentChanged()
}
interface HomeActionDelegate {

View File

@@ -114,34 +114,22 @@
</LinearLayout>
</FrameLayout>
<HorizontalScrollView
android:id="@+id/horizontal_tab_scroll_view"
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="52dp"
android:background="@color/black"
android:elevation="1dp"
android:fillViewport="false"
android:overScrollMode="never"
android:scrollbars="none">
app:tabIndicatorColor="@color/soda_400"
app:tabMode="scrollable"
app:tabSelectedTextColor="@color/white"
app:tabTextColor="@color/gray_500" />
<LinearLayout
android:id="@+id/tab_container"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="@dimen/spacing_20" />
</HorizontalScrollView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_home_sections"
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:paddingBottom="@dimen/spacing_32"
tools:itemCount="4"
tools:listitem="@layout/item_creator_channel_home_audio" />
android:layout_height="1dp"
android:nestedScrollingEnabled="false" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@@ -2,7 +2,7 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_height="160dp"
android:background="@color/black">
<TextView
@@ -11,6 +11,5 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textColor="@color/gray_500"
android:visibility="gone"
tools:text="Live" />
</FrameLayout>

View File

@@ -35,17 +35,12 @@ class CreatorChannelActivitySourceTest {
assertTrue(source.contains("fun newIntent(context: Context, creatorId: Long): Intent"))
assertTrue(source.contains("Intent(context, CreatorChannelActivity::class.java)"))
assertTrue(source.contains("putExtra(EXTRA_CREATOR_ID, creatorId)"))
assertTrue(source.contains("private val viewModel: CreatorChannelHomeViewModel by viewModel()"))
assertTrue(source.contains("if (creatorId <= 0L)"))
assertTrue(source.contains("finish()"))
assertTrue(source.contains("viewModel.loadHome(creatorId)"))
assertTrue(source.contains("viewModel.homeStateLiveData.observe(this)"))
assertTrue(source.contains("viewModel.chatRoomIdLiveData.observe(this)"))
assertFalse(source.contains("is CreatorChannelHomeUiState.Error -> showToast"))
assertTrue(source.contains("event.consume()?.let"))
assertTrue(source.contains("ChatRoomActivity.newIntent(this, chatRoomId)"))
assertTrue(source.contains("DmChatRoomActivity.newIntentByCreatorId(this, creatorId)"))
assertTrue(source.contains("viewModel.createChatRoom(characterId)"))
assertTrue(source.contains("homeActionDelegate?.createChatRoom(characterId)"))
assertTrue(source.contains("updateActionButtonLayout"))
assertTrue(source.contains("marginStart = if (isChatVisible && isDmVisible)"))
}
@@ -61,32 +56,90 @@ class CreatorChannelActivitySourceTest {
assertTrue(source.contains("binding.ivBell.setOnClickListener"))
assertTrue(source.contains("private fun onFollowActionClicked"))
assertTrue(source.contains("if (!header.isFollow)"))
assertTrue(source.contains("viewModel.follow(follow = true, notify = true)"))
assertTrue(source.contains("homeActionDelegate?.follow(follow = true, notify = true)"))
assertTrue(source.contains("showFollowNotifyFragment"))
assertTrue(source.contains("onClickNotifyAll = { viewModel.follow(follow = true, notify = true) }"))
assertTrue(source.contains("onClickNotifyNone = { viewModel.follow(follow = true, notify = false) }"))
assertTrue(source.contains("onClickUnFollow = { viewModel.follow(follow = false, notify = false) }"))
assertTrue(source.contains("viewModel.isFollowInProgressLiveData.observe(this)"))
assertTrue(source.contains("onClickNotifyAll = { homeActionDelegate?.follow(follow = true, notify = true) }"))
assertTrue(source.contains("onClickNotifyNone = { homeActionDelegate?.follow(follow = true, notify = false) }"))
assertTrue(source.contains("onClickUnFollow = { homeActionDelegate?.follow(follow = false, notify = false) }"))
assertTrue(source.contains("onCreatorChannelFollowProgressChanged"))
assertTrue(source.contains("binding.layoutFollowCapsule.isEnabled = titleBarState.isActionEnabled"))
assertTrue(source.contains("binding.ivBell.isEnabled = titleBarState.isActionEnabled"))
}
@Test
fun `layout source는 HorizontalScrollView 기반 7개 탭 컨테이너와 RecyclerView를 가진다`() {
fun `Phase 10 컨테이너 layout은 TabLayout과 ViewPager2를 가진다`() {
val layout = projectFile("app/src/main/res/layout/activity_creator_channel.xml").readText()
assertTrue(layout.contains("<com.google.android.material.tabs.TabLayout"))
assertTrue(layout.contains("@+id/tab_layout"))
assertTrue(layout.contains("<androidx.viewpager2.widget.ViewPager2"))
assertTrue(layout.contains("@+id/view_pager"))
assertFalse(layout.contains("<HorizontalScrollView"))
assertFalse(layout.contains("@+id/horizontal_tab_scroll_view"))
assertFalse(layout.contains("@+id/tab_container"))
assertFalse(layout.contains("@+id/rv_home_sections"))
}
@Test
fun `Phase 10 Activity는 pager adapter를 사용하고 홈 탭 구현을 Fragment로 위임한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText()
assertTrue(layout.contains("<HorizontalScrollView"))
assertTrue(layout.contains("@+id/horizontal_tab_scroll_view"))
assertTrue(layout.contains("android:elevation=\"1dp\""))
assertTrue(layout.contains("@+id/tab_container"))
assertTrue(source.contains("CreatorChannelPagerAdapter"))
assertTrue(source.contains("binding.viewPager.adapter"))
assertTrue(source.contains("CreatorChannelHomeFragment"))
assertFalse(source.contains("private val viewModel: CreatorChannelHomeViewModel by viewModel()"))
assertFalse(source.contains("CreatorChannelHomeSectionAdapter"))
assertFalse(source.contains("sectionAdapter"))
assertFalse(source.contains("viewModel.loadHome(creatorId)"))
assertFalse(source.contains("bindTabs(content.tabs)"))
assertFalse(source.contains("private fun createTabView"))
assertFalse(source.contains("private fun onTabClicked"))
}
@Test
fun `home Fragment source는 creatorId argument와 home ViewModel RecyclerView를 소유한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeFragment.kt"
).readText()
val layout = projectFile("app/src/main/res/layout/fragment_creator_channel_home.xml").readText()
assertTrue(source.contains("BaseFragment<FragmentCreatorChannelHomeBinding>"))
assertTrue(source.contains("private const val ARG_CREATOR_ID"))
assertTrue(source.contains("fun newInstance(creatorId: Long): CreatorChannelHomeFragment"))
assertTrue(source.contains("arguments = Bundle().apply"))
assertTrue(source.contains("putLong(ARG_CREATOR_ID, creatorId)"))
assertTrue(source.contains("private val viewModel: CreatorChannelHomeViewModel by viewModel()"))
assertTrue(source.contains("CreatorChannelHomeSectionAdapter(::onScheduleClicked, ::onAudioContentClicked)"))
assertTrue(source.contains("binding.rvHomeSections.layoutManager = LinearLayoutManager(requireContext())"))
assertTrue(source.contains("binding.rvHomeSections.adapter = sectionAdapter"))
assertTrue(source.contains("viewModel.homeStateLiveData.observe(viewLifecycleOwner)"))
assertTrue(source.contains("viewModel.chatRoomIdLiveData.observe(viewLifecycleOwner)"))
assertTrue(source.contains("viewModel.toastLiveData.observe(viewLifecycleOwner)"))
assertTrue(source.contains("viewModel.isFollowInProgressLiveData.observe(viewLifecycleOwner)"))
assertTrue(source.contains("host.onCreatorChannelHomeContentChanged()"))
assertTrue(source.contains("binding.rvHomeSections.adapter = null"))
assertTrue(source.contains("if (creatorId > 0L)"))
assertTrue(source.contains("viewModel.loadHome(creatorId)"))
assertTrue(layout.contains("@+id/rv_home_sections"))
}
@Test
fun `layout source는 Activity 컨테이너와 홈 Fragment RecyclerView를 분리한다`() {
val layout = projectFile("app/src/main/res/layout/activity_creator_channel.xml").readText()
val fragmentLayout = projectFile("app/src/main/res/layout/fragment_creator_channel_home.xml").readText()
assertTrue(layout.contains("@+id/tab_layout"))
assertTrue(layout.contains("@+id/view_pager"))
assertTrue(layout.contains("android:layout_height=\"1dp\""))
assertFalse(layout.contains("@+id/horizontal_tab_scroll_view"))
assertFalse(layout.contains("@+id/tab_container"))
assertFalse(layout.contains("@+id/rv_home_sections"))
assertTrue(fragmentLayout.contains("@+id/rv_home_sections"))
assertTrue(fragmentLayout.contains("tools:listitem=\"@layout/item_creator_channel_home_audio\""))
assertTrue(layout.contains("android:drawableStart=\"@drawable/ic_new_talk\""))
assertTrue(layout.contains("android:drawableStart=\"@drawable/ic_new_dm\""))
assertFalse(layout.contains("TextTabBarView"))
assertTrue(source.contains("getString(tab.labelResId)"))
}
@Test
@@ -121,7 +174,7 @@ class CreatorChannelActivitySourceTest {
}
@Test
fun `scroll source는 tab sticky와 title bar black 전환을 연결한다`() {
fun `scroll source는 tab sticky와 title bar black 전환을 유지한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText()
@@ -129,7 +182,8 @@ class CreatorChannelActivitySourceTest {
assertTrue(source.contains("setupScrollListener"))
assertTrue(source.contains("binding.nestedScrollView.setOnScrollChangeListener"))
assertTrue(source.contains("CreatorChannelScrollState.calculateStickyTop"))
assertTrue(source.contains("binding.horizontalTabScrollView.translationY"))
assertTrue(source.contains("binding.tabLayout.translationY = tabTranslationY.toFloat()"))
assertTrue(source.contains("val tabBarTop = headerHeight - scrollY + tabTranslationY"))
assertTrue(source.contains("CreatorChannelScrollState.shouldUseBlackTitleBar"))
assertTrue(source.contains("binding.titleBarContainer.setBackgroundColor"))
assertTrue(source.contains("Color.BLACK"))
@@ -147,29 +201,67 @@ class CreatorChannelActivitySourceTest {
}
@Test
fun `tab source는 Figma 기준 selected indicator와 16sp 고정 폭 탭을 사용한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
fun `pager adapter source는 7개 탭 순서와 홈 placeholder Fragment를 연결한다`() {
val adapter = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapter.kt"
).readText()
val placeholder = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPlaceholderFragment.kt"
).readText()
val placeholderLayout = projectFile(
"app/src/main/res/layout/fragment_creator_channel_placeholder.xml"
).readText()
assertTrue(source.contains("createTabView(tab, isSelected = tab == selectedTab)"))
assertTrue(source.contains("tabText.textSize = 16f"))
assertTrue(source.contains("width = 110.dpToPx().toInt()"))
assertTrue(source.contains("indicator.setBackgroundColor(getColor(R.color.soda_400))"))
assertTrue(source.contains("indicator.isVisible = isSelected"))
assertTrue(adapter.contains("class CreatorChannelPagerAdapter"))
assertTrue(adapter.contains("FragmentStateAdapter"))
assertTrue(adapter.contains("private val tabs: List<CreatorChannelTab> = CreatorChannelTab.entries"))
assertTrue(adapter.contains("override fun getItemCount(): Int = tabs.size"))
assertTrue(adapter.contains("CreatorChannelTab.Home -> CreatorChannelHomeFragment.newInstance(creatorId)"))
assertTrue(adapter.contains("else -> CreatorChannelPlaceholderFragment.newInstance(tab)"))
assertTrue(placeholder.contains("private const val ARG_TAB_NAME"))
assertTrue(placeholder.contains("fun newInstance(tab: CreatorChannelTab): CreatorChannelPlaceholderFragment"))
assertTrue(placeholderLayout.contains("@+id/tv_placeholder"))
assertTrue(placeholderLayout.contains("android:layout_height=\"160dp\""))
assertFalse(placeholderLayout.contains("android:visibility=\"gone\""))
}
@Test
fun `tab source는 홈 기본 선택과 홈 외 탭 no op 정책을 명시한다`() {
fun `tab source는 TabLayoutMediator와 ViewPager2를 연결한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText()
assertTrue(source.contains("private var selectedTab: CreatorChannelTab = CreatorChannelTab.Home"))
assertTrue(source.contains("createTabView(tab, isSelected = tab == selectedTab)"))
assertTrue(source.contains("setOnClickListener { onTabClicked(tab) }"))
assertTrue(source.contains("private fun onTabClicked(tab: CreatorChannelTab)"))
assertTrue(source.contains("if (tab != CreatorChannelTab.Home) return"))
assertTrue(source.contains("TabLayoutMediator"))
assertTrue(source.contains("private var tabLayoutMediator: TabLayoutMediator? = null"))
assertTrue(source.contains("private var pageChangeCallback: ViewPager2.OnPageChangeCallback? = null"))
assertTrue(source.contains("binding.viewPager.adapter = CreatorChannelPagerAdapter(this, creatorId)"))
assertTrue(source.contains("tabLayoutMediator = TabLayoutMediator(binding.tabLayout, binding.viewPager)"))
assertTrue(source.contains("tab.text = getString(CreatorChannelTab.entries[position].labelResId)"))
assertTrue(source.contains(".attach()"))
assertTrue(source.contains("binding.viewPager.isUserInputEnabled = true"))
assertTrue(source.contains("binding.viewPager.offscreenPageLimit = CreatorChannelTab.entries.size - 1"))
assertTrue(source.contains("binding.viewPager.registerOnPageChangeCallback(callback)"))
assertTrue(source.contains("override fun onDestroy()"))
assertTrue(source.contains("tabLayoutMediator?.detach()"))
assertTrue(source.contains("binding.viewPager.unregisterOnPageChangeCallback(callback)"))
assertTrue(source.contains("private fun updateViewPagerHeight()"))
assertTrue(source.contains("findViewByPosition(binding.viewPager.currentItem)"))
assertTrue(source.contains("currentPage.measure(widthSpec, heightSpec)"))
assertTrue(source.contains("binding.viewPager.updateLayoutParams"))
assertFalse(source.contains("binding.tabLayout.addTab"))
assertFalse(source.contains("private fun createTabView"))
}
@Test
fun `tab source는 기존 custom tab no op 정책을 제거한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText()
assertFalse(source.contains("private var selectedTab: CreatorChannelTab = CreatorChannelTab.Home"))
assertFalse(source.contains("setOnClickListener { onTabClicked(tab) }"))
assertFalse(source.contains("private fun onTabClicked(tab: CreatorChannelTab)"))
assertFalse(source.contains("if (tab != CreatorChannelTab.Home) return"))
assertFalse(source.contains("CreatorChannelTab.Live ->"))
assertFalse(source.contains("CreatorChannelTab.Audio ->"))
assertFalse(source.contains("CreatorChannelTab.Series ->"))
@@ -589,8 +681,13 @@ class CreatorChannelActivitySourceTest {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText()
val fragment = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeFragment.kt"
).readText()
assertTrue(source.contains("CreatorChannelHomeSectionAdapter(::onScheduleClicked"))
assertTrue(fragment.contains("CreatorChannelHomeSectionAdapter(::onScheduleClicked"))
assertTrue(fragment.contains("private fun onScheduleClicked(schedule: CreatorChannelScheduleResponse)"))
assertTrue(fragment.contains("host.onCreatorChannelScheduleClicked(schedule)"))
assertTrue(source.contains("private fun onScheduleClicked(schedule: CreatorChannelScheduleResponse)"))
assertTrue(source.contains("CreatorActivityType.Audio"))
assertTrue(source.contains("CreatorActivityType.LiveReplay"))
@@ -663,8 +760,13 @@ class CreatorChannelActivitySourceTest {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
).readText()
val fragment = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeFragment.kt"
).readText()
assertTrue(source.contains("CreatorChannelHomeSectionAdapter(::onScheduleClicked, ::onAudioContentClicked)"))
assertTrue(fragment.contains("CreatorChannelHomeSectionAdapter(::onScheduleClicked, ::onAudioContentClicked)"))
assertTrue(fragment.contains("private fun onAudioContentClicked(audioContent: CreatorChannelAudioContentResponse)"))
assertTrue(fragment.contains("host.onCreatorChannelAudioContentClicked(audioContent)"))
assertTrue(source.contains("private fun onAudioContentClicked(audioContent: CreatorChannelAudioContentResponse)"))
assertTrue(source.contains("AudioContentDetailActivity::class.java"))
assertTrue(source.contains("putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, audioContent.audioContentId)"))