fix(AppBar): Sizing issue when user flick too hard

This commit is contained in:
Ahmad Ansori Palembani 2024-12-25 21:06:38 +07:00
parent 120d2cfb96
commit f78d4e9e6a
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
2 changed files with 31 additions and 7 deletions

View file

@ -51,6 +51,7 @@ fun SettingsScaffold(
scrollBehavior = enterAlwaysCollapsedScrollBehavior( scrollBehavior = enterAlwaysCollapsedScrollBehavior(
state = rememberTopAppBarState(), state = rememberTopAppBarState(),
canScroll = { listState.canScrollForward || listState.canScrollBackward }, canScroll = { listState.canScrollForward || listState.canScrollBackward },
isAtTop = { listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0 },
), ),
) { innerPadding -> ) { innerPadding ->
alertDialog.content?.let { it() } alertDialog.content?.let { it() }

View file

@ -489,9 +489,21 @@ private suspend fun settleAppBar(
return Velocity(0f, remainingVelocity) return Velocity(0f, remainingVelocity)
} }
/**
* Default values:
* - Top app bar height: 128px
* - Total app bar height: 304px
* - Bottom app bar height: 176px
* - Top offset limit: (-(Total), (Top - Total)) = (-304px, -176px)
* - Bottom offset limit: ((Top - Total), 0) = (-176px, 0px)
*/
private fun TopAppBarState.rawTopHeightOffset(topHeightPx: Float, totalHeightPx: Float): Float {
return heightOffset + (totalHeightPx - topHeightPx)
}
private fun TopAppBarState.topHeightOffset(topHeightPx: Float, totalHeightPx: Float): Float { private fun TopAppBarState.topHeightOffset(topHeightPx: Float, totalHeightPx: Float): Float {
val offset = heightOffset + (totalHeightPx - topHeightPx) return rawTopHeightOffset(topHeightPx, totalHeightPx).coerceIn(-topHeightPx, 0f)
return offset.coerceIn(-topHeightPx, 0f)
} }
private fun TopAppBarState.bottomHeightOffset(topHeightPx: Float, totalHeightPx: Float): Float { private fun TopAppBarState.bottomHeightOffset(topHeightPx: Float, totalHeightPx: Float): Float {
@ -512,6 +524,7 @@ private fun TopAppBarState.bottomCollapsedFraction(topHeightPx: Float, totalHeig
fun enterAlwaysCollapsedScrollBehavior( fun enterAlwaysCollapsedScrollBehavior(
state: TopAppBarState = rememberTopAppBarState(), state: TopAppBarState = rememberTopAppBarState(),
canScroll: () -> Boolean = { true }, canScroll: () -> Boolean = { true },
isAtTop: () -> Boolean = { true },
snapAnimationSpec: AnimationSpec<Float>? = spring(stiffness = Spring.StiffnessMediumLow), snapAnimationSpec: AnimationSpec<Float>? = spring(stiffness = Spring.StiffnessMediumLow),
flingAnimationSpec: DecayAnimationSpec<Float>? = rememberSplineBasedDecay() flingAnimationSpec: DecayAnimationSpec<Float>? = rememberSplineBasedDecay()
): TopAppBarScrollBehavior { ): TopAppBarScrollBehavior {
@ -527,30 +540,40 @@ fun enterAlwaysCollapsedScrollBehavior(
snapAnimationSpec = snapAnimationSpec, snapAnimationSpec = snapAnimationSpec,
flingAnimationSpec = flingAnimationSpec, flingAnimationSpec = flingAnimationSpec,
canScroll = canScroll, canScroll = canScroll,
isAtTop = isAtTop,
topHeightPx = topHeightPx, topHeightPx = topHeightPx,
totalHeightPx = totalHeightPx, totalHeightPx = totalHeightPx,
) )
} }
// FIXME: AppBar size is overflowing if user flick the screen too fast
private class EnterAlwaysCollapsedScrollBehavior( private class EnterAlwaysCollapsedScrollBehavior(
override val state: TopAppBarState, override val state: TopAppBarState,
override val snapAnimationSpec: AnimationSpec<Float>?, override val snapAnimationSpec: AnimationSpec<Float>?,
override val flingAnimationSpec: DecayAnimationSpec<Float>?, override val flingAnimationSpec: DecayAnimationSpec<Float>?,
val canScroll: () -> Boolean = { true }, val canScroll: () -> Boolean = { true },
// FIXME: See if it's possible to eliminate this argument
val isAtTop: () -> Boolean = { true },
val topHeightPx: Float, val topHeightPx: Float,
val totalHeightPx: Float, val totalHeightPx: Float,
) : TopAppBarScrollBehavior { ) : TopAppBarScrollBehavior {
override val isPinned: Boolean = false override val isPinned: Boolean = false
override var nestedScrollConnection = override var nestedScrollConnection =
object : NestedScrollConnection { object : NestedScrollConnection {
private fun TopAppBarState.setClampedOffsetIfAtTop(offset: Float) {
heightOffset = if (isAtTop()) {
offset
} else {
offset.coerceIn(-totalHeightPx, (topHeightPx - totalHeightPx))
}
}
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() || (available.y > 0f && state.topHeightOffset(topHeightPx, totalHeightPx) >= 0f)) if (!canScroll() || (available.y > 0f && state.rawTopHeightOffset(topHeightPx, totalHeightPx) >= 0f))
return Offset.Zero return Offset.Zero
val prevHeightOffset = state.heightOffset val prevHeightOffset = state.heightOffset
state.heightOffset += available.y state.setClampedOffsetIfAtTop(state.heightOffset + available.y)
return if (prevHeightOffset != state.heightOffset) { return if (prevHeightOffset != state.heightOffset) {
// We're in the middle of top app bar collapse or expand. // We're in the middle of top app bar collapse or expand.
// Consume only the scroll on the Y axis. // Consume only the scroll on the Y axis.
@ -571,7 +594,7 @@ private class EnterAlwaysCollapsedScrollBehavior(
if (available.y < 0f || consumed.y < 0f) { if (available.y < 0f || consumed.y < 0f) {
// When scrolling up, just update the state's height offset. // When scrolling up, just update the state's height offset.
val oldHeightOffset = state.heightOffset val oldHeightOffset = state.heightOffset
state.heightOffset += consumed.y state.setClampedOffsetIfAtTop(state.heightOffset + consumed.y)
return Offset(0f, state.heightOffset - oldHeightOffset) return Offset(0f, state.heightOffset - oldHeightOffset)
} }
@ -585,7 +608,7 @@ private class EnterAlwaysCollapsedScrollBehavior(
// Adjust the height offset in case the consumed delta Y is less than what was // Adjust the height offset in case the consumed delta Y is less than what was
// recorded as available delta Y in the pre-scroll. // recorded as available delta Y in the pre-scroll.
val oldHeightOffset = state.heightOffset val oldHeightOffset = state.heightOffset
state.heightOffset += available.y state.setClampedOffsetIfAtTop(state.heightOffset + available.y)
return Offset(0f, state.heightOffset - oldHeightOffset) return Offset(0f, state.heightOffset - oldHeightOffset)
} }
return Offset.Zero return Offset.Zero