FloatDecayAnimationSpec.kt
/*
* Copyright 2019 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 kotlin.math.abs
import kotlin.math.exp
import kotlin.math.ln
import kotlin.math.max
/**
* This animation interface is intended to be stateless, just like Animation<T>. But unlike
* Animation<T>, DecayAnimation does not have an end value defined. The end value is a
* result of the animation rather than an input.
*/
interface FloatDecayAnimationSpec {
/**
* This is the absolute value of a velocity threshold, below which the animation is considered
* finished.
*/
val absVelocityThreshold: Float
/**
* Returns the value of the animation at the given time.
*
* @param playTimeNanos The time elapsed in milliseconds since the start of the animation
* @param initialValue The start value of the animation
* @param initialVelocity The start velocity of the animation
*/
fun getValueFromNanos(
playTimeNanos: Long,
initialValue: Float,
initialVelocity: Float
): Float
/**
* Returns the duration of the decay animation, in nanoseconds.
*
* @param initialValue start value of the animation
* @param initialVelocity start velocity of the animation
*/
@Suppress("MethodNameUnits")
fun getDurationNanos(
initialValue: Float,
initialVelocity: Float
): Long
/**
* Returns the velocity of the animation at the given time.
*
* @param playTimeNanos The time elapsed in milliseconds since the start of the animation
* @param initialValue The start value of the animation
* @param initialVelocity The start velocity of the animation
*/
fun getVelocityFromNanos(
playTimeNanos: Long,
initialValue: Float,
initialVelocity: Float
): Float
/**
* Returns the target value of the animation based on the starting condition of the animation (
* i.e. start value and start velocity).
*
* @param initialValue The start value of the animation
* @param initialVelocity The start velocity of the animation
*/
fun getTargetValue(
initialValue: Float,
initialVelocity: Float
): Float
}
private const val ExponentialDecayFriction = -4.2f
/**
* This is a decay animation 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.
* @param frictionMultiplier The friction multiplier, indicating how quickly the animation should
* stop. This should be greater than `0`, with a default value of `1.0`.
* @param absVelocityThreshold The speed at which the animation is considered close enough to
* rest for the animation to finish.
*/
class FloatExponentialDecaySpec(
/*@FloatRange(
from = 0.0,
fromInclusive = false
)*/
frictionMultiplier: Float = 1f,
/*@FloatRange(
from = 0.0,
fromInclusive = false
)*/
absVelocityThreshold: Float = 0.1f
) : FloatDecayAnimationSpec {
override val absVelocityThreshold: Float = max(0.0000001f, abs(absVelocityThreshold))
private val friction: Float = ExponentialDecayFriction * max(0.0001f, frictionMultiplier)
override fun getValueFromNanos(
playTimeNanos: Long,
initialValue: Float,
initialVelocity: Float
): Float {
// TODO: Properly support nanos
val playTimeMillis = playTimeNanos / MillisToNanos
return initialValue - initialVelocity / friction +
initialVelocity / friction * exp(friction * playTimeMillis / 1000f)
}
override fun getVelocityFromNanos(
playTimeNanos: Long,
initialValue: Float,
initialVelocity: Float
): Float {
// TODO: Properly support nanos
val playTimeMillis = playTimeNanos / MillisToNanos
return (initialVelocity * exp(((playTimeMillis / 1000f) * friction)))
}
@Suppress("MethodNameUnits")
override fun getDurationNanos(initialValue: Float, initialVelocity: Float): Long {
// Inverse of getVelocity
return (1000f * ln(absVelocityThreshold / abs(initialVelocity)) / friction)
.toLong() * MillisToNanos
}
override fun getTargetValue(
initialValue: Float,
initialVelocity: Float
): Float {
if (abs(initialVelocity) <= absVelocityThreshold) {
return initialValue
}
val duration: Double =
ln(abs(absVelocityThreshold / initialVelocity).toDouble()) / friction * 1000
return initialValue - initialVelocity / friction +
initialVelocity / friction * exp((friction * duration / 1000f)).toFloat()
}
}
/**
* Creates a [Animation] (with a fixed start value and start velocity) that decays over time
* based on the given [FloatDecayAnimationSpec].
*
* @param startValue the starting value of the fixed animation.
* @param startVelocity the starting velocity of the fixed animation.
*/
internal fun FloatDecayAnimationSpec.createAnimation(
startValue: Float,
startVelocity: Float = 0f
): Animation<Float, AnimationVector1D> {
return DecayAnimation(this, startValue, startVelocity)
}