/*
* Copyright 2021 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.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collection.mutableVectorOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.unit.Dp
/**
* Creates a [InfiniteTransition] that runs infinite child animations. Child animations can be
* added using [InfiniteTransition.animateColor][androidx.compose.animation.animateColor],
* [InfiniteTransition.animateFloat], or [InfiniteTransition.animateValue]. Child animations will
* start running as soon as they enter the composition, and will not stop until they are removed
* from the composition.
*
* @sample androidx.compose.animation.core.samples.InfiniteTransitionSample
*/
@Composable
fun rememberInfiniteTransition(): InfiniteTransition {
val infiniteTransition = remember { InfiniteTransition() }
infiniteTransition.run()
return infiniteTransition
}
/**
* [InfiniteTransition] is responsible for running child animations. Child animations can be
* added using [InfiniteTransition.animateColor][androidx.compose.animation.animateColor],
* [InfiniteTransition.animateFloat], or [InfiniteTransition.animateValue]. Child animations will
* start running as soon as they enter the composition, and will not stop until they are removed
* from the composition.
*
* @sample androidx.compose.animation.core.samples.InfiniteTransitionSample
*/
class InfiniteTransition internal constructor() {
internal inner class TransitionAnimationState<T, V : AnimationVector>(
var initialValue: T,
var targetValue: T,
val typeConverter: TwoWayConverter<T, V>,
var animationSpec: AnimationSpec<T>
) : State<T> {
override var value by mutableStateOf(initialValue)
internal set
var animation = TargetBasedAnimation(
animationSpec,
typeConverter,
initialValue,
targetValue
)
// This is used to signal parent for less work in a normal running mode, but in seeking
// this is ignored since time can go both ways.
var isFinished = false
// If animation is refreshed during the run, start the new animation in the next frame
var startOnTheNextFrame = false
// When the animation changes, it needs to start from playtime 0 again, offsetting from
// parent's playtime to achieve that.
var playTimeNanosOffset = 0L
// This gets called when the initial/target value changes, which should be a rare case.
fun updateValues(initialValue: T, targetValue: T, animationSpec: AnimationSpec<T>) {
this.initialValue = initialValue
this.targetValue = targetValue
this.animationSpec = animationSpec
// Create a new animation if anything (i.e. initial/target) has changed
// TODO: Consider providing some continuity maybe?
animation = TargetBasedAnimation(
animationSpec,
typeConverter,
initialValue,
targetValue
)
refreshChildNeeded = true
isFinished = false
startOnTheNextFrame = true
}
fun onPlayTimeChanged(playTimeNanos: Long) {
refreshChildNeeded = false
if (startOnTheNextFrame) {
startOnTheNextFrame = false
playTimeNanosOffset = playTimeNanos
}
val playTime = playTimeNanos - playTimeNanosOffset
value = animation.getValueFromNanos(playTime)
isFinished = animation.isFinishedFromNanos(playTime)
}
}
internal val animations = mutableVectorOf<TransitionAnimationState<*, *>>()
private var refreshChildNeeded by mutableStateOf(false)
private var startTimeNanos = AnimationConstants.UnspecifiedTime
private var isRunning by mutableStateOf(true)
internal fun addAnimation(animation: TransitionAnimationState<*, *>) {
animations.add(animation)
refreshChildNeeded = true
}
internal fun removeAnimation(animation: TransitionAnimationState<*, *>) {
animations.remove(animation)
}
@Suppress("ComposableNaming")
@Composable
internal fun run() {
if (isRunning || refreshChildNeeded) {
LaunchedEffect(this) {
while (true) {
withInfiniteAnimationFrameNanos(::onFrame)
}
}
}
}
private fun onFrame(frameTimeNanos: Long) {
if (startTimeNanos == AnimationConstants.UnspecifiedTime) {
startTimeNanos = frameTimeNanos
}
val playTimeNanos = frameTimeNanos - startTimeNanos
var allFinished = true
// Pulse new playtime
animations.forEach {
if (!it.isFinished) {
it.onPlayTimeChanged(playTimeNanos)
}
// Check isFinished flag again after the animation pulse
if (!it.isFinished) {
allFinished = false
}
}
isRunning = !allFinished
}
}
/**
* Creates an animation of type [T] that runs infinitely as a part of the given
* [InfiniteTransition]. Any data type can be animated so long as it can be converted from and to
* an [AnimationVector]. This conversion needs to be provided as a [typeConverter]. Some examples
* of such [TwoWayConverter] are: [Int.VectorConverter][Int.Companion.VectorConverter],
* [Dp.VectorConverter][Dp.Companion.VectorConverter],
* [Size.VectorConverter][Size.Companion.VectorConverter], etc
*
* Once the animation is created, it will run from [initialValue] to [targetValue] and repeat.
* Depending on the [RepeatMode] of the provided [animationSpec], the animation could either
* restart after each iteration (i.e. [RepeatMode.Restart]), or reverse after each iteration (i.e
* . [RepeatMode.Reverse]).
*
* If [initialValue] or [targetValue] is changed at any point during the animation, the animation
* will be restarted with the new [initialValue] and [targetValue]. __Note__: this means
* continuity will *not* be preserved.
*
* @sample androidx.compose.animation.core.samples.InfiniteTransitionAnimateValueSample
*
* @see [InfiniteTransition.animateFloat]
* @see [androidx.compose.animation.animateColor]
*/
@Composable
fun <T, V : AnimationVector> InfiniteTransition.animateValue(
initialValue: T,
targetValue: T,
typeConverter: TwoWayConverter<T, V>,
animationSpec: InfiniteRepeatableSpec<T>
): State<T> {
val transitionAnimation =
remember {
TransitionAnimationState(
initialValue, targetValue, typeConverter, animationSpec
)
}
SideEffect {
if (initialValue != transitionAnimation.initialValue ||
targetValue != transitionAnimation.targetValue
) {
transitionAnimation.updateValues(
initialValue = initialValue,
targetValue = targetValue,
animationSpec = animationSpec
)
}
}
DisposableEffect(transitionAnimation) {
addAnimation(transitionAnimation)
onDispose {
removeAnimation(transitionAnimation)
}
}
return transitionAnimation
}
/**
* Creates an animation of Float type that runs infinitely as a part of the given
* [InfiniteTransition].
*
* Once the animation is created, it will run from [initialValue] to [targetValue] and repeat.
* Depending on the [RepeatMode] of the provided [animationSpec], the animation could either
* restart after each iteration (i.e. [RepeatMode.Restart]), or reverse after each iteration (i.e
* . [RepeatMode.Reverse]).
*
* If [initialValue] or [targetValue] is changed at any point during the animation, the animation
* will be restarted with the new [initialValue] and [targetValue]. __Note__: this means
* continuity will *not* be preserved.
*
* @sample androidx.compose.animation.core.samples.InfiniteTransitionSample
*
* @see [InfiniteTransition.animateValue]
* @see [androidx.compose.animation.animateColor]
*/
@Composable
fun InfiniteTransition.animateFloat(
initialValue: Float,
targetValue: Float,
animationSpec: InfiniteRepeatableSpec<Float>
): State<Float> =
animateValue(initialValue, targetValue, Float.VectorConverter, animationSpec)