feat(creator): 채널 홈 탭 전환을 연결한다
This commit is contained in:
@@ -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"
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)"))
|
||||
|
||||
Reference in New Issue
Block a user