Skip to content

Commit baf21ef

Browse files
authored
Merge pull request #8 from RevenueCat/feat/slide-states
Introduce slide states
2 parents 05eaaa3 + 3a009ea commit baf21ef

9 files changed

Lines changed: 780 additions & 442 deletions

File tree

README.md

Lines changed: 105 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -80,16 +80,71 @@ You can easily implement a slide-to-unlock feature using the `SlideToUnlock` com
8080

8181
The `SlideToUnlock` composable exposes relevant state parameters, allowing you to hoist the slide status and track when the slide action is completed via a callback.
8282

83-
- `isSlided`: A `Boolean` state that controls the component's state. When `true`, the thumb moves to the end and becomes disabled.
84-
- `onSlideCompleted`: A lambda that is invoked when the user successfully slides the thumb to the end. You should typically use this to update your `isSlided` state.
83+
- `state`: A `SlideState` value that controls the component. `SlideState.Idle` keeps the thumb draggable; `Loading`, `Success` and `Error` lock the thumb at the end of the track and show the matching indicator.
84+
- `onSlideCompleted`: A lambda that is invoked when the user successfully slides the thumb to the end (or activates the component via an accessibility action / keyboard). You should typically use this to move your `state` to `SlideState.Loading`.
8585

8686
```kotlin
87-
var isSlided by remember { mutableStateOf(false) }
87+
var slideState by remember { mutableStateOf(SlideState.Idle) }
8888

8989
SlideToUnlock(
90-
isSlided = isSlided,
90+
state = slideState,
9191
modifier = Modifier.fillMaxWidth(),
92-
onSlideCompleted = { isSlided = true },
92+
onSlideCompleted = { slideState = SlideState.Loading },
93+
)
94+
```
95+
96+
### Slide States
97+
98+
`SlideToUnlock` models the full lifecycle of the action triggered by sliding via `SlideState`:
99+
100+
| State | Behavior | Default indicator | Hint text |
101+
| --- | --- | --- | --- |
102+
| `Idle` | Thumb is draggable | Arrow | `HintTexts.defaultText` |
103+
| `Loading` | Thumb locked at end | Circular progress | `HintTexts.slidedText` |
104+
| `Success` | Thumb locked at end | Check icon | `HintTexts.successText` (falls back to `slidedText`) |
105+
| `Error` | Thumb locked at end | Close icon | `HintTexts.errorText` (falls back to `slidedText`) |
106+
107+
A typical asynchronous flow:
108+
109+
```kotlin
110+
var slideState by remember { mutableStateOf(SlideState.Idle) }
111+
112+
LaunchedEffect(slideState) {
113+
if (slideState == SlideState.Loading) {
114+
slideState = try {
115+
doSomethingSuspending()
116+
SlideState.Success
117+
} catch (e: Exception) {
118+
SlideState.Error
119+
}
120+
}
121+
}
122+
123+
SlideToUnlock(
124+
state = slideState,
125+
hintTexts = HintTexts(
126+
defaultText = "Slide to confirm",
127+
slidedText = "Confirming...",
128+
successText = "Confirmed",
129+
errorText = "Something went wrong",
130+
),
131+
onSlideCompleted = { slideState = SlideState.Loading },
132+
)
133+
```
134+
135+
> If you're upgrading from a previous version, the `SlideToUnlock(isSlided: Boolean, ...)` overload still exists but is deprecated — `isSlided = true` maps to `SlideState.Loading`. Migrate to the `state`-based API to use the success and error states.
136+
137+
### Accessibility & Keyboard Support
138+
139+
The track exposes a `Role.Button` semantics node with a state description derived from `HintTexts`. While `state` is `Idle`, it also publishes an accessibility "click" action (labeled by the `actionLabel` parameter) and is focusable, so pressing **Enter** or **Space** while focused triggers `onSlideCompleted` — the same as completing the gesture. Provide a `contentDescription` to override the default announcement, and set `animationsEnabled = false` (e.g. from your "reduce motion" preference) to make the thumb snap between positions and the hint switch without a cross-fade.
140+
141+
```kotlin
142+
SlideToUnlock(
143+
state = slideState,
144+
actionLabel = "Confirm",
145+
contentDescription = "Confirm your action by sliding the thumb to the end",
146+
animationsEnabled = !reduceMotionEnabled,
147+
onSlideCompleted = { slideState = SlideState.Loading },
93148
)
94149
```
95150

@@ -98,10 +153,10 @@ SlideToUnlock(
98153
You can customize all colors of the component by leveraging an instance of `DefaultSlideToUnlockColors`, which contains some prebuilt behaviors for providing proper color sets depending on the states, and you can pass it to the `colors` parameter. This allows you to style the track, hint text, thumb, and progress indicator to match your app's theme.
99154

100155
```kotlin
101-
var isSlided by remember { mutableStateOf(false) }
156+
var slideState by remember { mutableStateOf(SlideState.Idle) }
102157

103158
SlideToUnlock(
104-
isSlided = isSlided,
159+
state = slideState,
105160
modifier = Modifier
106161
.fillMaxWidth()
107162
.padding(16.dp),
@@ -111,7 +166,7 @@ SlideToUnlock(
111166
thumbColor = Color.White,
112167
slidedHintColor = Color.White,
113168
),
114-
onSlideCompleted = { isSlided = true },
169+
onSlideCompleted = { slideState = SlideState.Loading },
115170
)
116171
```
117172

@@ -124,13 +179,13 @@ val colorStops = arrayOf(
124179
)
125180

126181
SlideToUnlock(
127-
isSlided = isSlided,
182+
state = slideState,
128183
// ...
129184
colors = DefaultSlideToUnlockColors(
130185
trackBrush = Brush.verticalGradient(colorStops = colorStops),
131186
// ...
132187
),
133-
onSlideCompleted = { isSlided = true },
188+
onSlideCompleted = { slideState = SlideState.Loading },
134189
)
135190
```
136191

@@ -209,18 +264,18 @@ private class MaterialThemedSlideToUnlockColors : SlideToUnlockColors {
209264
The text displayed in the track can be customized by passing a `HintTexts` object. This allows you to define different messages for the initial state and the final "slided" state.
210265

211266
```kotlin
212-
var isSlided by remember { mutableStateOf(false) }
267+
var slideState by remember { mutableStateOf(SlideState.Idle) }
213268

214269
SlideToUnlock(
215-
isSlided = isSlided,
270+
state = slideState,
216271
modifier = Modifier
217272
.fillMaxWidth()
218273
.padding(16.dp),
219274
hintTexts = HintTexts(
220275
defaultText = "Slide to subscribe",
221276
slidedText = "Subscribing...",
222277
),
223-
onSlideCompleted = { isSlided = true },
278+
onSlideCompleted = { slideState = SlideState.Loading },
224279
)
225280
```
226281

@@ -230,47 +285,49 @@ For complete control over the appearance and behavior of the thumb and hint, you
230285

231286
#### Customizing the Thumb
232287

233-
The `thumb` slot provides you with the `isSlided` state, the current `slideFraction`, the `colors` object, and the `thumbSize`. You can use these to create dynamic and interactive thumbs.
288+
The `thumb` slot provides you with the current `state` (`SlideState`), the current `slideFraction`, the `colors` object, the `thumbSize`, and the `orientation`. You can use these to create dynamic and interactive thumbs.
234289

235-
This example replaces the default arrow icon with a rotating app icon and shows a success checkmark when completed.
290+
This example replaces the default arrow icon with a rotating restore icon and shows a success checkmark when the action completes.
236291

237292
```kotlin
238-
var isSlided by remember { mutableStateOf(false) }
239-
var isCompleted by remember { mutableStateOf(false) }
293+
var slideState by remember { mutableStateOf(SlideState.Idle) }
240294

241-
LaunchedEffect(isSlided) {
242-
if (isSlided) {
295+
LaunchedEffect(slideState) {
296+
if (slideState == SlideState.Loading) {
243297
delay(1500) // Simulate in-app purchases or any tasks
244-
isCompleted = true
298+
slideState = SlideState.Success
245299
}
246300
}
247301

248302
SlideToUnlock(
249-
isSlided = isSlided,
303+
state = slideState,
250304
// ...
251-
onSlideCompleted = { isSlided = true },
252-
thumb = { slided, fraction, colors, size ->
305+
onSlideCompleted = { slideState = SlideState.Loading },
306+
thumb = { state, fraction, colors, size, _ ->
253307
Box(
254308
modifier = Modifier
255309
.size(size)
256310
.background(color = colors.thumbColor(), shape = CircleShape),
311+
contentAlignment = Alignment.Center,
257312
) {
258-
if (isCompleted) {
259-
Icon(
260-
imageVector = Icons.Default.Done,
261-
contentDescription = "Completed",
262-
tint = Color(0xFF11D483), // Green for success
263-
)
264-
} else if (slided) {
265-
CircularProgressIndicator(
313+
when (state) {
314+
SlideState.Loading -> CircularProgressIndicator(
266315
modifier = Modifier.padding(8.dp),
267316
color = colors.progressColor(),
268317
strokeWidth = 3.dp,
269318
)
270-
} else {
271-
Icon(
319+
SlideState.Success -> Icon(
320+
imageVector = Icons.Default.Check,
321+
contentDescription = "Completed",
322+
tint = colors.successIconColor(),
323+
)
324+
SlideState.Error -> Icon(
325+
imageVector = Icons.Default.Close,
326+
contentDescription = "Failed",
327+
tint = colors.errorIconColor(),
328+
)
329+
SlideState.Idle -> Icon(
272330
modifier = Modifier
273-
.align(Alignment.Center)
274331
.size(30.dp)
275332
.rotate(fraction * -360), // Rotate the icon as the user slides
276333
imageVector = Icons.Default.Restore,
@@ -292,15 +349,15 @@ This example replaces the default text with a `Row` that includes a `CircularPro
292349

293350
```kotlin
294351
SlideToUnlock(
295-
isSlided = isSlided,
352+
state = slideState,
296353
// ...
297-
onSlideCompleted = { isSlided = true },
298-
hint = { slided, fraction, hintTexts, colors, paddings ->
354+
onSlideCompleted = { slideState = SlideState.Loading },
355+
hint = { state, fraction, hintTexts, colors, paddings, _ ->
299356
AnimatedContent(
300357
modifier = Modifier.align(Alignment.Center),
301-
targetState = isSlided,
302-
) { isSlidedTarget ->
303-
if (isSlidedTarget) {
358+
targetState = state,
359+
) { current ->
360+
if (current != SlideState.Idle) {
304361
Row(
305362
horizontalArrangement = Arrangement.Center,
306363
verticalAlignment = Alignment.CenterVertically,
@@ -343,11 +400,11 @@ When using a vertical orientation, it's important to provide appropriate size co
343400
**Example: A Vertical Slider**
344401

345402
```kotlin
346-
var isSlided by remember { mutableStateOf(false) }
403+
var slideState by remember { mutableStateOf(SlideState.Idle) }
347404

348405
SlideToUnlock(
349-
isSlided = isSlided,
350-
onSlideCompleted = { isSlided = true },
406+
state = slideState,
407+
onSlideCompleted = { slideState = SlideState.Loading },
351408
orientation = SlideOrientation.Vertical,
352409
modifier = Modifier
353410
.fillMaxHeight()
@@ -367,11 +424,11 @@ The default value is `0.85f` (85%), which requires a deliberate gesture. You can
367424
This slider will "snap" to the end and trigger `onSlideCompleted` if the user drags it just past the halfway point.
368425

369426
```kotlin
370-
var isSlided by remember { mutableStateOf(false) }
427+
var slideState by remember { mutableStateOf(SlideState.Idle) }
371428

372429
SlideToUnlock(
373-
isSlided = isSlided,
374-
onSlideCompleted = { isSlided = true },
430+
state = slideState,
431+
onSlideCompleted = { slideState = SlideState.Loading },
375432
// The slide will complete if the user drags past the 50% mark.
376433
fractionalThreshold = 0.5f,
377434
modifier = Modifier
@@ -396,11 +453,11 @@ Text(
396453
)
397454

398455
SlideToUnlock(
399-
isSlided = isSlided,
456+
state = slideState,
400457
modifier = Modifier
401458
.fillMaxWidth()
402459
.padding(16.dp),
403-
onSlideCompleted = { isSlided = true },
460+
onSlideCompleted = { slideState = SlideState.Loading },
404461
onSlideFractionChanged = { fraction ->
405462
slideProgress = fraction
406463
}

0 commit comments

Comments
 (0)