From f78d4e9e6a6c8a6ba59a582e0f1466d13a76f1bb Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Wed, 25 Dec 2024 21:06:38 +0700 Subject: [PATCH] fix(AppBar): Sizing issue when user flick too hard --- .../settings/SettingsCommonWidget.kt | 1 + .../java/yokai/presentation/core/AppBar.kt | 37 +++++++++++++++---- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/yokai/presentation/settings/SettingsCommonWidget.kt b/app/src/main/java/yokai/presentation/settings/SettingsCommonWidget.kt index 728948f4be..cff9066e2a 100644 --- a/app/src/main/java/yokai/presentation/settings/SettingsCommonWidget.kt +++ b/app/src/main/java/yokai/presentation/settings/SettingsCommonWidget.kt @@ -51,6 +51,7 @@ fun SettingsScaffold( scrollBehavior = enterAlwaysCollapsedScrollBehavior( state = rememberTopAppBarState(), canScroll = { listState.canScrollForward || listState.canScrollBackward }, + isAtTop = { listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0 }, ), ) { innerPadding -> alertDialog.content?.let { it() } diff --git a/presentation/core/src/main/java/yokai/presentation/core/AppBar.kt b/presentation/core/src/main/java/yokai/presentation/core/AppBar.kt index 429b68a77b..1d4974948d 100644 --- a/presentation/core/src/main/java/yokai/presentation/core/AppBar.kt +++ b/presentation/core/src/main/java/yokai/presentation/core/AppBar.kt @@ -489,9 +489,21 @@ private suspend fun settleAppBar( 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 { - val offset = heightOffset + (totalHeightPx - topHeightPx) - return offset.coerceIn(-topHeightPx, 0f) + return rawTopHeightOffset(topHeightPx, totalHeightPx).coerceIn(-topHeightPx, 0f) } private fun TopAppBarState.bottomHeightOffset(topHeightPx: Float, totalHeightPx: Float): Float { @@ -512,6 +524,7 @@ private fun TopAppBarState.bottomCollapsedFraction(topHeightPx: Float, totalHeig fun enterAlwaysCollapsedScrollBehavior( state: TopAppBarState = rememberTopAppBarState(), canScroll: () -> Boolean = { true }, + isAtTop: () -> Boolean = { true }, snapAnimationSpec: AnimationSpec? = spring(stiffness = Spring.StiffnessMediumLow), flingAnimationSpec: DecayAnimationSpec? = rememberSplineBasedDecay() ): TopAppBarScrollBehavior { @@ -527,30 +540,40 @@ fun enterAlwaysCollapsedScrollBehavior( snapAnimationSpec = snapAnimationSpec, flingAnimationSpec = flingAnimationSpec, canScroll = canScroll, + isAtTop = isAtTop, topHeightPx = topHeightPx, totalHeightPx = totalHeightPx, ) } -// FIXME: AppBar size is overflowing if user flick the screen too fast private class EnterAlwaysCollapsedScrollBehavior( override val state: TopAppBarState, override val snapAnimationSpec: AnimationSpec?, override val flingAnimationSpec: DecayAnimationSpec?, val canScroll: () -> Boolean = { true }, + // FIXME: See if it's possible to eliminate this argument + val isAtTop: () -> Boolean = { true }, val topHeightPx: Float, val totalHeightPx: Float, ) : TopAppBarScrollBehavior { override val isPinned: Boolean = false override var 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 { // 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 val prevHeightOffset = state.heightOffset - state.heightOffset += available.y + state.setClampedOffsetIfAtTop(state.heightOffset + available.y) return if (prevHeightOffset != state.heightOffset) { // We're in the middle of top app bar collapse or expand. // Consume only the scroll on the Y axis. @@ -571,7 +594,7 @@ private class EnterAlwaysCollapsedScrollBehavior( if (available.y < 0f || consumed.y < 0f) { // When scrolling up, just update the state's height offset. val oldHeightOffset = state.heightOffset - state.heightOffset += consumed.y + state.setClampedOffsetIfAtTop(state.heightOffset + consumed.y) 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 // recorded as available delta Y in the pre-scroll. val oldHeightOffset = state.heightOffset - state.heightOffset += available.y + state.setClampedOffsetIfAtTop(state.heightOffset + available.y) return Offset(0f, state.heightOffset - oldHeightOffset) } return Offset.Zero