1 /*
2  * Copyright 2019 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.animation.core
18 
19 import androidx.compose.animation.core.AnimationEndReason.BoundReached
20 import androidx.compose.animation.core.AnimationEndReason.Finished
21 import androidx.compose.runtime.State
22 import androidx.compose.runtime.annotation.RememberInComposition
23 import androidx.compose.runtime.getValue
24 import androidx.compose.runtime.mutableStateOf
25 import androidx.compose.runtime.setValue
26 import kotlinx.coroutines.CancellationException
27 
28 /**
29  * [Animatable] is a value holder that automatically animates its value when the value is changed
30  * via [animateTo]. If [animateTo] is invoked during an ongoing value change animation, a new
31  * animation will transition [Animatable] from its current value (i.e. value at the point of
32  * interruption) to the new [targetValue]. This ensures that the value change is __always__
33  * continuous using [animateTo]. If a [spring] animation (e.g. default animation) is used with
34  * [animateTo], the velocity change will guarantee to be continuous as well.
35  *
36  * Unlike [AnimationState], [Animatable] ensures *mutual exclusiveness* on its animations. To
37  * achieve this, when a new animation is started via [animateTo] (or [animateDecay]), any ongoing
38  * animation will be canceled via a [CancellationException].
39  *
40  * @sample androidx.compose.animation.core.samples.AnimatableAnimateToGenericsType
41  * @param initialValue initial value of the animatable value holder
42  * @param typeConverter A two-way converter that converts the given type [T] from and to
43  *   [AnimationVector]
44  * @param visibilityThreshold Threshold at which the animation may round off to its target value.
45  * @param label An optional label for differentiating this animation from others in android studio.
46  * @see animateTo
47  * @see animateDecay
48  */
49 @Suppress("NotCloseable")
50 public class Animatable<T, V : AnimationVector>
51 @RememberInComposition
52 constructor(
53     initialValue: T,
54     public val typeConverter: TwoWayConverter<T, V>,
55     private val visibilityThreshold: T? = null,
56     public val label: String = "Animatable"
57 ) {
58 
59     @Deprecated(
60         "Maintained for binary compatibility",
61         replaceWith =
62             ReplaceWith(
63                 "Animatable(initialValue, typeConverter, visibilityThreshold, \"Animatable\")"
64             ),
65         DeprecationLevel.HIDDEN
66     )
67     public constructor(
68         initialValue: T,
69         typeConverter: TwoWayConverter<T, V>,
70         visibilityThreshold: T? = null
71     ) : this(initialValue, typeConverter, visibilityThreshold, "Animatable")
72 
73     internal val internalState =
74         AnimationState(typeConverter = typeConverter, initialValue = initialValue)
75 
76     /** Current value of the animation. */
77     public val value: T
78         get() = internalState.value
79 
80     /** Velocity vector of the animation (in the form of [AnimationVector]. */
81     public val velocityVector: V
82         get() = internalState.velocityVector
83 
84     /** Returns the velocity, converted from [velocityVector]. */
85     public val velocity: T
86         get() = typeConverter.convertFromVector(velocityVector)
87 
88     /** Indicates whether the animation is running. */
89     public var isRunning: Boolean by mutableStateOf(false)
90         private set
91 
92     /**
93      * The target of the current animation. If the animation finishes un-interrupted, it will reach
94      * this target value.
95      */
96     public var targetValue: T by mutableStateOf(initialValue)
97         private set
98 
99     /**
100      * Lower bound of the animation, null by default (meaning no lower bound). Bounds can be changed
101      * using [updateBounds].
102      *
103      * Animation will stop as soon as *any* dimension specified in [lowerBound] is reached. For
104      * example: For an Animatable<Offset> with an [lowerBound] set to Offset(100f, 200f), when the
105      * [value].x drops below 100f *or* [value].y drops below 200f, the animation will stop.
106      */
107     public var lowerBound: T? = null
108         private set
109 
110     /**
111      * Upper bound of the animation, null by default (meaning no upper bound). Bounds can be changed
112      * using [updateBounds].
113      *
114      * Animation will stop as soon as *any* dimension specified in [upperBound] is reached. For
115      * example: For an Animatable<Offset> with an [upperBound] set to Offset(100f, 200f), when the
116      * [value].x exceeds 100f *or* [value].y exceeds 200f, the animation will stop.
117      */
118     public var upperBound: T? = null
119         private set
120 
121     private val mutatorMutex = MutatorMutex()
122     internal val defaultSpringSpec: SpringSpec<T> =
123         SpringSpec(visibilityThreshold = visibilityThreshold)
124 
125     @Suppress("UNCHECKED_CAST")
126     private val negativeInfinityBounds: V =
127         when (velocityVector) {
128             is AnimationVector1D -> negativeInfinityBounds1D
129             is AnimationVector2D -> negativeInfinityBounds2D
130             is AnimationVector3D -> negativeInfinityBounds3D
131             else -> negativeInfinityBounds4D
132         }
133             as V
134 
135     @Suppress("UNCHECKED_CAST")
136     private val positiveInfinityBounds =
137         when (velocityVector) {
138             is AnimationVector1D -> positiveInfinityBounds1D
139             is AnimationVector2D -> positiveInfinityBounds2D
140             is AnimationVector3D -> positiveInfinityBounds3D
141             else -> positiveInfinityBounds4D
142         }
143             as V
144 
145     private var lowerBoundVector: V = negativeInfinityBounds
146     private var upperBoundVector: V = positiveInfinityBounds
147 
148     /**
149      * Updates either [lowerBound] or [upperBound], or both. This will update
150      * [Animatable.lowerBound] and/or [Animatable.upperBound] accordingly after a check to ensure
151      * the provided [lowerBound] is no greater than [upperBound] in any dimension.
152      *
153      * Setting the bounds will immediate clamp the [value], only if the animation isn't running. For
154      * the on-going animation, the value at the next frame update will be checked against the
155      * bounds. If the value reaches the bound, then the animation will end with [BoundReached] end
156      * reason.
157      *
158      * @param lowerBound lower bound of the animation. Defaults to the [Animatable.lowerBound] that
159      *   is currently set.
160      * @param upperBound upper bound of the animation. Defaults to the [Animatable.upperBound] that
161      *   is currently set.
162      * @throws [IllegalStateException] if the [lowerBound] is greater than [upperBound] in any
163      *   dimension.
164      */
updateBoundsnull165     public fun updateBounds(lowerBound: T? = this.lowerBound, upperBound: T? = this.upperBound) {
166         val lowerBoundVector =
167             lowerBound?.run { typeConverter.convertToVector(this) } ?: negativeInfinityBounds
168 
169         val upperBoundVector =
170             upperBound?.run { typeConverter.convertToVector(this) } ?: positiveInfinityBounds
171 
172         for (i in 0 until lowerBoundVector.size) {
173             // TODO: is this check too aggressive?
174             checkPrecondition(lowerBoundVector[i] <= upperBoundVector[i]) {
175                 "Lower bound must be no greater than upper bound on *all* dimensions. The " +
176                     "provided lower bound: $lowerBoundVector is greater than upper bound " +
177                     "$upperBoundVector on index $i"
178             }
179         }
180         // After the correctness check:
181         this.lowerBoundVector = lowerBoundVector
182         this.upperBoundVector = upperBoundVector
183 
184         this.upperBound = upperBound
185         this.lowerBound = lowerBound
186         if (!isRunning) {
187             val clampedValue = clampToBounds(value)
188             if (clampedValue != value) {
189                 this.internalState.value = clampedValue
190             }
191         }
192     }
193 
194     /**
195      * Starts an animation to animate from [value] to the provided [targetValue]. If there is
196      * already an animation in-flight, this method will cancel the ongoing animation before starting
197      * a new animation continuing the current [value] and [velocity]. It's recommended to set the
198      * optional [initialVelocity] only when [animateTo] is used immediately after a fling. In most
199      * of the other cases, altering velocity would result in visual discontinuity.
200      *
201      * The animation will use the provided [animationSpec] to animate the value towards the
202      * [targetValue]. When no [animationSpec] is specified, a [spring] will be used. [block] will be
203      * invoked on each animation frame.
204      *
205      * Returns an [AnimationResult] object. It contains: 1) the reason for ending the animation,
206      * and 2) an end state of the animation. The reason for ending the animation can be either of
207      * the following two:
208      * - [Finished], when the animation finishes successfully without any interruption,
209      * - [BoundReached] If the animation reaches the either [lowerBound] or [upperBound] in any
210      *   dimension, the animation will end with [BoundReached] being the end reason.
211      *
212      * If the animation gets interrupted by 1) another call to start an animation (i.e.
213      * [animateTo]/[animateDecay]), 2) [Animatable.stop], or 3)[Animatable.snapTo], the canceled
214      * animation will throw a [CancellationException] as the job gets canceled. As a result, all the
215      * subsequent work in the caller's coroutine will be canceled. This is often the desired
216      * behavior. If there's any cleanup that needs to be done when an animation gets canceled,
217      * consider starting the animation in a `try-catch` block.
218      *
219      * __Note__: once the animation ends, its velocity will be reset to 0. The animation state at
220      * the point of interruption/reaching bound is captured in the returned [AnimationResult]. If
221      * there's a need to continue the momentum that the animation had before it was interrupted or
222      * reached the bound, it's recommended to use the velocity in the returned
223      * [AnimationResult.endState] to start another animation.
224      *
225      * @sample androidx.compose.animation.core.samples.AnimatableFadeIn
226      */
animateTonull227     public suspend fun animateTo(
228         targetValue: T,
229         animationSpec: AnimationSpec<T> = defaultSpringSpec,
230         initialVelocity: T = velocity,
231         block: (Animatable<T, V>.() -> Unit)? = null
232     ): AnimationResult<T, V> {
233         val anim =
234             TargetBasedAnimation(
235                 animationSpec = animationSpec,
236                 initialValue = value,
237                 targetValue = targetValue,
238                 typeConverter = typeConverter,
239                 initialVelocity = initialVelocity
240             )
241         return runAnimation(anim, initialVelocity, block)
242     }
243 
244     /**
245      * Start a decay animation (i.e. an animation that *slows down* from the given [initialVelocity]
246      * starting at current [Animatable.value] until the velocity reaches 0. If there's already an
247      * ongoing animation, the animation in-flight will be immediately cancelled. Decay animation is
248      * often used after a fling gesture.
249      *
250      * [animationSpec] defines the decay animation that will be used for this animation. Some
251      * options for this [animationSpec] include: [splineBasedDecay][androidx.compose
252      * .animation.splineBasedDecay] and [exponentialDecay]. [block] will be invoked on each
253      * animation frame.
254      *
255      * Returns an [AnimationResult] object, that contains the [reason][AnimationEndReason] for
256      * ending the animation, and an end state of the animation. The reason for ending the animation
257      * will be [Finished] if the animation finishes successfully without any interruption. If the
258      * animation reaches the either [lowerBound] or [upperBound] in any dimension, the animation
259      * will end with [BoundReached] being the end reason.
260      *
261      * If the animation gets interrupted by 1) another call to start an animation (i.e.
262      * [animateTo]/[animateDecay]), 2) [Animatable.stop], or 3)[Animatable.snapTo], the canceled
263      * animation will throw a [CancellationException] as the job gets canceled. As a result, all the
264      * subsequent work in the caller's coroutine will be canceled. This is often the desired
265      * behavior. If there's any cleanup that needs to be done when an animation gets canceled,
266      * consider starting the animation in a `try-catch` block.
267      *
268      * __Note__, once the animation ends, its velocity will be reset to 0. If there's a need to
269      * continue the momentum before the animation gets interrupted or reaches the bound, it's
270      * recommended to use the velocity in the returned [AnimationResult.endState] to start another
271      * animation.
272      *
273      * @sample androidx.compose.animation.core.samples.AnimatableDecayAndAnimateToSample
274      */
animateDecaynull275     public suspend fun animateDecay(
276         initialVelocity: T,
277         animationSpec: DecayAnimationSpec<T>,
278         block: (Animatable<T, V>.() -> Unit)? = null
279     ): AnimationResult<T, V> {
280         val anim =
281             DecayAnimation(
282                 animationSpec = animationSpec,
283                 initialValue = value,
284                 initialVelocityVector = typeConverter.convertToVector(initialVelocity),
285                 typeConverter = typeConverter
286             )
287         return runAnimation(anim, initialVelocity, block)
288     }
289 
290     // All the different types of animation code paths eventually converge to this method.
runAnimationnull291     private suspend fun runAnimation(
292         animation: Animation<T, V>,
293         initialVelocity: T,
294         block: (Animatable<T, V>.() -> Unit)?
295     ): AnimationResult<T, V> {
296 
297         // Store the start time before it's reset during job cancellation.
298         val startTime = internalState.lastFrameTimeNanos
299         return mutatorMutex.mutate {
300             try {
301                 internalState.velocityVector = typeConverter.convertToVector(initialVelocity)
302                 targetValue = animation.targetValue
303                 isRunning = true
304 
305                 val endState =
306                     internalState.copy(finishedTimeNanos = AnimationConstants.UnspecifiedTime)
307                 var clampingNeeded = false
308                 endState.animate(animation, startTime) {
309                     updateState(internalState)
310                     val clamped = clampToBounds(value)
311                     if (clamped != value) {
312                         internalState.value = clamped
313                         endState.value = clamped
314                         block?.invoke(this@Animatable)
315                         cancelAnimation()
316                         clampingNeeded = true
317                     } else {
318                         block?.invoke(this@Animatable)
319                     }
320                 }
321                 val endReason = if (clampingNeeded) BoundReached else Finished
322                 endAnimation()
323                 AnimationResult(endState, endReason)
324             } catch (e: CancellationException) {
325                 // Clean up internal states first, then throw.
326                 endAnimation()
327                 throw e
328             }
329         }
330     }
331 
clampToBoundsnull332     private fun clampToBounds(value: T): T {
333         if (
334             lowerBoundVector == negativeInfinityBounds && upperBoundVector == positiveInfinityBounds
335         ) {
336             // Expect this to be the most common use case
337             return value
338         }
339         val valueVector = typeConverter.convertToVector(value)
340         var clamped = false
341         for (i in 0 until valueVector.size) {
342             if (valueVector[i] < lowerBoundVector[i] || valueVector[i] > upperBoundVector[i]) {
343                 clamped = true
344                 valueVector[i] = valueVector[i].coerceIn(lowerBoundVector[i], upperBoundVector[i])
345             }
346         }
347         if (clamped) {
348             return typeConverter.convertFromVector(valueVector)
349         } else {
350             return value
351         }
352     }
353 
endAnimationnull354     private fun endAnimation() {
355         // Reset velocity
356         internalState.apply {
357             velocityVector.reset()
358             lastFrameTimeNanos = AnimationConstants.UnspecifiedTime
359         }
360         isRunning = false
361     }
362 
363     /**
364      * Sets the current value to the target value, without any animation. This will also cancel any
365      * on-going animation with a [CancellationException]. This function will return *after*
366      * canceling any on-going animation and updating the [Animatable.value] and
367      * [Animatable.targetValue] to the provided [targetValue].
368      *
369      * __Note__: If the [lowerBound] or [upperBound] is specified, the provided [targetValue] will
370      * be clamped to the bounds to ensure [Animatable.value] is always within bounds.
371      *
372      * See [animateTo] and [animateDecay] for more details about animation being canceled.
373      *
374      * @param targetValue The new target value to set [value] to.
375      * @see animateTo
376      * @see animateDecay
377      * @see stop
378      */
snapTonull379     public suspend fun snapTo(targetValue: T) {
380         mutatorMutex.mutate {
381             endAnimation()
382             val clampedValue = clampToBounds(targetValue)
383             internalState.value = clampedValue
384             this.targetValue = clampedValue
385         }
386     }
387 
388     /**
389      * Stops any on-going animation with a [CancellationException].
390      *
391      * This function will not return until the ongoing animation has been canceled (if any). Note,
392      * [stop] function does **not** skip the animation value to its target value. Rather the
393      * animation will be stopped in its track. Consider [snapTo] if it's desired to not only stop
394      * the animation but also snap the [value] to a given value.
395      *
396      * See [animateTo] and [animateDecay] for more details about animation being canceled.
397      *
398      * @see animateTo
399      * @see animateDecay
400      * @see snapTo
401      */
stopnull402     public suspend fun stop() {
403         mutatorMutex.mutate { endAnimation() }
404     }
405 
406     /**
407      * Returns a [State] representing the current [value] of this animation. This allows hoisting
408      * the animation's current value without causing unnecessary recompositions when the value
409      * changes.
410      */
asStatenull411     public fun asState(): State<T> = internalState
412 }
413 
414 /**
415  * This [Animatable] function creates a float value holder that automatically animates its value
416  * when the value is changed via [animateTo]. [Animatable] supports value change during an ongoing
417  * value change animation. When that happens, a new animation will transition [Animatable] from its
418  * current value (i.e. value at the point of interruption) to the new target. This ensures that the
419  * value change is *always* continuous using [animateTo]. If [spring] animation (i.e. default
420  * animation) is used with [animateTo], the velocity change will be guaranteed to be continuous as
421  * well.
422  *
423  * Unlike [AnimationState], [Animatable] ensures mutual exclusiveness on its animation. To do so,
424  * when a new animation is started via [animateTo] (or [animateDecay]), any ongoing animation job
425  * will be cancelled.
426  *
427  * @sample androidx.compose.animation.core.samples.AnimatableFadeIn
428  * @param initialValue initial value of the animatable value holder
429  * @param visibilityThreshold Threshold at which the animation may round off to its target value.
430  *   [Spring.DefaultDisplacementThreshold] by default.
431  */
432 @RememberInComposition
433 public fun Animatable(
434     initialValue: Float,
435     visibilityThreshold: Float = Spring.DefaultDisplacementThreshold
436 ): Animatable<Float, AnimationVector1D> =
437     Animatable(initialValue, Float.VectorConverter, visibilityThreshold)
438 
439 // TODO: Consider some version of @Composable fun<T, V: AnimationVector> Animatable<T, V>.animateTo
440 /**
441  * AnimationResult contains information about an animation at the end of the animation. [endState]
442  * captures the value/velocity/frame time, etc of the animation at its last frame. It can be useful
443  * for starting another animation to continue the velocity from the previously interrupted
444  * animation. [endReason] describes why the animation ended, it could be either of the following:
445  * - [Finished], when the animation finishes successfully without any interruption
446  * - [BoundReached] If the animation reaches the either [lowerBound][Animatable.lowerBound] or
447  *   [upperBound][Animatable.upperBound] in any dimension, the animation will end with
448  *   [BoundReached] being the end reason.
449  *
450  * @sample androidx.compose.animation.core.samples.AnimatableAnimationResultSample
451  */
452 public class AnimationResult<T, V : AnimationVector>(
453     /**
454      * The state of the animation in its last frame before it's canceled or reset. This captures the
455      * animation value/velocity/frame time, etc at the point of interruption, or before the velocity
456      * is reset when the animation finishes successfully.
457      */
458     public val endState: AnimationState<T, V>,
459     /**
460      * The reason why the animation has ended. Could be either of the following:
461      * - [Finished], when the animation finishes successfully without any interruption
462      * - [BoundReached] If the animation reaches the either [lowerBound][Animatable.lowerBound] or
463      *   [upperBound][Animatable.upperBound] in any dimension, the animation will end with
464      *   [BoundReached] being the end reason.
465      */
466     public val endReason: AnimationEndReason
467 ) {
468     override fun toString(): String = "AnimationResult(endReason=$endReason, endState=$endState)"
469 }
470 
471 private val positiveInfinityBounds1D = AnimationVector(Float.POSITIVE_INFINITY)
472 private val positiveInfinityBounds2D =
473     AnimationVector(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY)
474 private val positiveInfinityBounds3D =
475     AnimationVector(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY)
476 private val positiveInfinityBounds4D =
477     AnimationVector(
478         Float.POSITIVE_INFINITY,
479         Float.POSITIVE_INFINITY,
480         Float.POSITIVE_INFINITY,
481         Float.POSITIVE_INFINITY
482     )
483 
484 private val negativeInfinityBounds1D = AnimationVector(Float.NEGATIVE_INFINITY)
485 private val negativeInfinityBounds2D =
486     AnimationVector(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY)
487 private val negativeInfinityBounds3D =
488     AnimationVector(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY)
489 private val negativeInfinityBounds4D =
490     AnimationVector(
491         Float.NEGATIVE_INFINITY,
492         Float.NEGATIVE_INFINITY,
493         Float.NEGATIVE_INFINITY,
494         Float.NEGATIVE_INFINITY
495     )
496