@@ -8,7 +8,6 @@ import androidx.compose.animation.core.RepeatMode
88import androidx.compose.animation.core.animateFloat
99import androidx.compose.animation.core.infiniteRepeatable
1010import androidx.compose.animation.core.rememberInfiniteTransition
11- import androidx.compose.animation.core.snap
1211import androidx.compose.animation.core.tween
1312import androidx.compose.foundation.Canvas
1413import 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
478472fun 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 */
714747internal 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