1 /*
<lambda>null2  * Copyright 2020 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.constraintlayout.compose.carousel
18 
19 import androidx.compose.animation.core.Animatable
20 import androidx.compose.animation.core.AnimationSpec
21 import androidx.compose.animation.core.SpringSpec
22 import androidx.compose.foundation.gestures.DraggableState
23 import androidx.compose.foundation.gestures.Orientation
24 import androidx.compose.foundation.gestures.draggable
25 import androidx.compose.foundation.interaction.MutableInteractionSource
26 import androidx.compose.runtime.Composable
27 import androidx.compose.runtime.DisposableEffect
28 import androidx.compose.runtime.FloatState
29 import androidx.compose.runtime.Immutable
30 import androidx.compose.runtime.LaunchedEffect
31 import androidx.compose.runtime.Stable
32 import androidx.compose.runtime.getValue
33 import androidx.compose.runtime.mutableFloatStateOf
34 import androidx.compose.runtime.mutableStateOf
35 import androidx.compose.runtime.remember
36 import androidx.compose.runtime.saveable.Saver
37 import androidx.compose.runtime.saveable.rememberSaveable
38 import androidx.compose.runtime.setValue
39 import androidx.compose.runtime.snapshotFlow
40 import androidx.compose.ui.Modifier
41 import androidx.compose.ui.composed
42 import androidx.compose.ui.geometry.Offset
43 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
44 import androidx.compose.ui.input.nestedscroll.NestedScrollSource
45 import androidx.compose.ui.platform.LocalDensity
46 import androidx.compose.ui.platform.debugInspectorInfo
47 import androidx.compose.ui.unit.Density
48 import androidx.compose.ui.unit.Dp
49 import androidx.compose.ui.unit.Velocity
50 import androidx.compose.ui.unit.dp
51 import androidx.compose.ui.util.fastMaxBy
52 import androidx.compose.ui.util.fastMinByOrNull
53 import androidx.compose.ui.util.lerp
54 import kotlin.math.PI
55 import kotlin.math.abs
56 import kotlin.math.sign
57 import kotlin.math.sin
58 import kotlinx.coroutines.CancellationException
59 import kotlinx.coroutines.flow.Flow
60 import kotlinx.coroutines.flow.filter
61 import kotlinx.coroutines.flow.take
62 import kotlinx.coroutines.launch
63 
64 /**
65  * State of the [carouselSwipeable] modifier.
66  *
67  * This contains necessary information about any ongoing swipe or animation and provides methods to
68  * change the state either immediately or by starting an animation. To create and remember a
69  * [CarouselSwipeableState] with the default animation clock, use [rememberCarouselSwipeableState].
70  *
71  * @param initialValue The initial value of the state.
72  * @param animationSpec The default animation that will be used to animate to a new state.
73  * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
74  */
75 @Stable
76 internal open class CarouselSwipeableState<T>(
77     initialValue: T,
78     internal val animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
79     internal val confirmStateChange: (newValue: T) -> Boolean = { true }
80 ) {
81     /**
82      * The current value of the state.
83      *
84      * If no swipe or animation is in progress, this corresponds to the anchor at which the
85      * [carouselSwipeable] is currently settled. If a swipe or animation is in progress, this
86      * corresponds the last anchor at which the [carouselSwipeable] was settled before the swipe or
87      * animation started.
88      */
89     var currentValue: T by mutableStateOf(initialValue)
90         private set
91 
92     /** Whether the state is currently animating. */
93     var isAnimationRunning: Boolean by mutableStateOf(false)
94         private set
95 
96     /**
97      * The current position (in pixels) of the [carouselSwipeable].
98      *
99      * You should use this state to offset your content accordingly. The recommended way is to use
100      * `Modifier.offsetPx`. This includes the resistance by default, if resistance is enabled.
101      */
102     val offset: FloatState
103         get() = offsetState
104 
105     /** The amount by which the [carouselSwipeable] has been swiped past its bounds. */
106     val overflow: FloatState
107         get() = overflowState
108 
109     // Use `Float.NaN` as a placeholder while the state is uninitialised.
110     private val offsetState = mutableFloatStateOf(0f)
111     private val overflowState = mutableFloatStateOf(0f)
112 
113     // the source of truth for the "real"(non ui) position
114     // basically position in bounds + overflow
115     private val absoluteOffset = mutableFloatStateOf(0f)
116 
117     // current animation target, if animating, otherwise null
118     private val animationTarget = mutableStateOf<Float?>(null)
119 
120     internal var anchors by mutableStateOf(emptyMap<Float, T>())
121 
122     private val latestNonEmptyAnchorsFlow: Flow<Map<Float, T>> =
<lambda>null123         snapshotFlow { anchors }.filter { it.isNotEmpty() }.take(1)
124 
125     internal var minBound = Float.NEGATIVE_INFINITY
126     internal var maxBound = Float.POSITIVE_INFINITY
127 
ensureInitnull128     internal fun ensureInit(newAnchors: Map<Float, T>) {
129         if (anchors.isEmpty()) {
130             // need to do initial synchronization synchronously :(
131             val initialOffset = newAnchors.getOffset(currentValue)
132             requireNotNull(initialOffset) { "The initial value must have an associated anchor." }
133             offsetState.floatValue = initialOffset
134             absoluteOffset.floatValue = initialOffset
135         }
136     }
137 
processNewAnchorsnull138     internal suspend fun processNewAnchors(oldAnchors: Map<Float, T>, newAnchors: Map<Float, T>) {
139         if (oldAnchors.isEmpty()) {
140             // If this is the first time that we receive anchors, then we need to initialise
141             // the state so we snap to the offset associated to the initial value.
142             minBound = newAnchors.keys.minOrNull()!!
143             maxBound = newAnchors.keys.maxOrNull()!!
144             val initialOffset = newAnchors.getOffset(currentValue)
145             requireNotNull(initialOffset) { "The initial value must have an associated anchor." }
146             snapInternalToOffset(initialOffset)
147         } else if (newAnchors != oldAnchors) {
148             // If we have received new anchors, then the offset of the current value might
149             // have changed, so we need to animate to the new offset. If the current value
150             // has been removed from the anchors then we animate to the closest anchor
151             // instead. Note that this stops any ongoing animation.
152             minBound = Float.NEGATIVE_INFINITY
153             maxBound = Float.POSITIVE_INFINITY
154             val animationTargetValue = animationTarget.value
155             // if we're in the animation already, let's find it a new home
156             val targetOffset =
157                 if (animationTargetValue != null) {
158                     // first, try to map old state to the new state
159                     val oldState = oldAnchors[animationTargetValue]
160                     val newState = newAnchors.getOffset(oldState)
161                     // return new state if exists, or find the closes one among new anchors
162                     newState ?: newAnchors.keys.minByOrNull { abs(it - animationTargetValue) }!!
163                 } else {
164                     // we're not animating, proceed by finding the new anchors for an old value
165                     val actualOldValue = oldAnchors[offset.floatValue]
166                     val value = if (actualOldValue == currentValue) currentValue else actualOldValue
167                     newAnchors.getOffset(value)
168                         ?: newAnchors.keys.minByOrNull { abs(it - offset.floatValue) }!!
169                 }
170             try {
171                 animateInternalToOffset(targetOffset, animationSpec)
172             } catch (c: CancellationException) {
173                 // If the animation was interrupted for any reason, snap as a last resort.
174                 snapInternalToOffset(targetOffset)
175             } finally {
176                 currentValue = newAnchors.getValue(targetOffset)
177                 minBound = newAnchors.keys.minOrNull()!!
178                 maxBound = newAnchors.keys.maxOrNull()!!
179             }
180         }
181     }
182 
_null183     internal var thresholds: (Float, Float) -> Float by mutableStateOf({ _, _ -> 0f })
184 
185     internal var velocityThreshold by mutableFloatStateOf(0f)
186 
187     internal var resistance: ResistanceConfig? by mutableStateOf(null)
188 
<lambda>null189     internal val draggableState = DraggableState {
190         val newAbsolute = absoluteOffset.floatValue + it
191         val clamped = newAbsolute.coerceIn(minBound, maxBound)
192         val overflow = newAbsolute - clamped
193         val resistanceDelta = resistance?.computeResistance(overflow) ?: 0f
194         offsetState.floatValue = clamped + resistanceDelta
195         overflowState.floatValue = overflow
196         absoluteOffset.floatValue = newAbsolute
197     }
198 
snapInternalToOffsetnull199     private suspend fun snapInternalToOffset(target: Float) {
200         draggableState.drag { dragBy(target - absoluteOffset.floatValue) }
201     }
202 
animateInternalToOffsetnull203     private suspend fun animateInternalToOffset(target: Float, spec: AnimationSpec<Float>) {
204         draggableState.drag {
205             var prevValue = absoluteOffset.floatValue
206             animationTarget.value = target
207             isAnimationRunning = true
208             try {
209                 Animatable(prevValue).animateTo(target, spec) {
210                     dragBy(this.value - prevValue)
211                     prevValue = this.value
212                 }
213             } finally {
214                 animationTarget.value = null
215                 isAnimationRunning = false
216             }
217         }
218     }
219 
220     /**
221      * The target value of the state.
222      *
223      * If a swipe is in progress, this is the value that the [carouselSwipeable] would animate to if
224      * the swipe finished. If an animation is running, this is the target value of that animation.
225      * Finally, if no swipe or animation is in progress, this is the same as the [currentValue].
226      */
227     val targetValue: T
228         get() {
229             // TODO(calintat): Track current velocity (b/149549482) and use that here.
230             val target =
231                 animationTarget.value
232                     ?: computeTarget(
233                         offset = offset.floatValue,
234                         lastValue = anchors.getOffset(currentValue) ?: offset.floatValue,
235                         anchors = anchors.keys,
236                         thresholds = thresholds,
237                         velocity = 0f,
238                         velocityThreshold = Float.POSITIVE_INFINITY
239                     )
240             return anchors[target] ?: currentValue
241         }
242 
243     /**
244      * Information about the ongoing swipe or animation, if any. See [SwipeProgress] for details.
245      *
246      * If no swipe or animation is in progress, this returns `SwipeProgress(value, value, 1f)`.
247      */
248     val progress: SwipeProgress<T>
249         get() {
250             val bounds = findBounds(offset.floatValue, anchors.keys)
251             val from: T
252             val to: T
253             val fraction: Float
254             when (bounds.size) {
255                 0 -> {
256                     from = currentValue
257                     to = currentValue
258                     fraction = 1f
259                 }
260                 1 -> {
261                     from = anchors.getValue(bounds[0])
262                     to = anchors.getValue(bounds[0])
263                     fraction = 1f
264                 }
265                 else -> {
266                     val (a, b) =
267                         if (direction > 0f) {
268                             bounds[0] to bounds[1]
269                         } else {
270                             bounds[1] to bounds[0]
271                         }
272                     from = anchors.getValue(a)
273                     to = anchors.getValue(b)
274                     fraction = (offset.floatValue - a) / (b - a)
275                 }
276             }
277             return SwipeProgress(from, to, fraction)
278         }
279 
280     /**
281      * The direction in which the [carouselSwipeable] is moving, relative to the current
282      * [currentValue].
283      *
284      * This will be either 1f if it is is moving from left to right or top to bottom, -1f if it is
285      * moving from right to left or bottom to top, or 0f if no swipe or animation is in progress.
286      */
287     val direction: Float
<lambda>null288         get() = anchors.getOffset(currentValue)?.let { sign(offset.floatValue - it) } ?: 0f
289 
290     /**
291      * Set the state without any animation and suspend until it's set
292      *
293      * @param targetValue The new target value to set [currentValue] to.
294      */
snapTonull295     suspend fun snapTo(targetValue: T) {
296         latestNonEmptyAnchorsFlow.collect { anchors ->
297             val targetOffset = anchors.getOffset(targetValue)
298             requireNotNull(targetOffset) { "The target value must have an associated anchor." }
299             snapInternalToOffset(targetOffset)
300             currentValue = targetValue
301         }
302     }
303 
304     /**
305      * Set the state to the target value by starting an animation.
306      *
307      * @param targetValue The new value to animate to.
308      * @param anim The animation that will be used to animate to the new value.
309      */
animateTonull310     suspend fun animateTo(targetValue: T, anim: AnimationSpec<Float> = animationSpec) {
311         latestNonEmptyAnchorsFlow.collect { anchors ->
312             try {
313                 val targetOffset = anchors.getOffset(targetValue)
314                 requireNotNull(targetOffset) { "The target value must have an associated anchor." }
315                 animateInternalToOffset(targetOffset, anim)
316             } finally {
317                 val endOffset = absoluteOffset.floatValue
318                 val endValue =
319                     anchors
320                         // fighting rounding error once again, anchor should be as close as 0.5
321                         // pixels
322                         .filterKeys { anchorOffset -> abs(anchorOffset - endOffset) < 0.5f }
323                         .values
324                         .firstOrNull() ?: currentValue
325                 currentValue = endValue
326             }
327         }
328     }
329 
330     /**
331      * Perform fling with settling to one of the anchors which is determined by the given
332      * [velocity]. Fling with settling [carouselSwipeable] will always consume all the velocity
333      * provided since it will settle at the anchor.
334      *
335      * In general cases, [carouselSwipeable] flings by itself when being swiped. This method is to
336      * be used for nested scroll logic that wraps the [carouselSwipeable]. In nested scroll
337      * developer may want to trigger settling fling when the child scroll container reaches the
338      * bound.
339      *
340      * @param velocity velocity to fling and settle with
341      * @return the reason fling ended
342      */
performFlingnull343     suspend fun performFling(velocity: Float) {
344         latestNonEmptyAnchorsFlow.collect { anchors ->
345             val lastAnchor = anchors.getOffset(currentValue)!!
346             val targetValue =
347                 computeTarget(
348                     offset = offset.floatValue,
349                     lastValue = lastAnchor,
350                     anchors = anchors.keys,
351                     thresholds = thresholds,
352                     velocity = velocity,
353                     velocityThreshold = velocityThreshold
354                 )
355             val targetState = anchors[targetValue]
356             if (targetState != null && confirmStateChange(targetState)) animateTo(targetState)
357             // If the user vetoed the state change, rollback to the previous state.
358             else animateInternalToOffset(lastAnchor, animationSpec)
359         }
360     }
361 
362     /**
363      * Force [carouselSwipeable] to consume drag delta provided from outside of the regular
364      * [carouselSwipeable] gesture flow.
365      *
366      * Note: This method performs generic drag and it won't settle to any particular anchor, *
367      * leaving swipeable in between anchors. When done dragging, [performFling] must be called as
368      * well to ensure swipeable will settle at the anchor.
369      *
370      * In general cases, [carouselSwipeable] drags by itself when being swiped. This method is to be
371      * used for nested scroll logic that wraps the [carouselSwipeable]. In nested scroll developer
372      * may want to force drag when the child scroll container reaches the bound.
373      *
374      * @param delta delta in pixels to drag by
375      * @return the amount of [delta] consumed
376      */
performDragnull377     fun performDrag(delta: Float): Float {
378         val potentiallyConsumed = absoluteOffset.floatValue + delta
379         val clamped = potentiallyConsumed.coerceIn(minBound, maxBound)
380         val deltaToConsume = clamped - absoluteOffset.floatValue
381         if (abs(deltaToConsume) > 0) {
382             draggableState.dispatchRawDelta(deltaToConsume)
383         }
384         return deltaToConsume
385     }
386 
387     companion object {
388         /** The default [Saver] implementation for [CarouselSwipeableState]. */
Savernull389         fun <T : Any> Saver(
390             animationSpec: AnimationSpec<Float>,
391             confirmStateChange: (T) -> Boolean
392         ) =
393             Saver<CarouselSwipeableState<T>, T>(
394                 save = { it.currentValue },
<lambda>null395                 restore = { CarouselSwipeableState(it, animationSpec, confirmStateChange) }
396             )
397     }
398 }
399 
400 /**
401  * Collects information about the ongoing swipe or animation in [carouselSwipeable].
402  *
403  * To access this information, use [CarouselSwipeableState.progress].
404  *
405  * @param from The state corresponding to the anchor we are moving away from.
406  * @param to The state corresponding to the anchor we are moving towards.
407  * @param fraction The fraction that the current position represents between [from] and [to]. Must
408  *   be between `0` and `1`.
409  */
410 @Immutable
411 internal class SwipeProgress<T>(
412     val from: T,
413     val to: T,
414     /*@FloatRange(from = 0.0, to = 1.0)*/
415     val fraction: Float
416 ) {
equalsnull417     override fun equals(other: Any?): Boolean {
418         if (this === other) return true
419         if (other !is SwipeProgress<*>) return false
420 
421         if (from != other.from) return false
422         if (to != other.to) return false
423         if (fraction != other.fraction) return false
424 
425         return true
426     }
427 
hashCodenull428     override fun hashCode(): Int {
429         var result = from?.hashCode() ?: 0
430         result = 31 * result + (to?.hashCode() ?: 0)
431         result = 31 * result + fraction.hashCode()
432         return result
433     }
434 
toStringnull435     override fun toString(): String {
436         return "SwipeProgress(from=$from, to=$to, fraction=$fraction)"
437     }
438 }
439 
440 /**
441  * Create and [remember] a [CarouselSwipeableState] with the default animation clock.
442  *
443  * @param initialValue The initial value of the state.
444  * @param animationSpec The default animation that will be used to animate to a new state.
445  * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
446  */
447 @Composable
rememberCarouselSwipeableStatenull448 internal fun <T : Any> rememberCarouselSwipeableState(
449     initialValue: T,
450     animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
451     confirmStateChange: (newValue: T) -> Boolean = { true }
452 ): CarouselSwipeableState<T> {
453     return rememberSaveable(
454         saver =
455             CarouselSwipeableState.Saver(
456                 animationSpec = animationSpec,
457                 confirmStateChange = confirmStateChange
458             )
<lambda>null459     ) {
460         CarouselSwipeableState(
461             initialValue = initialValue,
462             animationSpec = animationSpec,
463             confirmStateChange = confirmStateChange
464         )
465     }
466 }
467 
468 /**
469  * Create and [remember] a [CarouselSwipeableState] which is kept in sync with another state, i.e.:
470  * 1. Whenever the [value] changes, the [CarouselSwipeableState] will be animated to that new value.
471  * 2. Whenever the value of the [CarouselSwipeableState] changes (e.g. after a swipe), the owner of
472  *    the [value] will be notified to update their state to the new value of the
473  *    [CarouselSwipeableState] by invoking [onValueChange]. If the owner does not update their state
474  *    to the provided value for some reason, then the [CarouselSwipeableState] will perform a
475  *    rollback to the previous, correct value.
476  */
477 @Composable
rememberCarouselSwipeableStateFornull478 internal fun <T : Any> rememberCarouselSwipeableStateFor(
479     value: T,
480     onValueChange: (T) -> Unit,
481     animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec
482 ): CarouselSwipeableState<T> {
483     val swipeableState = remember {
484         CarouselSwipeableState(
485             initialValue = value,
486             animationSpec = animationSpec,
487             confirmStateChange = { true }
488         )
489     }
490     val forceAnimationCheck = remember { mutableStateOf(false) }
491     LaunchedEffect(value, forceAnimationCheck.value) {
492         if (value != swipeableState.currentValue) {
493             swipeableState.animateTo(value)
494         }
495     }
496     DisposableEffect(swipeableState.currentValue) {
497         if (value != swipeableState.currentValue) {
498             onValueChange(swipeableState.currentValue)
499             forceAnimationCheck.value = !forceAnimationCheck.value
500         }
501         onDispose {}
502     }
503     return swipeableState
504 }
505 
506 /**
507  * Enable swipe gestures between a set of predefined states.
508  *
509  * To use this, you must provide a map of anchors (in pixels) to states (of type [T]). Note that
510  * this map cannot be empty and cannot have two anchors mapped to the same state.
511  *
512  * When a swipe is detected, the offset of the [CarouselSwipeableState] will be updated with the
513  * swipe delta. You should use this offset to move your content accordingly (see
514  * `Modifier.offsetPx`). When the swipe ends, the offset will be animated to one of the anchors and
515  * when that anchor is reached, the value of the [CarouselSwipeableState] will also be updated to
516  * the state corresponding to the new anchor. The target anchor is calculated based on the provided
517  * positional [thresholds].
518  *
519  * Swiping is constrained between the minimum and maximum anchors. If the user attempts to swipe
520  * past these bounds, a resistance effect will be applied by default. The amount of resistance at
521  * each edge is specified by the [resistance] config. To disable all resistance, set it to `null`.
522  *
523  * For an example of a [carouselSwipeable] with three states, see:
524  *
525  * @param T The type of the state.
526  * @param state The state of the [carouselSwipeable].
527  * @param anchors Pairs of anchors and states, used to map anchors to states and vice versa.
528  * @param thresholds Specifies where the thresholds between the states are. The thresholds will be
529  *   used to determine which state to animate to when swiping stops. This is represented as a lambda
530  *   that takes two states and returns the threshold between them in the form of a
531  *   [ThresholdConfig]. Note that the order of the states corresponds to the swipe direction.
532  * @param orientation The orientation in which the [carouselSwipeable] can be swiped.
533  * @param enabled Whether this [carouselSwipeable] is enabled and should react to the user's input.
534  * @param reverseDirection Whether to reverse the direction of the swipe, so a top to bottom swipe
535  *   will behave like bottom to top, and a left to right swipe will behave like right to left.
536  * @param interactionSource Optional [MutableInteractionSource] that will passed on to the internal
537  *   [Modifier.draggable].
538  * @param resistance Controls how much resistance will be applied when swiping past the bounds.
539  * @param velocityThreshold The threshold (in dp per second) that the end velocity has to exceed in
540  *   order to animate to the next state, even if the positional [thresholds] have not been reached.
541  */
carouselSwipeablenull542 internal fun <T> Modifier.carouselSwipeable(
543     state: CarouselSwipeableState<T>,
544     anchors: Map<Float, T>,
545     orientation: Orientation,
546     enabled: Boolean = true,
547     reverseDirection: Boolean = false,
548     interactionSource: MutableInteractionSource? = null,
549     thresholds: (from: T, to: T) -> ThresholdConfig = { _, _ -> FixedThreshold(56.dp) },
550     resistance: ResistanceConfig? = SwipeableDefaults.resistanceConfig(anchors.keys),
551     velocityThreshold: Dp = SwipeableDefaults.VelocityThreshold
552 ) =
553     composed(
554         inspectorInfo =
<lambda>null555             debugInspectorInfo {
556                 name = "swipeable"
557                 properties["state"] = state
558                 properties["anchors"] = anchors
559                 properties["orientation"] = orientation
560                 properties["enabled"] = enabled
561                 properties["reverseDirection"] = reverseDirection
562                 properties["interactionSource"] = interactionSource
563                 properties["thresholds"] = thresholds
564                 properties["resistance"] = resistance
565                 properties["velocityThreshold"] = velocityThreshold
566             }
<lambda>null567     ) {
568         require(anchors.isNotEmpty()) { "You must have at least one anchor." }
569         require(anchors.values.distinct().count() == anchors.size) {
570             "You cannot have two anchors mapped to the same state."
571         }
572         val density = LocalDensity.current
573         state.ensureInit(anchors)
574         LaunchedEffect(anchors, state) {
575             val oldAnchors = state.anchors
576             state.anchors = anchors
577             state.resistance = resistance
578             state.thresholds = { a, b ->
579                 val from = anchors.getValue(a)
580                 val to = anchors.getValue(b)
581                 with(thresholds(from, to)) { density.computeThreshold(a, b) }
582             }
583             with(density) { state.velocityThreshold = velocityThreshold.toPx() }
584             state.processNewAnchors(oldAnchors, anchors)
585         }
586 
587         Modifier.draggable(
588             orientation = orientation,
589             enabled = enabled,
590             reverseDirection = reverseDirection,
591             interactionSource = interactionSource,
592             startDragImmediately = state.isAnimationRunning,
593             onDragStopped = { velocity -> launch { state.performFling(velocity) } },
594             state = state.draggableState
595         )
596     }
597 
598 /**
599  * Interface to compute a threshold between two anchors/states in a [carouselSwipeable].
600  *
601  * To define a [ThresholdConfig], consider using [FixedThreshold] and [FractionalThreshold].
602  */
603 @Stable
604 internal interface ThresholdConfig {
605     /** Compute the value of the threshold (in pixels), once the values of the anchors are known. */
computeThresholdnull606     fun Density.computeThreshold(fromValue: Float, toValue: Float): Float
607 }
608 
609 /**
610  * A fixed threshold will be at an [offset] away from the first anchor.
611  *
612  * @param offset The offset (in dp) that the threshold will be at.
613  */
614 @Immutable
615 internal data class FixedThreshold(private val offset: Dp) : ThresholdConfig {
616     override fun Density.computeThreshold(fromValue: Float, toValue: Float): Float {
617         return fromValue + offset.toPx() * sign(toValue - fromValue)
618     }
619 }
620 
621 /**
622  * A fractional threshold will be at a [fraction] of the way between the two anchors.
623  *
624  * @param fraction The fraction (between 0 and 1) that the threshold will be at.
625  */
626 @Immutable
627 internal data class FractionalThreshold(
628     /*@FloatRange(from = 0.0, to = 1.0)*/
629     private val fraction: Float
630 ) : ThresholdConfig {
computeThresholdnull631     override fun Density.computeThreshold(fromValue: Float, toValue: Float): Float {
632         return lerp(fromValue, toValue, fraction)
633     }
634 }
635 
636 /**
637  * Specifies how resistance is calculated in [carouselSwipeable].
638  *
639  * There are two things needed to calculate resistance: the resistance basis determines how much
640  * overflow will be consumed to achieve maximum resistance, and the resistance factor determines the
641  * amount of resistance (the larger the resistance factor, the stronger the resistance).
642  *
643  * The resistance basis is usually either the size of the component which [carouselSwipeable] is
644  * applied to, or the distance between the minimum and maximum anchors. For a constructor in which
645  * the resistance basis defaults to the latter, consider using [resistanceConfig].
646  *
647  * You may specify different resistance factors for each bound. Consider using one of the default
648  * resistance factors in [SwipeableDefaults]: `StandardResistanceFactor` to convey that the user has
649  * run out of things to see, and `StiffResistanceFactor` to convey that the user cannot swipe this
650  * right now. Also, you can set either factor to 0 to disable resistance at that bound.
651  *
652  * @param basis Specifies the maximum amount of overflow that will be consumed. Must be positive.
653  * @param factorAtMin The factor by which to scale the resistance at the minimum bound. Must not be
654  *   negative.
655  * @param factorAtMax The factor by which to scale the resistance at the maximum bound. Must not be
656  *   negative.
657  */
658 @Immutable
659 internal class ResistanceConfig(
660     /*@FloatRange(from = 0.0, fromInclusive = false)*/
661     val basis: Float,
662     /*@FloatRange(from = 0.0)*/
663     val factorAtMin: Float = SwipeableDefaults.StandardResistanceFactor,
664     /*@FloatRange(from = 0.0)*/
665     val factorAtMax: Float = SwipeableDefaults.StandardResistanceFactor
666 ) {
computeResistancenull667     fun computeResistance(overflow: Float): Float {
668         val factor = if (overflow < 0) factorAtMin else factorAtMax
669         if (factor == 0f) return 0f
670         val progress = (overflow / basis).coerceIn(-1f, 1f)
671         return basis / factor * sin(progress * PI.toFloat() / 2)
672     }
673 
equalsnull674     override fun equals(other: Any?): Boolean {
675         if (this === other) return true
676         if (other !is ResistanceConfig) return false
677 
678         if (basis != other.basis) return false
679         if (factorAtMin != other.factorAtMin) return false
680         if (factorAtMax != other.factorAtMax) return false
681 
682         return true
683     }
684 
hashCodenull685     override fun hashCode(): Int {
686         var result = basis.hashCode()
687         result = 31 * result + factorAtMin.hashCode()
688         result = 31 * result + factorAtMax.hashCode()
689         return result
690     }
691 
toStringnull692     override fun toString(): String {
693         return "ResistanceConfig(basis=$basis, factorAtMin=$factorAtMin, factorAtMax=$factorAtMax)"
694     }
695 }
696 
697 /**
698  * Given an offset x and a set of anchors, return a list of anchors:
699  * 1. [ ] if the set of anchors is empty,
700  * 2. [ x' ] if x is equal to one of the anchors, accounting for a small rounding error, where x' is
701  *    x rounded to the exact value of the matching anchor,
702  * 3. [ min ] if min is the minimum anchor and x < min,
703  * 4. [ max ] if max is the maximum anchor and x > max, or
704  * 5. [ a , b ] if a and b are anchors such that a < x < b and b - a is minimal.
705  */
findBoundsnull706 private fun findBounds(offset: Float, anchors: Set<Float>): List<Float> {
707     // Find the anchors the target lies between with a little bit of rounding error.
708     val a = anchors.filter { it <= offset + 0.001 }.fastMaxBy { it }
709     val b = anchors.filter { it >= offset - 0.001 }.fastMinByOrNull { it }
710 
711     return when {
712         a == null ->
713             // case 1 or 3
714             listOfNotNull(b)
715         b == null ->
716             // case 4
717             listOf(a)
718         a == b ->
719             // case 2
720             // Can't return offset itself here since it might not be exactly equal
721             // to the anchor, despite being considered an exact match.
722             listOf(a)
723         else ->
724             // case 5
725             listOf(a, b)
726     }
727 }
728 
computeTargetnull729 private fun computeTarget(
730     offset: Float,
731     lastValue: Float,
732     anchors: Set<Float>,
733     thresholds: (Float, Float) -> Float,
734     velocity: Float,
735     velocityThreshold: Float
736 ): Float {
737     val bounds = findBounds(offset, anchors)
738     return when (bounds.size) {
739         0 -> lastValue
740         1 -> bounds[0]
741         else -> {
742             val lower = bounds[0]
743             val upper = bounds[1]
744             if (lastValue <= offset) {
745                 // Swiping from lower to upper (positive).
746                 if (velocity >= velocityThreshold) {
747                     return upper
748                 } else {
749                     val threshold = thresholds(lower, upper)
750                     if (offset < threshold) lower else upper
751                 }
752             } else {
753                 // Swiping from upper to lower (negative).
754                 if (velocity <= -velocityThreshold) {
755                     return lower
756                 } else {
757                     val threshold = thresholds(upper, lower)
758                     if (offset > threshold) upper else lower
759                 }
760             }
761         }
762     }
763 }
764 
getOffsetnull765 private fun <T> Map<Float, T>.getOffset(state: T): Float? {
766     return entries.firstOrNull { it.value == state }?.key
767 }
768 
769 /** Contains useful defaults for [carouselSwipeable] and [CarouselSwipeableState]. */
770 internal object SwipeableDefaults {
771     /** The default animation used by [CarouselSwipeableState]. */
772     val AnimationSpec = SpringSpec<Float>()
773 
774     /** The default velocity threshold (1.8 dp per millisecond) used by [carouselSwipeable]. */
775     val VelocityThreshold = 125.dp
776 
777     /** A stiff resistance factor which indicates that swiping isn't available right now. */
778     const val StiffResistanceFactor = 20f
779 
780     /** A standard resistance factor which indicates that the user has run out of things to see. */
781     const val StandardResistanceFactor = 10f
782 
783     /**
784      * The default resistance config used by [carouselSwipeable].
785      *
786      * This returns `null` if there is one anchor. If there are at least two anchors, it returns a
787      * [ResistanceConfig] with the resistance basis equal to the distance between the two bounds.
788      */
resistanceConfignull789     fun resistanceConfig(
790         anchors: Set<Float>,
791         factorAtMin: Float = StandardResistanceFactor,
792         factorAtMax: Float = StandardResistanceFactor
793     ): ResistanceConfig? {
794         return if (anchors.size <= 1) {
795             null
796         } else {
797             val basis = anchors.maxOrNull()!! - anchors.minOrNull()!!
798             ResistanceConfig(basis, factorAtMin, factorAtMax)
799         }
800     }
801 }
802 
803 // temp default nested scroll connection for swipeables which desire as an opt in
804 // revisit in b/174756744 as all types will have their own specific connection probably
805 internal val <T> CarouselSwipeableState<T>.PreUpPostDownNestedScrollConnection:
806     NestedScrollConnection
807     get() =
808         object : NestedScrollConnection {
onPreScrollnull809             override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
810                 val delta = available.toFloat()
811                 return if (delta < 0 && source == NestedScrollSource.UserInput) {
812                     performDrag(delta).toOffset()
813                 } else {
814                     Offset.Zero
815                 }
816             }
817 
onPostScrollnull818             override fun onPostScroll(
819                 consumed: Offset,
820                 available: Offset,
821                 source: NestedScrollSource
822             ): Offset {
823                 return if (source == NestedScrollSource.UserInput) {
824                     performDrag(available.toFloat()).toOffset()
825                 } else {
826                     Offset.Zero
827                 }
828             }
829 
onPreFlingnull830             override suspend fun onPreFling(available: Velocity): Velocity {
831                 val toFling = Offset(available.x, available.y).toFloat()
832                 return if (toFling < 0 && offset.floatValue > minBound) {
833                     performFling(velocity = toFling)
834                     // since we go to the anchor with tween settling, consume all for the best UX
835                     available
836                 } else {
837                     Velocity.Zero
838                 }
839             }
840 
onPostFlingnull841             override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
842                 performFling(velocity = Offset(available.x, available.y).toFloat())
843                 return available
844             }
845 
Floatnull846             private fun Float.toOffset(): Offset = Offset(0f, this)
847 
848             private fun Offset.toFloat(): Float = this.y
849         }
850