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