Animatable.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 androidx.compose.animation.core.AnimationEndReason.BoundReached
import androidx.compose.animation.core.AnimationEndReason.Finished
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import kotlinx.coroutines.CancellationException

/**
 * [Animatable] is a value holder that automatically animates its value when the value is
 * changed via [animateTo]. If [animateTo] is invoked during an ongoing value change animation,
 * a new animation will transition [Animatable] from its current value (i.e. value at the point of
 * interruption) to the new [targetValue]. This ensures that the value change is __always__
 * continuous using [animateTo]. If a [spring] animation (e.g. default animation) is used with
 * [animateTo], the velocity change will guarantee to be continuous as well.
 *
 * Unlike [AnimationState], [Animatable] ensures *mutual exclusiveness* on its animations. To
 * achieve this, when a new animation is started via [animateTo] (or [animateDecay]), any ongoing
 * animation will be canceled via a [CancellationException].
 *
 * @sample androidx.compose.animation.core.samples.AnimatableAnimateToGenericsType
 *
 * @param initialValue initial value of the animatable value holder
 * @param typeConverter A two-way converter that converts the given type [T] from and to
 *                      [AnimationVector]
 * @param visibilityThreshold Threshold at which the animation may round off to its target value.
 *
 * @see animateTo
 * @see animateDecay
 */
@Suppress("NotCloseable")
class Animatable<T, V : AnimationVector>(
    initialValue: T,
    val typeConverter: TwoWayConverter<T, V>,
    private val visibilityThreshold: T? = null,
    val label: String = "Animatable"
) {

    @Deprecated(
        "Maintained for binary compatibility",
        replaceWith = ReplaceWith(
            "Animatable(initialValue, typeConverter, visibilityThreshold, \"Animatable\")"
        ),
        DeprecationLevel.HIDDEN
    )
    constructor(
        initialValue: T,
        typeConverter: TwoWayConverter<T, V>,
        visibilityThreshold: T? = null
    ) : this(initialValue, typeConverter, visibilityThreshold, "Animatable")

    internal val internalState = AnimationState(
        typeConverter = typeConverter,
        initialValue = initialValue
    )

    /**
     * Current value of the animation.
     */
    val value: T
        get() = internalState.value

    /**
     * Velocity vector of the animation (in the form of [AnimationVector].
     */
    val velocityVector: V
        get() = internalState.velocityVector

    /**
     * Returns the velocity, converted from [velocityVector].
     */
    val velocity: T
        get() = typeConverter.convertFromVector(velocityVector)

    /**
     * Indicates whether the animation is running.
     */
    var isRunning: Boolean by mutableStateOf(false)
        private set

    /**
     * The target of the current animation. If the animation finishes un-interrupted, it will
     * reach this target value.
     */
    var targetValue: T by mutableStateOf(initialValue)
        private set

    /**
     * Lower bound of the animation, null by default (meaning no lower bound). Bounds can be
     * changed using [updateBounds].
     *
     * Animation will stop as soon as *any* dimension specified in [lowerBound] is reached. For
     * example: For an Animatable<Offset> with an [lowerBound] set to Offset(100f, 200f), when
     * the [value].x drops below 100f *or* [value].y drops below 200f, the animation will stop.
     */
    var lowerBound: T? = null
        private set

    /**
     * Upper bound of the animation, null by default (meaning no upper bound). Bounds can be
     * changed using [updateBounds].
     *
     * Animation will stop as soon as *any* dimension specified in [upperBound] is reached. For
     * example: For an Animatable<Offset> with an [upperBound] set to Offset(100f, 200f), when
     * the [value].x exceeds 100f *or* [value].y exceeds 200f, the animation will stop.
     */
    var upperBound: T? = null
        private set

    private val mutatorMutex = MutatorMutex()
    internal val defaultSpringSpec: SpringSpec<T> =
        SpringSpec(visibilityThreshold = visibilityThreshold)

    private val negativeInfinityBounds = initialValue.createVector(Float.NEGATIVE_INFINITY)
    private val positiveInfinityBounds = initialValue.createVector(Float.POSITIVE_INFINITY)

    private var lowerBoundVector: V = negativeInfinityBounds
    private var upperBoundVector: V = positiveInfinityBounds

    private fun T.createVector(value: Float): V {
        val newVector = this@Animatable.typeConverter.convertToVector(this)
        for (i in 0 until newVector.size) {
            newVector[i] = value
        }
        return newVector
    }

    /**
     * Updates either [lowerBound] or [upperBound], or both. This will update
     * [Animatable.lowerBound] and/or [Animatable.upperBound] accordingly after a check to ensure
     * the provided [lowerBound] is no greater than [upperBound] in any dimension.
     *
     * Setting the bounds will immediate clamp the [value], only if the animation isn't running.
     * For the on-going animation, the value at the next frame update will be checked against the
     * bounds. If the value reaches the bound, then the animation will end with [BoundReached]
     * end reason.
     *
     * @param lowerBound lower bound of the animation. Defaults to the [Animatable.lowerBound]
     *                   that is currently set.
     * @param upperBound upper bound of the animation. Defaults to the [Animatable.upperBound]
     *                   that is currently set.
     * @throws [IllegalStateException] if the [lowerBound] is greater than [upperBound] in any
     *                                 dimension.
     */
    fun updateBounds(lowerBound: T? = this.lowerBound, upperBound: T? = this.upperBound) {
        val lowerBoundVector = lowerBound?.run { typeConverter.convertToVector(this) }
            ?: negativeInfinityBounds

        val upperBoundVector = upperBound?.run { typeConverter.convertToVector(this) }
            ?: positiveInfinityBounds

        for (i in 0 until lowerBoundVector.size) {
            // TODO: is this check too aggressive?
            check(lowerBoundVector[i] <= upperBoundVector[i]) {
                "Lower bound must be no greater than upper bound on *all* dimensions. The " +
                    "provided lower bound: $lowerBoundVector is greater than upper bound " +
                    "$upperBoundVector on index $i"
            }
        }
        // After the correctness check:
        this.lowerBoundVector = lowerBoundVector
        this.upperBoundVector = upperBoundVector

        this.upperBound = upperBound
        this.lowerBound = lowerBound
        if (!isRunning) {
            val clampedValue = clampToBounds(value)
            if (clampedValue != value) {
                this.internalState.value = clampedValue
            }
        }
    }

    /**
     * Starts an animation to animate from [value] to the provided [targetValue]. If there is
     * already an animation in-flight, this method will cancel the ongoing animation before
     * starting a new animation continuing the current [value] and [velocity]. It's recommended to
     * set the optional [initialVelocity] only when [animateTo] is used immediately after a fling.
     * In most of the other cases, altering velocity would result in visual discontinuity.
     *
     * The animation will use the provided [animationSpec] to animate the value towards the
     * [targetValue]. When no [animationSpec] is specified, a [spring] will be used.  [block] will
     * be invoked on each animation frame.
     *
     * Returns an [AnimationResult] object. It contains: 1) the reason for ending the animation,
     * and 2) an end state of the animation. The reason for ending the animation can be either of
     * the following two:
     * -  [Finished], when the animation finishes successfully without any interruption,
     * -  [BoundReached] If the animation reaches the either [lowerBound] or [upperBound] in any
     *    dimension, the animation will end with [BoundReached] being the end reason.
     *
     * If the animation gets interrupted by 1) another call to start an animation
     * (i.e. [animateTo]/[animateDecay]), 2) [Animatable.stop], or 3)[Animatable.snapTo], the
     * canceled animation will throw a [CancellationException] as the job gets canceled. As a
     * result, all the subsequent work in the caller's coroutine will be canceled. This is often
     * the desired behavior. If there's any cleanup that needs to be done when an animation gets
     * canceled, consider starting the animation in a `try-catch` block.
     *
     * __Note__: once the animation ends, its velocity will be reset to 0. The animation state at
     * the point of interruption/reaching bound is captured in the returned [AnimationResult].
     * If there's a need to continue the momentum that the animation had before it was interrupted
     * or reached the bound, it's recommended to use the velocity in the returned
     * [AnimationResult.endState] to start another animation.
     *
     * @sample androidx.compose.animation.core.samples.AnimatableFadeIn
     */
    suspend fun animateTo(
        targetValue: T,
        animationSpec: AnimationSpec<T> = defaultSpringSpec,
        initialVelocity: T = velocity,
        block: (Animatable<T, V>.() -> Unit)? = null
    ): AnimationResult<T, V> {
        val anim = TargetBasedAnimation(
            animationSpec = animationSpec,
            initialValue = value,
            targetValue = targetValue,
            typeConverter = typeConverter,
            initialVelocity = initialVelocity
        )
        return runAnimation(anim, initialVelocity, block)
    }

    /**
     * Start a decay animation (i.e. an animation that *slows down* from the given
     * [initialVelocity] starting at current [Animatable.value] until the velocity reaches 0.
     * If there's already an ongoing animation, the animation in-flight will be immediately
     * cancelled. Decay animation 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: [splineBasedDecay][androidx.compose
     * .animation.splineBasedDecay] and [exponentialDecay]. [block] will be
     * invoked on each animation frame.
     *
     * Returns an [AnimationResult] object, that contains the [reason][AnimationEndReason] for
     * ending the animation, and an end state of the animation. The reason for ending the animation
     * will be [Finished] if the animation finishes successfully without any interruption.
     * If the animation reaches the either [lowerBound] or [upperBound] in any dimension, the
     * animation will end with [BoundReached] being the end reason.
     *
     * If the animation gets interrupted by 1) another call to start an animation
     * (i.e. [animateTo]/[animateDecay]), 2) [Animatable.stop], or 3)[Animatable.snapTo], the
     * canceled animation will throw a [CancellationException] as the job gets canceled. As a
     * result, all the subsequent work in the caller's coroutine will be canceled. This is often
     * the desired behavior. If there's any cleanup that needs to be done when an animation gets
     * canceled, consider starting the animation in a `try-catch` block.
     *
     * __Note__, once the animation ends, its velocity will be reset to 0. If there's a need to
     * continue the momentum before the animation gets interrupted or reaches the bound, it's
     * recommended to use the velocity in the returned [AnimationResult.endState] to start
     * another animation.
     *
     * @sample androidx.compose.animation.core.samples.AnimatableDecayAndAnimateToSample
     */
    suspend fun animateDecay(
        initialVelocity: T,
        animationSpec: DecayAnimationSpec<T>,
        block: (Animatable<T, V>.() -> Unit)? = null
    ): AnimationResult<T, V> {
        val anim = DecayAnimation(
            animationSpec = animationSpec,
            initialValue = value,
            initialVelocityVector = typeConverter.convertToVector(initialVelocity),
            typeConverter = typeConverter
        )
        return runAnimation(anim, initialVelocity, block)
    }

    // All the different types of animation code paths eventually converge to this method.
    private suspend fun runAnimation(
        animation: Animation<T, V>,
        initialVelocity: T,
        block: (Animatable<T, V>.() -> Unit)?
    ): AnimationResult<T, V> {

        // Store the start time before it's reset during job cancellation.
        val startTime = internalState.lastFrameTimeNanos
        return mutatorMutex.mutate {
            try {
                internalState.velocityVector = typeConverter.convertToVector(initialVelocity)
                targetValue = animation.targetValue
                isRunning = true

                val endState = internalState.copy(
                    finishedTimeNanos = AnimationConstants.UnspecifiedTime
                )
                var clampingNeeded = false
                endState.animate(
                    animation,
                    startTime
                ) {
                    updateState(internalState)
                    val clamped = clampToBounds(value)
                    if (clamped != value) {
                        internalState.value = clamped
                        endState.value = clamped
                        block?.invoke(this@Animatable)
                        cancelAnimation()
                        clampingNeeded = true
                    } else {
                        block?.invoke(this@Animatable)
                    }
                }
                val endReason = if (clampingNeeded) BoundReached else Finished
                endAnimation()
                AnimationResult(endState, endReason)
            } catch (e: CancellationException) {
                // Clean up internal states first, then throw.
                endAnimation()
                throw e
            }
        }
    }

    private fun clampToBounds(value: T): T {
        if (
            lowerBoundVector == negativeInfinityBounds &&
            upperBoundVector == positiveInfinityBounds
        ) {
            // Expect this to be the most common use case
            return value
        }
        val valueVector = typeConverter.convertToVector(value)
        var clamped = false
        for (i in 0 until valueVector.size) {
            if (valueVector[i] < lowerBoundVector[i] || valueVector[i] > upperBoundVector[i]) {
                clamped = true
                valueVector[i] =
                    valueVector[i].coerceIn(lowerBoundVector[i], upperBoundVector[i])
            }
        }
        if (clamped) {
            return typeConverter.convertFromVector(valueVector)
        } else {
            return value
        }
    }

    private fun endAnimation() {
        // Reset velocity
        internalState.apply {
            velocityVector.reset()
            lastFrameTimeNanos = AnimationConstants.UnspecifiedTime
        }
        isRunning = false
    }

    /**
     * Sets the current value to the target value, without any animation. This will also cancel any
     * on-going animation with a [CancellationException]. This function will return *after*
     * canceling any on-going animation and updating the [Animatable.value] and
     * [Animatable.targetValue] to the provided [targetValue].
     *
     * __Note__: If the [lowerBound] or [upperBound] is specified, the provided [targetValue]
     * will be clamped to the bounds to ensure [Animatable.value] is always within bounds.
     *
     * See [animateTo] and [animateDecay] for more details about animation being canceled.
     *
     * @param targetValue The new target value to set [value] to.
     *
     * @see animateTo
     * @see animateDecay
     * @see stop
     */
    suspend fun snapTo(targetValue: T) {
        mutatorMutex.mutate {
            endAnimation()
            val clampedValue = clampToBounds(targetValue)
            internalState.value = clampedValue
            this.targetValue = clampedValue
        }
    }

    /**
     * Stops any on-going animation with a [CancellationException].
     *
     * This function will not return until the ongoing animation has been canceled (if any).
     * Note, [stop] function does **not** skip the animation value to its target value. Rather the
     * animation will be stopped in its track. Consider [snapTo] if it's desired to not only stop
     * the animation but also snap the [value] to a given value.
     *
     * See [animateTo] and [animateDecay] for more details about animation being canceled.
     *
     * @see animateTo
     * @see animateDecay
     * @see snapTo
     */
    suspend fun stop() {
        mutatorMutex.mutate {
            endAnimation()
        }
    }

    /**
     * Returns a [State] representing the current [value] of this animation. This allows
     * hoisting the animation's current value without causing unnecessary recompositions
     * when the value changes.
     */
    fun asState(): State<T> = internalState
}

/**
 * This [Animatable] function creates a float value holder that automatically
 * animates its value when the value is changed via [animateTo]. [Animatable] supports value
 * change during an ongoing value change animation. When that happens, a new animation will
 * transition [Animatable] from its current value (i.e. value at the point of interruption) to the
 * new target. This ensures that the value change is *always* continuous using [animateTo]. If
 * [spring] animation (i.e. default animation) is used with [animateTo], the velocity change will
 * be guaranteed to be continuous as well.
 *
 * Unlike [AnimationState], [Animatable] ensures mutual exclusiveness on its animation. To
 * do so, when a new animation is started via [animateTo] (or [animateDecay]), any ongoing
 * animation job will be cancelled.
 *
 * @sample androidx.compose.animation.core.samples.AnimatableFadeIn
 *
 * @param initialValue initial value of the animatable value holder
 * @param visibilityThreshold Threshold at which the animation may round off to its target value.
 *                            [Spring.DefaultDisplacementThreshold] by default.
 */
fun Animatable(
    initialValue: Float,
    visibilityThreshold: Float = Spring.DefaultDisplacementThreshold
) = Animatable(
    initialValue,
    Float.VectorConverter,
    visibilityThreshold
)

// TODO: Consider some version of @Composable fun<T, V: AnimationVector> Animatable<T, V>.animateTo
/**
 * AnimationResult contains information about an animation at the end of the animation. [endState]
 * captures the value/velocity/frame time, etc of the animation at its last frame. It can be
 * useful for starting another animation to continue the velocity from the previously interrupted
 * animation. [endReason] describes why the animation ended, it could be either of the following:
 * -  [Finished], when the animation finishes successfully without any interruption
 * -  [BoundReached] If the animation reaches the either [lowerBound][Animatable.lowerBound] or
 *    [upperBound][Animatable.upperBound] in any dimension, the animation will end with
 *    [BoundReached] being the end reason.
 *
 * @sample androidx.compose.animation.core.samples.AnimatableAnimationResultSample
 */
class AnimationResult<T, V : AnimationVector>(
    /**
     * The state of the animation in its last frame before it's canceled or reset. This captures
     * the animation value/velocity/frame time, etc at the point of interruption, or before the
     * velocity is reset when the animation finishes successfully.
     */
    val endState: AnimationState<T, V>,
    /**
     * The reason why the animation has ended. Could be either of the following:
     * -  [Finished], when the animation finishes successfully without any interruption
     * -  [BoundReached] If the animation reaches the either [lowerBound][Animatable.lowerBound] or
     *    [upperBound][Animatable.upperBound] in any dimension, the animation will end with
     *    [BoundReached] being the end reason.
     */
    val endReason: AnimationEndReason
) {
    override fun toString(): String = "AnimationResult(endReason=$endReason, endState=$endState)"
}