blob: f3ac5232cc37d5b78f648518f9082c2d5234de0b [file] [log] [blame]
package com.airbnb.lottie.compose
import androidx.compose.animation.core.*
import androidx.compose.runtime.*
import com.airbnb.lottie.LottieComposition
import java.util.concurrent.TimeUnit
/**
* Returns a mutable state representing the progress of an animation.
*
* Because the state is mutable, you can modify its value and the internal animation
* will continue animating from the value you set. The progress will snap to the value you
* set without changing the repeat count.
*
* There is also a suspending version of this that takes progress as a MutableState<Float>
* as a required second parameter.
*
* You do not have to use this to animate a Lottie composition. You may create your own animation
* and pass its progress to [LottieComposition].
*
* @param composition The composition to render. This should be retrieved with [lottieComposition].
* @param isPlaying Whether or not the animation is currently playing. Note that the internal
* animation may end due to reaching the target repeatCount. If that happens,
* the animation may stop even if this is still true. You may want to use
* onFinished to set isPlaying to false but in many cases, it won't matter.
* @param restartOnPlay If isPlaying switches from false to true, restartOnPlay determines whether
* the progress and repeatCount get reset.
* @param clipSpec A [LottieClipSpec] that specifies the bound the animation playback
* should be clipped to.
* @param speed The speed the animation should play at. Numbers larger than one will speed it up.
* Numbers between 0 and 1 will slow it down. Numbers less than 0 will play it backwards.
* @param repeatCount The number of times the animation should repeat before stopping. It must be
* a positive number. [Integer.MAX_VALUE] can be used to repeat forever.
* @param onRepeat An optional callback to be notified every time the animation repeats. Return whether
* or not the animation should continue to repeat.
* @param onFinished An optional callback that is invoked when animation completes. Note that the isPlaying
* parameter you pass in may still be true. If you want to restart the animation, increase the
* repeatCount or change isPlaying to false and then true again.
*/
@Composable
fun animateLottieComposition(
composition: LottieComposition?,
isPlaying: Boolean = true,
restartOnPlay: Boolean = true,
clipSpec: LottieClipSpec? = null,
speed: Float = 1f,
repeatCount: Int = 1,
onRepeat: ((repeatCount: Int) -> Unit)? = null,
onFinished: (() -> Unit)? = null,
): MutableState<Float> {
require(repeatCount > 0) { "Repeat count must be a positive number ($repeatCount)." }
require(speed != 0f) { "Speed must not be 0" }
require(speed.isFinite()) { "Speed must be a finite number. It is $speed." }
val progress = remember { mutableStateOf(0f) }
var currentRepeatCount by remember { mutableStateOf(0) }
val currentOnRepeat by rememberUpdatedState(onRepeat)
val currentOnFinished by rememberUpdatedState(onFinished)
LaunchedEffect(composition) {
progress.value = when (composition) {
null -> 0f
else -> if (speed >= 0) clipSpec?.getMinProgress(composition) ?: 0f else clipSpec?.getMaxProgress(composition) ?: 1f
}
currentRepeatCount = 0
}
LaunchedEffect(composition, isPlaying, repeatCount, clipSpec, speed) {
if (!isPlaying || composition == null) return@LaunchedEffect
val minProgress = clipSpec?.getMinProgress(composition) ?: 0f
val maxProgress = clipSpec?.getMaxProgress(composition) ?: 1f
if (speed > 0 && (progress.value == 1f || restartOnPlay)) {
progress.value = minProgress
} else if (speed < 0 && (progress.value == 0f || restartOnPlay)) {
progress.value = maxProgress
}
if (restartOnPlay || currentRepeatCount >= repeatCount) {
currentRepeatCount = 0
}
var lastFrameTime = withFrameNanos { it }
var done = false
while (!done) {
withFrameNanos { frameTime ->
val dTime = (frameTime - lastFrameTime) / TimeUnit.MILLISECONDS.toNanos(1).toFloat()
lastFrameTime = frameTime
val dProgress = (dTime * speed) / composition.duration
val rawProgress = minProgress + ((progress.value - minProgress) + dProgress)
if (speed > 0 && rawProgress > maxProgress) {
currentRepeatCount++
currentOnRepeat?.invoke(repeatCount)
} else if (speed < 0 && rawProgress < minProgress) {
currentRepeatCount++
currentOnRepeat?.invoke(repeatCount)
}
done = if (currentRepeatCount < repeatCount && !rawProgress.isInfinite()) {
progress.value = minProgress + ((rawProgress - minProgress) fmod (maxProgress - minProgress))
false
} else {
progress.value = when {
speed >= 0 -> clipSpec?.getMaxProgress(composition) ?: 1f
else -> clipSpec?.getMinProgress(composition) ?: 0f
}
true
}
}
}
currentOnFinished?.invoke()
}
return progress
}
/**
* Floor mod instead of % which is remainder. This allows negative speeds to properly wrap around to
* the max progress.
*/
private infix fun Float.fmod(other: Float) = ((this % other) + other) % other