fix(creator): 라이브 탭 하단 CTA와 sticky 전환을 보정한다

This commit is contained in:
2026-06-18 15:21:18 +09:00
parent a1e8f8edb3
commit f4af9868e6
3 changed files with 183 additions and 43 deletions

View File

@@ -14,7 +14,6 @@ import android.view.View.MeasureSpec
import android.widget.LinearLayout
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
@@ -80,6 +79,7 @@ class CreatorChannelActivity :
private var statusBarHeight: Int = 0
private var tabLayoutMediator: TabLayoutMediator? = null
private var pageChangeCallback: ViewPager2.OnPageChangeCallback? = null
private var lastSelectedCreatorChannelTabPosition: Int? = null
private var isOwnerFabExpanded: Boolean = false
private var isOwnerFabAnimating: Boolean = false
private lateinit var loadingDialog: LoadingDialog
@@ -129,6 +129,7 @@ class CreatorChannelActivity :
setStatusBarIconAppearance()
setTitleBarTopInset()
setupOwnerFabInsets()
setupLiveOwnerCtaInsets()
setupScrollListener()
setupClickListeners()
setupLiveEntryObservers()
@@ -159,6 +160,7 @@ class CreatorChannelActivity :
binding.ownerFabCommunityButton.setOnClickListener { onOwnerFabCommunityClicked() }
binding.ownerFabAudioButton.setOnClickListener { onOwnerFabAudioClicked() }
binding.ownerFabLiveButton.setOnClickListener { onOwnerFabLiveClicked() }
binding.btnCreatorChannelLiveOwnerCta.setOnClickListener { onLiveOwnerCtaClicked() }
binding.tvChatButton.setOnClickListener {
currentHeader?.characterId?.let { characterId -> homeActionDelegate?.createChatRoom(characterId) }
}
@@ -308,6 +310,22 @@ class CreatorChannelActivity :
binding.tvTitleNickname.isVisible = shouldUseBlackTitleBar
}
private fun adjustCreatorChannelStickyAnchorOnTabSelected(position: Int) {
val previousPosition = lastSelectedCreatorChannelTabPosition
lastSelectedCreatorChannelTabPosition = position
if (previousPosition == null || previousPosition == position) return
val stickyScrollY = calculateCreatorChannelStickyScrollY()
if (binding.nestedScrollView.scrollY < stickyScrollY) {
binding.nestedScrollView.scrollTo(0, stickyScrollY)
}
}
private fun calculateCreatorChannelStickyScrollY(): Int {
val stickyTop = CreatorChannelScrollState.calculateStickyTop(statusBarHeight, baseTitleBarHeight)
return (binding.headerContainer.height - stickyTop).coerceAtLeast(0)
}
private fun setStatusBarIconAppearance() {
WindowCompat.getInsetsController(window, binding.root).isAppearanceLightStatusBars = false
}
@@ -364,12 +382,16 @@ class CreatorChannelActivity :
}.also {
it.attach()
}
lastSelectedCreatorChannelTabPosition = binding.viewPager.currentItem
val callback = object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
adjustCreatorChannelStickyAnchorOnTabSelected(position)
if (position != CreatorChannelTab.Home.ordinal) {
collapseOwnerFab(animate = false)
}
updateOwnerFabVisibility()
updateLiveOwnerCtaVisibility()
updateCreatorChannelLiveViewportHeight()
updateViewPagerHeight()
if (position == CreatorChannelTab.Live.ordinal) {
binding.viewPager.post {
@@ -387,7 +409,7 @@ class CreatorChannelActivity :
bindHeader(header)
bindTitleBar(header)
updateOwnerFabVisibility()
findLiveFragment()?.onCreatorChannelOwnerChanged(header.isOwner)
updateLiveOwnerCtaVisibility()
}
override fun onCreatorChannelFollowProgressChanged(inProgress: Boolean) {
@@ -422,28 +444,28 @@ class CreatorChannelActivity :
}
override fun onCreatorChannelLiveContentChanged() {
updateCreatorChannelLiveViewportHeight()
updateViewPagerHeight()
postCheckCreatorChannelLiveNeedsMore()
}
override fun isCreatorChannelOwner(): Boolean {
return currentHeader?.isOwner == true
private fun setupOwnerFabInsets() {
binding.viewPager.updatePadding(bottom = OWNER_FAB_CONTENT_BOTTOM_PADDING_DP.dpToPx().toInt())
}
private fun setupOwnerFabInsets() {
ViewCompat.setOnApplyWindowInsetsListener(binding.ownerFabButton) { _, insets ->
val navigationBottomInset = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
val bottomMargin = OWNER_FAB_BASE_MARGIN_DP.dpToPx().toInt() + navigationBottomInset
binding.ownerFabButton.updateLayoutParams<ConstraintLayout.LayoutParams> {
this.bottomMargin = bottomMargin
}
binding.ownerFabExpandedContainer.updateLayoutParams<ConstraintLayout.LayoutParams> {
this.bottomMargin = bottomMargin
}
binding.viewPager.updatePadding(bottom = OWNER_FAB_CONTENT_BOTTOM_PADDING_DP.dpToPx().toInt())
insets
}
ViewCompat.requestApplyInsets(binding.ownerFabButton)
private fun setupLiveOwnerCtaInsets() {
updateLiveOwnerCtaVisibility()
}
private fun updateLiveOwnerCtaVisibility() {
val shouldShowLiveOwnerCta = shouldShowLiveOwnerCta()
binding.layoutCreatorChannelLiveOwnerCta.isVisible = shouldShowLiveOwnerCta
binding.btnCreatorChannelLiveOwnerCta.isEnabled = true
findLiveFragment()?.onCreatorChannelLiveOwnerCtaVisibilityChanged(shouldShowLiveOwnerCta)
}
private fun shouldShowLiveOwnerCta(): Boolean {
return currentHeader?.isOwner == true && binding.viewPager.currentItem == CreatorChannelTab.Live.ordinal
}
private fun expandOwnerFab() {
@@ -534,6 +556,11 @@ class CreatorChannelActivity :
liveRoomCreateLauncher.launch(Intent(this, LiveRoomCreateActivity::class.java))
}
private fun onLiveOwnerCtaClicked() {
binding.btnCreatorChannelLiveOwnerCta.isEnabled = false
onOwnerFabLiveClicked()
}
override fun onCreatorChannelDonationClicked() {
val header = currentHeader ?: return
if (header.isOwner) return
@@ -561,10 +588,6 @@ class CreatorChannelActivity :
startAudioContentDetail(audioContentId)
}
override fun onCreatorChannelLiveStartClicked() {
onOwnerFabLiveClicked()
}
private fun findLiveFragment(): CreatorChannelLiveFragment? {
val fragmentTag = "f${CreatorChannelTab.Live.ordinal}"
return supportFragmentManager.findFragmentByTag(fragmentTag) as? CreatorChannelLiveFragment
@@ -636,10 +659,10 @@ class CreatorChannelActivity :
}
private fun updateViewPagerHeight() {
updateCreatorChannelLiveViewportHeight()
binding.viewPager.post {
val recyclerView = binding.viewPager.getChildAt(0) as? RecyclerView ?: return@post
val currentPage = recyclerView.layoutManager?.findViewByPosition(binding.viewPager.currentItem) ?: return@post
currentPage.minimumHeight = calculateCreatorChannelTabViewportHeight()
val widthSpec = MeasureSpec.makeMeasureSpec(binding.viewPager.width, MeasureSpec.EXACTLY)
val heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
currentPage.measure(widthSpec, heightSpec)
@@ -652,6 +675,23 @@ class CreatorChannelActivity :
}
}
private fun updateCreatorChannelLiveViewportHeight() {
if (binding.viewPager.currentItem != CreatorChannelTab.Live.ordinal) return
findLiveFragment()?.onCreatorChannelLiveViewportHeightChanged(
calculateCreatorChannelLiveEmptyMinHeight()
)
}
private fun calculateCreatorChannelLiveEmptyMinHeight(): Int {
val stickyScrollY = calculateCreatorChannelStickyScrollY()
val visibleTabViewportHeight = binding.nestedScrollView.height - binding.tabLayout.height
val scrollRangeRequiredHeight = binding.nestedScrollView.height +
stickyScrollY -
binding.headerContainer.height
return maxOf(visibleTabViewportHeight, scrollRangeRequiredHeight, 0)
}
private fun postCheckCreatorChannelLiveNeedsMore() {
binding.nestedScrollView.post {
checkCreatorChannelLiveNeedsMore()
@@ -672,10 +712,6 @@ class CreatorChannelActivity :
}
}
private fun calculateCreatorChannelTabViewportHeight(): Int {
return (binding.nestedScrollView.height - binding.tabLayout.height).coerceAtLeast(0)
}
private fun onScheduleClicked(schedule: CreatorChannelScheduleResponse) {
when (schedule.type) {
CreatorActivityType.Audio,
@@ -711,6 +747,11 @@ class CreatorChannelActivity :
)
}
override fun onResume() {
super.onResume()
binding.btnCreatorChannelLiveOwnerCta.isEnabled = true
}
override fun onDestroy() {
tabLayoutMediator?.detach()
pageChangeCallback?.let { callback ->
@@ -741,7 +782,6 @@ class CreatorChannelActivity :
companion object {
const val EXTRA_CREATOR_ID: String = "extra_creator_id"
private const val OWNER_FAB_BASE_MARGIN_DP = 14
private const val OWNER_FAB_CONTENT_BOTTOM_PADDING_DP = 96
private const val OWNER_FAB_ANIMATION_DURATION_MS = 260L
private const val OWNER_FAB_SPRING_MASS = 1f

View File

@@ -238,6 +238,45 @@
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<FrameLayout
android:id="@+id/layout_creator_channel_live_owner_cta"
android:layout_width="0dp"
android:layout_height="100dp"
android:background="@color/black"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<LinearLayout
android:id="@+id/btn_creator_channel_live_owner_cta"
android:layout_width="match_parent"
android:layout_height="52dp"
android:layout_marginHorizontal="@dimen/spacing_14"
android:layout_marginTop="@dimen/spacing_14"
android:background="@drawable/bg_creator_channel_owner_fab"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:orientation="horizontal">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@null"
android:src="@drawable/ic_new_create_live" />
<TextView
style="@style/Typography.Heading3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_8"
android:includeFontPadding="false"
android:text="@string/creator_channel_live_start_button"
android:textColor="@color/white" />
</LinearLayout>
</FrameLayout>
<View
android:id="@+id/owner_fab_dim"
android:layout_width="0dp"