DecayAnimationSpec.kt
/*
* 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.ui.unit.IntOffset
/**
* [DecayAnimationSpec] stores the specification of an animation, including 1) the data type to be
* animated, and 2) the animation configuration (i.e. [VectorizedDecayAnimationSpec]) that will be
* used once the data (of type [T]) has been converted to [AnimationVector].
*
* Any type [T] can be animated by the system as long as a [TwoWayConverter] is supplied to convert
* the data type [T] from and to an [AnimationVector]. There are a number of converters
* available out of the box. For example, to animate [androidx.compose.ui.unit.IntOffset] the system
* uses [IntOffset.VectorConverter][IntOffset.Companion.VectorConverter] to convert the object to
* [AnimationVector2D], so that both x and y dimensions are animated independently with separate
* velocity tracking. This enables multidimensional objects to be animated in a true
* multi-dimensional way. It is particularly useful for smoothly handling animation interruptions
* (such as when the target changes during the animation).
*/
interface DecayAnimationSpec<T> {
/**
* Creates a [VectorizedDecayAnimationSpec] with the given [TwoWayConverter].
*
* The underlying animation system operates on [AnimationVector]s. [T] will be converted to
* [AnimationVector] to animate. [VectorizedDecayAnimationSpec] describes how the
* converted [AnimationVector] should be animated.
*
* @param typeConverter converts the type [T] from and to [AnimationVector] type
*/
fun <V : AnimationVector> vectorize(
typeConverter: TwoWayConverter<T, V>
): VectorizedDecayAnimationSpec<V>
}
/**
* Calculates the target value of a decay animation based on the [initialValue] and
* [initialVelocity], and the [typeConverter] that converts the given type [T] to [AnimationVector].
*
* @return target value where the animation will come to a natural stop
*/
fun <T, V : AnimationVector> DecayAnimationSpec<T>.calculateTargetValue(
typeConverter: TwoWayConverter<T, V>,
initialValue: T,
initialVelocity: T
): T {
val vectorizedSpec = vectorize(typeConverter)
val targetVector = vectorizedSpec.getTargetValue(
typeConverter.convertToVector(initialValue),
typeConverter.convertToVector(initialVelocity)
)
return typeConverter.convertFromVector(targetVector)
}
/**
* Calculates the target value of a Float decay animation based on the [initialValue] and
* [initialVelocity].
*
* @return target value where the animation will come to a natural stop
*/
fun DecayAnimationSpec<Float>.calculateTargetValue(
initialValue: Float,
initialVelocity: Float
): Float {
val vectorizedSpec = vectorize(Float.VectorConverter)
val targetVector = vectorizedSpec.getTargetValue(
AnimationVector(initialValue),
AnimationVector(initialVelocity)
)
return targetVector.value
}
/**
* Creates a decay animation spec where the friction/deceleration is always proportional to the
* velocity. As a result, the velocity goes under an exponential decay. The constructor parameter,
* [frictionMultiplier], can be tuned to adjust the amount of friction applied in the decay. The
* higher the multiplier, the higher the friction, the sooner the animation will stop, and the
* shorter distance the animation will travel with the same starting condition.
* [absVelocityThreshold] describes the absolute value of a velocity threshold, below which the
* animation is considered finished.
*
* @param frictionMultiplier The decay friction multiplier. This must be greater than `0`.
* @param absVelocityThreshold The minimum speed, below which the animation is considered finished.
* Must be greater than `0`.
*/
fun <T> exponentialDecay(
/*@FloatRange(
from = 0.0,
fromInclusive = false
)*/
frictionMultiplier: Float = 1f,
/*@FloatRange(
from = 0.0,
fromInclusive = false
)*/
absVelocityThreshold: Float = 0.1f
): DecayAnimationSpec<T> =
FloatExponentialDecaySpec(frictionMultiplier, absVelocityThreshold).generateDecayAnimationSpec()
/**
* Creates a [DecayAnimationSpec] from a [FloatDecayAnimationSpec] by applying the given
* [FloatDecayAnimationSpec] on every dimension of the [AnimationVector] that [T] converts to.
*/
fun <T> FloatDecayAnimationSpec.generateDecayAnimationSpec(): DecayAnimationSpec<T> {
return DecayAnimationSpecImpl(this)
}
private class DecayAnimationSpecImpl<T>(
private val floatDecaySpec: FloatDecayAnimationSpec
) : DecayAnimationSpec<T> {
override fun <V : AnimationVector> vectorize(
typeConverter: TwoWayConverter<T, V>
): VectorizedDecayAnimationSpec<V> = VectorizedFloatDecaySpec(floatDecaySpec)
}
private class VectorizedFloatDecaySpec<V : AnimationVector>(
val floatDecaySpec: FloatDecayAnimationSpec
) : VectorizedDecayAnimationSpec<V> {
private lateinit var valueVector: V
private lateinit var velocityVector: V
private lateinit var targetVector: V
override val absVelocityThreshold: Float = floatDecaySpec.absVelocityThreshold
override fun getValueFromNanos(playTimeNanos: Long, initialValue: V, initialVelocity: V): V {
if (!::valueVector.isInitialized) {
valueVector = initialValue.newInstance()
}
for (i in 0 until valueVector.size) {
valueVector[i] = floatDecaySpec.getValueFromNanos(
playTimeNanos,
initialValue[i],
initialVelocity[i]
)
}
return valueVector
}
override fun getDurationNanos(initialValue: V, initialVelocity: V): Long {
var maxDuration = 0L
if (!::velocityVector.isInitialized) {
velocityVector = initialValue.newInstance()
}
for (i in 0 until velocityVector.size) {
maxDuration = maxOf(
maxDuration,
floatDecaySpec.getDurationNanos(initialValue[i], initialVelocity[i])
)
}
return maxDuration
}
override fun getVelocityFromNanos(playTimeNanos: Long, initialValue: V, initialVelocity: V): V {
if (!::velocityVector.isInitialized) {
velocityVector = initialValue.newInstance()
}
for (i in 0 until velocityVector.size) {
velocityVector[i] = floatDecaySpec.getVelocityFromNanos(
playTimeNanos,
initialValue[i],
initialVelocity[i]
)
}
return velocityVector
}
override fun getTargetValue(initialValue: V, initialVelocity: V): V {
if (!::targetVector.isInitialized) {
targetVector = initialValue.newInstance()
}
for (i in 0 until targetVector.size) {
targetVector[i] = floatDecaySpec.getTargetValue(
initialValue[i],
initialVelocity[i]
)
}
return targetVector
}
}