/*
* 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.dispatch.withFrameNanos
import androidx.compose.ui.unit.Uptime
import kotlinx.coroutines.CancellationException
/**
* Target based animation that animates from the given [initialValue] towards the [targetValue],
* with an optional [initialVelocity]. The [initialVelocity] defaults to 0f. By default, a [spring]
* will be used for the animation. An alternative [animationSpec] can be provided to replace the
* default [spring]. On each frame, the [block] will be invoked with up-to-date value and velocity.
*
* 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.
*
* @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(
initialValue,
targetValue,
Float.VectorConverter,
AnimationVector1D(initialVelocity),
animationSpec
) { value, velocity ->
block(value, velocity.value)
}
}
/**
* 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.
*
* [animationSpec] defines the decay animation that will be used for this animation. Some options
* for this [animationSpec] include: [AndroidFlingDecaySpec][androidx.compose.foundation.animation
* .AndroidFlingDecaySpec] and [ExponentialDecay]. [block] will be invoked on each animation frame
* with up-to-date value and velocity.
*
* 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<Float,
* AnimationVector1D>.animateDecay].
*
* @see [AnimationState<Float, AnimationVector1D>.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 [initialVelocityVector] will be 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(
initialValue: T,
targetValue: T,
typeConverter: TwoWayConverter<T, V>,
initialVelocityVector: V? = null,
animationSpec: AnimationSpec<T> = spring(),
block: (value: T, velocity: V) -> Unit
) {
val anim = TargetBasedAnimation(
animationSpec = animationSpec,
initialValue = initialValue,
targetValue = targetValue,
converter = typeConverter,
initialVelocityVector = initialVelocityVector
)
AnimationState(initialValue, typeConverter, initialVelocityVector).animate(anim) {
block(value, 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.
*
* [sequentialAnimation] indicates whether the animation should use the
* [AnimationState.lastFrameTime] 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.lastFrameTime] creates
* a smoother animation.
*
* [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(),
// Indicates whether the animation should start from last frame
sequentialAnimation: Boolean = false,
block: AnimationScope<T, V>.() -> Unit = {}
) {
val anim = TargetBasedAnimation(
animationSpec = animationSpec,
initialValue = value,
targetValue = targetValue,
converter = typeConverter,
initialVelocityVector = velocityVector
)
animate(anim, if (sequentialAnimation) lastFrameTime else Uptime.Unspecified, 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.
*
* [animationSpec] defines the decay animation that will be used for this animation. Some options
* for [animationSpec] include: [AndroidFlingDecaySpec][androidx.compose.foundation.animation
* .AndroidFlingDecaySpec] and [ExponentialDecay].
*
* During the animation, [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].
*
* [sequentialAnimation] indicates whether the animation should use the
* [AnimationState.lastFrameTime] 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.lastFrameTime] creates
* a smoother animation.
*/
suspend fun AnimationState<Float, AnimationVector1D>.animateDecay(
animationSpec: FloatDecayAnimationSpec,
// Indicates whether the animation should start from last frame
sequentialAnimation: Boolean = false,
block: AnimationScope<Float, AnimationVector1D>.() -> Unit = {}
) {
val anim = DecayAnimation(
anim = animationSpec,
initialValue = value,
initialVelocity = velocityVector.value
)
animate(
anim,
startTime = if (sequentialAnimation) lastFrameTime else Uptime.Unspecified,
block = 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.
*
* If [startTime] is provided, it will be used as the time that the animation was started. By
* default, [startTime] is [Uptime.Unspecified], meaning the animation will start in the next frame.
*
* For [Animation]s that use [AnimationSpec], consider using these more convenient APIs:
* [animate], [AnimationState.animateTo], [animateDecay],
* [AnimationState<Float, AnimationVector1D>.animateDecay]
*
* [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.
private suspend fun <T, V : AnimationVector> AnimationState<T, V>.animate(
animation: Animation<T, V>,
startTime: Uptime = Uptime.Unspecified,
block: AnimationScope<T, V>.() -> Unit = {}
) {
val initialValue = animation.getValue(0)
val initialVelocityVector = animation.getVelocityVector(0)
var lateInitScope: AnimationScope<T, V>? = null
try {
val startTimeSpecified =
if (startTime == Uptime.Unspecified) Uptime(withFrameNanos { it }) else startTime
lateInitScope = AnimationScope(
initialValue = initialValue,
typeConverter = animation.converter,
initialVelocityVector = initialVelocityVector,
lastFrameTime = startTimeSpecified,
targetValue = animation.targetValue,
startTime = startTimeSpecified,
isRunning = true,
onCancel = { isRunning = false }
)
// First frame
lateInitScope.doAnimationFrame(startTimeSpecified.nanoseconds, animation, this, block)
// Subsequent frames
while (lateInitScope.isRunning) {
withFrameNanos {
lateInitScope.doAnimationFrame(it, animation, this, block)
}
}
// End of animation
} catch (e: CancellationException) {
lateInitScope?.isRunning = false
if (lateInitScope?.lastFrameTime == lastFrameTime) {
// There hasn't been another animation.
isRunning = false
}
throw e
}
}
private fun <T, V : AnimationVector> AnimationScope<T, V>.updateState(state: AnimationState<T, V>) {
state.value = value
state.velocityVector = velocityVector
state.finishedTime = finishedTime
state.lastFrameTime = lastFrameTime
state.isRunning = isRunning
}
// Impl detail, invoked every frame.
private fun <T, V : AnimationVector> AnimationScope<T, V>.doAnimationFrame(
frameTimeNanos: Long,
anim: Animation<T, V>,
state: AnimationState<T, V>,
block: AnimationScope<T, V>.() -> Unit
) {
lastFrameTime = Uptime(frameTimeNanos)
val playTimeMillis = (frameTimeNanos - startTime.nanoseconds) / 1_000_000L
// TODO: [Animation] should use nanos for all the value/velocity queries
value = anim.getValue(playTimeMillis)
velocityVector = anim.getVelocityVector(playTimeMillis)
val isLastFrame = anim.isFinished(playTimeMillis)
if (isLastFrame) {
// TODO: This could probably be a little more granular
// TODO: end time isn't necessarily last frame time
finishedTime = lastFrameTime
isRunning = false
}
updateState(state)
block()
}