Skip to content

Commit 84cd6a2

Browse files
committed
library: Remove ScrollCoordinator
* THX @sd086
1 parent 4ccbcd0 commit 84cd6a2

File tree

5 files changed

+62
-372
lines changed

5 files changed

+62
-372
lines changed

example/src/commonMain/kotlin/SecondPage.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ fun SecondPage(
4545
PullToRefresh(
4646
pullToRefreshState = pullToRefreshState,
4747
topAppBarScrollBehavior = topAppBarScrollBehavior,
48-
useScrollCoordinator = true,
4948
contentPadding = PaddingValues(top = padding.calculateTopPadding() + 12.dp)
5049
) {
5150
LazyColumn(

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

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -56,18 +56,14 @@ import androidx.compose.ui.platform.LocalHapticFeedback
5656
import androidx.compose.ui.text.TextStyle
5757
import androidx.compose.ui.text.font.FontWeight
5858
import androidx.compose.ui.unit.Dp
59+
import androidx.compose.ui.unit.Velocity
5960
import androidx.compose.ui.unit.dp
6061
import androidx.compose.ui.unit.sp
6162
import kotlinx.coroutines.CoroutineScope
6263
import kotlinx.coroutines.flow.collectLatest
6364
import kotlinx.coroutines.launch
64-
import top.yukonga.miuix.kmp.utils.LocalOverScrollState
65-
import top.yukonga.miuix.kmp.utils.LocalScrollCoordinatorState
66-
import top.yukonga.miuix.kmp.utils.OverScrollState
67-
import top.yukonga.miuix.kmp.utils.ScrollPriority
6865
import top.yukonga.miuix.kmp.utils.getWindowSize
6966
import top.yukonga.miuix.kmp.utils.overScrollVertical
70-
import top.yukonga.miuix.kmp.utils.rememberScrollCoordinator
7167
import kotlin.math.PI
7268
import kotlin.math.cos
7369
import kotlin.math.max
@@ -87,7 +83,6 @@ import kotlin.math.sin
8783
* @param refreshTexts The texts to show when refreshing.
8884
* @param refreshTextStyle The style of the refresh text.
8985
* @param topAppBarScrollBehavior The scroll behavior of the top app bar to coordinate with.
90-
* @param useScrollCoordinator Whether to use the scroll coordinator for better integration.
9186
* @param onRefresh The callback to be called when the refresh is triggered.
9287
* @param content the content to be shown when the [PullToRefresh] is expanded.
9388
*/
@@ -101,7 +96,6 @@ fun PullToRefresh(
10196
refreshTexts: List<String> = PullToRefreshDefaults.refreshTexts,
10297
refreshTextStyle: TextStyle = PullToRefreshDefaults.refreshTextStyle,
10398
topAppBarScrollBehavior: ScrollBehavior? = null,
104-
useScrollCoordinator: Boolean = false,
10599
onRefresh: () -> Unit = {},
106100
content: @Composable () -> Unit
107101
) {
@@ -111,19 +105,38 @@ fun PullToRefresh(
111105
pullToRefreshState.syncDragOffsetWithRawOffset()
112106
}
113107

114-
val overScrollState = LocalOverScrollState.current
108+
val nestedScrollConnection = remember(pullToRefreshState, topAppBarScrollBehavior) {
109+
object : NestedScrollConnection {
110+
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
111+
val consumedByAppBar = topAppBarScrollBehavior?.nestedScrollConnection?.onPreScroll(available, source) ?: Offset.Zero
115112

116-
val scrollCoordinator = if (useScrollCoordinator && topAppBarScrollBehavior != null) {
117-
rememberScrollCoordinator(
118-
topAppBarScrollBehavior = topAppBarScrollBehavior,
119-
pullToRefreshState = pullToRefreshState,
120-
overScrollState = overScrollState
121-
)
122-
} else null
113+
val remaining = available - consumedByAppBar
114+
val consumedByRefresh = pullToRefreshState.createNestedScrollConnection().onPreScroll(remaining, source)
115+
116+
return consumedByAppBar + consumedByRefresh
117+
}
118+
119+
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
120+
val consumedByAppBar = topAppBarScrollBehavior?.nestedScrollConnection?.onPostScroll(consumed, available, source) ?: Offset.Zero
121+
122+
val remaining = available - consumedByAppBar
123+
val consumedByRefresh = pullToRefreshState.createNestedScrollConnection().onPostScroll(consumed, remaining, source)
124+
125+
return consumedByAppBar + consumedByRefresh
126+
}
127+
128+
override suspend fun onPreFling(available: Velocity): Velocity {
129+
return topAppBarScrollBehavior?.nestedScrollConnection?.onPreFling(available) ?: Velocity.Zero
130+
}
123131

124-
val nestedScrollConnection = remember(pullToRefreshState, overScrollState, scrollCoordinator) {
125-
scrollCoordinator ?: pullToRefreshState.createNestedScrollConnection(overScrollState)
132+
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
133+
val consumedByAppBar = topAppBarScrollBehavior?.nestedScrollConnection?.onPostFling(consumed, available) ?: Velocity.Zero
134+
135+
return consumedByAppBar
136+
}
137+
}
126138
}
139+
127140
val pointerModifier = Modifier.pointerInput(Unit) {
128141
awaitPointerEventScope {
129142
while (true) {
@@ -142,18 +155,12 @@ fun PullToRefresh(
142155

143156
CompositionLocalProvider(
144157
LocalPullToRefreshState provides pullToRefreshState,
145-
LocalScrollCoordinatorState provides scrollCoordinator?.state
146158
) {
147159
val boxModifier = modifier
148160
.nestedScroll(nestedScrollConnection)
149161
.then(pointerModifier)
150-
.then(
151-
if (scrollCoordinator != null && scrollCoordinator.state.currentPriority == ScrollPriority.OverScroll) {
152-
Modifier.overScrollVertical()
153-
} else {
154-
Modifier
155-
}
156-
)
162+
.overScrollVertical()
163+
157164
Box(modifier = boxModifier) {
158165
Column {
159166
RefreshHeader(
@@ -646,11 +653,8 @@ class PullToRefreshState(
646653
isRefreshingInProgress = false
647654
}
648655

649-
fun createNestedScrollConnection(
650-
overScrollState: OverScrollState
651-
): NestedScrollConnection = object : NestedScrollConnection {
656+
fun createNestedScrollConnection(): NestedScrollConnection = object : NestedScrollConnection {
652657
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
653-
if (overScrollState.isOverScrollActive) return Offset.Zero
654658
if (isRefreshingInProgress || refreshState == RefreshState.Refreshing || refreshState == RefreshState.RefreshComplete) return Offset.Zero
655659
return if (source == NestedScrollSource.UserInput && available.y < 0 && rawDragOffset > 0f) {
656660
if (isRebounding && dragOffsetAnimatable.isRunning) {
@@ -665,7 +669,7 @@ class PullToRefreshState(
665669
}
666670

667671
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset = when {
668-
overScrollState.isOverScrollActive || isRefreshingInProgress || refreshState == RefreshState.Refreshing || refreshState == RefreshState.RefreshComplete -> Offset.Zero
672+
isRefreshingInProgress || refreshState == RefreshState.Refreshing || refreshState == RefreshState.RefreshComplete -> Offset.Zero
669673
source == NestedScrollSource.UserInput -> {
670674
if (available.y > 0f && consumed.y == 0f) {
671675
if (isRebounding && dragOffsetAnimatable.isRunning) {

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

Lines changed: 28 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -424,14 +424,10 @@ private class ExitUntilCollapsedScrollBehavior(
424424
override var nestedScrollConnection =
425425
object : NestedScrollConnection {
426426
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
427-
// Don't intercept if scrolling down.
428-
if (!canScroll() || available.y > 0f) return Offset.Zero
429-
427+
if (!canScroll() || available.y > 0) return Offset.Zero
430428
val prevHeightOffset = state.heightOffset
431-
state.heightOffset = state.heightOffset + available.y
429+
state.heightOffset += available.y
432430
return if (prevHeightOffset != state.heightOffset) {
433-
// We're in the middle of top app bar collapse or expand.
434-
// Consume only the scroll on the Y axis.
435431
available.copy(x = 0f)
436432
} else {
437433
Offset.Zero
@@ -443,59 +439,55 @@ private class ExitUntilCollapsedScrollBehavior(
443439
available: Offset,
444440
source: NestedScrollSource,
445441
): Offset {
446-
if (!canScroll()) return Offset.Zero
442+
if (!canScroll() || available.y < 0) return Offset.Zero
447443
state.contentOffset += consumed.y
444+
val oldHeightOffset = state.heightOffset
445+
state.heightOffset += available.y
446+
return Offset(0f, state.heightOffset - oldHeightOffset)
447+
}
448448

449-
if (available.y < 0f || consumed.y < 0f) {
450-
// When scrolling up, just update the state's height offset.
451-
val oldHeightOffset = state.heightOffset
452-
state.heightOffset = state.heightOffset + consumed.y
453-
return Offset(0f, state.heightOffset - oldHeightOffset)
454-
}
455-
456-
if (available.y > 0f) {
457-
// Adjust the height offset in case the consumed delta Y is less than what was
458-
// recorded as available delta Y in the pre-scroll.
459-
val oldHeightOffset = state.heightOffset
460-
state.heightOffset = state.heightOffset + available.y
461-
return Offset(0f, state.heightOffset - oldHeightOffset)
449+
override suspend fun onPreFling(available: Velocity): Velocity {
450+
if (available.y < 0 && state.heightOffset < 0f) {
451+
return settleAppBar(state, available.y, flingAnimationSpec, snapAnimationSpec)
462452
}
463-
return Offset.Zero
453+
return Velocity.Zero
464454
}
465455

466456
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
467457
if (available.y > 0) {
468-
// Reset the total content offset to zero when scrolling all the way down. This
469-
// will eliminate some float precision inaccuracies.
470-
state.contentOffset = 0f
458+
return settleAppBar(state, available.y, flingAnimationSpec, snapAnimationSpec)
471459
}
472-
val superConsumed = super.onPostFling(consumed, available)
473-
return superConsumed +
474-
settleAppBar(state, available.y, flingAnimationSpec, snapAnimationSpec)
460+
return Velocity.Zero
475461
}
476462
}
477463
}
478464

479465
/**
480-
* Settles the app bar by flinging, in case the given velocity is greater than zero, and snapping
481-
* after the fling settles.
466+
* Settles the app bar to a stable state (fully expanded or collapsed) by animating
467+
* its height offset.
468+
*
469+
* This function is invoked after a drag or fling gesture, using the provided velocity
470+
* to drive a decay animation, followed by a snap animation if the bar is left in an
471+
* intermediate state.
472+
*
473+
* @param state The [TopAppBarState] that holds the current and target height offsets.
474+
* @param velocity The velocity from the fling gesture to be consumed.
475+
* @param flingAnimationSpec The [DecayAnimationSpec] for the fling animation.
476+
* @param snapAnimationSpec The [AnimationSpec] for the final snap to a stable state.
477+
* @return The [Velocity] that was actually consumed by the fling decay animation. This
478+
* ensures accurate reporting within the nested scroll system, allowing any unconsumed
479+
* velocity to be propagated to parent consumers.
482480
*/
483481
private suspend fun settleAppBar(
484482
state: TopAppBarState,
485483
velocity: Float,
486484
flingAnimationSpec: DecayAnimationSpec<Float>?,
487485
snapAnimationSpec: AnimationSpec<Float>?,
488486
): Velocity {
489-
// Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar,
490-
// and just return Zero Velocity.
491-
// Note that we don't check for 0f due to float precision with the collapsedFraction
492-
// calculation.
493487
if (state.collapsedFraction < 0.01f || state.collapsedFraction == 1f) {
494488
return Velocity.Zero
495489
}
496490
var remainingVelocity = velocity
497-
// In case there is an initial velocity that was left after a previous user fling, animate to
498-
// continue the motion to expand or collapse the app bar.
499491
if (flingAnimationSpec != null && abs(velocity) > 1f) {
500492
var lastValue = 0f
501493
AnimationState(initialValue = 0f, initialVelocity = velocity).animateDecay(
@@ -511,7 +503,6 @@ private suspend fun settleAppBar(
511503
if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
512504
}
513505
}
514-
// Snap if animation specs were provided.
515506
if (snapAnimationSpec != null) {
516507
if (state.heightOffset < 0 && state.heightOffset > state.heightOffsetLimit) {
517508
AnimationState(initialValue = state.heightOffset).animateTo(
@@ -524,7 +515,7 @@ private suspend fun settleAppBar(
524515
) {
525516
state.heightOffset = value
526517
}
527-
return Velocity(0f, velocity)
518+
return Velocity(0f, velocity - remainingVelocity)
528519
}
529520
}
530521
return Velocity(0f, velocity - remainingVelocity)

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

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,8 @@ import androidx.compose.animation.core.Spring.StiffnessMediumLow
99
import androidx.compose.animation.core.spring
1010
import androidx.compose.runtime.Composable
1111
import androidx.compose.runtime.Stable
12-
import androidx.compose.runtime.compositionLocalOf
1312
import androidx.compose.runtime.getValue
1413
import androidx.compose.runtime.mutableFloatStateOf
15-
import androidx.compose.runtime.mutableStateOf
1614
import androidx.compose.runtime.remember
1715
import androidx.compose.runtime.rememberUpdatedState
1816
import androidx.compose.runtime.setValue
@@ -123,7 +121,6 @@ fun Modifier.overScrollOutOfBound(
123121
): Modifier = composed {
124122
if (!isEnabled()) return@composed this
125123

126-
val overScrollState = LocalOverScrollState.current
127124
val pullToRefreshState = LocalPullToRefreshState.current
128125
val currentNestedScrollToParent by rememberUpdatedState(nestedScrollToParent)
129126
val currentScrollEasing by rememberUpdatedState(scrollEasing ?: DefaultParabolaScrollEasing)
@@ -149,8 +146,6 @@ fun Modifier.overScrollOutOfBound(
149146
}
150147

151148
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
152-
// Check if overScroll should be disabled for drop-down direction
153-
overScrollState.isOverScrollActive = abs(offset) > visibilityThreshold
154149
if (shouldBypassForPullToRefresh(available.y)) {
155150
return dispatcher.dispatchPreScroll(available, source)
156151
}
@@ -193,8 +188,6 @@ fun Modifier.overScrollOutOfBound(
193188
}
194189

195190
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
196-
// Check if overScroll should be disabled for drop-down direction
197-
overScrollState.isOverScrollActive = abs(offset) > visibilityThreshold
198191
if (shouldBypassForPullToRefresh(available.y)) {
199192
return dispatcher.dispatchPostScroll(consumed, available, source)
200193
}
@@ -215,8 +208,6 @@ fun Modifier.overScrollOutOfBound(
215208
}
216209

217210
override suspend fun onPreFling(available: Velocity): Velocity {
218-
// Check if overScroll should be disabled for drop-down direction
219-
overScrollState.isOverScrollActive = abs(offset) > visibilityThreshold
220211
if (shouldBypassForPullToRefresh(available.y)) {
221212
return dispatcher.dispatchPreFling(available)
222213
}
@@ -253,8 +244,6 @@ fun Modifier.overScrollOutOfBound(
253244
}
254245

255246
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
256-
// Check if overScroll should be disabled for drop-down direction
257-
overScrollState.isOverScrollActive = abs(offset) > visibilityThreshold
258247
if (shouldBypassForPullToRefresh(available.y)) {
259248
return dispatcher.dispatchPostFling(consumed, available)
260249
}
@@ -286,21 +275,3 @@ fun Modifier.overScrollOutOfBound(
286275
if (currentIsVertical) translationY = offset else translationX = offset
287276
}
288277
}
289-
290-
/**
291-
* OverScrollState is used to control the overscroll effect.
292-
*
293-
* @param isOverScrollActive Whether the overscroll effect is active.
294-
*/
295-
@Stable
296-
class OverScrollState {
297-
var isOverScrollActive by mutableStateOf(false)
298-
internal set
299-
}
300-
301-
/**
302-
* [LocalOverScrollState] is used to provide the [OverScrollState] instance to the composition.
303-
*
304-
* @see OverScrollState
305-
*/
306-
val LocalOverScrollState = compositionLocalOf { OverScrollState() }

0 commit comments

Comments
 (0)