1 /*
<lambda>null2 * Copyright 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.compose.material
18
19 import androidx.annotation.FloatRange
20 import androidx.compose.animation.core.AnimationSpec
21 import androidx.compose.animation.core.SpringSpec
22 import androidx.compose.animation.core.animate
23 import androidx.compose.foundation.MutatePriority
24 import androidx.compose.foundation.gestures.DragScope
25 import androidx.compose.foundation.gestures.DraggableState
26 import androidx.compose.foundation.gestures.Orientation
27 import androidx.compose.foundation.gestures.draggable
28 import androidx.compose.foundation.interaction.MutableInteractionSource
29 import androidx.compose.foundation.layout.offset
30 import androidx.compose.material.internal.PlatformOptimizedCancellationException
31 import androidx.compose.runtime.Stable
32 import androidx.compose.runtime.derivedStateOf
33 import androidx.compose.runtime.getValue
34 import androidx.compose.runtime.mutableFloatStateOf
35 import androidx.compose.runtime.mutableStateOf
36 import androidx.compose.runtime.saveable.Saver
37 import androidx.compose.runtime.setValue
38 import androidx.compose.runtime.snapshotFlow
39 import androidx.compose.runtime.structuralEqualityPolicy
40 import androidx.compose.ui.Modifier
41 import androidx.compose.ui.layout.Measurable
42 import androidx.compose.ui.layout.MeasureResult
43 import androidx.compose.ui.layout.MeasureScope
44 import androidx.compose.ui.node.LayoutModifierNode
45 import androidx.compose.ui.node.ModifierNodeElement
46 import androidx.compose.ui.platform.InspectorInfo
47 import androidx.compose.ui.platform.debugInspectorInfo
48 import androidx.compose.ui.unit.Constraints
49 import androidx.compose.ui.unit.IntSize
50 import kotlin.math.abs
51 import kotlin.math.roundToInt
52 import kotlinx.coroutines.CancellationException
53 import kotlinx.coroutines.CoroutineStart
54 import kotlinx.coroutines.Job
55 import kotlinx.coroutines.cancel
56 import kotlinx.coroutines.coroutineScope
57 import kotlinx.coroutines.launch
58
59 /**
60 * Structure that represents the anchors of a [AnchoredDraggableState].
61 *
62 * See the DraggableAnchors factory method to construct drag anchors using a default implementation.
63 */
64 @ExperimentalMaterialApi
65 internal interface DraggableAnchors<T> {
66
67 /**
68 * Get the anchor position for an associated [value]
69 *
70 * @return The position of the anchor, or [Float.NaN] if the anchor does not exist
71 */
72 fun positionOf(value: T): Float
73
74 /**
75 * Whether there is an anchor position associated with the [value]
76 *
77 * @param value The value to look up
78 * @return true if there is an anchor for this value, false if there is no anchor for this value
79 */
80 fun hasAnchorFor(value: T): Boolean
81
82 /**
83 * Find the closest anchor to the [position].
84 *
85 * @param position The position to start searching from
86 * @return The closest anchor or null if the anchors are empty
87 */
88 fun closestAnchor(position: Float): T?
89
90 /**
91 * Find the closest anchor to the [position], in the specified direction.
92 *
93 * @param position The position to start searching from
94 * @param searchUpwards Whether to search upwards from the current position or downwards
95 * @return The closest anchor or null if the anchors are empty
96 */
97 fun closestAnchor(position: Float, searchUpwards: Boolean): T?
98
99 /** The smallest anchor, or [Float.NEGATIVE_INFINITY] if the anchors are empty. */
100 fun minAnchor(): Float
101
102 /** The biggest anchor, or [Float.POSITIVE_INFINITY] if the anchors are empty. */
103 fun maxAnchor(): Float
104
105 /** The amount of anchors */
106 val size: Int
107 }
108
109 /**
110 * [DraggableAnchorsConfig] stores a mutable configuration anchors, comprised of values of [T] and
111 * corresponding [Float] positions. This [DraggableAnchorsConfig] is used to construct an immutable
112 * [DraggableAnchors] instance later on.
113 */
114 @ExperimentalMaterialApi
115 internal class DraggableAnchorsConfig<T> {
116
117 internal val anchors = mutableMapOf<T, Float>()
118
119 /**
120 * Set the anchor position for [this] anchor.
121 *
122 * @param position The anchor position.
123 */
124 @Suppress("BuilderSetStyle")
atnull125 infix fun T.at(position: Float) {
126 anchors[this] = position
127 }
128 }
129
130 /**
131 * Create a new [DraggableAnchors] instance using a builder function.
132 *
133 * @param builder A function with a [DraggableAnchorsConfig] that offers APIs to configure anchors
134 * @return A new [DraggableAnchors] instance with the anchor positions set by the `builder`
135 * function.
136 */
137 @ExperimentalMaterialApi
DraggableAnchorsnull138 internal fun <T : Any> DraggableAnchors(
139 builder: DraggableAnchorsConfig<T>.() -> Unit
140 ): DraggableAnchors<T> = MapDraggableAnchors(DraggableAnchorsConfig<T>().apply(builder).anchors)
141
142 /**
143 * Enable drag gestures between a set of predefined values.
144 *
145 * When a drag is detected, the offset of the [AnchoredDraggableState] will be updated with the drag
146 * delta. You should use this offset to move your content accordingly (see [Modifier.offset]). When
147 * the drag ends, the offset will be animated to one of the anchors and when that anchor is reached,
148 * the value of the [AnchoredDraggableState] will also be updated to the value corresponding to the
149 * new anchor.
150 *
151 * Dragging is constrained between the minimum and maximum anchors.
152 *
153 * @param state The associated [AnchoredDraggableState].
154 * @param orientation The orientation in which the [anchoredDraggable] can be dragged.
155 * @param enabled Whether this [anchoredDraggable] is enabled and should react to the user's input.
156 * @param reverseDirection Whether to reverse the direction of the drag, so a top to bottom drag
157 * will behave like bottom to top, and a left to right drag will behave like right to left.
158 * @param interactionSource Optional [MutableInteractionSource] that will passed on to the internal
159 * [Modifier.draggable].
160 * @param startDragImmediately when set to false, [draggable] will start dragging only when the
161 * gesture crosses the touchSlop. This is useful to prevent users from "catching" an animating
162 * widget when pressing on it. See [draggable] to learn more about startDragImmediately.
163 */
164 @ExperimentalMaterialApi
165 internal fun <T> Modifier.anchoredDraggable(
166 state: AnchoredDraggableState<T>,
167 orientation: Orientation,
168 enabled: Boolean = true,
169 reverseDirection: Boolean = false,
170 interactionSource: MutableInteractionSource? = null,
171 startDragImmediately: Boolean = state.isAnimationRunning
172 ) =
173 draggable(
174 state = state.draggableState,
175 orientation = orientation,
176 enabled = enabled,
177 interactionSource = interactionSource,
178 reverseDirection = reverseDirection,
179 startDragImmediately = startDragImmediately,
180 onDragStopped = { velocity -> launch { state.settle(velocity) } }
181 )
182
183 /**
184 * Scope used for suspending anchored drag blocks. Allows to set [AnchoredDraggableState.offset] to
185 * a new value.
186 *
187 * @see [AnchoredDraggableState.anchoredDrag] to learn how to start the anchored drag and get the
188 * access to this scope.
189 */
190 @ExperimentalMaterialApi
191 internal interface AnchoredDragScope {
192 /**
193 * Assign a new value for an offset value for [AnchoredDraggableState].
194 *
195 * @param newOffset new value for [AnchoredDraggableState.offset].
196 * @param lastKnownVelocity last known velocity (if known)
197 */
dragTonull198 fun dragTo(newOffset: Float, lastKnownVelocity: Float = 0f)
199 }
200
201 /**
202 * State of the [anchoredDraggable] modifier. Use the constructor overload with anchors if the
203 * anchors are defined in composition, or update the anchors using [updateAnchors].
204 *
205 * This contains necessary information about any ongoing drag or animation and provides methods to
206 * change the state either immediately or by starting an animation.
207 *
208 * @param initialValue The initial value of the state.
209 * @param positionalThreshold The positional threshold, in px, to be used when calculating the
210 * target state while a drag is in progress and when settling after the drag ends. This is the
211 * distance from the start of a transition. It will be, depending on the direction of the
212 * interaction, added or subtracted from/to the origin offset. It should always be a positive
213 * value.
214 * @param velocityThreshold The velocity threshold (in px per second) that the end velocity has to
215 * exceed in order to animate to the next state, even if the [positionalThreshold] has not been
216 * reached.
217 * @param animationSpec The default animation that will be used to animate to a new state.
218 * @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
219 */
220 @Stable
221 @ExperimentalMaterialApi
222 internal class AnchoredDraggableState<T>(
223 initialValue: T,
224 internal val positionalThreshold: (totalDistance: Float) -> Float,
225 internal val velocityThreshold: () -> Float,
226 val animationSpec: AnimationSpec<Float>,
227 internal val confirmValueChange: (newValue: T) -> Boolean = { true }
228 ) {
229
230 /**
231 * Construct an [AnchoredDraggableState] instance with anchors.
232 *
233 * @param initialValue The initial value of the state.
234 * @param anchors The anchors of the state. Use [updateAnchors] to update the anchors later.
235 * @param animationSpec The default animation that will be used to animate to a new state.
236 * @param confirmValueChange Optional callback invoked to confirm or veto a pending state
237 * change.
238 * @param positionalThreshold The positional threshold, in px, to be used when calculating the
239 * target state while a drag is in progress and when settling after the drag ends. This is the
240 * distance from the start of a transition. It will be, depending on the direction of the
241 * interaction, added or subtracted from/to the origin offset. It should always be a positive
242 * value.
243 * @param velocityThreshold The velocity threshold (in px per second) that the end velocity has
244 * to exceed in order to animate to the next state, even if the [positionalThreshold] has not
245 * been reached.
246 */
247 @ExperimentalMaterialApi
248 constructor(
249 initialValue: T,
250 anchors: DraggableAnchors<T>,
251 positionalThreshold: (totalDistance: Float) -> Float,
252 velocityThreshold: () -> Float,
253 animationSpec: AnimationSpec<Float>,
<lambda>null254 confirmValueChange: (newValue: T) -> Boolean = { true }
255 ) : this(
256 initialValue,
257 positionalThreshold,
258 velocityThreshold,
259 animationSpec,
260 confirmValueChange
261 ) {
262 this.anchors = anchors
263 trySnapTo(initialValue)
264 }
265
266 private val dragMutex = InternalMutatorMutex()
267
268 internal val draggableState =
269 object : DraggableState {
270
271 private val dragScope =
272 object : DragScope {
dragBynull273 override fun dragBy(pixels: Float) {
274 with(anchoredDragScope) { dragTo(newOffsetForDelta(pixels)) }
275 }
276 }
277
dragnull278 override suspend fun drag(
279 dragPriority: MutatePriority,
280 block: suspend DragScope.() -> Unit
281 ) {
282 this@AnchoredDraggableState.anchoredDrag(dragPriority) {
283 with(dragScope) { block() }
284 }
285 }
286
dispatchRawDeltanull287 override fun dispatchRawDelta(delta: Float) {
288 this@AnchoredDraggableState.dispatchRawDelta(delta)
289 }
290 }
291
292 /** The current value of the [AnchoredDraggableState]. */
293 var currentValue: T by mutableStateOf(initialValue)
294 private set
295
296 /**
297 * The target value. This is the closest value to the current offset, taking into account
298 * positional thresholds. If no interactions like animations or drags are in progress, this will
299 * be the current value.
300 */
<lambda>null301 val targetValue: T by derivedStateOf {
302 dragTarget
303 ?: run {
304 val currentOffset = offset
305 if (!currentOffset.isNaN()) {
306 computeTarget(currentOffset, currentValue, velocity = 0f)
307 } else currentValue
308 }
309 }
310
311 /**
312 * The closest value in the swipe direction from the current offset, not considering thresholds.
313 * If an [anchoredDrag] is in progress, this will be the target of that anchoredDrag (if
314 * specified).
315 */
<lambda>null316 internal val closestValue: T by derivedStateOf {
317 dragTarget
318 ?: run {
319 val currentOffset = offset
320 if (!currentOffset.isNaN()) {
321 computeTargetWithoutThresholds(currentOffset, currentValue)
322 } else currentValue
323 }
324 }
325
326 /**
327 * The current offset, or [Float.NaN] if it has not been initialized yet.
328 *
329 * The offset will be initialized when the anchors are first set through [updateAnchors].
330 *
331 * Strongly consider using [requireOffset] which will throw if the offset is read before it is
332 * initialized. This helps catch issues early in your workflow.
333 */
334 var offset: Float by mutableFloatStateOf(Float.NaN)
335 private set
336
337 /**
338 * Require the current offset.
339 *
340 * @throws IllegalStateException If the offset has not been initialized yet
341 * @see offset
342 */
requireOffsetnull343 fun requireOffset(): Float {
344 check(!offset.isNaN()) {
345 "The offset was read before being initialized. Did you access the offset in a phase " +
346 "before layout, like effects or composition?"
347 }
348 return offset
349 }
350
351 /** Whether an animation is currently in progress. */
352 val isAnimationRunning: Boolean
353 get() = dragTarget != null
354
355 /**
356 * The fraction of the progress going from [currentValue] to [closestValue], within [0f..1f]
357 * bounds, or 1f if the [AnchoredDraggableState] is in a settled state.
358 */
359 @get:FloatRange(from = 0.0, to = 1.0)
360 val progress: Float by
<lambda>null361 derivedStateOf(structuralEqualityPolicy()) {
362 val a = anchors.positionOf(currentValue)
363 val b = anchors.positionOf(closestValue)
364 val distance = abs(b - a)
365 if (!distance.isNaN() && distance > 1e-6f) {
366 val progress = (this.requireOffset() - a) / (b - a)
367 // If we are very close to 0f or 1f, we round to the closest
368 if (progress < 1e-6f) 0f else if (progress > 1 - 1e-6f) 1f else progress
369 } else 1f
370 }
371
372 /**
373 * The velocity of the last known animation. Gets reset to 0f when an animation completes
374 * successfully, but does not get reset when an animation gets interrupted. You can use this
375 * value to provide smooth reconciliation behavior when re-targeting an animation.
376 */
377 var lastVelocity: Float by mutableFloatStateOf(0f)
378 private set
379
380 private var dragTarget: T? by mutableStateOf(null)
381
382 var anchors: DraggableAnchors<T> by mutableStateOf(emptyDraggableAnchors())
383 private set
384
385 /**
386 * Update the anchors. If there is no ongoing [anchoredDrag] operation, snap to the [newTarget],
387 * otherwise restart the ongoing [anchoredDrag] operation (e.g. an animation) with the new
388 * anchors.
389 *
390 * <b>If your anchors depend on the size of the layout, updateAnchors should be called in the
391 * layout (placement) phase, e.g. through Modifier.onSizeChanged.</b> This ensures that the
392 * state is set up within the same frame. For static anchors, or anchors with different data
393 * dependencies, [updateAnchors] is safe to be called from side effects or layout.
394 *
395 * @param newAnchors The new anchors.
396 * @param newTarget The new target, by default the closest anchor or the current target if there
397 * are no anchors.
398 */
updateAnchorsnull399 fun updateAnchors(
400 newAnchors: DraggableAnchors<T>,
401 newTarget: T =
402 if (!offset.isNaN()) {
403 newAnchors.closestAnchor(offset) ?: targetValue
404 } else targetValue
405 ) {
406 if (anchors != newAnchors) {
407 anchors = newAnchors
408 // Attempt to snap. If nobody is holding the lock, we can immediately update the offset.
409 // If anybody is holding the lock, we send a signal to restart the ongoing work with the
410 // updated anchors.
411 val snapSuccessful = trySnapTo(newTarget)
412 if (!snapSuccessful) {
413 dragTarget = newTarget
414 }
415 }
416 }
417
418 /**
419 * Find the closest anchor, taking into account the [velocityThreshold] and
420 * [positionalThreshold], and settle at it with an animation.
421 *
422 * If the [velocity] is lower than the [velocityThreshold], the closest anchor by distance and
423 * [positionalThreshold] will be the target. If the [velocity] is higher than the
424 * [velocityThreshold], the [positionalThreshold] will <b>not</b> be considered and the next
425 * anchor in the direction indicated by the sign of the [velocity] will be the target.
426 */
settlenull427 suspend fun settle(velocity: Float) {
428 val previousValue = this.currentValue
429 val targetValue =
430 computeTarget(
431 offset = requireOffset(),
432 currentValue = previousValue,
433 velocity = velocity
434 )
435 if (confirmValueChange(targetValue)) {
436 animateTo(targetValue, velocity)
437 } else {
438 // If the user vetoed the state change, rollback to the previous state.
439 animateTo(previousValue, velocity)
440 }
441 }
442
computeTargetnull443 private fun computeTarget(offset: Float, currentValue: T, velocity: Float): T {
444 val currentAnchors = anchors
445 val currentAnchorPosition = currentAnchors.positionOf(currentValue)
446 val velocityThresholdPx = velocityThreshold()
447 return if (currentAnchorPosition == offset || currentAnchorPosition.isNaN()) {
448 currentValue
449 } else if (currentAnchorPosition < offset) {
450 // Swiping from lower to upper (positive).
451 if (velocity >= velocityThresholdPx) {
452 currentAnchors.closestAnchor(offset, true)!!
453 } else {
454 val upper = currentAnchors.closestAnchor(offset, true)!!
455 val distance = abs(currentAnchors.positionOf(upper) - currentAnchorPosition)
456 val relativeThreshold = abs(positionalThreshold(distance))
457 val absoluteThreshold = abs(currentAnchorPosition + relativeThreshold)
458 if (offset < absoluteThreshold) currentValue else upper
459 }
460 } else {
461 // Swiping from upper to lower (negative).
462 if (velocity <= -velocityThresholdPx) {
463 currentAnchors.closestAnchor(offset, false)!!
464 } else {
465 val lower = currentAnchors.closestAnchor(offset, false)!!
466 val distance = abs(currentAnchorPosition - currentAnchors.positionOf(lower))
467 val relativeThreshold = abs(positionalThreshold(distance))
468 val absoluteThreshold = abs(currentAnchorPosition - relativeThreshold)
469 if (offset < 0) {
470 // For negative offsets, larger absolute thresholds are closer to lower anchors
471 // than smaller ones.
472 if (abs(offset) < absoluteThreshold) currentValue else lower
473 } else {
474 if (offset > absoluteThreshold) currentValue else lower
475 }
476 }
477 }
478 }
479
computeTargetWithoutThresholdsnull480 private fun computeTargetWithoutThresholds(
481 offset: Float,
482 currentValue: T,
483 ): T {
484 val currentAnchors = anchors
485 val currentAnchorPosition = currentAnchors.positionOf(currentValue)
486 return if (currentAnchorPosition == offset || currentAnchorPosition.isNaN()) {
487 currentValue
488 } else if (currentAnchorPosition < offset) {
489 currentAnchors.closestAnchor(offset, true) ?: currentValue
490 } else {
491 currentAnchors.closestAnchor(offset, false) ?: currentValue
492 }
493 }
494
495 private val anchoredDragScope: AnchoredDragScope =
496 object : AnchoredDragScope {
dragTonull497 override fun dragTo(newOffset: Float, lastKnownVelocity: Float) {
498 offset = newOffset
499 lastVelocity = lastKnownVelocity
500 }
501 }
502
503 /**
504 * Call this function to take control of drag logic and perform anchored drag with the latest
505 * anchors.
506 *
507 * All actions that change the [offset] of this [AnchoredDraggableState] must be performed
508 * within an [anchoredDrag] block (even if they don't call any other methods on this object) in
509 * order to guarantee that mutual exclusion is enforced.
510 *
511 * If [anchoredDrag] is called from elsewhere with the [dragPriority] higher or equal to ongoing
512 * drag, the ongoing drag will be cancelled.
513 *
514 * <b>If the [anchors] change while the [block] is being executed, it will be cancelled and
515 * re-executed with the latest anchors and target.</b> This allows you to target the correct
516 * state.
517 *
518 * @param dragPriority of the drag operation
519 * @param block perform anchored drag given the current anchor provided
520 */
anchoredDragnull521 suspend fun anchoredDrag(
522 dragPriority: MutatePriority = MutatePriority.Default,
523 block: suspend AnchoredDragScope.(anchors: DraggableAnchors<T>) -> Unit
524 ) {
525 try {
526 dragMutex.mutate(dragPriority) {
527 restartable(inputs = { anchors }) { latestAnchors ->
528 anchoredDragScope.block(latestAnchors)
529 }
530 }
531 } finally {
532 val closest = anchors.closestAnchor(offset)
533 if (
534 closest != null &&
535 abs(offset - anchors.positionOf(closest)) <= 0.5f &&
536 confirmValueChange.invoke(closest)
537 ) {
538 currentValue = closest
539 }
540 }
541 }
542
543 /**
544 * Call this function to take control of drag logic and perform anchored drag with the latest
545 * anchors and target.
546 *
547 * All actions that change the [offset] of this [AnchoredDraggableState] must be performed
548 * within an [anchoredDrag] block (even if they don't call any other methods on this object) in
549 * order to guarantee that mutual exclusion is enforced.
550 *
551 * This overload allows the caller to hint the target value that this [anchoredDrag] is intended
552 * to arrive to. This will set [AnchoredDraggableState.targetValue] to provided value so
553 * consumers can reflect it in their UIs.
554 *
555 * <b>If the [anchors] or [AnchoredDraggableState.targetValue] change while the [block] is being
556 * executed, it will be cancelled and re-executed with the latest anchors and target.</b> This
557 * allows you to target the correct state.
558 *
559 * If [anchoredDrag] is called from elsewhere with the [dragPriority] higher or equal to ongoing
560 * drag, the ongoing drag will be cancelled.
561 *
562 * @param targetValue hint the target value that this [anchoredDrag] is intended to arrive to
563 * @param dragPriority of the drag operation
564 * @param block perform anchored drag given the current anchor provided
565 */
anchoredDragnull566 suspend fun anchoredDrag(
567 targetValue: T,
568 dragPriority: MutatePriority = MutatePriority.Default,
569 block: suspend AnchoredDragScope.(anchors: DraggableAnchors<T>, targetValue: T) -> Unit
570 ) {
571 if (anchors.hasAnchorFor(targetValue)) {
572 try {
573 dragMutex.mutate(dragPriority) {
574 dragTarget = targetValue
575 restartable(inputs = { anchors to this@AnchoredDraggableState.targetValue }) {
576 (latestAnchors, latestTarget) ->
577 anchoredDragScope.block(latestAnchors, latestTarget)
578 }
579 }
580 } finally {
581 dragTarget = null
582 val closest = anchors.closestAnchor(offset)
583 if (
584 closest != null &&
585 abs(offset - anchors.positionOf(closest)) <= 0.5f &&
586 confirmValueChange.invoke(closest)
587 ) {
588 currentValue = closest
589 }
590 }
591 } else {
592 // Todo: b/283467401, revisit this behavior
593 currentValue = targetValue
594 }
595 }
596
newOffsetForDeltanull597 internal fun newOffsetForDelta(delta: Float) =
598 ((if (offset.isNaN()) 0f else offset) + delta).coerceIn(
599 anchors.minAnchor(),
600 anchors.maxAnchor()
601 )
602
603 /**
604 * Drag by the [delta], coerce it in the bounds and dispatch it to the [AnchoredDraggableState].
605 *
606 * @return The delta the consumed by the [AnchoredDraggableState]
607 */
608 fun dispatchRawDelta(delta: Float): Float {
609 val newOffset = newOffsetForDelta(delta)
610 val oldOffset = if (offset.isNaN()) 0f else offset
611 offset = newOffset
612 return newOffset - oldOffset
613 }
614
615 /**
616 * Attempt to snap synchronously. Snapping can happen synchronously when there is no other drag
617 * transaction like a drag or an animation is progress. If there is another interaction in
618 * progress, the suspending [snapTo] overload needs to be used.
619 *
620 * @return true if the synchronous snap was successful, or false if we couldn't snap synchronous
621 */
trySnapTonull622 private fun trySnapTo(targetValue: T): Boolean =
623 dragMutex.tryMutate {
624 with(anchoredDragScope) {
625 val targetOffset = anchors.positionOf(targetValue)
626 if (!targetOffset.isNaN()) {
627 dragTo(targetOffset)
628 dragTarget = null
629 }
630 currentValue = targetValue
631 }
632 }
633
634 companion object {
635 /** The default [Saver] implementation for [AnchoredDraggableState]. */
636 @ExperimentalMaterialApi
Savernull637 fun <T : Any> Saver(
638 animationSpec: AnimationSpec<Float>,
639 confirmValueChange: (T) -> Boolean,
640 positionalThreshold: (distance: Float) -> Float,
641 velocityThreshold: () -> Float,
642 ) =
643 Saver<AnchoredDraggableState<T>, T>(
644 save = { it.currentValue },
<lambda>null645 restore = {
646 AnchoredDraggableState(
647 initialValue = it,
648 animationSpec = animationSpec,
649 confirmValueChange = confirmValueChange,
650 positionalThreshold = positionalThreshold,
651 velocityThreshold = velocityThreshold
652 )
653 }
654 )
655 }
656 }
657
658 /**
659 * Snap to a [targetValue] without any animation. If the [targetValue] is not in the set of anchors,
660 * the [AnchoredDraggableState.currentValue] will be updated to the [targetValue] without updating
661 * the offset.
662 *
663 * @param targetValue The target value of the animation
664 * @throws CancellationException if the interaction interrupted by another interaction like a
665 * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
666 */
667 @ExperimentalMaterialApi
snapTonull668 internal suspend fun <T> AnchoredDraggableState<T>.snapTo(targetValue: T) {
669 anchoredDrag(targetValue = targetValue) { anchors, latestTarget ->
670 val targetOffset = anchors.positionOf(latestTarget)
671 if (!targetOffset.isNaN()) dragTo(targetOffset)
672 }
673 }
674
675 /**
676 * Animate to a [targetValue]. If the [targetValue] is not in the set of anchors, the
677 * [AnchoredDraggableState.currentValue] will be updated to the [targetValue] without updating the
678 * offset.
679 *
680 * @param targetValue The target value of the animation
681 * @param velocity The velocity the animation should start with
682 * @throws CancellationException if the interaction interrupted by another interaction like a
683 * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
684 */
685 @ExperimentalMaterialApi
animateTonull686 internal suspend fun <T> AnchoredDraggableState<T>.animateTo(
687 targetValue: T,
688 velocity: Float = this.lastVelocity,
689 ) {
690 anchoredDrag(targetValue = targetValue) { anchors, latestTarget ->
691 val targetOffset = anchors.positionOf(latestTarget)
692 if (!targetOffset.isNaN()) {
693 var prev = if (offset.isNaN()) 0f else offset
694 animate(prev, targetOffset, velocity, animationSpec) { value, velocity ->
695 // Our onDrag coerces the value within the bounds, but an animation may
696 // overshoot, for example a spring animation or an overshooting interpolator
697 // We respect the user's intention and allow the overshoot, but still use
698 // DraggableState's drag for its mutex.
699 dragTo(value, velocity)
700 prev = value
701 }
702 }
703 }
704 }
705
706 /** Contains useful defaults for [anchoredDraggable] and [AnchoredDraggableState]. */
707 @Stable
708 @ExperimentalMaterialApi
709 internal object AnchoredDraggableDefaults {
710 /** The default animation used by [AnchoredDraggableState]. */
711 @get:ExperimentalMaterialApi
712 @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
713 @ExperimentalMaterialApi
714 val AnimationSpec = SpringSpec<Float>()
715 }
716
717 internal class AnchoredDragFinishedSignal :
718 PlatformOptimizedCancellationException("Anchored drag finished")
719
restartablenull720 private suspend fun <I> restartable(inputs: () -> I, block: suspend (I) -> Unit) {
721 try {
722 coroutineScope {
723 var previousDrag: Job? = null
724 snapshotFlow(inputs).collect { latestInputs ->
725 previousDrag?.apply {
726 cancel(AnchoredDragFinishedSignal())
727 join()
728 }
729 previousDrag =
730 launch(start = CoroutineStart.UNDISPATCHED) {
731 block(latestInputs)
732 this@coroutineScope.cancel(AnchoredDragFinishedSignal())
733 }
734 }
735 }
736 } catch (anchoredDragFinished: AnchoredDragFinishedSignal) {
737 // Ignored
738 }
739 }
740
emptyDraggableAnchorsnull741 private fun <T> emptyDraggableAnchors() = MapDraggableAnchors<T>(emptyMap())
742
743 @OptIn(ExperimentalMaterialApi::class)
744 private class MapDraggableAnchors<T>(private val anchors: Map<T, Float>) : DraggableAnchors<T> {
745
746 override fun positionOf(value: T): Float = anchors[value] ?: Float.NaN
747
748 override fun hasAnchorFor(value: T) = anchors.containsKey(value)
749
750 override fun closestAnchor(position: Float): T? =
751 anchors.minByOrNull { abs(position - it.value) }?.key
752
753 override fun closestAnchor(position: Float, searchUpwards: Boolean): T? {
754 return anchors
755 .minByOrNull { (_, anchor) ->
756 val delta = if (searchUpwards) anchor - position else position - anchor
757 if (delta < 0) Float.POSITIVE_INFINITY else delta
758 }
759 ?.key
760 }
761
762 override fun minAnchor() = anchors.values.minOrNull() ?: Float.NaN
763
764 override fun maxAnchor() = anchors.values.maxOrNull() ?: Float.NaN
765
766 override val size: Int
767 get() = anchors.size
768
769 override fun equals(other: Any?): Boolean {
770 if (this === other) return true
771 if (other !is MapDraggableAnchors<*>) return false
772
773 return anchors == other.anchors
774 }
775
776 override fun hashCode() = 31 * anchors.hashCode()
777
778 override fun toString() = "MapDraggableAnchors($anchors)"
779 }
780
781 /**
782 * This Modifier allows configuring an [AnchoredDraggableState]'s anchors based on this layout
783 * node's size and offsetting it. It considers lookahead and reports the appropriate size and
784 * measurement for the appropriate phase.
785 *
786 * @param state The state the anchors should be attached to
787 * @param orientation The orientation the component should be offset in
788 * @param anchors Lambda to calculate the anchors based on this layout's size and the incoming
789 * constraints. These can be useful to avoid subcomposition.
790 */
791 @ExperimentalMaterialApi
draggableAnchorsnull792 internal fun <T> Modifier.draggableAnchors(
793 state: AnchoredDraggableState<T>,
794 orientation: Orientation,
795 anchors: (size: IntSize, constraints: Constraints) -> Pair<DraggableAnchors<T>, T>,
796 ) = this then DraggableAnchorsElement(state, anchors, orientation)
797
798 @OptIn(ExperimentalMaterialApi::class)
799 private class DraggableAnchorsElement<T>(
800 private val state: AnchoredDraggableState<T>,
801 private val anchors: (size: IntSize, constraints: Constraints) -> Pair<DraggableAnchors<T>, T>,
802 private val orientation: Orientation
803 ) : ModifierNodeElement<DraggableAnchorsNode<T>>() {
804
805 override fun create() = DraggableAnchorsNode(state, anchors, orientation)
806
807 override fun update(node: DraggableAnchorsNode<T>) {
808 node.state = state
809 node.anchors = anchors
810 node.orientation = orientation
811 }
812
813 override fun equals(other: Any?): Boolean {
814 if (this === other) return true
815
816 if (other !is DraggableAnchorsElement<*>) return false
817
818 if (state != other.state) return false
819 if (anchors !== other.anchors) return false
820 if (orientation != other.orientation) return false
821
822 return true
823 }
824
825 override fun hashCode(): Int {
826 var result = state.hashCode()
827 result = 31 * result + anchors.hashCode()
828 result = 31 * result + orientation.hashCode()
829 return result
830 }
831
832 override fun InspectorInfo.inspectableProperties() {
833 debugInspectorInfo {
834 properties["state"] = state
835 properties["anchors"] = anchors
836 properties["orientation"] = orientation
837 }
838 }
839 }
840
841 @OptIn(ExperimentalMaterialApi::class)
842 private class DraggableAnchorsNode<T>(
843 var state: AnchoredDraggableState<T>,
844 var anchors: (size: IntSize, constraints: Constraints) -> Pair<DraggableAnchors<T>, T>,
845 var orientation: Orientation
846 ) : Modifier.Node(), LayoutModifierNode {
847 private var didLookahead: Boolean = false
848
onDetachnull849 override fun onDetach() {
850 didLookahead = false
851 }
852
measurenull853 override fun MeasureScope.measure(
854 measurable: Measurable,
855 constraints: Constraints
856 ): MeasureResult {
857 val placeable = measurable.measure(constraints)
858 // If we are in a lookahead pass, we only want to update the anchors here and not in
859 // post-lookahead. If there is no lookahead happening (!isLookingAhead && !didLookahead),
860 // update the anchors in the main pass.
861 if (!isLookingAhead || !didLookahead) {
862 val size = IntSize(placeable.width, placeable.height)
863 val newAnchorResult = anchors(size, constraints)
864 state.updateAnchors(newAnchorResult.first, newAnchorResult.second)
865 }
866 didLookahead = isLookingAhead || didLookahead
867 return layout(placeable.width, placeable.height) {
868 // In a lookahead pass, we use the position of the current target as this is where any
869 // ongoing animations would move. If the component is in a settled state, lookahead
870 // and post-lookahead will converge.
871 val offset =
872 if (isLookingAhead) {
873 state.anchors.positionOf(state.targetValue)
874 } else state.requireOffset()
875 val xOffset = if (orientation == Orientation.Horizontal) offset else 0f
876 val yOffset = if (orientation == Orientation.Vertical) offset else 0f
877 placeable.place(xOffset.roundToInt(), yOffset.roundToInt())
878 }
879 }
880 }
881