@@ -60,6 +60,8 @@ import top.yukonga.miuix.kmp.utils.OverScrollState
6060import top.yukonga.miuix.kmp.utils.getWindowSize
6161import kotlin.math.PI
6262import kotlin.math.cos
63+ import kotlin.math.max
64+ import kotlin.math.min
6365import kotlin.math.sin
6466
6567/* *
@@ -87,13 +89,12 @@ fun PullToRefresh(
8789 content : @Composable () -> Unit
8890) {
8991 LaunchedEffect (pullToRefreshState.rawDragOffset) {
90- pullToRefreshState.updateDragOffsetAnimatable ()
92+ pullToRefreshState.syncDragOffsetWithRawOffset ()
9193 }
9294 val overScrollState = LocalOverScrollState .current
9395 val nestedScrollConnection = remember(pullToRefreshState) {
9496 pullToRefreshState.createNestedScrollConnection(overScrollState)
9597 }
96-
9798 val pointerModifier = Modifier .pointerInput(Unit ) {
9899 awaitPointerEventScope {
99100 while (true ) {
@@ -107,11 +108,9 @@ fun PullToRefresh(
107108 }
108109 }
109110 }
110-
111111 LaunchedEffect (pullToRefreshState.pointerReleasedValue, pullToRefreshState.isRefreshing) {
112112 pullToRefreshState.handlePointerReleased(onRefresh)
113113 }
114-
115114 CompositionLocalProvider (LocalPullToRefreshState provides pullToRefreshState) {
116115 Box (
117116 modifier = modifier
@@ -153,7 +152,6 @@ fun RefreshHeader(
153152) {
154153 val hapticFeedback = LocalHapticFeedback .current
155154 val density = LocalDensity .current
156-
157155 val dragOffset = pullToRefreshState.dragOffsetAnimatable.value
158156 val thresholdOffset = pullToRefreshState.refreshThresholdOffset
159157 val rotation by animateRotation()
@@ -165,25 +163,27 @@ fun RefreshHeader(
165163 }
166164 }
167165
168- val refreshText by derivedStateOf {
169- when (pullToRefreshState.refreshState) {
170- RefreshState .Idle -> " "
171- RefreshState .Pulling -> if (pullToRefreshState.pullProgress > 0.5 ) refreshTexts[0 ] else " "
172- RefreshState .ThresholdReached -> refreshTexts[1 ]
173- RefreshState .Refreshing -> refreshTexts[2 ]
174- RefreshState .RefreshComplete -> refreshTexts[3 ]
166+ val refreshText by remember(pullToRefreshState.refreshState, pullToRefreshState.pullProgress) {
167+ derivedStateOf {
168+ when (pullToRefreshState.refreshState) {
169+ RefreshState .Idle -> " "
170+ RefreshState .Pulling -> if (pullToRefreshState.pullProgress > 0.5 ) refreshTexts[0 ] else " "
171+ RefreshState .ThresholdReached -> refreshTexts[1 ]
172+ RefreshState .Refreshing -> refreshTexts[2 ]
173+ RefreshState .RefreshComplete -> refreshTexts[3 ]
174+ }
175175 }
176176 }
177-
178- val textAlpha by derivedStateOf {
179- when (pullToRefreshState.refreshState) {
180- RefreshState .Idle -> 0f
181- RefreshState .Pulling -> if (pullToRefreshState.pullProgress > 0.6f ) (pullToRefreshState.pullProgress - 0.5f ) * 2f else 0f
182- RefreshState .RefreshComplete -> (1f - refreshCompleteAnimProgress * 1.8f ).coerceAtLeast(0f )
183- else -> 1f
177+ val textAlpha by remember(pullToRefreshState.refreshState, pullToRefreshState.pullProgress, refreshCompleteAnimProgress) {
178+ derivedStateOf {
179+ when (pullToRefreshState.refreshState) {
180+ RefreshState .Idle -> 0f
181+ RefreshState .Pulling -> if (pullToRefreshState.pullProgress > 0.6f ) (pullToRefreshState.pullProgress - 0.5f ) * 2f else 0f
182+ RefreshState .RefreshComplete -> (1f - refreshCompleteAnimProgress * 1.8f ).coerceAtLeast(0f )
183+ else -> 1f
184+ }
184185 }
185186 }
186-
187187 val sHeight = with (density) {
188188 when (pullToRefreshState.refreshState) {
189189 RefreshState .Idle -> 0 .dp
@@ -193,7 +193,6 @@ fun RefreshHeader(
193193 RefreshState .RefreshComplete -> circleSize.coerceIn(0 .dp, circleSize - circleSize * refreshCompleteAnimProgress)
194194 }
195195 }
196-
197196 val headerHeight = with (density) {
198197 when (pullToRefreshState.refreshState) {
199198 RefreshState .Idle -> 0 .dp
@@ -203,8 +202,6 @@ fun RefreshHeader(
203202 RefreshState .RefreshComplete -> (circleSize + 36 .dp).coerceIn(0 .dp, (circleSize + 36 .dp) - (circleSize + 36 .dp) * refreshCompleteAnimProgress)
204203 }
205204 }
206-
207- // Header layout
208205 Column (
209206 modifier = modifier
210207 .fillMaxWidth()
@@ -217,52 +214,47 @@ fun RefreshHeader(
217214 circleSize = circleSize
218215 ) {
219216 val ringStrokeWidthPx = circleSize.toPx() / 11
220- val indicatorRadiusPx = (size.minDimension / 2 ).coerceAtLeast( circleSize.toPx() / 3.5f )
217+ val indicatorRadiusPx = max (size.minDimension / 2 , circleSize.toPx() / 3.5f )
221218 val center = Offset (circleSize.toPx() / 2 , circleSize.toPx() / 1.8f )
222219 val alpha = (pullToRefreshState.pullProgress - 0.2f ).coerceAtLeast(0f )
223-
224220 when (pullToRefreshState.refreshState) {
225221 RefreshState .Idle -> return @RefreshContent
226- RefreshState .Pulling -> {
227- drawInitialState(
228- center = center,
229- radius = indicatorRadiusPx,
230- strokeWidth = ringStrokeWidthPx,
231- color = color,
232- alpha = alpha
233- )
234222
235- }
223+ RefreshState .Pulling -> drawInitialState(
224+ center,
225+ indicatorRadiusPx,
226+ ringStrokeWidthPx,
227+ color,
228+ alpha
229+ )
236230
237231 RefreshState .ThresholdReached -> drawThresholdExceededState(
238- center = center ,
239- radius = indicatorRadiusPx,
240- strokeWidth = ringStrokeWidthPx,
241- color = color ,
242- dragOffset = dragOffset ,
243- thresholdOffset = thresholdOffset ,
244- maxDrag = pullToRefreshState.maxDragDistancePx
232+ center,
233+ indicatorRadiusPx,
234+ ringStrokeWidthPx,
235+ color,
236+ dragOffset,
237+ thresholdOffset,
238+ pullToRefreshState.maxDragDistancePx
245239 )
246240
247241 RefreshState .Refreshing -> drawRefreshingState(
248- center = center ,
249- radius = indicatorRadiusPx,
250- strokeWidth = ringStrokeWidthPx,
251- color = color ,
252- rotation = rotation
242+ center,
243+ indicatorRadiusPx,
244+ ringStrokeWidthPx,
245+ color,
246+ rotation
253247 )
254248
255249 RefreshState .RefreshComplete -> drawRefreshCompleteState(
256- center = center ,
257- radius = indicatorRadiusPx,
258- strokeWidth = ringStrokeWidthPx,
259- color = color ,
260- refreshCompleteProgress = refreshCompleteAnimProgress
250+ center,
251+ indicatorRadiusPx,
252+ ringStrokeWidthPx,
253+ color,
254+ refreshCompleteAnimProgress
261255 )
262256 }
263257 }
264-
265- // Animated text with height and alpha
266258 Text (
267259 text = refreshText,
268260 style = refreshTextStyle,
@@ -325,10 +317,8 @@ private fun DrawScope.drawInitialState(
325317 color : Color ,
326318 alpha : Float
327319) {
328-
329- val alphaColor = color.copy(alpha = alpha)
330320 drawCircle(
331- color = alphaColor ,
321+ color = color.copy(alpha = alpha) ,
332322 radius = radius,
333323 center = center,
334324 style = Stroke (strokeWidth, cap = StrokeCap .Round )
@@ -347,17 +337,11 @@ private fun DrawScope.drawThresholdExceededState(
347337 thresholdOffset : Float ,
348338 maxDrag : Float
349339) {
350- val lineLength =
351- if (dragOffset > thresholdOffset) {
352- (dragOffset - thresholdOffset)
353- .coerceAtMost(maxDrag - thresholdOffset)
354- .coerceAtLeast(0f )
355- } else {
356- 0f
357- }
340+ val lineLength = if (dragOffset > thresholdOffset) {
341+ min(max(dragOffset - thresholdOffset, 0f ), maxDrag - thresholdOffset)
342+ } else 0f
358343 val topY = center.y
359344 val bottomY = center.y + lineLength
360-
361345 drawArc(
362346 color = color,
363347 startAngle = 180f ,
@@ -433,14 +417,12 @@ private fun DrawScope.drawRefreshCompleteState(
433417) {
434418 val animatedRadius = radius * ((1f - refreshCompleteProgress).coerceAtLeast(0.9f ))
435419 val alphaColor = color.copy(alpha = (1f - refreshCompleteProgress - 0.2f ).coerceAtLeast(0f ))
436-
437420 drawCircle(
438421 color = alphaColor,
439422 radius = animatedRadius,
440423 center = center,
441424 style = Stroke (strokeWidth, cap = StrokeCap .Round )
442425 )
443-
444426}
445427
446428/* *
@@ -474,7 +456,6 @@ fun rememberPullToRefreshState(): PullToRefreshState {
474456 val screenHeight = getWindowSize().height.toFloat()
475457 val maxDragDistancePx = screenHeight * maxDragRatio
476458 val refreshThresholdOffset = maxDragDistancePx * thresholdRatio
477-
478459 return remember {
479460 PullToRefreshState (
480461 coroutineScope,
@@ -510,16 +491,14 @@ class PullToRefreshState(
510491 val isRefreshing: Boolean by derivedStateOf { refreshState is RefreshState .Refreshing }
511492 private var pointerReleased by mutableStateOf(false )
512493
513- /* * 是否正在回弹过渡中 */
494+ /* * Whether it is rebounding */
514495 private var isRebounding by mutableStateOf(false )
515496
516497 /* * Pull progress */
517498 val pullProgress: Float by derivedStateOf {
518499 if (refreshThresholdOffset > 0f ) {
519500 (dragOffsetAnimatable.value / refreshThresholdOffset).coerceIn(0f , 1f )
520- } else {
521- 0f
522- }
501+ } else 0f
523502 }
524503 private val _refreshCompleteAnimProgress = mutableFloatStateOf(1f )
525504
@@ -546,7 +525,7 @@ class PullToRefreshState(
546525 internalRefreshState = RefreshState .Refreshing
547526 }
548527
549- suspend fun updateDragOffsetAnimatable () {
528+ suspend fun syncDragOffsetWithRawOffset () {
550529 if (! dragOffsetAnimatable.isRunning) {
551530 dragOffsetAnimatable.snapTo(rawDragOffset)
552531 }
@@ -588,9 +567,7 @@ class PullToRefreshState(
588567 block()
589568 } finally {
590569 internalRefreshState = RefreshState .RefreshComplete
591- launch {
592- startManualRefreshCompleteAnimation()
593- }
570+ launch { startManualRefreshCompleteAnimation() }
594571 }
595572 }
596573 }
@@ -619,76 +596,50 @@ class PullToRefreshState(
619596
620597 fun createNestedScrollConnection (
621598 overScrollState : OverScrollState
622- ): NestedScrollConnection =
623- object : NestedScrollConnection {
624- override fun onPreScroll (available : Offset , source : NestedScrollSource ): Offset {
625-
626- if (overScrollState.isOverScrollActive) {
627- return Offset .Zero
628- }
629-
630- if (isRefreshingInProgress || refreshState == RefreshState .Refreshing || refreshState == RefreshState .RefreshComplete ) {
631- return Offset .Zero
599+ ): NestedScrollConnection = object : NestedScrollConnection {
600+ override fun onPreScroll (available : Offset , source : NestedScrollSource ): Offset {
601+ if (overScrollState.isOverScrollActive) return Offset .Zero
602+ if (isRefreshingInProgress || refreshState == RefreshState .Refreshing || refreshState == RefreshState .RefreshComplete ) return Offset .Zero
603+ return if (source == NestedScrollSource .UserInput && available.y < 0 && rawDragOffset > 0f ) {
604+ if (isRebounding && dragOffsetAnimatable.isRunning) {
605+ coroutineScope.launch { dragOffsetAnimatable.stop() }
606+ isRebounding = false
632607 }
608+ val delta = available.y.coerceAtLeast(- rawDragOffset)
609+ rawDragOffset + = delta
610+ coroutineScope.launch { dragOffsetAnimatable.snapTo(rawDragOffset) }
611+ Offset (0f , delta)
612+ } else Offset .Zero
613+ }
633614
634- return if (source == NestedScrollSource .UserInput && available.y < 0 && rawDragOffset > 0f ) {
615+ override fun onPostScroll (consumed : Offset , available : Offset , source : NestedScrollSource ): Offset = when {
616+ overScrollState.isOverScrollActive || isRefreshingInProgress || refreshState == RefreshState .Refreshing || refreshState == RefreshState .RefreshComplete -> Offset .Zero
617+ source == NestedScrollSource .UserInput -> {
618+ if (available.y > 0f && consumed.y == 0f ) {
635619 if (isRebounding && dragOffsetAnimatable.isRunning) {
636- coroutineScope.launch {
637- dragOffsetAnimatable.stop()
638- }
620+ coroutineScope.launch { dragOffsetAnimatable.stop() }
639621 isRebounding = false
640622 }
641-
642- val delta = available.y.coerceAtLeast(- rawDragOffset)
643- rawDragOffset + = delta
644- coroutineScope.launch {
645- dragOffsetAnimatable.snapTo(rawDragOffset)
646- }
647- Offset (0f , delta)
623+ val resistanceFactor = calculateResistanceFactor(rawDragOffset)
624+ val effectiveY = available.y * resistanceFactor
625+ val newOffset = rawDragOffset + effectiveY
626+ val consumedY = effectiveY
627+ rawDragOffset = newOffset
628+ coroutineScope.launch { dragOffsetAnimatable.snapTo(newOffset) }
629+ Offset (0f , consumedY)
630+ } else if (available.y < 0f ) {
631+ val newOffset = max(rawDragOffset + available.y, 0f )
632+ val consumedY = rawDragOffset - newOffset
633+ rawDragOffset = newOffset
634+ Offset (0f , - consumedY)
648635 } else Offset .Zero
649636 }
650637
651- override fun onPostScroll (
652- consumed : Offset ,
653- available : Offset ,
654- source : NestedScrollSource
655- ): Offset = when {
656- overScrollState.isOverScrollActive || isRefreshingInProgress || refreshState == RefreshState .Refreshing || refreshState == RefreshState .RefreshComplete -> Offset .Zero
657- source == NestedScrollSource .UserInput -> {
658- if (available.y > 0f && consumed.y == 0f ) {
659- if (isRebounding && dragOffsetAnimatable.isRunning) {
660- coroutineScope.launch {
661- dragOffsetAnimatable.stop()
662- }
663- isRebounding = false
664- }
665-
666- val resistanceFactor = calculateResistanceFactor(rawDragOffset)
667- val effectiveY = available.y * resistanceFactor
668- val newOffset = rawDragOffset + effectiveY
669- val consumedY = effectiveY
670- rawDragOffset = newOffset
671- coroutineScope.launch {
672- dragOffsetAnimatable.snapTo(newOffset)
673- }
674- Offset (0f , consumedY)
675- } else if (available.y < 0f ) {
676- val newOffset = (rawDragOffset + available.y).coerceAtLeast(0f )
677- val consumedY = rawDragOffset - newOffset
678- rawDragOffset = newOffset
679- Offset (0f , - consumedY)
680- } else {
681- Offset .Zero
682- }
683- }
684-
685- else -> Offset .Zero
686- }
638+ else -> Offset .Zero
687639 }
640+ }
688641
689- fun handlePointerReleased (
690- onRefresh : () -> Unit
691- ) {
642+ fun handlePointerReleased (onRefresh : () -> Unit ) {
692643 if (isRefreshingInProgress) {
693644 resetPointerReleased()
694645 return
@@ -779,4 +730,3 @@ object PullToRefreshDefaults {
779730 color = color
780731 )
781732}
782-
0 commit comments