diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt index 0799014b..b8dc2482 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt @@ -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 { - this.bottomMargin = bottomMargin - } - binding.ownerFabExpandedContainer.updateLayoutParams { - 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 diff --git a/app/src/main/res/layout/activity_creator_channel.xml b/app/src/main/res/layout/activity_creator_channel.xml index 9917d62a..9896596b 100644 --- a/app/src/main/res/layout/activity_creator_channel.xml +++ b/app/src/main/res/layout/activity_creator_channel.xml @@ -238,6 +238,45 @@ + + + + + + + + + + ")) + assertFalse(source.contains("binding.ownerFabExpandedContainer.updateLayoutParams")) + assertFalse(source.contains("binding.layoutCreatorChannelLiveOwnerCta.updateLayoutParams")) + assertFalse(source.contains("WindowInsetsCompat.Type.navigationBars()).bottom")) + assertTrue( + source.contains( + "binding.viewPager.updatePadding(bottom = OWNER_FAB_CONTENT_BOTTOM_PADDING_DP.dpToPx().toInt())" + ) + ) + assertFalse(source.contains("binding.nestedScrollView.updatePadding(bottom = liveOwnerCtaBottomPadding)")) } @Test @@ -1644,17 +1706,16 @@ class CreatorChannelActivitySourceTest { } @Test - fun `Phase 13 owner FAB source는 spring animation과 navigation inset을 적용한다`() { + fun `Phase 13 owner FAB source는 spring animation과 content padding을 적용한다`() { val source = projectFile( "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt" ).readText() assertTrue(source.contains("private var isOwnerFabAnimating: Boolean = false")) assertTrue(source.contains("setupOwnerFabInsets()")) - assertTrue(source.contains("WindowInsetsCompat.Type.navigationBars()")) - assertTrue(source.contains("OWNER_FAB_BASE_MARGIN_DP.dpToPx().toInt() + navigationBottomInset")) - assertTrue(source.contains("binding.ownerFabButton.updateLayoutParams")) - assertTrue(source.contains("binding.ownerFabExpandedContainer.updateLayoutParams")) + assertFalse(source.contains("OWNER_FAB_BASE_MARGIN_DP.dpToPx().toInt() + navigationBottomInset")) + assertFalse(source.contains("binding.ownerFabButton.updateLayoutParams")) + assertFalse(source.contains("binding.ownerFabExpandedContainer.updateLayoutParams")) assertTrue( source.contains( "binding.viewPager.updatePadding(bottom = OWNER_FAB_CONTENT_BOTTOM_PADDING_DP.dpToPx().toInt())"