net.peanuuutz.fork.ui.animation.Animatable.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of fork-ui Show documentation
Show all versions of fork-ui Show documentation
Comprehensive API designed for Minecraft modders
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
}