| 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 |