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.animation.core.AnimationEndReason.Interrupted
import androidx.compose.runtime.AtomicReference
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.unit.Uptime
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope

/**
 * [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.
 *
 * @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.
 */
@Suppress("NotCloseable")
class Animatable<T, V : AnimationVector>(
    initialValue: T,
    val typeConverter: TwoWayConverter<T, V>,
    private val visibilityThreshold: T? = null
) {

    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.
     */
    val isRunning: Boolean
        get() = currentJob.get() != null

    /**
     * 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. Defaults to null, which means 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. Defaults to null, which means 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 var currentJob = AtomicReference<Job?>(null)
    internal val defaultSpringSpec: SpringSpec<T> =
        SpringSpec(visibilityThreshold = visibilityThreshold)

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

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

    private fun createVector(value: Float): V {
        val newVector = typeConverter.convertToVector(this.value)
        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 = value
            }
        }
    }

    /**
     * Sets the target value, which effectively starts an animation to change the value from [value]
     * to the [targetValue]. If there is already an animation in-flight, this method will cancel
     * the ongoing animation and start 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 any of the
     * following three:
     * -  [Finished], when the animation finishes successfully without any interruption,
     * -  [Interrupted], if/when the animation gets interrupted by 1) another call to start an
     *    animation (i.e. [animateTo]/[animateDecay]), 2) [Animatable.stop], or 3)
     *    [Animatable.snapTo].
     * -  [BoundReached] If the animation reaches the either [lowerBound] or [upperBound] in any
     *    dimension, the animation will end with [BoundReached] being the end reason.
     *
     * __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.AnimatableAnimateToGenericsType
     * @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> {
        internalState.velocityVector = typeConverter.convertToVector(initialVelocity)
        val anim = TargetBasedAnimation(
            animationSpec = animationSpec,
            initialValue = value,
            targetValue = targetValue,
            typeConverter = typeConverter,
            initialVelocity = initialVelocity
        )
        return runAnimation(anim, block)
    }

    /**
     * Starts 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: [androidFlingDecay][androidx.compose
     * .foundation.animation.androidFlingDecay] 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], when the animation finishes successfully without any interruption,
     * If/when the animation gets interrupted by 1) another call to start an animation
     * (i.e. [animateTo]/[animateDecay]), 2) [stop], or 3) [snapTo]
     * [Interrupted] will be returned. If the animation reaches the either [lowerBound] or
     * [upperBound] in any dimension, the animation will end with [BoundReached] being the
     * end reason.
     *
     * 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> {
        internalState.velocityVector = typeConverter.convertToVector(initialVelocity)
        val anim = DecayAnimation(
            animationSpec = animationSpec,
            initialValue = value,
            initialVelocityVector = velocityVector.copy(),
            typeConverter = typeConverter
        )
        return runAnimation(anim, block)
    }

    // All the different types of animation code paths eventually converge to this method.
    private suspend fun runAnimation(
        animation: Animation<T, V>,
        block: (Animatable<T, V>.() -> Unit)?
    ): AnimationResult<T, V> {
        targetValue = animation.targetValue
        return coroutineScope {
            // Update current job, and cancel old job (i.e. existing animation)
            val oldJob = currentJob.getAndSet(coroutineContext[Job])
            oldJob?.also { it.cancelAnimation() } != null

            val startState = internalState.copy(finishedTime = Uptime.Unspecified)
            val endReason = try {
                var clampingNeeded = false
                startState.animate(
                    animation,
                    internalState.lastFrameTime
                ) {
                    if (currentJob.get() == coroutineContext[Job]) {
                        updateState(internalState)
                        val clamped = clampToBounds(value)
                        if (clamped != value) {
                            internalState.value = clamped
                            startState.value = clamped
                            block?.invoke(this@Animatable)
                            cancelAnimation()
                            clampingNeeded = true
                        } else {
                            block?.invoke(this@Animatable)
                        }
                    } else {
                        // Cancelled by another job *initiated by* Animatable
                        cancelAnimation()
                    }
                }
                if (startState.isFinished) {
                    Finished
                } else {
                    if (clampingNeeded) BoundReached else Interrupted
                }
            } catch (e: CancellationException) {
                if (e is AnimationCancellationException) {
                    Interrupted
                } else {
                    // External cancellation. Clean up internal states first, then throw.
                    if (currentJob.compareAndSet(coroutineContext[Job], null)) {
                        endAnimation()
                    }
                    throw e
                }
            }

            // Reset the animation if it wasn't interrupted
            if (currentJob.compareAndSet(coroutineContext[Job], null)) {
                endAnimation()
            }

            AnimationResult(startState, endReason)
        }
    }

    private fun clampToBounds(value: T): T {
        if (
            lowerBoundVector == negativeInfinityBounds &&
            upperBoundVector == negativeInfinityBounds
        ) {
            // 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()
            lastFrameTime = Uptime.Unspecified
        }
    }

    /**
     * Sets the current value to the target value immediately, without any animation. This will
     * also cancel any on-going animation
     *
     * @param targetValue The new target value to set [value] to.
     */
    fun snapTo(targetValue: T) {
        stop()
        internalState.value = targetValue
        this.targetValue = targetValue
    }

    /**
     * Stops any on-going animation. No op if no animation is running. Note that this method does
     * not skip the animation value to its target value. Rather the animation will be stopped in its
     * track.
     */
    fun stop() {
        currentJob.getAndSet(null)?.cancelAnimation()
        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

    private fun Job.cancelAnimation() {
        cancel(AnimationCancellationException())
    }

    private class AnimationCancellationException : CancellationException(
        "Interrupted by another animation, or stopped."
    )
}

/**
 * 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.AnimatableDecayAndAnimateToSample
 *
 * @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 one of the following three:
 * -  [Finished], when the animation finishes successfully without any interruption
 * -  [Interrupted], if/when the animation gets interrupted by 1) another call to start an
 *    animation (i.e. [animateTo]/[animateDecay]), 2) [Animatable.stop], or 3)
 *    [Animatable.snapTo].
 * -  [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>(
    val endState: AnimationState<T, V>,
    val endReason: AnimationEndReason
)