From 64560481c0dd2cc1f9169551547427247e798cf1 Mon Sep 17 00:00:00 2001 From: 25748 <2574822036@qq.com> Date: Fri, 18 Apr 2025 19:26:13 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E8=B0=83=E6=95=B4=E4=B8=8B=E6=8B=89?= =?UTF-8?q?=E5=8A=A8=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yukonga/miuix/kmp/basic/PullToRefresh.kt | 119 +++++++++--------- 1 file changed, 62 insertions(+), 57 deletions(-) diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/PullToRefresh.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/PullToRefresh.kt index 623dff34..8ca14df1 100644 --- a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/PullToRefresh.kt +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/PullToRefresh.kt @@ -1,10 +1,9 @@ package top.yukonga.miuix.kmp.basic -import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.CubicBezierEasing import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.LinearOutSlowInEasing import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable @@ -12,6 +11,7 @@ import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.snap import androidx.compose.animation.core.tween import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -178,21 +178,16 @@ fun RefreshHeader( } } - val textAlpha by derivedStateOf { + val textAlpha by derivedStateOf{ when (pullToRefreshState.refreshState) { - RefreshState.Pulling -> { - if (pullProgress > 0.5f) (pullProgress - 0.5f) * 2f else 0f - } - - RefreshState.RefreshComplete -> { - (1f - refreshCompleteAnimProgress * 1.2f).coerceAtLeast(0f) - } - + RefreshState.Idle -> 0f + RefreshState.Pulling -> if (pullProgress > 0.6f) (pullProgress - 0.5f) * 2f else 0f + RefreshState.RefreshComplete -> (1f - refreshCompleteAnimProgress * 1.8f).coerceAtLeast(0f) else -> 1f } } - val headerHeight = with(density) { + val sHeight = with(density) { when (pullToRefreshState.refreshState) { RefreshState.Idle -> 0.dp RefreshState.Pulling -> circleSize * pullProgress @@ -202,31 +197,47 @@ fun RefreshHeader( }.coerceAtMost(maxDrag.toDp()) } + + val headerHeight = with(density) { + when (pullToRefreshState.refreshState) { + RefreshState.Idle -> 0.dp + RefreshState.Pulling -> (circleSize+36.dp) * pullProgress + RefreshState.ThresholdReached -> (circleSize+36.dp) + (dragOffset - thresholdOffset).toDp() + RefreshState.Refreshing -> (circleSize+36.dp) + RefreshState.RefreshComplete -> (circleSize+36.dp).coerceIn(0.dp, (circleSize+36.dp) - (circleSize+36.dp) * refreshCompleteAnimProgress) + }.coerceAtMost(maxDrag.toDp()+36.dp) + } + + + + // Header layout Column( - modifier = modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally + modifier = modifier + .fillMaxWidth() + .height(headerHeight), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top ) { RefreshContent( - modifier = Modifier.height(headerHeight), + modifier = Modifier.height(sHeight), circleSize = circleSize ) { val ringStrokeWidthPx = circleSize.toPx() / 11 - val indicatorRadiusPx = size.minDimension / 2 - val center = Offset(size.width / 2, size.height / 1.8f) + val indicatorRadiusPx = (size.minDimension / 2).coerceAtLeast(circleSize.toPx() / 3.5f) + val center = Offset(circleSize.toPx() / 2, circleSize.toPx() / 1.8f) + val alpha = (pullProgress-0.2f).coerceAtLeast(0f) when (pullToRefreshState.refreshState) { RefreshState.Idle -> return@RefreshContent RefreshState.Pulling -> { - if (pullProgress > 0.3f) { - drawInitialState( - center = center, - radius = indicatorRadiusPx, - strokeWidth = ringStrokeWidthPx, - color = color, - alpha = pullProgress, - refreshProgress = pullProgress - ) - } + drawInitialState( + center = center, + radius = indicatorRadiusPx, + strokeWidth = ringStrokeWidthPx, + color = color, + alpha = alpha + ) + } RefreshState.ThresholdReached -> drawThresholdExceededState( @@ -257,20 +268,15 @@ fun RefreshHeader( } } - AnimatedVisibility( - visible = pullProgress >= 0.5f - && pullToRefreshState.refreshState != RefreshState.Idle - && pullToRefreshState.refreshState != RefreshState.RefreshComplete - ) { - Text( - text = refreshText, - style = refreshTextStyle, - color = color, - modifier = Modifier - .alpha(textAlpha) - .padding(top = 6.dp) - ) - } + // Animated text with height and alpha + Text( + text = refreshText, + style = refreshTextStyle, + color = color, + modifier = Modifier + .padding(top = 6.dp) + .alpha(textAlpha) + ) } } @@ -323,13 +329,13 @@ private fun DrawScope.drawInitialState( radius: Float, strokeWidth: Float, color: Color, - alpha: Float, - refreshProgress: Float + alpha: Float ) { + val alphaColor = color.copy(alpha = alpha) drawCircle( color = alphaColor, - radius = radius * refreshProgress, + radius = radius, center = center, style = Stroke(strokeWidth, cap = StrokeCap.Round) ) @@ -431,17 +437,16 @@ private fun DrawScope.drawRefreshCompleteState( color: Color, refreshCompleteProgress: Float ) { - val animatedRadius = radius * (1f - refreshCompleteProgress) - val alphaColor = color.copy(alpha = 1f - refreshCompleteProgress) - - if (animatedRadius > 0) { - drawCircle( - color = alphaColor, - radius = animatedRadius, - center = center, - style = Stroke(strokeWidth, cap = StrokeCap.Round) - ) - } + val animatedRadius = radius * ((1f - refreshCompleteProgress).coerceAtLeast(0.9f)) + val alphaColor = color.copy(alpha = (1f - refreshCompleteProgress-0.2f).coerceAtLeast(0f)) + + drawCircle( + color = alphaColor, + radius = animatedRadius, + center = center, + style = Stroke(strokeWidth, cap = StrokeCap.Round) + ) + } /** @@ -592,7 +597,7 @@ class PullToRefreshState( targetValue = 1f, animationSpec = tween( durationMillis = 200, - easing = LinearOutSlowInEasing + easing = CubicBezierEasing(0f, 0f, 0f, 0.37f) ) ) { _refreshCompleteAnimProgress.floatValue = this.value @@ -676,7 +681,7 @@ class PullToRefreshState( targetValue = refreshThresholdOffset, animationSpec = tween( durationMillis = 200, - easing = LinearOutSlowInEasing + easing = CubicBezierEasing(0f, 0f, 0f, 0.37f) ) ) rawDragOffset = refreshThresholdOffset From dd693ad720f99595168b8d6af41a43c97489bf07 Mon Sep 17 00:00:00 2001 From: YuKongA <70465933+YuKongA@users.noreply.github.com> Date: Fri, 18 Apr 2025 20:31:13 +0800 Subject: [PATCH 2/3] Update PullToRefresh.kt --- .../yukonga/miuix/kmp/basic/PullToRefresh.kt | 101 ++++++++++++------ 1 file changed, 67 insertions(+), 34 deletions(-) diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/PullToRefresh.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/PullToRefresh.kt index 8ca14df1..f41430a1 100644 --- a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/PullToRefresh.kt +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/PullToRefresh.kt @@ -8,7 +8,6 @@ import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.snap import androidx.compose.animation.core.tween import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.Arrangement @@ -157,8 +156,6 @@ fun RefreshHeader( val dragOffset = pullToRefreshState.dragOffsetAnimatable.value val thresholdOffset = pullToRefreshState.refreshThresholdOffset - val maxDrag = pullToRefreshState.maxDragDistancePx - val pullProgress = pullToRefreshState.pullProgress val rotation by animateRotation() val refreshCompleteAnimProgress = pullToRefreshState.refreshCompleteAnimProgress @@ -171,17 +168,17 @@ fun RefreshHeader( val refreshText by derivedStateOf { when (pullToRefreshState.refreshState) { RefreshState.Idle -> "" - RefreshState.Pulling -> if (pullProgress > 0.5) refreshTexts[0] else "" + RefreshState.Pulling -> if (pullToRefreshState.pullProgress > 0.5) refreshTexts[0] else "" RefreshState.ThresholdReached -> refreshTexts[1] RefreshState.Refreshing -> refreshTexts[2] RefreshState.RefreshComplete -> refreshTexts[3] } } - val textAlpha by derivedStateOf{ + val textAlpha by derivedStateOf { when (pullToRefreshState.refreshState) { RefreshState.Idle -> 0f - RefreshState.Pulling -> if (pullProgress > 0.6f) (pullProgress - 0.5f) * 2f else 0f + RefreshState.Pulling -> if (pullToRefreshState.pullProgress > 0.6f) (pullToRefreshState.pullProgress - 0.5f) * 2f else 0f RefreshState.RefreshComplete -> (1f - refreshCompleteAnimProgress * 1.8f).coerceAtLeast(0f) else -> 1f } @@ -190,26 +187,23 @@ fun RefreshHeader( val sHeight = with(density) { when (pullToRefreshState.refreshState) { RefreshState.Idle -> 0.dp - RefreshState.Pulling -> circleSize * pullProgress + RefreshState.Pulling -> circleSize * pullToRefreshState.pullProgress RefreshState.ThresholdReached -> circleSize + (dragOffset - thresholdOffset).toDp() RefreshState.Refreshing -> circleSize RefreshState.RefreshComplete -> circleSize.coerceIn(0.dp, circleSize - circleSize * refreshCompleteAnimProgress) - }.coerceAtMost(maxDrag.toDp()) + } } - val headerHeight = with(density) { when (pullToRefreshState.refreshState) { RefreshState.Idle -> 0.dp - RefreshState.Pulling -> (circleSize+36.dp) * pullProgress - RefreshState.ThresholdReached -> (circleSize+36.dp) + (dragOffset - thresholdOffset).toDp() - RefreshState.Refreshing -> (circleSize+36.dp) - RefreshState.RefreshComplete -> (circleSize+36.dp).coerceIn(0.dp, (circleSize+36.dp) - (circleSize+36.dp) * refreshCompleteAnimProgress) - }.coerceAtMost(maxDrag.toDp()+36.dp) + RefreshState.Pulling -> (circleSize + 36.dp) * pullToRefreshState.pullProgress + RefreshState.ThresholdReached -> (circleSize + 36.dp) + (dragOffset - thresholdOffset).toDp() + RefreshState.Refreshing -> (circleSize + 36.dp) + RefreshState.RefreshComplete -> (circleSize + 36.dp).coerceIn(0.dp, (circleSize + 36.dp) - (circleSize + 36.dp) * refreshCompleteAnimProgress) + } } - - // Header layout Column( modifier = modifier @@ -225,7 +219,7 @@ fun RefreshHeader( val ringStrokeWidthPx = circleSize.toPx() / 11 val indicatorRadiusPx = (size.minDimension / 2).coerceAtLeast(circleSize.toPx() / 3.5f) val center = Offset(circleSize.toPx() / 2, circleSize.toPx() / 1.8f) - val alpha = (pullProgress-0.2f).coerceAtLeast(0f) + val alpha = (pullToRefreshState.pullProgress - 0.2f).coerceAtLeast(0f) when (pullToRefreshState.refreshState) { RefreshState.Idle -> return@RefreshContent @@ -247,7 +241,7 @@ fun RefreshHeader( color = color, dragOffset = dragOffset, thresholdOffset = thresholdOffset, - maxDrag = maxDrag + maxDrag = pullToRefreshState.maxDragDistancePx ) RefreshState.Refreshing -> drawRefreshingState( @@ -438,7 +432,7 @@ private fun DrawScope.drawRefreshCompleteState( refreshCompleteProgress: Float ) { val animatedRadius = radius * ((1f - refreshCompleteProgress).coerceAtLeast(0.9f)) - val alphaColor = color.copy(alpha = (1f - refreshCompleteProgress-0.2f).coerceAtLeast(0f)) + val alphaColor = color.copy(alpha = (1f - refreshCompleteProgress - 0.2f).coerceAtLeast(0f)) drawCircle( color = alphaColor, @@ -477,14 +471,14 @@ sealed class RefreshState { @Composable fun rememberPullToRefreshState(): PullToRefreshState { val coroutineScope = rememberCoroutineScope() - val screenHeight = getWindowSize().height + val screenHeight = getWindowSize().height.toFloat() val maxDragDistancePx = screenHeight * maxDragRatio val refreshThresholdOffset = maxDragDistancePx * thresholdRatio return remember { PullToRefreshState( coroutineScope, - maxDragDistancePx, + screenHeight, refreshThresholdOffset ) } @@ -516,9 +510,16 @@ class PullToRefreshState( val isRefreshing: Boolean by derivedStateOf { refreshState is RefreshState.Refreshing } private var pointerReleased by mutableStateOf(false) + /** 是否正在回弹过渡中 */ + private var isRebounding by mutableStateOf(false) + /** Pull progress */ val pullProgress: Float by derivedStateOf { - (dragOffsetAnimatable.value / refreshThresholdOffset).coerceIn(0f, 1f) + if (refreshThresholdOffset > 0f) { + (dragOffsetAnimatable.value / refreshThresholdOffset).coerceIn(0f, 1f) + } else { + 0f + } } private val _refreshCompleteAnimProgress = mutableFloatStateOf(1f) @@ -562,6 +563,10 @@ class PullToRefreshState( pointerReleased = false } + internal fun onPointerRelease() { + pointerReleased = true + } + val pointerReleasedValue: Boolean get() = pointerReleased fun completeRefreshing(block: suspend () -> Unit) { @@ -627,6 +632,13 @@ class PullToRefreshState( } return if (source == NestedScrollSource.UserInput && available.y < 0 && rawDragOffset > 0f) { + if (isRebounding && dragOffsetAnimatable.isRunning) { + coroutineScope.launch { + dragOffsetAnimatable.stop() + } + isRebounding = false + } + val delta = available.y.coerceAtLeast(-rawDragOffset) rawDragOffset += delta coroutineScope.launch { @@ -644,8 +656,17 @@ class PullToRefreshState( overScrollState.isOverScrollActive || isRefreshingInProgress || refreshState == RefreshState.Refreshing || refreshState == RefreshState.RefreshComplete -> Offset.Zero source == NestedScrollSource.UserInput -> { if (available.y > 0f && consumed.y == 0f) { - val newOffset = (rawDragOffset + available.y).coerceAtMost(maxDragDistancePx) - val consumedY = newOffset - rawDragOffset + if (isRebounding && dragOffsetAnimatable.isRunning) { + coroutineScope.launch { + dragOffsetAnimatable.stop() + } + isRebounding = false + } + + val resistanceFactor = calculateResistanceFactor(rawDragOffset) + val effectiveY = available.y * resistanceFactor + val newOffset = rawDragOffset + effectiveY + val consumedY = effectiveY rawDragOffset = newOffset coroutineScope.launch { dragOffsetAnimatable.snapTo(newOffset) @@ -665,7 +686,7 @@ class PullToRefreshState( } } - suspend fun handlePointerReleased( + fun handlePointerReleased( onRefresh: () -> Unit ) { if (isRefreshingInProgress) { @@ -692,23 +713,35 @@ class PullToRefreshState( } } } else { - animateDragOffset( - targetValue = 0f, - animationSpec = snap() - ) - rawDragOffset = 0f + isRebounding = true + coroutineScope.launch { + try { + animateDragOffset( + targetValue = 0f, + animationSpec = tween( + durationMillis = 250, + easing = CubicBezierEasing(0.33f, 0f, 0.67f, 1f) + ) + ) + rawDragOffset = 0f + } finally { + isRebounding = false + } + } } resetPointerReleased() } } - fun onPointerRelease() { - pointerReleased = true + private fun calculateResistanceFactor(offset: Float): Float { + if (offset < refreshThresholdOffset) return 1.0f + val overThreshold = offset - refreshThresholdOffset + return 1.0f / (1.0f + overThreshold / refreshThresholdOffset * 0.8f) } } /** Maximum drag ratio */ -internal const val maxDragRatio = 1 / 5f +internal const val maxDragRatio = 1 / 6f /** Threshold ratio */ internal const val thresholdRatio = 1 / 4f @@ -745,5 +778,5 @@ object PullToRefreshDefaults { fontWeight = FontWeight.Bold, color = color ) - } + From 1cfdc4d294e9f392e44f1f9f9190ceb5844de545 Mon Sep 17 00:00:00 2001 From: YuKongA <70465933+YuKongA@users.noreply.github.com> Date: Fri, 18 Apr 2025 21:06:42 +0800 Subject: [PATCH 3/3] Update PullToRefresh.kt --- .../yukonga/miuix/kmp/basic/PullToRefresh.kt | 224 +++++++----------- 1 file changed, 87 insertions(+), 137 deletions(-) diff --git a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/PullToRefresh.kt b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/PullToRefresh.kt index f41430a1..8d360334 100644 --- a/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/PullToRefresh.kt +++ b/miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/PullToRefresh.kt @@ -60,6 +60,8 @@ import top.yukonga.miuix.kmp.utils.OverScrollState import top.yukonga.miuix.kmp.utils.getWindowSize import kotlin.math.PI import kotlin.math.cos +import kotlin.math.max +import kotlin.math.min import kotlin.math.sin /** @@ -87,13 +89,12 @@ fun PullToRefresh( content: @Composable () -> Unit ) { LaunchedEffect(pullToRefreshState.rawDragOffset) { - pullToRefreshState.updateDragOffsetAnimatable() + pullToRefreshState.syncDragOffsetWithRawOffset() } val overScrollState = LocalOverScrollState.current val nestedScrollConnection = remember(pullToRefreshState) { pullToRefreshState.createNestedScrollConnection(overScrollState) } - val pointerModifier = Modifier.pointerInput(Unit) { awaitPointerEventScope { while (true) { @@ -107,11 +108,9 @@ fun PullToRefresh( } } } - LaunchedEffect(pullToRefreshState.pointerReleasedValue, pullToRefreshState.isRefreshing) { pullToRefreshState.handlePointerReleased(onRefresh) } - CompositionLocalProvider(LocalPullToRefreshState provides pullToRefreshState) { Box( modifier = modifier @@ -153,7 +152,6 @@ fun RefreshHeader( ) { val hapticFeedback = LocalHapticFeedback.current val density = LocalDensity.current - val dragOffset = pullToRefreshState.dragOffsetAnimatable.value val thresholdOffset = pullToRefreshState.refreshThresholdOffset val rotation by animateRotation() @@ -165,25 +163,27 @@ fun RefreshHeader( } } - val refreshText by derivedStateOf { - when (pullToRefreshState.refreshState) { - RefreshState.Idle -> "" - RefreshState.Pulling -> if (pullToRefreshState.pullProgress > 0.5) refreshTexts[0] else "" - RefreshState.ThresholdReached -> refreshTexts[1] - RefreshState.Refreshing -> refreshTexts[2] - RefreshState.RefreshComplete -> refreshTexts[3] + val refreshText by remember(pullToRefreshState.refreshState, pullToRefreshState.pullProgress) { + derivedStateOf { + when (pullToRefreshState.refreshState) { + RefreshState.Idle -> "" + RefreshState.Pulling -> if (pullToRefreshState.pullProgress > 0.5) refreshTexts[0] else "" + RefreshState.ThresholdReached -> refreshTexts[1] + RefreshState.Refreshing -> refreshTexts[2] + RefreshState.RefreshComplete -> refreshTexts[3] + } } } - - val textAlpha by derivedStateOf { - when (pullToRefreshState.refreshState) { - RefreshState.Idle -> 0f - RefreshState.Pulling -> if (pullToRefreshState.pullProgress > 0.6f) (pullToRefreshState.pullProgress - 0.5f) * 2f else 0f - RefreshState.RefreshComplete -> (1f - refreshCompleteAnimProgress * 1.8f).coerceAtLeast(0f) - else -> 1f + val textAlpha by remember(pullToRefreshState.refreshState, pullToRefreshState.pullProgress, refreshCompleteAnimProgress) { + derivedStateOf { + when (pullToRefreshState.refreshState) { + RefreshState.Idle -> 0f + RefreshState.Pulling -> if (pullToRefreshState.pullProgress > 0.6f) (pullToRefreshState.pullProgress - 0.5f) * 2f else 0f + RefreshState.RefreshComplete -> (1f - refreshCompleteAnimProgress * 1.8f).coerceAtLeast(0f) + else -> 1f + } } } - val sHeight = with(density) { when (pullToRefreshState.refreshState) { RefreshState.Idle -> 0.dp @@ -193,7 +193,6 @@ fun RefreshHeader( RefreshState.RefreshComplete -> circleSize.coerceIn(0.dp, circleSize - circleSize * refreshCompleteAnimProgress) } } - val headerHeight = with(density) { when (pullToRefreshState.refreshState) { RefreshState.Idle -> 0.dp @@ -203,8 +202,6 @@ fun RefreshHeader( RefreshState.RefreshComplete -> (circleSize + 36.dp).coerceIn(0.dp, (circleSize + 36.dp) - (circleSize + 36.dp) * refreshCompleteAnimProgress) } } - - // Header layout Column( modifier = modifier .fillMaxWidth() @@ -217,52 +214,47 @@ fun RefreshHeader( circleSize = circleSize ) { val ringStrokeWidthPx = circleSize.toPx() / 11 - val indicatorRadiusPx = (size.minDimension / 2).coerceAtLeast(circleSize.toPx() / 3.5f) + val indicatorRadiusPx = max(size.minDimension / 2, circleSize.toPx() / 3.5f) val center = Offset(circleSize.toPx() / 2, circleSize.toPx() / 1.8f) val alpha = (pullToRefreshState.pullProgress - 0.2f).coerceAtLeast(0f) - when (pullToRefreshState.refreshState) { RefreshState.Idle -> return@RefreshContent - RefreshState.Pulling -> { - drawInitialState( - center = center, - radius = indicatorRadiusPx, - strokeWidth = ringStrokeWidthPx, - color = color, - alpha = alpha - ) - } + RefreshState.Pulling -> drawInitialState( + center, + indicatorRadiusPx, + ringStrokeWidthPx, + color, + alpha + ) RefreshState.ThresholdReached -> drawThresholdExceededState( - center = center, - radius = indicatorRadiusPx, - strokeWidth = ringStrokeWidthPx, - color = color, - dragOffset = dragOffset, - thresholdOffset = thresholdOffset, - maxDrag = pullToRefreshState.maxDragDistancePx + center, + indicatorRadiusPx, + ringStrokeWidthPx, + color, + dragOffset, + thresholdOffset, + pullToRefreshState.maxDragDistancePx ) RefreshState.Refreshing -> drawRefreshingState( - center = center, - radius = indicatorRadiusPx, - strokeWidth = ringStrokeWidthPx, - color = color, - rotation = rotation + center, + indicatorRadiusPx, + ringStrokeWidthPx, + color, + rotation ) RefreshState.RefreshComplete -> drawRefreshCompleteState( - center = center, - radius = indicatorRadiusPx, - strokeWidth = ringStrokeWidthPx, - color = color, - refreshCompleteProgress = refreshCompleteAnimProgress + center, + indicatorRadiusPx, + ringStrokeWidthPx, + color, + refreshCompleteAnimProgress ) } } - - // Animated text with height and alpha Text( text = refreshText, style = refreshTextStyle, @@ -325,10 +317,8 @@ private fun DrawScope.drawInitialState( color: Color, alpha: Float ) { - - val alphaColor = color.copy(alpha = alpha) drawCircle( - color = alphaColor, + color = color.copy(alpha = alpha), radius = radius, center = center, style = Stroke(strokeWidth, cap = StrokeCap.Round) @@ -347,17 +337,11 @@ private fun DrawScope.drawThresholdExceededState( thresholdOffset: Float, maxDrag: Float ) { - val lineLength = - if (dragOffset > thresholdOffset) { - (dragOffset - thresholdOffset) - .coerceAtMost(maxDrag - thresholdOffset) - .coerceAtLeast(0f) - } else { - 0f - } + val lineLength = if (dragOffset > thresholdOffset) { + min(max(dragOffset - thresholdOffset, 0f), maxDrag - thresholdOffset) + } else 0f val topY = center.y val bottomY = center.y + lineLength - drawArc( color = color, startAngle = 180f, @@ -433,14 +417,12 @@ private fun DrawScope.drawRefreshCompleteState( ) { val animatedRadius = radius * ((1f - refreshCompleteProgress).coerceAtLeast(0.9f)) val alphaColor = color.copy(alpha = (1f - refreshCompleteProgress - 0.2f).coerceAtLeast(0f)) - drawCircle( color = alphaColor, radius = animatedRadius, center = center, style = Stroke(strokeWidth, cap = StrokeCap.Round) ) - } /** @@ -474,7 +456,6 @@ fun rememberPullToRefreshState(): PullToRefreshState { val screenHeight = getWindowSize().height.toFloat() val maxDragDistancePx = screenHeight * maxDragRatio val refreshThresholdOffset = maxDragDistancePx * thresholdRatio - return remember { PullToRefreshState( coroutineScope, @@ -510,16 +491,14 @@ class PullToRefreshState( val isRefreshing: Boolean by derivedStateOf { refreshState is RefreshState.Refreshing } private var pointerReleased by mutableStateOf(false) - /** 是否正在回弹过渡中 */ + /** Whether it is rebounding */ private var isRebounding by mutableStateOf(false) /** Pull progress */ val pullProgress: Float by derivedStateOf { if (refreshThresholdOffset > 0f) { (dragOffsetAnimatable.value / refreshThresholdOffset).coerceIn(0f, 1f) - } else { - 0f - } + } else 0f } private val _refreshCompleteAnimProgress = mutableFloatStateOf(1f) @@ -546,7 +525,7 @@ class PullToRefreshState( internalRefreshState = RefreshState.Refreshing } - suspend fun updateDragOffsetAnimatable() { + suspend fun syncDragOffsetWithRawOffset() { if (!dragOffsetAnimatable.isRunning) { dragOffsetAnimatable.snapTo(rawDragOffset) } @@ -588,9 +567,7 @@ class PullToRefreshState( block() } finally { internalRefreshState = RefreshState.RefreshComplete - launch { - startManualRefreshCompleteAnimation() - } + launch { startManualRefreshCompleteAnimation() } } } } @@ -619,76 +596,50 @@ class PullToRefreshState( fun createNestedScrollConnection( overScrollState: OverScrollState - ): NestedScrollConnection = - object : NestedScrollConnection { - override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { - - if (overScrollState.isOverScrollActive) { - return Offset.Zero - } - - if (isRefreshingInProgress || refreshState == RefreshState.Refreshing || refreshState == RefreshState.RefreshComplete) { - return Offset.Zero + ): NestedScrollConnection = object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + if (overScrollState.isOverScrollActive) return Offset.Zero + if (isRefreshingInProgress || refreshState == RefreshState.Refreshing || refreshState == RefreshState.RefreshComplete) return Offset.Zero + return if (source == NestedScrollSource.UserInput && available.y < 0 && rawDragOffset > 0f) { + if (isRebounding && dragOffsetAnimatable.isRunning) { + coroutineScope.launch { dragOffsetAnimatable.stop() } + isRebounding = false } + val delta = available.y.coerceAtLeast(-rawDragOffset) + rawDragOffset += delta + coroutineScope.launch { dragOffsetAnimatable.snapTo(rawDragOffset) } + Offset(0f, delta) + } else Offset.Zero + } - return if (source == NestedScrollSource.UserInput && available.y < 0 && rawDragOffset > 0f) { + override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset = when { + overScrollState.isOverScrollActive || isRefreshingInProgress || refreshState == RefreshState.Refreshing || refreshState == RefreshState.RefreshComplete -> Offset.Zero + source == NestedScrollSource.UserInput -> { + if (available.y > 0f && consumed.y == 0f) { if (isRebounding && dragOffsetAnimatable.isRunning) { - coroutineScope.launch { - dragOffsetAnimatable.stop() - } + coroutineScope.launch { dragOffsetAnimatable.stop() } isRebounding = false } - - val delta = available.y.coerceAtLeast(-rawDragOffset) - rawDragOffset += delta - coroutineScope.launch { - dragOffsetAnimatable.snapTo(rawDragOffset) - } - Offset(0f, delta) + val resistanceFactor = calculateResistanceFactor(rawDragOffset) + val effectiveY = available.y * resistanceFactor + val newOffset = rawDragOffset + effectiveY + val consumedY = effectiveY + rawDragOffset = newOffset + coroutineScope.launch { dragOffsetAnimatable.snapTo(newOffset) } + Offset(0f, consumedY) + } else if (available.y < 0f) { + val newOffset = max(rawDragOffset + available.y, 0f) + val consumedY = rawDragOffset - newOffset + rawDragOffset = newOffset + Offset(0f, -consumedY) } else Offset.Zero } - override fun onPostScroll( - consumed: Offset, - available: Offset, - source: NestedScrollSource - ): Offset = when { - overScrollState.isOverScrollActive || isRefreshingInProgress || refreshState == RefreshState.Refreshing || refreshState == RefreshState.RefreshComplete -> Offset.Zero - source == NestedScrollSource.UserInput -> { - if (available.y > 0f && consumed.y == 0f) { - if (isRebounding && dragOffsetAnimatable.isRunning) { - coroutineScope.launch { - dragOffsetAnimatable.stop() - } - isRebounding = false - } - - val resistanceFactor = calculateResistanceFactor(rawDragOffset) - val effectiveY = available.y * resistanceFactor - val newOffset = rawDragOffset + effectiveY - val consumedY = effectiveY - rawDragOffset = newOffset - coroutineScope.launch { - dragOffsetAnimatable.snapTo(newOffset) - } - Offset(0f, consumedY) - } else if (available.y < 0f) { - val newOffset = (rawDragOffset + available.y).coerceAtLeast(0f) - val consumedY = rawDragOffset - newOffset - rawDragOffset = newOffset - Offset(0f, -consumedY) - } else { - Offset.Zero - } - } - - else -> Offset.Zero - } + else -> Offset.Zero } + } - fun handlePointerReleased( - onRefresh: () -> Unit - ) { + fun handlePointerReleased(onRefresh: () -> Unit) { if (isRefreshingInProgress) { resetPointerReleased() return @@ -779,4 +730,3 @@ object PullToRefreshDefaults { color = color ) } -