AnimatorAnimationSpecs.kt

/*
 * Copyright 2022 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.graphics.vector

import androidx.compose.animation.core.AnimationVector
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.AnimationVector2D
import androidx.compose.animation.core.AnimationVector3D
import androidx.compose.animation.core.AnimationVector4D
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.TwoWayConverter
import androidx.compose.animation.core.VectorizedFiniteAnimationSpec

private const val MillisToNanos = 1_000_000L

/**
 * Returns this [FiniteAnimationSpec] reversed.
 */
internal fun <T> FiniteAnimationSpec<T>.reversed(durationMillis: Int): FiniteAnimationSpec<T> {
    return ReversedSpec(this, durationMillis)
}

private class ReversedSpec<T>(
    private val spec: FiniteAnimationSpec<T>,
    private val durationMillis: Int
) : FiniteAnimationSpec<T> {
    override fun <V : AnimationVector> vectorize(
        converter: TwoWayConverter<T, V>
    ): VectorizedFiniteAnimationSpec<V> {
        return VectorizedReversedSpec(spec.vectorize(converter), durationMillis * MillisToNanos)
    }
}

private class VectorizedReversedSpec<V : AnimationVector>(
    private val animation: VectorizedFiniteAnimationSpec<V>,
    private val durationNanos: Long
) : VectorizedFiniteAnimationSpec<V> {

    override fun getValueFromNanos(
        playTimeNanos: Long,
        initialValue: V,
        targetValue: V,
        initialVelocity: V
    ): V {
        return animation.getValueFromNanos(
            durationNanos - playTimeNanos,
            targetValue,
            initialValue,
            initialVelocity
        )
    }

    override fun getVelocityFromNanos(
        playTimeNanos: Long,
        initialValue: V,
        targetValue: V,
        initialVelocity: V
    ): V {
        return animation.getVelocityFromNanos(
            durationNanos - playTimeNanos,
            targetValue,
            initialValue,
            initialVelocity
        ).reversed()
    }

    override fun getDurationNanos(initialValue: V, targetValue: V, initialVelocity: V): Long {
        return durationNanos
    }
}

/**
 * Creates a [FiniteAnimationSpec] that combine and run multiple [specs] based on the start time
 * (in milliseconds) specified as the first half of the pairs.
 */
internal fun <T> combined(
    specs: List<Pair<Int, FiniteAnimationSpec<T>>>
): FiniteAnimationSpec<T> {
    return CombinedSpec(specs)
}

private class CombinedSpec<T>(
    private val specs: List<Pair<Int, FiniteAnimationSpec<T>>>
) : FiniteAnimationSpec<T> {

    override fun <V : AnimationVector> vectorize(
        converter: TwoWayConverter<T, V>
    ): VectorizedFiniteAnimationSpec<V> {
        return VectorizedCombinedSpec(
            specs.map { (timeMillis, spec) ->
                timeMillis * MillisToNanos to spec.vectorize(converter)
            }
        )
    }
}

private class VectorizedCombinedSpec<V : AnimationVector>(
    private val animations: List<Pair<Long, VectorizedFiniteAnimationSpec<V>>>
) : VectorizedFiniteAnimationSpec<V> {

    private fun chooseAnimation(playTimeNanos: Long): Pair<Long, VectorizedFiniteAnimationSpec<V>> {
        return animations.lastOrNull { (timeNanos, _) ->
            timeNanos <= playTimeNanos
        } ?: animations.first()
    }

    override fun getValueFromNanos(
        playTimeNanos: Long,
        initialValue: V,
        targetValue: V,
        initialVelocity: V
    ): V {
        val (timeNanos, animation) = chooseAnimation(playTimeNanos)
        val internalPlayTimeNanos = playTimeNanos - timeNanos
        return animation.getValueFromNanos(
            internalPlayTimeNanos,
            initialValue,
            targetValue,
            initialVelocity
        )
    }

    override fun getVelocityFromNanos(
        playTimeNanos: Long,
        initialValue: V,
        targetValue: V,
        initialVelocity: V
    ): V {
        val (timeNanos, animation) = chooseAnimation(playTimeNanos)
        return animation.getVelocityFromNanos(
            playTimeNanos - timeNanos,
            initialValue,
            targetValue,
            initialVelocity
        )
    }

    override fun getDurationNanos(initialValue: V, targetValue: V, initialVelocity: V): Long {
        val (timeNanos, animation) = animations.last()
        return timeNanos + animation.getDurationNanos(initialValue, targetValue, initialVelocity)
    }
}

private fun <V : AnimationVector> V.reversed(): V {
    @Suppress("UNCHECKED_CAST")
    return when (this) {
        is AnimationVector1D -> AnimationVector1D(value * -1) as V
        is AnimationVector2D -> AnimationVector2D(v1 * -1, v2 * -1) as V
        is AnimationVector3D -> AnimationVector3D(v1 * -1, v2 * -1, v3 * -1) as V
        is AnimationVector4D -> AnimationVector4D(v1 * -1, v2 * -1, v3 * -1, v4 * -1) as V
        else -> throw RuntimeException("Unknown AnimationVector: $this")
    }
}