All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.peanuuutz.fork.ui.animation.Animatable.kt Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2020 The Android Open Source Project
 * Modifications Copyright 2022 Peanuuutz
 *
 * 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 net.peanuuutz.fork.ui.animation

import kotlinx.coroutines.CancellationException
import net.peanuuutz.fork.ui.animation.AnimationEndReason.Companion.BoundReached
import net.peanuuutz.fork.ui.animation.AnimationEndReason.Companion.Finished
import net.peanuuutz.fork.ui.animation.TimeConstants.UnspecifiedTimeMillis
import net.peanuuutz.fork.ui.animation.spec.decay.DefaultFloatDecayAnimationSpec
import net.peanuuutz.fork.ui.animation.spec.decay.composite.DecayAnimationSpec
import net.peanuuutz.fork.ui.animation.spec.decay.composite.ExponentialDecaySpec
import net.peanuuutz.fork.ui.animation.spec.target.DefaultFloatAnimationSpec
import net.peanuuutz.fork.ui.animation.spec.target.composite.AnimationSpec
import net.peanuuutz.fork.ui.animation.spec.target.composite.SpringSpec
import net.peanuuutz.fork.ui.animation.vector.AnimationVector
import net.peanuuutz.fork.ui.animation.vector.AnimationVector1D
import net.peanuuutz.fork.ui.animation.vector.VectorConvertor
import net.peanuuutz.fork.ui.animation.vector.copy
import net.peanuuutz.fork.ui.animation.vector.reset
import net.peanuuutz.fork.ui.util.MutatorMutex

class Animatable>(
    initialValue: S,
    private val convertor: VectorConvertor
) {
    val animationState: AnimationState
        get() = internalAnimationState

    val value: S
        get() = internalAnimationState.value

    val velocityVector: V
        get() = internalAnimationState.velocityVector

    val velocity: S
        get() = internalAnimationState.velocity

    val lowerBounds: S
        get() = convertor.convertFromVector(lowerBoundsVector)

    val upperBounds: S
        get() = convertor.convertFromVector(upperBoundsVector)

    val isRunning: Boolean
        get() = internalAnimationState.isRunning

    val targetValue: S
        get() = internalAnimationState.targetValue

    suspend fun animateTo(
        targetValue: S,
        animationSpec: AnimationSpec = defaultSpringSpec,
        initialVelocityVector: V = velocityVector,
        onFrame: (Animatable.() -> Unit)? = null
    ): AnimationResult {
        val animation = TargetBasedAnimation(
            vectorizedAnimationSpec = animationSpec.vectorize(convertor),
            convertor = convertor,
            initialValue = value,
            targetValue = targetValue,
            initialVelocityVector = initialVelocityVector
        )
        return startAnimation(animation, onFrame)
    }

    suspend fun snapTo(
        targetValue: S
    ) {
        mutatorMutex.mutate {
            endAnimation()
            val coercedTargetValue = coerceValue(targetValue)
            with(internalAnimationState) {
                value = coercedTargetValue
                this.targetValue = coercedTargetValue
            }
        }
    }

    suspend fun animateDecay(
        initialVelocityVector: V = velocityVector,
        decayAnimationSpec: DecayAnimationSpec = defaultExponentialDecaySpec,
        onFrame: (Animatable.() -> Unit)? = null
    ): AnimationResult {
        val animation = DecayAnimation(
            vectorizedDecayAnimationSpec = decayAnimationSpec.vectorize(convertor),
            convertor = convertor,
            initialValue = value,
            initialVelocityVector = initialVelocityVector
        )
        return startAnimation(animation, onFrame)
    }

    suspend fun stop() {
        mutatorMutex.mutate {
            endAnimation()
        }
    }

    fun updateBounds(
        lower: S = lowerBounds,
        upper: S = upperBounds
    ) {
        val newLowerBoundsVector = convertor.convertToVector(lower)
        val newUpperBoundsVector = convertor.convertToVector(upper)
        for (index in 0 ..< newLowerBoundsVector.size) {
            check(newLowerBoundsVector[index] <= newUpperBoundsVector[index]) {
                "Lower bounds should be smaller than upper bounds on all dimensions, " +
                        "but found lower $newLowerBoundsVector, upper $newUpperBoundsVector"
            }
        }
        lowerBoundsVector = newLowerBoundsVector
        upperBoundsVector = newUpperBoundsVector
        if (!isRunning) {
            val currentValue = value
            val coercedValue = coerceValue(currentValue)
            if (coercedValue != currentValue) {
                internalAnimationState.value = coercedValue
            }
        }
    }

    // ======== Internal ========

    private val internalAnimationState: AnimationStateImpl = AnimationStateImpl(
        convertor = convertor,
        initialValue = initialValue
    )

    private val mutatorMutex: MutatorMutex = MutatorMutex()

    private val defaultSpringSpec: AnimationSpec = SpringSpec()

    private val defaultExponentialDecaySpec: DecayAnimationSpec = ExponentialDecaySpec()

    private val negativeInfinityVector: V = velocityVector.new().infinite(isPositive = false)

    private val positiveInfinityVector: V = velocityVector.new().infinite(isPositive = true)

    private var lowerBoundsVector: V = negativeInfinityVector

    private var upperBoundsVector: V = positiveInfinityVector

    private suspend fun startAnimation(
        animation: Animation,
        onFrame: (Animatable.() -> Unit)?
    ): AnimationResult {
        val startTimeMillis = internalAnimationState.lastFrameTimeMillis
        return mutatorMutex.mutate {
            try {
                val executionState = with(internalAnimationState) {
                    targetValue = animation.targetValue
                    velocityVector = animation.initialVelocityVector.copy()
                    isRunning = true
                    copy(finishedTimeMillis = UnspecifiedTimeMillis)
                }
                var coerced = false
                animation.run(startTimeMillis, executionState) { state ->
                    internalAnimationState.copyFrom(state)
                    val coercedValue = coerceValue(state.value)
                    if (coercedValue == state.value) {
                        onFrame?.invoke(this)
                    } else {
                        state.value = coercedValue
                        internalAnimationState.value = coercedValue
                        onFrame?.invoke(this)
                        state.isRunning = false
                        coerced = true
                    }
                }
                endAnimation()
                val reason = if (!coerced) Finished else BoundReached
                AnimationResult(executionState, reason)
            } catch (e: CancellationException) {
                endAnimation()
                throw e
            }
        }
    }

    private fun endAnimation() {
        with(internalAnimationState) {
            velocityVector.reset()
            lastFrameTimeMillis = UnspecifiedTimeMillis
            isRunning = false
        }
    }

    private fun coerceValue(value: S): S {
        if (lowerBoundsVector == negativeInfinityVector && upperBoundsVector == positiveInfinityVector) {
            return value
        }
        val newVector = convertor.convertToVector(value)
        var coerced = false
        for (index in 0 ..< newVector.size) {
            val currentEntry = newVector[index]
            val currentLowerBoundsEntry = lowerBoundsVector[index]
            val currentUpperBoundsEntry = upperBoundsVector[index]
            if (currentEntry !in currentLowerBoundsEntry..currentUpperBoundsEntry) {
                newVector[index] = currentEntry.coerceIn(currentLowerBoundsEntry, currentUpperBoundsEntry)
                coerced = true
            }
        }
        return if (!coerced) value else convertor.convertFromVector(newVector)
    }
}

fun Animatable(
    initialValue: Float
): Animatable {
    return Animatable(initialValue, Float.VectorConvertor)
}

suspend fun Animatable.animateTo(
    targetValue: Float,
    animationSpec: AnimationSpec = DefaultFloatAnimationSpec,
    initialVelocity: Float = velocity,
    onFrame: (Animatable.() -> Unit)? = null
): AnimationResult {
    return animateTo(
        targetValue = targetValue,
        animationSpec = animationSpec,
        initialVelocityVector = Float.VectorConvertor.convertToVector(initialVelocity),
        onFrame = onFrame
    )
}

suspend fun Animatable.animateDecay(
    initialVelocity: Float = velocity,
    decayAnimationSpec: DecayAnimationSpec = DefaultFloatDecayAnimationSpec,
    onFrame: (Animatable.() -> Unit)? = null
): AnimationResult {
    return animateDecay(
        initialVelocityVector = Float.VectorConvertor.convertToVector(initialVelocity),
        decayAnimationSpec = decayAnimationSpec,
        onFrame = onFrame
    )
}

// ======== Internal ========

private fun > V.infinite(isPositive: Boolean): V {
    val value = if (isPositive) Float.POSITIVE_INFINITY else Float.NEGATIVE_INFINITY
    for (index in 0 ..< size) {
        this[index] = value
    }
    return this
}