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