Skip to content

Commit dd693ad

Browse files
committed
Update PullToRefresh.kt
1 parent 6456048 commit dd693ad

File tree

1 file changed

+67
-34
lines changed

1 file changed

+67
-34
lines changed

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

Lines changed: 67 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import androidx.compose.animation.core.RepeatMode
88
import androidx.compose.animation.core.animateFloat
99
import androidx.compose.animation.core.infiniteRepeatable
1010
import androidx.compose.animation.core.rememberInfiniteTransition
11-
import androidx.compose.animation.core.snap
1211
import androidx.compose.animation.core.tween
1312
import androidx.compose.foundation.Canvas
1413
import androidx.compose.foundation.layout.Arrangement
@@ -157,8 +156,6 @@ fun RefreshHeader(
157156

158157
val dragOffset = pullToRefreshState.dragOffsetAnimatable.value
159158
val thresholdOffset = pullToRefreshState.refreshThresholdOffset
160-
val maxDrag = pullToRefreshState.maxDragDistancePx
161-
val pullProgress = pullToRefreshState.pullProgress
162159
val rotation by animateRotation()
163160
val refreshCompleteAnimProgress = pullToRefreshState.refreshCompleteAnimProgress
164161

@@ -171,17 +168,17 @@ fun RefreshHeader(
171168
val refreshText by derivedStateOf {
172169
when (pullToRefreshState.refreshState) {
173170
RefreshState.Idle -> ""
174-
RefreshState.Pulling -> if (pullProgress > 0.5) refreshTexts[0] else ""
171+
RefreshState.Pulling -> if (pullToRefreshState.pullProgress > 0.5) refreshTexts[0] else ""
175172
RefreshState.ThresholdReached -> refreshTexts[1]
176173
RefreshState.Refreshing -> refreshTexts[2]
177174
RefreshState.RefreshComplete -> refreshTexts[3]
178175
}
179176
}
180177

181-
val textAlpha by derivedStateOf{
178+
val textAlpha by derivedStateOf {
182179
when (pullToRefreshState.refreshState) {
183180
RefreshState.Idle -> 0f
184-
RefreshState.Pulling -> if (pullProgress > 0.6f) (pullProgress - 0.5f) * 2f else 0f
181+
RefreshState.Pulling -> if (pullToRefreshState.pullProgress > 0.6f) (pullToRefreshState.pullProgress - 0.5f) * 2f else 0f
185182
RefreshState.RefreshComplete -> (1f - refreshCompleteAnimProgress * 1.8f).coerceAtLeast(0f)
186183
else -> 1f
187184
}
@@ -190,26 +187,23 @@ fun RefreshHeader(
190187
val sHeight = with(density) {
191188
when (pullToRefreshState.refreshState) {
192189
RefreshState.Idle -> 0.dp
193-
RefreshState.Pulling -> circleSize * pullProgress
190+
RefreshState.Pulling -> circleSize * pullToRefreshState.pullProgress
194191
RefreshState.ThresholdReached -> circleSize + (dragOffset - thresholdOffset).toDp()
195192
RefreshState.Refreshing -> circleSize
196193
RefreshState.RefreshComplete -> circleSize.coerceIn(0.dp, circleSize - circleSize * refreshCompleteAnimProgress)
197-
}.coerceAtMost(maxDrag.toDp())
194+
}
198195
}
199196

200-
201197
val headerHeight = with(density) {
202198
when (pullToRefreshState.refreshState) {
203199
RefreshState.Idle -> 0.dp
204-
RefreshState.Pulling -> (circleSize+36.dp) * pullProgress
205-
RefreshState.ThresholdReached -> (circleSize+36.dp) + (dragOffset - thresholdOffset).toDp()
206-
RefreshState.Refreshing -> (circleSize+36.dp)
207-
RefreshState.RefreshComplete -> (circleSize+36.dp).coerceIn(0.dp, (circleSize+36.dp) - (circleSize+36.dp) * refreshCompleteAnimProgress)
208-
}.coerceAtMost(maxDrag.toDp()+36.dp)
200+
RefreshState.Pulling -> (circleSize + 36.dp) * pullToRefreshState.pullProgress
201+
RefreshState.ThresholdReached -> (circleSize + 36.dp) + (dragOffset - thresholdOffset).toDp()
202+
RefreshState.Refreshing -> (circleSize + 36.dp)
203+
RefreshState.RefreshComplete -> (circleSize + 36.dp).coerceIn(0.dp, (circleSize + 36.dp) - (circleSize + 36.dp) * refreshCompleteAnimProgress)
204+
}
209205
}
210206

211-
212-
213207
// Header layout
214208
Column(
215209
modifier = modifier
@@ -225,7 +219,7 @@ fun RefreshHeader(
225219
val ringStrokeWidthPx = circleSize.toPx() / 11
226220
val indicatorRadiusPx = (size.minDimension / 2).coerceAtLeast(circleSize.toPx() / 3.5f)
227221
val center = Offset(circleSize.toPx() / 2, circleSize.toPx() / 1.8f)
228-
val alpha = (pullProgress-0.2f).coerceAtLeast(0f)
222+
val alpha = (pullToRefreshState.pullProgress - 0.2f).coerceAtLeast(0f)
229223

230224
when (pullToRefreshState.refreshState) {
231225
RefreshState.Idle -> return@RefreshContent
@@ -247,7 +241,7 @@ fun RefreshHeader(
247241
color = color,
248242
dragOffset = dragOffset,
249243
thresholdOffset = thresholdOffset,
250-
maxDrag = maxDrag
244+
maxDrag = pullToRefreshState.maxDragDistancePx
251245
)
252246

253247
RefreshState.Refreshing -> drawRefreshingState(
@@ -438,7 +432,7 @@ private fun DrawScope.drawRefreshCompleteState(
438432
refreshCompleteProgress: Float
439433
) {
440434
val animatedRadius = radius * ((1f - refreshCompleteProgress).coerceAtLeast(0.9f))
441-
val alphaColor = color.copy(alpha = (1f - refreshCompleteProgress-0.2f).coerceAtLeast(0f))
435+
val alphaColor = color.copy(alpha = (1f - refreshCompleteProgress - 0.2f).coerceAtLeast(0f))
442436

443437
drawCircle(
444438
color = alphaColor,
@@ -477,14 +471,14 @@ sealed class RefreshState {
477471
@Composable
478472
fun rememberPullToRefreshState(): PullToRefreshState {
479473
val coroutineScope = rememberCoroutineScope()
480-
val screenHeight = getWindowSize().height
474+
val screenHeight = getWindowSize().height.toFloat()
481475
val maxDragDistancePx = screenHeight * maxDragRatio
482476
val refreshThresholdOffset = maxDragDistancePx * thresholdRatio
483477

484478
return remember {
485479
PullToRefreshState(
486480
coroutineScope,
487-
maxDragDistancePx,
481+
screenHeight,
488482
refreshThresholdOffset
489483
)
490484
}
@@ -516,9 +510,16 @@ class PullToRefreshState(
516510
val isRefreshing: Boolean by derivedStateOf { refreshState is RefreshState.Refreshing }
517511
private var pointerReleased by mutableStateOf(false)
518512

513+
/** 是否正在回弹过渡中 */
514+
private var isRebounding by mutableStateOf(false)
515+
519516
/** Pull progress */
520517
val pullProgress: Float by derivedStateOf {
521-
(dragOffsetAnimatable.value / refreshThresholdOffset).coerceIn(0f, 1f)
518+
if (refreshThresholdOffset > 0f) {
519+
(dragOffsetAnimatable.value / refreshThresholdOffset).coerceIn(0f, 1f)
520+
} else {
521+
0f
522+
}
522523
}
523524
private val _refreshCompleteAnimProgress = mutableFloatStateOf(1f)
524525

@@ -562,6 +563,10 @@ class PullToRefreshState(
562563
pointerReleased = false
563564
}
564565

566+
internal fun onPointerRelease() {
567+
pointerReleased = true
568+
}
569+
565570
val pointerReleasedValue: Boolean get() = pointerReleased
566571

567572
fun completeRefreshing(block: suspend () -> Unit) {
@@ -627,6 +632,13 @@ class PullToRefreshState(
627632
}
628633

629634
return if (source == NestedScrollSource.UserInput && available.y < 0 && rawDragOffset > 0f) {
635+
if (isRebounding && dragOffsetAnimatable.isRunning) {
636+
coroutineScope.launch {
637+
dragOffsetAnimatable.stop()
638+
}
639+
isRebounding = false
640+
}
641+
630642
val delta = available.y.coerceAtLeast(-rawDragOffset)
631643
rawDragOffset += delta
632644
coroutineScope.launch {
@@ -644,8 +656,17 @@ class PullToRefreshState(
644656
overScrollState.isOverScrollActive || isRefreshingInProgress || refreshState == RefreshState.Refreshing || refreshState == RefreshState.RefreshComplete -> Offset.Zero
645657
source == NestedScrollSource.UserInput -> {
646658
if (available.y > 0f && consumed.y == 0f) {
647-
val newOffset = (rawDragOffset + available.y).coerceAtMost(maxDragDistancePx)
648-
val consumedY = newOffset - rawDragOffset
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
649670
rawDragOffset = newOffset
650671
coroutineScope.launch {
651672
dragOffsetAnimatable.snapTo(newOffset)
@@ -665,7 +686,7 @@ class PullToRefreshState(
665686
}
666687
}
667688

668-
suspend fun handlePointerReleased(
689+
fun handlePointerReleased(
669690
onRefresh: () -> Unit
670691
) {
671692
if (isRefreshingInProgress) {
@@ -692,23 +713,35 @@ class PullToRefreshState(
692713
}
693714
}
694715
} else {
695-
animateDragOffset(
696-
targetValue = 0f,
697-
animationSpec = snap()
698-
)
699-
rawDragOffset = 0f
716+
isRebounding = true
717+
coroutineScope.launch {
718+
try {
719+
animateDragOffset(
720+
targetValue = 0f,
721+
animationSpec = tween(
722+
durationMillis = 250,
723+
easing = CubicBezierEasing(0.33f, 0f, 0.67f, 1f)
724+
)
725+
)
726+
rawDragOffset = 0f
727+
} finally {
728+
isRebounding = false
729+
}
730+
}
700731
}
701732
resetPointerReleased()
702733
}
703734
}
704735

705-
fun onPointerRelease() {
706-
pointerReleased = true
736+
private fun calculateResistanceFactor(offset: Float): Float {
737+
if (offset < refreshThresholdOffset) return 1.0f
738+
val overThreshold = offset - refreshThresholdOffset
739+
return 1.0f / (1.0f + overThreshold / refreshThresholdOffset * 0.8f)
707740
}
708741
}
709742

710743
/** Maximum drag ratio */
711-
internal const val maxDragRatio = 1 / 5f
744+
internal const val maxDragRatio = 1 / 6f
712745

713746
/** Threshold ratio */
714747
internal const val thresholdRatio = 1 / 4f
@@ -745,5 +778,5 @@ object PullToRefreshDefaults {
745778
fontWeight = FontWeight.Bold,
746779
color = color
747780
)
748-
749781
}
782+

0 commit comments

Comments
 (0)