Skip to content

Commit 36b09f7

Browse files
committed
library: Add back topappbar snapAnimationSpec
1 parent b9e0885 commit 36b09f7

File tree

3 files changed

+41
-104
lines changed

3 files changed

+41
-104
lines changed

miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/LazyColumn.kt

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package top.yukonga.miuix.kmp.basic
22

3+
import androidx.compose.foundation.gestures.FlingBehavior
34
import androidx.compose.foundation.gestures.ScrollableDefaults
45
import androidx.compose.foundation.layout.Arrangement
56
import androidx.compose.foundation.layout.PaddingValues
@@ -16,7 +17,6 @@ import androidx.compose.ui.unit.dp
1617
import top.yukonga.miuix.kmp.utils.Platform
1718
import top.yukonga.miuix.kmp.utils.overScrollVertical
1819
import top.yukonga.miuix.kmp.utils.platform
19-
import top.yukonga.miuix.kmp.utils.rememberOverscrollFlingBehavior
2020

2121
/**
2222
* A [LazyColumn] that supports over-scroll and top app bar scroll behavior.
@@ -38,14 +38,15 @@ fun LazyColumn(
3838
verticalArrangement: Arrangement.Vertical =
3939
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
4040
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
41+
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
4142
userScrollEnabled: Boolean = true,
4243
isEnabledOverScroll: () -> Boolean = { platform() == Platform.Android },
4344
topAppBarScrollBehavior: ScrollBehavior? = null,
4445
content: LazyListScope.() -> Unit
4546
) {
4647
val firstModifier = remember(isEnabledOverScroll) {
4748
if (isEnabledOverScroll.invoke()) {
48-
modifier.overScrollVertical(onOverscroll = { topAppBarScrollBehavior?.isPinned = it }, isEnabled = isEnabledOverScroll)
49+
modifier.overScrollVertical(isEnabled = isEnabledOverScroll)
4950
} else {
5051
modifier
5152
}
@@ -55,12 +56,6 @@ fun LazyColumn(
5556
firstModifier.nestedScroll(it.nestedScrollConnection)
5657
} ?: firstModifier
5758
}
58-
val flingBehavior =
59-
if (isEnabledOverScroll.invoke()) {
60-
rememberOverscrollFlingBehavior { state }
61-
} else {
62-
ScrollableDefaults.flingBehavior()
63-
}
6459

6560
LazyColumn(
6661
modifier = finalModifier,

miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/TopAppBar.kt

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import androidx.compose.animation.core.DecayAnimationSpec
66
import androidx.compose.animation.core.Spring
77
import androidx.compose.animation.core.animateDecay
88
import androidx.compose.animation.core.animateFloatAsState
9+
import androidx.compose.animation.core.animateTo
910
import androidx.compose.animation.core.spring
1011
import androidx.compose.animation.core.tween
1112
import androidx.compose.animation.rememberSplineBasedDecay
@@ -212,7 +213,7 @@ fun SmallTopAppBar(
212213
fun MiuixScrollBehavior(
213214
state: TopAppBarState = rememberTopAppBarState(),
214215
canScroll: () -> Boolean = { true },
215-
snapAnimationSpec: AnimationSpec<Float>? = spring(stiffness = Spring.StiffnessMediumLow),
216+
snapAnimationSpec: AnimationSpec<Float>? = spring(stiffness = Spring.StiffnessMedium),
216217
flingAnimationSpec: DecayAnimationSpec<Float>? = rememberSplineBasedDecay()
217218
): ScrollBehavior =
218219
remember(state, canScroll, snapAnimationSpec, flingAnimationSpec) {
@@ -363,7 +364,7 @@ interface ScrollBehavior {
363364
* A pinned app bar will stay fixed in place when content is scrolled and will not react to any
364365
* drag gestures.
365366
*/
366-
var isPinned: Boolean
367+
val isPinned: Boolean
367368

368369
/**
369370
* An optional [AnimationSpec] that defines how the top app bar snaps to either fully collapsed
@@ -402,17 +403,17 @@ interface ScrollBehavior {
402403
* [ExitUntilCollapsedScrollBehavior]
403404
*/
404405
private class ExitUntilCollapsedScrollBehavior(
405-
override var isPinned: Boolean = false,
406406
override val state: TopAppBarState,
407407
override val snapAnimationSpec: AnimationSpec<Float>?,
408408
override val flingAnimationSpec: DecayAnimationSpec<Float>?,
409409
val canScroll: () -> Boolean = { true }
410410
) : ScrollBehavior {
411+
override val isPinned: Boolean = false
411412
override var nestedScrollConnection =
412413
object : NestedScrollConnection {
413414
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
414-
// Don't intercept if scrolling down & if is pinned.
415-
if (!canScroll() || available.y > 0f || isPinned) return Offset.Zero
415+
// Don't intercept if scrolling down.
416+
if (!canScroll() || available.y > 0f) return Offset.Zero
416417

417418
val prevHeightOffset = state.heightOffset
418419
state.heightOffset += available.y
@@ -459,7 +460,7 @@ private class ExitUntilCollapsedScrollBehavior(
459460
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
460461
val superConsumed = super.onPostFling(consumed, available)
461462
return superConsumed +
462-
settleAppBar(state, available.y, flingAnimationSpec)
463+
settleAppBar(state, available.y, flingAnimationSpec, snapAnimationSpec)
463464
}
464465
}
465466
}
@@ -471,7 +472,8 @@ private class ExitUntilCollapsedScrollBehavior(
471472
private suspend fun settleAppBar(
472473
state: TopAppBarState,
473474
velocity: Float,
474-
flingAnimationSpec: DecayAnimationSpec<Float>?
475+
flingAnimationSpec: DecayAnimationSpec<Float>?,
476+
snapAnimationSpec: AnimationSpec<Float>?,
475477
): Velocity {
476478
// Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar,
477479
// and just return Zero Velocity.
@@ -485,20 +487,33 @@ private suspend fun settleAppBar(
485487
// continue the motion to expand or collapse the app bar.
486488
if (flingAnimationSpec != null && abs(velocity) > 1f) {
487489
var lastValue = 0f
488-
AnimationState(
489-
initialValue = 0f,
490-
initialVelocity = velocity,
491-
)
492-
.animateDecay(flingAnimationSpec) {
493-
val delta = value - lastValue
494-
val initialHeightOffset = state.heightOffset
495-
state.heightOffset = initialHeightOffset + delta
496-
val consumed = abs(initialHeightOffset - state.heightOffset)
497-
lastValue = value
498-
remainingVelocity = this.velocity
499-
// avoid rounding errors and stop if anything is unconsumed
500-
if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
490+
AnimationState(initialValue = 0f, initialVelocity = velocity).animateDecay(
491+
flingAnimationSpec
492+
) {
493+
val delta = value - lastValue
494+
val initialHeightOffset = state.heightOffset
495+
state.heightOffset = initialHeightOffset + delta
496+
val consumed = abs(initialHeightOffset - state.heightOffset)
497+
lastValue = value
498+
remainingVelocity = this.velocity
499+
// avoid rounding errors and stop if anything is unconsumed
500+
if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
501+
}
502+
}
503+
// Snap if animation specs were provided.
504+
if (snapAnimationSpec != null) {
505+
if (state.heightOffset < 0 && state.heightOffset > state.heightOffsetLimit) {
506+
AnimationState(initialValue = state.heightOffset).animateTo(
507+
if (state.collapsedFraction < 0.5f) {
508+
0f
509+
} else {
510+
state.heightOffsetLimit
511+
},
512+
animationSpec = snapAnimationSpec,
513+
) {
514+
state.heightOffset = value
501515
}
516+
}
502517
}
503518

504519
return Velocity(0f, velocity - remainingVelocity)

miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/utils/Overscroll.kt

Lines changed: 3 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,9 @@
11
package top.yukonga.miuix.kmp.utils
22

33
import androidx.compose.animation.core.Animatable
4-
import androidx.compose.animation.core.AnimationState
54
import androidx.compose.animation.core.AnimationVector1D
6-
import androidx.compose.animation.core.DecayAnimationSpec
75
import androidx.compose.animation.core.Spring.StiffnessMediumLow
8-
import androidx.compose.animation.core.animateDecay
9-
import androidx.compose.animation.core.exponentialDecay
106
import androidx.compose.animation.core.spring
11-
import androidx.compose.animation.rememberSplineBasedDecay
12-
import androidx.compose.foundation.gestures.FlingBehavior
13-
import androidx.compose.foundation.gestures.ScrollScope
14-
import androidx.compose.foundation.gestures.ScrollableState
15-
import androidx.compose.foundation.lazy.LazyListState
16-
import androidx.compose.foundation.lazy.LazyRow
177
import androidx.compose.runtime.Composable
188
import androidx.compose.runtime.Stable
199
import androidx.compose.runtime.getValue
@@ -33,7 +23,6 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
3323
import androidx.compose.ui.platform.LocalDensity
3424
import androidx.compose.ui.unit.Velocity
3525
import kotlinx.coroutines.launch
36-
import top.yukonga.miuix.kmp.basic.LazyColumn
3726
import kotlin.math.abs
3827
import kotlin.math.exp
3928
import kotlin.math.sign
@@ -87,9 +76,8 @@ fun Modifier.overScrollVertical(
8776
scrollEasing: ((currentOffset: Float, newOffset: Float) -> Float)? = null,
8877
springStiff: Float = OutBoundSpringStiff,
8978
springDamp: Float = OutBoundSpringDamp,
90-
onOverscroll: ((Boolean) -> Unit)? = null,
9179
isEnabled: () -> Boolean = { platform() == Platform.Android }
92-
): Modifier = overScrollOutOfBound(isVertical = true, nestedScrollToParent, scrollEasing, springStiff, springDamp, onOverscroll, isEnabled)
80+
): Modifier = overScrollOutOfBound(isVertical = true, nestedScrollToParent, scrollEasing, springStiff, springDamp, isEnabled)
9381

9482
/**
9583
* @see overScrollOutOfBound
@@ -100,9 +88,8 @@ fun Modifier.overScrollHorizontal(
10088
scrollEasing: ((currentOffset: Float, newOffset: Float) -> Float)? = null,
10189
springStiff: Float = OutBoundSpringStiff,
10290
springDamp: Float = OutBoundSpringDamp,
103-
onOverscroll: ((Boolean) -> Unit)? = null,
10491
isEnabled: () -> Boolean = { platform() == Platform.Android }
105-
): Modifier = overScrollOutOfBound(isVertical = false, nestedScrollToParent, scrollEasing, springStiff, springDamp, onOverscroll, isEnabled)
92+
): Modifier = overScrollOutOfBound(isVertical = false, nestedScrollToParent, scrollEasing, springStiff, springDamp, isEnabled)
10693

10794
/**
10895
* OverScroll effect for scrollable Composable.
@@ -118,7 +105,6 @@ fun Modifier.overScrollHorizontal(
118105
* The current default easing comes from iOS, you don't need to modify it!
119106
* @param springStiff springStiff for overscroll effect,For better user experience, the new value is not recommended to be higher than[StiffnessMediumLow]
120107
* @param springDamp springDamp for overscroll effect,generally do not need to set。
121-
* @param onOverscroll Callback when the overscroll state changes, the parameter is whether the current state is Overscrolling.
122108
* @param isEnabled Whether to enable Overscroll effect, default is true.
123109
*/
124110
@Stable
@@ -129,11 +115,9 @@ fun Modifier.overScrollOutOfBound(
129115
scrollEasing: ((currentOffset: Float, newOffset: Float) -> Float)?,
130116
springStiff: Float = OutBoundSpringStiff,
131117
springDamp: Float = OutBoundSpringDamp,
132-
onOverscroll: ((Boolean) -> Unit)? = null,
133118
isEnabled: () -> Boolean = { platform() == Platform.Android }
134119
): Modifier = composed {
135120
if (!isEnabled()) return@composed this
136-
val onOverscroll by rememberUpdatedState(onOverscroll)
137121
val nestedScrollToParent by rememberUpdatedState(nestedScrollToParent)
138122
val scrollEasing by rememberUpdatedState(scrollEasing ?: DefaultParabolaScrollEasing)
139123
val springStiff by rememberUpdatedState(springStiff)
@@ -174,15 +158,13 @@ fun Modifier.overScrollOutOfBound(
174158
// sign changed, coerce to start scrolling and exit
175159
return if (sign(offset) != sign(offsetAtLast)) {
176160
offset = 0f
177-
onOverscroll?.invoke(false)
178161
if (isVertical) {
179162
Offset(x = available.x - realAvailable.x, y = available.y - realAvailable.y + realOffset)
180163
} else {
181164
Offset(x = available.x - realAvailable.x + realOffset, y = available.y - realAvailable.y)
182165
}
183166
} else {
184167
offset = offsetAtLast
185-
onOverscroll?.invoke(true)
186168
if (isVertical) {
187169
Offset(x = available.x - realAvailable.x, y = available.y)
188170
} else {
@@ -194,14 +176,13 @@ fun Modifier.overScrollOutOfBound(
194176
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
195177
// Found fling behavior in the wrong direction.
196178
if (source != NestedScrollSource.UserInput) {
197-
return dispatcher.dispatchPreScroll(available, source)
179+
return dispatcher.dispatchPostScroll(consumed, available, source)
198180
}
199181
val realAvailable = when {
200182
nestedScrollToParent -> available - dispatcher.dispatchPostScroll(consumed, available, source)
201183
else -> available
202184
}
203185
offset = scrollEasing(offset, if (isVertical) realAvailable.y else realAvailable.x)
204-
onOverscroll?.invoke(abs(offset) > visibilityThreshold)
205186
return if (isVertical) {
206187
Offset(x = available.x - realAvailable.x, y = available.y)
207188
} else {
@@ -210,7 +191,6 @@ fun Modifier.overScrollOutOfBound(
210191
}
211192

212193
override suspend fun onPreFling(available: Velocity): Velocity {
213-
onOverscroll?.invoke(false)
214194
if (::lastFlingAnimator.isInitialized && lastFlingAnimator.isRunning) {
215195
lastFlingAnimator.stop()
216196
}
@@ -240,7 +220,6 @@ fun Modifier.overScrollOutOfBound(
240220
}
241221

242222
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
243-
onOverscroll?.invoke(false)
244223
val realAvailable = when {
245224
nestedScrollToParent -> available - dispatcher.dispatchPostFling(consumed, available)
246225
else -> available
@@ -265,55 +244,3 @@ fun Modifier.overScrollOutOfBound(
265244
if (isVertical) translationY = offset else translationX = offset
266245
}
267246
}
268-
269-
270-
/**
271-
* You should use it with [overScrollVertical]
272-
* @param decaySpec You can use instead [rememberSplineBasedDecay]
273-
* @param getScrollState Pass in your [ScrollableState], for [LazyColumn]/[LazyRow] , it's [LazyListState]
274-
*/
275-
@Composable
276-
fun rememberOverscrollFlingBehavior(
277-
decaySpec: DecayAnimationSpec<Float> = exponentialDecay(),
278-
getScrollState: () -> ScrollableState,
279-
): FlingBehavior = remember(decaySpec, getScrollState) {
280-
object : FlingBehavior {
281-
/**
282-
* - We should check it every frame of fling
283-
* - Should stop fling when returning true and return the remaining speed immediately.
284-
* - Without this detection, scrollBy() will continue to consume velocity,
285-
* which will cause a velocity error in nestedScroll.
286-
*/
287-
private val Float.canNotBeConsumed: Boolean // this is Velocity
288-
get() {
289-
val state = getScrollState()
290-
return !(this < 0 && state.canScrollBackward || this > 0 && state.canScrollForward)
291-
}
292-
293-
override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
294-
if (initialVelocity.canNotBeConsumed) {
295-
return initialVelocity
296-
}
297-
return if (abs(initialVelocity) > 1f) {
298-
var velocityLeft = initialVelocity
299-
var lastValue = 0f
300-
AnimationState(
301-
initialValue = 0f,
302-
initialVelocity = initialVelocity,
303-
).animateDecay(decaySpec) {
304-
val delta = value - lastValue
305-
val consumed = scrollBy(delta)
306-
lastValue = value
307-
velocityLeft = this.velocity
308-
// avoid rounding errors and stop if anything is unconsumed
309-
if (abs(delta - consumed) > 0.5f || velocityLeft.canNotBeConsumed) {
310-
cancelAnimation()
311-
}
312-
}
313-
velocityLeft
314-
} else {
315-
initialVelocity
316-
}
317-
}
318-
}
319-
}

0 commit comments

Comments
 (0)