feat(creator): 본인 FAB 액션을 연결한다
This commit is contained in:
@@ -1,13 +1,18 @@
|
|||||||
package kr.co.vividnext.sodalive.v2.creator.channel
|
package kr.co.vividnext.sodalive.v2.creator.channel
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
|
import android.animation.AnimatorListenerAdapter
|
||||||
|
import android.animation.ValueAnimator
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.view.animation.Interpolator
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.View.MeasureSpec
|
import android.view.View.MeasureSpec
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
@@ -20,6 +25,9 @@ import com.google.android.material.tabs.TabLayoutMediator
|
|||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
|
import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
|
||||||
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
|
import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity
|
||||||
|
import kr.co.vividnext.sodalive.live.room.create.LiveRoomCreateActivity
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.creator_community.write.CreatorCommunityWriteActivity
|
||||||
|
import kr.co.vividnext.sodalive.audio_content.upload.AudioContentUploadActivity
|
||||||
import kr.co.vividnext.sodalive.base.BaseActivity
|
import kr.co.vividnext.sodalive.base.BaseActivity
|
||||||
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomActivity
|
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomActivity
|
||||||
import kr.co.vividnext.sodalive.common.Constants
|
import kr.co.vividnext.sodalive.common.Constants
|
||||||
@@ -54,6 +62,7 @@ class CreatorChannelActivity :
|
|||||||
private var tabLayoutMediator: TabLayoutMediator? = null
|
private var tabLayoutMediator: TabLayoutMediator? = null
|
||||||
private var pageChangeCallback: ViewPager2.OnPageChangeCallback? = null
|
private var pageChangeCallback: ViewPager2.OnPageChangeCallback? = null
|
||||||
private var isOwnerFabExpanded: Boolean = false
|
private var isOwnerFabExpanded: Boolean = false
|
||||||
|
private var isOwnerFabAnimating: Boolean = false
|
||||||
private val baseTitleBarHeight: Int by lazy { 60.dpToPx().toInt() }
|
private val baseTitleBarHeight: Int by lazy { 60.dpToPx().toInt() }
|
||||||
|
|
||||||
override val shouldApplySystemBarTopInset: Boolean = false
|
override val shouldApplySystemBarTopInset: Boolean = false
|
||||||
@@ -68,6 +77,7 @@ class CreatorChannelActivity :
|
|||||||
setupTabsAndPager()
|
setupTabsAndPager()
|
||||||
setStatusBarIconAppearance()
|
setStatusBarIconAppearance()
|
||||||
setTitleBarTopInset()
|
setTitleBarTopInset()
|
||||||
|
setupOwnerFabInsets()
|
||||||
setupScrollListener()
|
setupScrollListener()
|
||||||
setupClickListeners()
|
setupClickListeners()
|
||||||
}
|
}
|
||||||
@@ -80,6 +90,9 @@ class CreatorChannelActivity :
|
|||||||
binding.ownerFabButton.setOnClickListener { expandOwnerFab() }
|
binding.ownerFabButton.setOnClickListener { expandOwnerFab() }
|
||||||
binding.ownerFabDim.setOnClickListener { collapseOwnerFab() }
|
binding.ownerFabDim.setOnClickListener { collapseOwnerFab() }
|
||||||
binding.ownerFabCloseButton.setOnClickListener { collapseOwnerFab() }
|
binding.ownerFabCloseButton.setOnClickListener { collapseOwnerFab() }
|
||||||
|
binding.ownerFabCommunityButton.setOnClickListener { onOwnerFabCommunityClicked() }
|
||||||
|
binding.ownerFabAudioButton.setOnClickListener { onOwnerFabAudioClicked() }
|
||||||
|
binding.ownerFabLiveButton.setOnClickListener { onOwnerFabLiveClicked() }
|
||||||
binding.tvChatButton.setOnClickListener {
|
binding.tvChatButton.setOnClickListener {
|
||||||
currentHeader?.characterId?.let { characterId -> homeActionDelegate?.createChatRoom(characterId) }
|
currentHeader?.characterId?.let { characterId -> homeActionDelegate?.createChatRoom(characterId) }
|
||||||
}
|
}
|
||||||
@@ -271,7 +284,7 @@ class CreatorChannelActivity :
|
|||||||
val callback = object : ViewPager2.OnPageChangeCallback() {
|
val callback = object : ViewPager2.OnPageChangeCallback() {
|
||||||
override fun onPageSelected(position: Int) {
|
override fun onPageSelected(position: Int) {
|
||||||
if (position != CreatorChannelTab.Home.ordinal) {
|
if (position != CreatorChannelTab.Home.ordinal) {
|
||||||
isOwnerFabExpanded = false
|
collapseOwnerFab(animate = false)
|
||||||
}
|
}
|
||||||
updateOwnerFabVisibility()
|
updateOwnerFabVisibility()
|
||||||
updateViewPagerHeight()
|
updateViewPagerHeight()
|
||||||
@@ -319,14 +332,75 @@ class CreatorChannelActivity :
|
|||||||
updateViewPagerHeight()
|
updateViewPagerHeight()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun expandOwnerFab() {
|
private fun setupOwnerFabInsets() {
|
||||||
isOwnerFabExpanded = true
|
ViewCompat.setOnApplyWindowInsetsListener(binding.ownerFabButton) { _, insets ->
|
||||||
updateOwnerFabVisibility()
|
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 collapseOwnerFab() {
|
private fun expandOwnerFab() {
|
||||||
|
if (isOwnerFabAnimating) return
|
||||||
|
if (isOwnerFabExpanded) return
|
||||||
|
|
||||||
|
isOwnerFabExpanded = true
|
||||||
|
animateOwnerFab(expand = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun collapseOwnerFab(animate: Boolean = true) {
|
||||||
|
if (isOwnerFabAnimating) return
|
||||||
|
if (!isOwnerFabExpanded) {
|
||||||
|
updateOwnerFabVisibility()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
isOwnerFabExpanded = false
|
isOwnerFabExpanded = false
|
||||||
updateOwnerFabVisibility()
|
if (animate) {
|
||||||
|
animateOwnerFab(expand = false)
|
||||||
|
} else {
|
||||||
|
updateOwnerFabVisibility()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animateOwnerFab(expand: Boolean) {
|
||||||
|
isOwnerFabAnimating = true
|
||||||
|
binding.ownerFabDim.isVisible = true
|
||||||
|
binding.ownerFabExpandedContainer.isVisible = true
|
||||||
|
binding.ownerFabButton.isVisible = true
|
||||||
|
val start = if (expand) 0f else 1f
|
||||||
|
val end = if (expand) 1f else 0f
|
||||||
|
ValueAnimator.ofFloat(start, end).apply {
|
||||||
|
duration = OWNER_FAB_ANIMATION_DURATION_MS
|
||||||
|
interpolator = SpringInterpolator(
|
||||||
|
mass = OWNER_FAB_SPRING_MASS,
|
||||||
|
stiffness = OWNER_FAB_SPRING_STIFFNESS,
|
||||||
|
damping = OWNER_FAB_SPRING_DAMPING
|
||||||
|
)
|
||||||
|
addUpdateListener { animator ->
|
||||||
|
val value = animator.animatedValue as Float
|
||||||
|
binding.ownerFabDim.alpha = value
|
||||||
|
binding.ownerFabExpandedContainer.alpha = value
|
||||||
|
binding.ownerFabExpandedContainer.scaleX = value
|
||||||
|
binding.ownerFabExpandedContainer.scaleY = value
|
||||||
|
binding.ownerFabButton.alpha = 1f - value
|
||||||
|
}
|
||||||
|
addListener(
|
||||||
|
onEnd = {
|
||||||
|
isOwnerFabAnimating = false
|
||||||
|
updateOwnerFabVisibility()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
start()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateOwnerFabVisibility() {
|
private fun updateOwnerFabVisibility() {
|
||||||
@@ -335,6 +409,31 @@ class CreatorChannelActivity :
|
|||||||
binding.ownerFabDim.isVisible = shouldShowOwnerFab && isOwnerFabExpanded
|
binding.ownerFabDim.isVisible = shouldShowOwnerFab && isOwnerFabExpanded
|
||||||
binding.ownerFabExpandedContainer.isVisible = shouldShowOwnerFab && isOwnerFabExpanded
|
binding.ownerFabExpandedContainer.isVisible = shouldShowOwnerFab && isOwnerFabExpanded
|
||||||
binding.ownerFabButton.isVisible = shouldShowOwnerFab && !isOwnerFabExpanded
|
binding.ownerFabButton.isVisible = shouldShowOwnerFab && !isOwnerFabExpanded
|
||||||
|
if (!shouldShowOwnerFab) {
|
||||||
|
isOwnerFabExpanded = false
|
||||||
|
}
|
||||||
|
if (!binding.ownerFabExpandedContainer.isVisible) {
|
||||||
|
binding.ownerFabDim.alpha = 1f
|
||||||
|
binding.ownerFabExpandedContainer.alpha = 1f
|
||||||
|
binding.ownerFabExpandedContainer.scaleX = 1f
|
||||||
|
binding.ownerFabExpandedContainer.scaleY = 1f
|
||||||
|
binding.ownerFabButton.alpha = 1f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onOwnerFabCommunityClicked() {
|
||||||
|
collapseOwnerFab(animate = false)
|
||||||
|
startActivity(Intent(this, CreatorCommunityWriteActivity::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onOwnerFabAudioClicked() {
|
||||||
|
collapseOwnerFab(animate = false)
|
||||||
|
startActivity(Intent(this, AudioContentUploadActivity::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onOwnerFabLiveClicked() {
|
||||||
|
collapseOwnerFab(animate = false)
|
||||||
|
startActivity(Intent(this, LiveRoomCreateActivity::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreatorChannelDonationClicked() {
|
override fun onCreatorChannelDonationClicked() {
|
||||||
@@ -424,8 +523,33 @@ class CreatorChannelActivity :
|
|||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun ValueAnimator.addListener(onEnd: () -> Unit) {
|
||||||
|
addListener(object : AnimatorListenerAdapter() {
|
||||||
|
override fun onAnimationEnd(animation: Animator) = onEnd()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SpringInterpolator(
|
||||||
|
private val mass: Float,
|
||||||
|
private val stiffness: Float,
|
||||||
|
private val damping: Float
|
||||||
|
) : Interpolator {
|
||||||
|
override fun getInterpolation(input: Float): Float {
|
||||||
|
val angularFrequency = kotlin.math.sqrt(stiffness / mass)
|
||||||
|
val decay = kotlin.math.exp(-damping / (2f * mass) * input)
|
||||||
|
val oscillation = kotlin.math.cos(angularFrequency * input)
|
||||||
|
return (1f - decay * oscillation).coerceIn(0f, 1f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val EXTRA_CREATOR_ID: String = "extra_creator_id"
|
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
|
||||||
|
private const val OWNER_FAB_SPRING_STIFFNESS = 256f
|
||||||
|
private const val OWNER_FAB_SPRING_DAMPING = 24f
|
||||||
|
|
||||||
fun newIntent(context: Context, creatorId: Long): Intent {
|
fun newIntent(context: Context, creatorId: Long): Intent {
|
||||||
return Intent(context, CreatorChannelActivity::class.java).apply {
|
return Intent(context, CreatorChannelActivity::class.java).apply {
|
||||||
|
|||||||
@@ -1415,7 +1415,11 @@ class CreatorChannelActivitySourceTest {
|
|||||||
assertTrue(strings.contains("name=\"creator_channel_owner_fab_close\">닫기"))
|
assertTrue(strings.contains("name=\"creator_channel_owner_fab_close\">닫기"))
|
||||||
assertTrue(source.contains("private var isOwnerFabExpanded: Boolean = false"))
|
assertTrue(source.contains("private var isOwnerFabExpanded: Boolean = false"))
|
||||||
assertTrue(source.contains("updateOwnerFabVisibility()"))
|
assertTrue(source.contains("updateOwnerFabVisibility()"))
|
||||||
assertTrue(source.contains("currentHeader?.isOwner == true && binding.viewPager.currentItem == CreatorChannelTab.Home.ordinal"))
|
assertTrue(
|
||||||
|
source.contains(
|
||||||
|
"currentHeader?.isOwner == true && binding.viewPager.currentItem == CreatorChannelTab.Home.ordinal"
|
||||||
|
)
|
||||||
|
)
|
||||||
assertTrue(source.contains("binding.ownerFabDim.setOnClickListener { collapseOwnerFab() }"))
|
assertTrue(source.contains("binding.ownerFabDim.setOnClickListener { collapseOwnerFab() }"))
|
||||||
assertTrue(source.contains("binding.ownerFabCloseButton.setOnClickListener { collapseOwnerFab() }"))
|
assertTrue(source.contains("binding.ownerFabCloseButton.setOnClickListener { collapseOwnerFab() }"))
|
||||||
assertTrue(source.contains("binding.ownerFabButton.setOnClickListener { expandOwnerFab() }"))
|
assertTrue(source.contains("binding.ownerFabButton.setOnClickListener { expandOwnerFab() }"))
|
||||||
@@ -1424,6 +1428,55 @@ class CreatorChannelActivitySourceTest {
|
|||||||
assertTrue(source.contains("binding.ownerFabButton.isVisible = shouldShowOwnerFab && !isOwnerFabExpanded"))
|
assertTrue(source.contains("binding.ownerFabButton.isVisible = shouldShowOwnerFab && !isOwnerFabExpanded"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Phase 13 owner FAB source는 spring animation과 navigation inset을 적용한다`() {
|
||||||
|
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<ConstraintLayout.LayoutParams>"))
|
||||||
|
assertTrue(source.contains("binding.ownerFabExpandedContainer.updateLayoutParams<ConstraintLayout.LayoutParams>"))
|
||||||
|
assertTrue(
|
||||||
|
source.contains(
|
||||||
|
"binding.viewPager.updatePadding(bottom = OWNER_FAB_CONTENT_BOTTOM_PADDING_DP.dpToPx().toInt())"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertTrue(source.contains("ValueAnimator.ofFloat"))
|
||||||
|
assertTrue(source.contains("SpringInterpolator("))
|
||||||
|
assertTrue(source.contains("mass = OWNER_FAB_SPRING_MASS"))
|
||||||
|
assertTrue(source.contains("stiffness = OWNER_FAB_SPRING_STIFFNESS"))
|
||||||
|
assertTrue(source.contains("damping = OWNER_FAB_SPRING_DAMPING"))
|
||||||
|
assertTrue(source.contains("if (isOwnerFabAnimating) return"))
|
||||||
|
assertTrue(source.contains("duration = OWNER_FAB_ANIMATION_DURATION_MS"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Phase 13 owner FAB source는 3개 액션 진입점을 연결하고 클릭 후 닫는다`() {
|
||||||
|
val source = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
assertTrue(source.contains("CreatorCommunityWriteActivity"))
|
||||||
|
assertTrue(source.contains("AudioContentUploadActivity"))
|
||||||
|
assertTrue(source.contains("LiveRoomCreateActivity"))
|
||||||
|
assertTrue(
|
||||||
|
source.contains("binding.ownerFabCommunityButton.setOnClickListener { onOwnerFabCommunityClicked() }")
|
||||||
|
)
|
||||||
|
assertTrue(source.contains("binding.ownerFabAudioButton.setOnClickListener { onOwnerFabAudioClicked() }"))
|
||||||
|
assertTrue(source.contains("binding.ownerFabLiveButton.setOnClickListener { onOwnerFabLiveClicked() }"))
|
||||||
|
assertTrue(source.contains("private fun onOwnerFabCommunityClicked()"))
|
||||||
|
assertTrue(source.contains("private fun onOwnerFabAudioClicked()"))
|
||||||
|
assertTrue(source.contains("private fun onOwnerFabLiveClicked()"))
|
||||||
|
assertTrue(source.contains("startActivity(Intent(this, CreatorCommunityWriteActivity::class.java))"))
|
||||||
|
assertTrue(source.contains("startActivity(Intent(this, AudioContentUploadActivity::class.java))"))
|
||||||
|
assertTrue(source.contains("startActivity(Intent(this, LiveRoomCreateActivity::class.java))"))
|
||||||
|
assertTrue(source.contains("collapseOwnerFab(animate = false)"))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `남은 section item layouts는 legacy generic card id를 제거한다`() {
|
fun `남은 section item layouts는 legacy generic card id를 제거한다`() {
|
||||||
val layoutNames = listOf(
|
val layoutNames = listOf(
|
||||||
|
|||||||
Reference in New Issue
Block a user