/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.animation.core
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.MotionDurationScale
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.coroutineContext
import kotlinx.coroutines.CancellationException
/**
* Target based animation that animates from the given [initialValue] towards the [targetValue],
* with an optional [initialVelocity]. By default, a [spring] will be used for the animation. An
* alternative [animationSpec] can be provided to replace the default [spring].
*
* This is a convenient method for Float animation. If there's a need to access more info related to
* the animation such as start time, target, etc, consider using [AnimationState.animateTo].
* To animate non-[Float] data types, consider the [animate] overload/variant for generic types.
*
* @param initialVelocity The velocity to use for the animation. 0f by default.
* @param animationSpec The animation configuration that will be used. [spring] by default.
* @param block Will be invoked on every frame with the current value and velocity of the animation
* for that frame.
*
* @sample androidx.compose.animation.core.samples.suspendAnimateFloatVariant
* @see AnimationState.animateTo
*/
suspend fun animate(
initialValue: Float,
targetValue: Float,
initialVelocity: Float = 0f,
animationSpec: AnimationSpec<Float> = spring(),
block: (value: Float, velocity: Float) -> Unit
) {
animate(
Float.VectorConverter,
initialValue,
targetValue,
initialVelocity,
animationSpec,
block
)
}
/**
* Decay animation that slows down from the given [initialVelocity] starting at [initialValue] until
* the velocity reaches 0. This is often used after a fling gesture.
*
* This is a convenient method for decay animation. If there's a need to access more info related to
* the animation such as start time, target, etc, consider using [AnimationState.animateDecay].
*
* @param animationSpec Defines the decay animation that will be used for this animation. Some
* options for this [animationSpec] include:
* [splineBasedDecay][androidx.compose.animation.splineBasedDecay] and [exponentialDecay].
*
* @param block Will be invoked on each animation frame with up-to-date value and velocity.
*
* @see AnimationState.animateDecay
*/
suspend fun animateDecay(
initialValue: Float,
initialVelocity: Float,
animationSpec: FloatDecayAnimationSpec,
block: (value: Float, velocity: Float) -> Unit
) {
val anim = DecayAnimation(animationSpec, initialValue, initialVelocity)
AnimationState(initialValue, initialVelocity).animate(anim) {
block(value, velocityVector.value)
}
}
/**
* Target based animation for animating any data type [T], so long as [T] can be converted to an
* [AnimationVector] using [typeConverter]. The animation will start from the [initialValue] and
* animate to the [targetValue] value. The [initialVelocity] will be derived from an all-0
* [AnimationVector] unless specified. [animationSpec] can be provided to create a specific look and
* feel for the animation. By default, a [spring] will be used.
*
* This is a convenient method for target-based animation. If there's a need to access more info
* related to the animation such as start time, target, etc, consider using
* [AnimationState.animateTo].
*
* @see AnimationState.animateTo
*/
suspend fun <T, V : AnimationVector> animate(
typeConverter: TwoWayConverter<T, V>,
initialValue: T,
targetValue: T,
initialVelocity: T? = null,
animationSpec: AnimationSpec<T> = spring(),
block: (value: T, velocity: T) -> Unit
) {
val initialVelocityVector = initialVelocity?.let { typeConverter.convertToVector(it) }
?: typeConverter.convertToVector(initialValue).newInstance()
val anim = TargetBasedAnimation(
animationSpec = animationSpec,
initialValue = initialValue,
targetValue = targetValue,
typeConverter = typeConverter,
initialVelocityVector = initialVelocityVector
)
AnimationState(typeConverter, initialValue, initialVelocityVector).animate(anim) {
block(value, typeConverter.convertFromVector(velocityVector))
}
}
/**
* Target based animation that takes the value and velocity from the [AnimationState] as the
* starting condition, and animate to the [targetValue], using the [animationSpec]. During the
* animation, the given [AnimationState] will be updated with the up-to-date value/velocity,
* frame time, etc.
*
* @param targetValue The target value that the animation will animate to.
*
* @param animationSpec The animation configuration that will be used. [spring] by default.
*
* @param sequentialAnimation Indicates whether the animation should use the
* [AnimationState.lastFrameTimeNanos] as the starting time (if true), or start in a new frame. By
* default, [sequentialAnimation] is false, to start the animation in a few frame. In cases where
* an on-going animation is interrupted and a new animation is started to carry over the
* momentum, using the interruption time (captured in [AnimationState.lastFrameTimeNanos]) creates
* a smoother animation.
*
* @param block Will be invoked on every frame, and the [AnimationScope] will be checked against
* cancellation before the animation continues. To cancel the animation from the [block], simply
* call [AnimationScope.cancelAnimation]. After [AnimationScope.cancelAnimation] is called, [block]
* will not be invoked again. The animation loop will exit after the [block] returns. All the
* animation related info can be accessed via [AnimationScope].
*
* @sample androidx.compose.animation.core.samples.animateToOnAnimationState
*/
suspend fun <T, V : AnimationVector> AnimationState<T, V>.animateTo(
targetValue: T,
animationSpec: AnimationSpec<T> = spring(),
sequentialAnimation: Boolean = false,
block: AnimationScope<T, V>.() -> Unit = {}
) {
val anim = TargetBasedAnimation(
animationSpec = animationSpec,
initialValue = value,
targetValue = targetValue,
typeConverter = typeConverter,
initialVelocityVector = velocityVector
)
animate(
anim,
if (sequentialAnimation) lastFrameTimeNanos else AnimationConstants.UnspecifiedTime,
block
)
}
/**
* Decay animation that slows down from the current velocity and value captured in [AnimationState]
* until the velocity reaches 0. During the animation, the given [AnimationState] will be updated
* with the up-to-date value/velocity, frame time, etc. This is often used to animate the result
* of a fling gesture.
*
* @param animationSpec Defines the decay animation that will be used for this animation. Some
* options for [animationSpec] include:
* [splineBasedDecay][androidx.compose.animation.splineBasedDecay] and [exponentialDecay].
*
* @param sequentialAnimation Indicates whether the animation should use the
* [AnimationState.lastFrameTimeNanos] as the starting time (if true), or start in a new frame. By
* default, [sequentialAnimation] is false, to start the animation in a few frame. In cases where
* an on-going animation is interrupted and a new animation is started to carry over the
* momentum, using the interruption time (captured in [AnimationState.lastFrameTimeNanos]) creates
* a smoother animation.
*
* @param block will be invoked on every frame during the animation, and the [AnimationScope] will
* be checked against cancellation before the animation continues. To cancel the animation from the
* [block], simply call [AnimationScope.cancelAnimation]. After [AnimationScope.cancelAnimation] is
* called, [block] will not be invoked again. The animation loop will exit after the [block]
* returns. All the animation related info can be accessed via [AnimationScope].
*/
suspend fun <T, V : AnimationVector> AnimationState<T, V>.animateDecay(
animationSpec: DecayAnimationSpec<T>,
sequentialAnimation: Boolean = false,
block: AnimationScope<T, V>.() -> Unit = {}
) {
val anim = DecayAnimation<T, V>(
animationSpec = animationSpec,
initialValue = value,
initialVelocityVector = velocityVector,
typeConverter = typeConverter
)
animate(
anim,
if (sequentialAnimation) lastFrameTimeNanos else AnimationConstants.UnspecifiedTime,
block
)
}
/**
* This animation function runs the animation defined in the given [animation] from start to
* finish. During the animation, the [AnimationState] will be updated with the up-to-date
* value/velocity, frame time, etc.
*
* For [Animation]s that use [AnimationSpec], consider using these more convenient APIs:
* [animate], [AnimationState.animateTo], [animateDecay], [AnimationState.animateDecay].
*
* @param startTimeNanos If provided, it will be used as the time that the animation was started. By
* default, [startTimeNanos] is [AnimationConstants.UnspecifiedTime], meaning the animation will
* start in the next frame.
*
* @param block Will be invoked on every frame, and the [AnimationScope] will be checked against
* cancellation before the animation continues. To cancel the animation from the [block], simply
* call [AnimationScope.cancelAnimation]. After [AnimationScope.cancelAnimation] is called, [block]
* will not be invoked again. The animation loop will exit after the [block] returns. All the
* animation related info can be accessed via [AnimationScope].
*/
// TODO: This method uses AnimationState and Animation at the same time, it's potentially confusing
// as to which is the source of truth for initial value/velocity. Consider letting [Animation] have
// some suspend fun differently.
internal suspend fun <T, V : AnimationVector> AnimationState<T, V>.animate(
animation: Animation<T, V>,
startTimeNanos: Long = AnimationConstants.UnspecifiedTime,
block: AnimationScope<T, V>.() -> Unit = {}
) {
val initialValue = animation.getValueFromNanos(0)
val initialVelocityVector = animation.getVelocityVectorFromNanos(0)
var lateInitScope: AnimationScope<T, V>? = null
try {
if (startTimeNanos == AnimationConstants.UnspecifiedTime) {
val durationScale = coroutineContext.durationScale
animation.callWithFrameNanos {
lateInitScope = AnimationScope(
initialValue = initialValue,
typeConverter = animation.typeConverter,
initialVelocityVector = initialVelocityVector,
lastFrameTimeNanos = it,
targetValue = animation.targetValue,
startTimeNanos = it,
isRunning = true,
onCancel = { isRunning = false }
).apply {
// First frame
doAnimationFrameWithScale(it, durationScale, animation, this@animate, block)
}
}
} else {
lateInitScope = AnimationScope(
initialValue = initialValue,
typeConverter = animation.typeConverter,
initialVelocityVector = initialVelocityVector,
lastFrameTimeNanos = startTimeNanos,
targetValue = animation.targetValue,
startTimeNanos = startTimeNanos,
isRunning = true,
onCancel = { isRunning = false }
).apply {
// First frame
doAnimationFrameWithScale(
startTimeNanos,
coroutineContext.durationScale,
animation,
this@animate,
block
)
}
}
// Subsequent frames
while (lateInitScope!!.isRunning) {
val durationScale = coroutineContext.durationScale
animation.callWithFrameNanos {
lateInitScope!!.doAnimationFrameWithScale(it, durationScale, animation, this, block)
}
}
// End of animation
} catch (e: CancellationException) {
lateInitScope?.isRunning = false
if (lateInitScope?.lastFrameTimeNanos == lastFrameTimeNanos) {
// There hasn't been another animation.
isRunning = false
}
throw e
}
}
/**
* Calls the [finite][withFrameNanos] or [infinite][withInfiniteAnimationFrameNanos]
* variant of `withFrameNanos`, depending on the value of [Animation.isInfinite].
*/
private suspend fun <R, T, V : AnimationVector> Animation<T, V>.callWithFrameNanos(
onFrame: (frameTimeNanos: Long) -> R
): R {
return if (isInfinite) {
withInfiniteAnimationFrameNanos(onFrame)
} else {
withFrameNanos {
onFrame.invoke(it / AnimationDebugDurationScale)
}
}
}
internal val CoroutineContext.durationScale: Float
get() {
val scale = this[MotionDurationScale]?.scaleFactor ?: 1f
check(scale >= 0f)
return scale
}
internal fun <T, V : AnimationVector> AnimationScope<T, V>.updateState(
state: AnimationState<T, V>
) {
state.value = value
state.velocityVector.copyFrom(velocityVector)
state.finishedTimeNanos = finishedTimeNanos
state.lastFrameTimeNanos = lastFrameTimeNanos
state.isRunning = isRunning
}
private fun <T, V : AnimationVector> AnimationScope<T, V>.doAnimationFrameWithScale(
frameTimeNanos: Long,
durationScale: Float,
anim: Animation<T, V>,
state: AnimationState<T, V>,
block: AnimationScope<T, V>.() -> Unit
) {
val playTimeNanos =
if (durationScale == 0f) {
anim.durationNanos
} else {
((frameTimeNanos - startTimeNanos) / durationScale).toLong()
}
doAnimationFrame(frameTimeNanos, playTimeNanos, anim, state, block)
}
// Impl detail, invoked every frame.
private fun <T, V : AnimationVector> AnimationScope<T, V>.doAnimationFrame(
frameTimeNanos: Long,
playTimeNanos: Long,
anim: Animation<T, V>,
state: AnimationState<T, V>,
block: AnimationScope<T, V>.() -> Unit
) {
lastFrameTimeNanos = frameTimeNanos
value = anim.getValueFromNanos(playTimeNanos)
velocityVector = anim.getVelocityVectorFromNanos(playTimeNanos)
val isLastFrame = anim.isFinishedFromNanos(playTimeNanos)
if (isLastFrame) {
// TODO: This could probably be a little more granular
// TODO: end time isn't necessarily last frame time
finishedTimeNanos = lastFrameTimeNanos
isRunning = false
}
updateState(state)
block()
}