blob: af863df87e84703c72c3f21fb5c02ce32dbeab7a [file] [log] [blame]
package com.airbnb.lottie.compose
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.withFrameNanos
import com.airbnb.lottie.LottieComposition
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.withContext
import java.util.concurrent.TimeUnit
import kotlin.coroutines.EmptyCoroutineContext
/**
* Determines how the animation should behave if the calling CoroutineScope is cancelled.
*/
enum class LottieCancellationBehavior {
/**
* Stop animation immediately and return early.
*
* @see lottieTransition
*/
Immediate,
/**
* Continue suspending until the animation completes, effectively ignoring the cancellation request.
* This may be useful if you have a segmented animation with multiple states and you want them to
* animate smoothly from one to the next when the state changes.
*
* @see lottieTransition
*/
AtEnd,
}
/**
* This is similar to [animateLottieComposition] which returns progress as a MutableState<Float>.
* However, instead of returning state, this takes [progress] as a parameter which it updates
* internally and suspends until the animation completes.
*
* To repeat an animation multiple times, just wrap this invocation in a while loop.
*
* If the calling CoroutineScope is cancelled, [cancellationBehavior] determines how cancellation
* should happen. [LottieCancellationBehavior.AtEnd] will ignore the cancellation and continue suspending
* until the animation completes. [LottieCancellationBehavior.Immediate] will immediately cancel the
* animation and return early.
*
* @return the ending frame time nanos
*
* @see lottieTransition
*/
suspend fun animateLottieComposition(
composition: LottieComposition?,
progress: MutableState<Float>,
clipSpec: LottieClipSpec? = null,
cancellationBehavior: LottieCancellationBehavior = LottieCancellationBehavior.Immediate,
speed: Float = 1f,
lastFrameTimeNanos: Long? = null,
startAtMinProgress: Boolean = true,
): Long {
require(speed != 0f) { "Speed must not be 0" }
require(speed.isFinite()) { "Speed must be a finite number. It is $speed." }
var lastFrameTime = lastFrameTimeNanos ?: withFrameNanos { it }
composition ?: return lastFrameTime
val context = when (cancellationBehavior) {
LottieCancellationBehavior.Immediate -> EmptyCoroutineContext
LottieCancellationBehavior.AtEnd -> NonCancellable
}
withContext(context) {
val minProgress = clipSpec?.getMinProgress(composition) ?: 0f
val maxProgress = clipSpec?.getMaxProgress(composition) ?: 1f
progress.value = when {
!startAtMinProgress -> progress.value.coerceIn(minProgress, maxProgress)
speed >= 0 -> minProgress
else -> maxProgress
}
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)
progress.value = rawProgress.coerceIn(minProgress, maxProgress)
done = when {
speed >= 0 -> rawProgress >= maxProgress
else -> rawProgress <= minProgress
}
}
}
}
return lastFrameTime
}