Skip to content

Commit d5f2d75

Browse files
committed
library: Optimize PullToRefresh component
* Add haptic feedback when pull down to refresh position * Fixed the issue of occasional refresh not being executed * Fixed the maximum pull down distance
1 parent 6025309 commit d5f2d75

File tree

1 file changed

+62
-45
lines changed

1 file changed

+62
-45
lines changed

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

Lines changed: 62 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import androidx.compose.animation.core.Animatable
77
import androidx.compose.animation.core.AnimationSpec
88
import androidx.compose.animation.core.Easing
99
import androidx.compose.animation.core.LinearEasing
10+
import androidx.compose.animation.core.LinearOutSlowInEasing
1011
import androidx.compose.animation.core.RepeatMode
1112
import androidx.compose.animation.core.animateFloat
1213
import androidx.compose.animation.core.infiniteRepeatable
@@ -41,11 +42,13 @@ import androidx.compose.ui.graphics.Color
4142
import androidx.compose.ui.graphics.StrokeCap
4243
import androidx.compose.ui.graphics.drawscope.DrawScope
4344
import androidx.compose.ui.graphics.drawscope.Stroke
45+
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
4446
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
4547
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
4648
import androidx.compose.ui.input.nestedscroll.nestedScroll
4749
import androidx.compose.ui.input.pointer.pointerInput
4850
import androidx.compose.ui.platform.LocalDensity
51+
import androidx.compose.ui.platform.LocalHapticFeedback
4952
import androidx.compose.ui.text.TextStyle
5053
import androidx.compose.ui.text.font.FontWeight
5154
import androidx.compose.ui.unit.Dp
@@ -68,7 +71,8 @@ import kotlin.time.TimeSource
6871
* @param pullToRefreshState pullToRefreshState
6972
* @param color The color of the refresh indicator.
7073
* @param circleSize The size of the refresh indicator circle.
71-
* @param textStyle The style of the refresh text.
74+
* @param refreshTexts The texts to show when refreshing.
75+
* @param refreshTextStyle The style of the refresh text.
7276
* @param onRefresh The callback to be called when the refresh is triggered.
7377
* @param content the content to be shown when the [PullToRefresh] is expanded.
7478
*
@@ -79,7 +83,8 @@ fun PullToRefresh(
7983
pullToRefreshState: PullToRefreshState,
8084
color: Color = PullToRefreshDefaults.color,
8185
circleSize: Dp = PullToRefreshDefaults.circleSize,
82-
textStyle: TextStyle = PullToRefreshDefaults.textStyle,
86+
refreshTexts: List<String> = PullToRefreshDefaults.refreshTexts,
87+
refreshTextStyle: TextStyle = PullToRefreshDefaults.refreshTextStyle,
8388
onRefresh: () -> Unit = {},
8489
content: @Composable () -> Unit
8590
) {
@@ -98,6 +103,8 @@ fun PullToRefresh(
98103
if (event.changes.all { !it.pressed }) {
99104
pullToRefreshState.onPointerRelease()
100105
continue
106+
} else {
107+
pullToRefreshState.resetPointerReleased()
101108
}
102109
}
103110
}
@@ -117,38 +124,65 @@ fun PullToRefresh(
117124
pullToRefreshState = pullToRefreshState,
118125
circleSize = circleSize,
119126
color = color,
120-
textStyle = textStyle
127+
refreshTexts = refreshTexts,
128+
refreshTextStyle = refreshTextStyle
121129
)
122130
content()
123131
}
124132
}
125133
}
126134

127135
/**
128-
* 刷新头部
136+
* Refresh header
129137
*
130-
* @param modifier 修饰符
131-
* @param pullToRefreshState 下拉刷新状态
132-
* @param circleSize 指示器圆圈大小
133-
* @param color 指示器颜色
134-
* @param textStyle 刷新文本样式
138+
* @param modifier The modifier to be applied to the [RefreshHeader]
139+
* @param pullToRefreshState The state of the pull-to-refresh
140+
* @param circleSize The size of the refresh indicator circle
141+
* @param color The color of the refresh indicator
142+
* @param refreshTexts The texts to show when refreshing
143+
* @param refreshTextStyle The style of the refresh text
135144
*/
136145
@Composable
137146
fun RefreshHeader(
138147
modifier: Modifier = Modifier,
139148
pullToRefreshState: PullToRefreshState,
140149
circleSize: Dp,
141150
color: Color,
142-
textStyle: TextStyle
151+
refreshTexts: List<String>,
152+
refreshTextStyle: TextStyle
143153
) {
154+
val hapticFeedback = LocalHapticFeedback.current
155+
val density = LocalDensity.current
156+
144157
val dragOffset = pullToRefreshState.dragOffsetAnimatable.value
145158
val thresholdOffset = pullToRefreshState.refreshThresholdOffset
146159
val maxDrag = pullToRefreshState.maxDragDistancePx
147-
val density = LocalDensity.current
148160
val pullProgress = pullToRefreshState.pullProgress
149161
val rotation by animateRotation()
150162
val refreshCompleteAnimProgress = pullToRefreshState.refreshCompleteAnimProgress
151163

164+
LaunchedEffect(pullToRefreshState.refreshState) {
165+
if (pullToRefreshState.refreshState == RefreshState.ThresholdReached) {
166+
hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureThresholdActivate)
167+
}
168+
}
169+
170+
val refreshText = when (pullToRefreshState.refreshState) {
171+
RefreshState.Idle -> ""
172+
RefreshState.Pulling -> if (pullToRefreshState.pullProgress > 0.5) refreshTexts[0] else ""
173+
RefreshState.ThresholdReached -> refreshTexts[1]
174+
RefreshState.Refreshing -> refreshTexts[2]
175+
RefreshState.RefreshComplete -> refreshTexts[3]
176+
}
177+
178+
val textAlpha = when (pullToRefreshState.refreshState) {
179+
RefreshState.Pulling -> {
180+
if (pullToRefreshState.pullProgress > 0.5f) (pullToRefreshState.pullProgress - 0.5f) * 2 else 0f
181+
}
182+
183+
else -> 1f
184+
}
185+
152186
val headerHeight = with(density) {
153187
when (pullToRefreshState.refreshState) {
154188
RefreshState.Idle -> 0.dp
@@ -216,31 +250,15 @@ fun RefreshHeader(
216250
}
217251
}
218252

219-
val refreshText = when (pullToRefreshState.refreshState) {
220-
RefreshState.Idle -> ""
221-
RefreshState.Pulling -> if (pullToRefreshState.pullProgress > 0.5) "下拉刷新" else ""
222-
RefreshState.ThresholdReached -> "松开刷新"
223-
RefreshState.Refreshing -> "正在刷新"
224-
RefreshState.RefreshComplete -> "刷新完成"
225-
}
226-
227-
val textAlpha = when (pullToRefreshState.refreshState) {
228-
RefreshState.Pulling -> {
229-
if (pullToRefreshState.pullProgress > 0.5f) (pullToRefreshState.pullProgress - 0.5f) * 2 else 0f
230-
}
231-
232-
else -> 1f
233-
}
234-
235253
AnimatedVisibility(
236254
visible = pullToRefreshState.refreshState != RefreshState.Idle
237255
) {
238256
Text(
239257
text = refreshText,
240-
style = textStyle,
258+
style = refreshTextStyle,
241259
color = color,
242260
modifier = Modifier
243-
.padding(vertical = 6.dp)
261+
.padding(top = 12.dp)
244262
.alpha(textAlpha)
245263
)
246264
}
@@ -271,7 +289,7 @@ private fun RefreshContent(
271289
}
272290

273291
/**
274-
* Create rotation animation state
292+
* Animate the rotation angle
275293
*
276294
* @return rotation angle state
277295
*/
@@ -395,7 +413,7 @@ private fun DrawScope.drawRefreshingState(
395413
}
396414

397415
/**
398-
* Draw the circle that shrinks to the refresh complete state
416+
* Draw the circle that expands to the refresh complete state
399417
*/
400418
private fun DrawScope.drawRefreshCompleteState(
401419
center: Offset,
@@ -436,17 +454,15 @@ sealed class RefreshState {
436454
}
437455

438456
/**
439-
* Remember the PullToRefreshState state object
457+
* Remember the [PullToRefreshState] state object
440458
*
441-
* @return PullToRefreshState state object
459+
* @return [PullToRefreshState] state object
442460
*/
443461
@Composable
444462
fun rememberPullToRefreshState(): PullToRefreshState {
445463
val coroutineScope = rememberCoroutineScope()
446-
val density = LocalDensity.current
447-
val screenHeightDp = getWindowSize().height
448-
449-
val maxDragDistancePx = with(density) { (screenHeightDp.dp * maxDragRatio).toPx() }
464+
val screenHeight = getWindowSize().height
465+
val maxDragDistancePx = screenHeight * maxDragRatio
450466
val refreshThresholdOffset = maxDragDistancePx * thresholdRatio
451467

452468
return remember {
@@ -459,9 +475,9 @@ fun rememberPullToRefreshState(): PullToRefreshState {
459475
}
460476

461477
/**
462-
* PullToRefreshState
478+
* The PullToRefreshState class
463479
*
464-
* @param coroutineScope CoroutineScope
480+
* @param coroutineScope Coroutine scope
465481
* @param maxDragDistancePx Maximum drag distance
466482
* @param refreshThresholdOffset Refresh threshold offset
467483
*/
@@ -523,7 +539,7 @@ class PullToRefreshState(
523539
)
524540
}
525541

526-
private fun resetPointerReleased() {
542+
internal fun resetPointerReleased() {
527543
pointerReleased = false
528544
}
529545

@@ -631,8 +647,8 @@ class PullToRefreshState(
631647
animateDragOffset(
632648
targetValue = refreshThresholdOffset,
633649
animationSpec = tween(
634-
durationMillis = 100,
635-
easing = LinearEasing
650+
durationMillis = 200,
651+
easing = LinearOutSlowInEasing
636652
)
637653
)
638654
rawDragOffset = refreshThresholdOffset
@@ -661,12 +677,13 @@ internal const val maxDragRatio = 1 / 5f
661677
internal const val thresholdRatio = 1 / 4f
662678

663679
/**
664-
* PullToRefreshDefaults
680+
* The default values of the [PullToRefresh] component.
665681
*/
666682
object PullToRefreshDefaults {
667683
val color: Color = Color.Gray
668-
val circleSize: Dp = 24.dp
669-
val textStyle = TextStyle(
684+
val circleSize: Dp = 20.dp
685+
val refreshTexts = listOf("Pull down to refresh", "Release to refresh", "Refreshing...", "Refreshed successfully")
686+
val refreshTextStyle = TextStyle(
670687
fontSize = 14.sp,
671688
fontWeight = FontWeight.Bold,
672689
color = color

0 commit comments

Comments
 (0)