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