fix(AppBar): Re-introduce snap but only do it to the top bar

This commit is contained in:
Ahmad Ansori Palembani 2024-12-25 16:12:11 +07:00
parent 448c93365a
commit dec1a70091
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
2 changed files with 32 additions and 24 deletions

View file

@ -19,7 +19,8 @@ The format is simplified version of [Keep a Changelog](https://keepachangelog.co
- Refactor Library to utilize Flow even more - Refactor Library to utilize Flow even more
- Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-bom to v1.10.1 - Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-bom to v1.10.1
- Refactor EmptyView to use Compose - Refactor EmptyView to use Compose
- Refactor Reader ChapterTransition to use Compose - Refactor Reader ChapterTransition to use Compose (@arkon)
- [Experimental] Add modified version of LargeTopAppBar that mimic J2K's ExpandedAppBarLayout
## [1.9.7] ## [1.9.7]

View file

@ -5,8 +5,10 @@ import androidx.compose.animation.core.AnimationState
import androidx.compose.animation.core.CubicBezierEasing import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.DecayAnimationSpec import androidx.compose.animation.core.DecayAnimationSpec
import androidx.compose.animation.core.FastOutLinearInEasing import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDecay import androidx.compose.animation.core.animateDecay
import androidx.compose.animation.core.animateTo import androidx.compose.animation.core.animateTo
import androidx.compose.animation.core.spring
import androidx.compose.animation.rememberSplineBasedDecay import androidx.compose.animation.rememberSplineBasedDecay
import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable import androidx.compose.foundation.gestures.draggable
@ -141,9 +143,7 @@ private fun TwoRowsTopAppBar(
// collapse. // collapse.
// This will potentially animate or interpolate a transition between the container color and the // This will potentially animate or interpolate a transition between the container color and the
// container's scrolled color according to the app bar's scroll state. // container's scrolled color according to the app bar's scroll state.
val colorTransitionFraction = scrollBehavior?.state?.collapsedFraction ?: 0f val colorTransitionFraction = scrollBehavior?.state?.bottomCollapsedFraction(collapsedHeightPx, expandedHeightPx) ?: 0f
val topColorTransitionFraction = scrollBehavior?.state?.topCollapsedFraction(collapsedHeightPx) ?: 0f
val bottomColorTransitionFraction = scrollBehavior?.state?.bottomCollapsedFraction(collapsedHeightPx, expandedHeightPx) ?: 0f
val appBarContainerColor = val appBarContainerColor =
lerp( lerp(
@ -161,12 +161,12 @@ private fun TwoRowsTopAppBar(
content = actions content = actions
) )
} }
val topTitleAlpha = TopTitleAlphaEasing.transform(topColorTransitionFraction) val topTitleAlpha = TitleAlphaEasing.transform(colorTransitionFraction)
val bottomTitleAlpha = 1f - bottomColorTransitionFraction val bottomTitleAlpha = 1f - colorTransitionFraction
// Hide the top row title semantics when its alpha value goes below 0.5 threshold. // Hide the top row title semantics when its alpha value goes below 0.5 threshold.
// Hide the bottom row title semantics when the top title semantics are active. // Hide the bottom row title semantics when the top title semantics are active.
val hideTopRowSemantics = topColorTransitionFraction < 0.5f val hideTopRowSemantics = colorTransitionFraction < 0.5f
val hideBottomRowSemantics = bottomColorTransitionFraction < 0.5f val hideBottomRowSemantics = !hideTopRowSemantics
// Set up support for resizing the top app bar when vertically dragging the bar itself. // Set up support for resizing the top app bar when vertically dragging the bar itself.
val appBarDragModifier = val appBarDragModifier =
@ -179,6 +179,8 @@ private fun TwoRowsTopAppBar(
settleAppBar( settleAppBar(
scrollBehavior.state, scrollBehavior.state,
velocity, velocity,
collapsedHeightPx,
expandedHeightPx,
scrollBehavior.flingAnimationSpec, scrollBehavior.flingAnimationSpec,
scrollBehavior.snapAnimationSpec scrollBehavior.snapAnimationSpec
) )
@ -461,14 +463,16 @@ private fun interface ScrolledOffset {
private suspend fun settleAppBar( private suspend fun settleAppBar(
state: TopAppBarState, state: TopAppBarState,
velocity: Float, velocity: Float,
topHeightPx: Float,
totalHeightPx: Float,
flingAnimationSpec: DecayAnimationSpec<Float>?, flingAnimationSpec: DecayAnimationSpec<Float>?,
snapAnimationSpec: AnimationSpec<Float>? snapAnimationSpec: AnimationSpec<Float>?,
): Velocity { ): Velocity {
// Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar, // Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar,
// and just return Zero Velocity. // and just return Zero Velocity.
// Note that we don't check for 0f due to float precision with the collapsedFraction // Note that we don't check for 0f due to float precision with the collapsedFraction
// calculation. // calculation.
if (state.collapsedFraction < 0.01f || state.collapsedFraction == 1f) { if (state.topCollapsedFraction(topHeightPx, totalHeightPx) < 0.01f || state.topCollapsedFraction(topHeightPx, totalHeightPx) == 1f) {
return Velocity.Zero return Velocity.Zero
} }
var remainingVelocity = velocity var remainingVelocity = velocity
@ -493,17 +497,16 @@ private suspend fun settleAppBar(
} }
// Snap if animation specs were provided. // Snap if animation specs were provided.
if (snapAnimationSpec != null) { if (snapAnimationSpec != null) {
// FIXME: Only snap the top app bar if (state.topHeightOffset(topHeightPx, totalHeightPx) < 0 && state.topHeightOffset(topHeightPx, totalHeightPx) > -topHeightPx) {
if (state.heightOffset < 0 && state.heightOffset > state.heightOffsetLimit) { AnimationState(initialValue = state.topHeightOffset(topHeightPx, totalHeightPx)).animateTo(
AnimationState(initialValue = state.heightOffset).animateTo( if (state.topCollapsedFraction(topHeightPx, totalHeightPx) < 0.5f) {
if (state.collapsedFraction < 0.5f) {
0f 0f
} else { } else {
state.heightOffsetLimit -topHeightPx
}, },
animationSpec = snapAnimationSpec animationSpec = snapAnimationSpec
) { ) {
state.heightOffset = value state.heightOffset = value + (topHeightPx - totalHeightPx)
} }
} }
} }
@ -520,18 +523,21 @@ private fun TopAppBarState.bottomHeightOffset(topHeightPx: Float, totalHeightPx:
return heightOffset.coerceIn(topHeightPx - totalHeightPx, 0f) return heightOffset.coerceIn(topHeightPx - totalHeightPx, 0f)
} }
private fun TopAppBarState.topCollapsedFraction(topHeightPx: Float): Float { private fun TopAppBarState.topCollapsedFraction(topHeightPx: Float, totalHeightPx: Float): Float {
return heightOffset / -topHeightPx val offset = topHeightOffset(topHeightPx, totalHeightPx)
return offset / -topHeightPx
} }
private fun TopAppBarState.bottomCollapsedFraction(topHeightPx: Float, totalHeightPx: Float): Float { private fun TopAppBarState.bottomCollapsedFraction(topHeightPx: Float, totalHeightPx: Float): Float {
return heightOffset / (topHeightPx - totalHeightPx) val offset = bottomHeightOffset(topHeightPx, totalHeightPx)
return offset / (topHeightPx - totalHeightPx)
} }
@Composable @Composable
fun enterAlwaysCollapsedScrollBehavior( fun enterAlwaysCollapsedScrollBehavior(
state: TopAppBarState = rememberTopAppBarState(), state: TopAppBarState = rememberTopAppBarState(),
canScroll: () -> Boolean = { true }, canScroll: () -> Boolean = { true },
snapAnimationSpec: AnimationSpec<Float>? = spring(stiffness = Spring.StiffnessMediumLow),
flingAnimationSpec: DecayAnimationSpec<Float>? = rememberSplineBasedDecay() flingAnimationSpec: DecayAnimationSpec<Float>? = rememberSplineBasedDecay()
): TopAppBarScrollBehavior { ): TopAppBarScrollBehavior {
val topHeightPx: Float val topHeightPx: Float
@ -543,6 +549,7 @@ fun enterAlwaysCollapsedScrollBehavior(
return EnterAlwaysCollapsedScrollBehavior( return EnterAlwaysCollapsedScrollBehavior(
state = state, state = state,
snapAnimationSpec = snapAnimationSpec,
flingAnimationSpec = flingAnimationSpec, flingAnimationSpec = flingAnimationSpec,
canScroll = canScroll, canScroll = canScroll,
topHeightPx = topHeightPx, topHeightPx = topHeightPx,
@ -550,21 +557,21 @@ fun enterAlwaysCollapsedScrollBehavior(
) )
} }
// TODO: Current it behaves exactly like EnterAlways
private class EnterAlwaysCollapsedScrollBehavior( private class EnterAlwaysCollapsedScrollBehavior(
override val state: TopAppBarState, override val state: TopAppBarState,
override val snapAnimationSpec: AnimationSpec<Float>?,
override val flingAnimationSpec: DecayAnimationSpec<Float>?, override val flingAnimationSpec: DecayAnimationSpec<Float>?,
val canScroll: () -> Boolean = { true }, val canScroll: () -> Boolean = { true },
val topHeightPx: Float, val topHeightPx: Float,
val totalHeightPx: Float, val totalHeightPx: Float,
) : TopAppBarScrollBehavior { ) : TopAppBarScrollBehavior {
override val snapAnimationSpec: AnimationSpec<Float>? = null // J2K's app bar doesn't do snap
override val isPinned: Boolean = false override val isPinned: Boolean = false
override var nestedScrollConnection = override var nestedScrollConnection =
object : NestedScrollConnection { object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
// Don't intercept if scrolling down. // Don't intercept if scrolling down.
if (!canScroll()) return Offset.Zero if (!canScroll() || (available.y > 0f && state.topHeightOffset(topHeightPx, totalHeightPx) >= 0f))
return Offset.Zero
val prevHeightOffset = state.heightOffset val prevHeightOffset = state.heightOffset
state.heightOffset += available.y state.heightOffset += available.y
@ -611,14 +618,14 @@ private class EnterAlwaysCollapsedScrollBehavior(
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
val superConsumed = super.onPostFling(consumed, available) val superConsumed = super.onPostFling(consumed, available)
return superConsumed + return superConsumed +
settleAppBar(state, available.y, flingAnimationSpec, snapAnimationSpec) settleAppBar(state, available.y, topHeightPx, totalHeightPx, flingAnimationSpec, snapAnimationSpec)
} }
} }
} }
val CollapsedContainerHeight = 64.0.dp val CollapsedContainerHeight = 64.0.dp
val ExpandedContainerHeight = 152.0.dp val ExpandedContainerHeight = 152.0.dp
internal val TopTitleAlphaEasing = CubicBezierEasing(.8f, 0f, .8f, .15f) internal val TitleAlphaEasing = CubicBezierEasing(.8f, 0f, .8f, .15f)
private val MediumTitleBottomPadding = 24.dp private val MediumTitleBottomPadding = 24.dp
private val LargeTitleBottomPadding = 28.dp private val LargeTitleBottomPadding = 28.dp
private val TopAppBarHorizontalPadding = 4.dp private val TopAppBarHorizontalPadding = 4.dp