Skip to content

Commit 1cfdc4d

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

File tree

1 file changed

+87
-137
lines changed

1 file changed

+87
-137
lines changed

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

Lines changed: 87 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ import top.yukonga.miuix.kmp.utils.OverScrollState
6060
import top.yukonga.miuix.kmp.utils.getWindowSize
6161
import kotlin.math.PI
6262
import kotlin.math.cos
63+
import kotlin.math.max
64+
import kotlin.math.min
6365
import 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

Comments
 (0)