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>