<lambda>null1 package com.airbnb.lottie.compose
2
3 import androidx.compose.animation.core.AnimationConstants
4 import androidx.compose.animation.core.withInfiniteAnimationFrameNanos
5 import androidx.compose.foundation.MutatorMutex
6 import androidx.compose.runtime.Composable
7 import androidx.compose.runtime.Stable
8 import androidx.compose.runtime.derivedStateOf
9 import androidx.compose.runtime.getValue
10 import androidx.compose.runtime.mutableStateOf
11 import androidx.compose.runtime.remember
12 import androidx.compose.runtime.setValue
13 import androidx.compose.runtime.withFrameNanos
14 import com.airbnb.lottie.LottieComposition
15 import kotlinx.coroutines.CancellationException
16 import kotlinx.coroutines.NonCancellable
17 import kotlinx.coroutines.ensureActive
18 import kotlinx.coroutines.job
19 import kotlinx.coroutines.withContext
20 import kotlin.coroutines.EmptyCoroutineContext
21 import kotlin.coroutines.coroutineContext
22
23 /**
24 * Use this to create a [LottieAnimatable] in a composable.
25 *
26 * @see LottieAnimatable
27 */
28 @Composable
29 fun rememberLottieAnimatable(): LottieAnimatable = remember { LottieAnimatable() }
30
31 /**
32 * Use this to create a [LottieAnimatable] outside of a composable such as a hoisted state class.
33 *
34 * @see rememberLottieAnimatable
35 * @see LottieAnimatable
36 */
LottieAnimatablenull37 fun LottieAnimatable(): LottieAnimatable = LottieAnimatableImpl()
38
39 /**
40 * Reset the animation back to the minimum progress and first iteration.
41 */
42 suspend fun LottieAnimatable.resetToBeginning() {
43 snapTo(
44 progress = defaultProgress(composition, clipSpec, speed),
45 iteration = 1,
46 )
47 }
48
49 /**
50 * [LottieAnimatable] is an extension of [LottieAnimationState] that contains imperative
51 * suspend functions to control animation playback.
52 *
53 * To create one, call:
54 * ```
55 * val animatable = rememberLottieAnimatable()
56 * ```
57 *
58 * This is the imperative version of [animateLottieCompositionAsState].
59 *
60 * [LottieAnimationState] ensures *mutual exclusiveness* on its animations. To
61 * achieve this, when a new animation is started via [animate] or [snapTo], any ongoing
62 * animation will be canceled via a [CancellationException]. Because of this, it is possible
63 * that your animation will not start synchronously. As a result, if you switch from animating
64 * one composition to another, it is not safe to render the second composition immediately after
65 * calling animate. Instead, you should always rely on [LottieAnimationState.composition] and
66 * [LottieAnimationState.progress].
67 *
68 * This class is comparable to [androidx.compose.animation.core.Animatable]. It is a relatively
69 * low-level API that gives maximum control over animations. In most cases, you can use
70 * [animateLottieCompositionAsState] which provides declarative APIs to create, update, and animate
71 * a [LottieComposition].
72 *
73 * @see animate
74 * @see snapTo
75 * @see animateLottieCompositionAsState
76 */
77 @Stable
78 interface LottieAnimatable : LottieAnimationState {
79 /**
80 * Snap to a specific point in an animation. This can be used to update the progress
81 * or iteration count of an ongoing animation. It will cancel any ongoing animations
82 * on this state class. To update and then resume an animation, call [animate] again with
83 * continueFromPreviousAnimate set to true after calling [snapTo].
84 *
85 * @param composition The [LottieComposition] that should be rendered.
86 * Defaults to [LottieAnimatable.composition].
87 * @param progress The progress that should be set.
88 * Defaults to [LottieAnimatable.progress]
89 * @param iteration Updates the current iteration count. This can be used to "rewind" or
90 * "fast-forward" an ongoing animation to a past/future iteration count.
91 * Defaults to [LottieAnimatable.iteration]
92 * @param resetLastFrameNanos [rememberLottieAnimatable] keeps track of the frame time of the most
93 * recent animation. When [animate] is called with continueFromPreviousAnimate
94 * set to true, a delta will be calculated from the most recent [animate] call
95 * to ensure that the original progress is unaffected by [snapTo] calls in the
96 * middle.
97 * Defaults to false if progress is not being snapped to.
98 * Defaults to true if progress is being snapped to.
99 */
snapTonull100 suspend fun snapTo(
101 composition: LottieComposition? = this.composition,
102 progress: Float = this.progress,
103 iteration: Int = this.iteration,
104 resetLastFrameNanos: Boolean = progress != this.progress,
105 )
106
107 /**
108 * Animate a [LottieComposition].
109 *
110 * @param composition The [LottieComposition] that should be rendered.
111 * @param iteration The iteration to start the animation at. Defaults to 1 and carries over from previous animates.
112 * @param iterations The number of iterations to continue running for. Set to 1 to play one time
113 * set to [LottieConstants.IterateForever] to iterate forever. Can be set to arbitrary
114 * numbers. Defaults to 1 and carries over from previous animates.
115 * @param speed The speed at which the composition should be animated. Can be negative. Defaults to 1 and
116 * carries over from previous animates.
117 * @param clipSpec An optional [LottieClipSpec] to trim the playback of the composition between two values.
118 * Defaults to null and carries over from previous animates.
119 * @param initialProgress An optional progress value that the animation should start at. Defaults to the
120 * starting progress as defined by the clipSpec and speed. Because the default value
121 * isn't the existing progress value, if you are resuming an animation, you
122 * probably want to set this to [progress].
123 * @param continueFromPreviousAnimate When set to true, instead of starting at the minimum progress,
124 * the initial progress will be advanced in accordance to the amount
125 * of time that has passed since the last frame was rendered.
126 * @param cancellationBehavior The behavior that this animation should have when cancelled. In most cases,
127 * you will want it to cancel immediately. However, if you have a state based
128 * transition and you want an animation to finish playing before moving on to
129 * the next one then you may want to set this to [LottieCancellationBehavior.OnIterationFinish].
130 * @param ignoreSystemAnimationsDisabled When set to true, the animation will animate even if animations
131 * are disabled at the OS level.
132 * Defaults to false.
133 * @param useCompositionFrameRate Lottie files can specify a target frame rate. By default, Lottie ignores it
134 * and re-renders on every frame. If that behavior is undesirable, you can set
135 * this to true to use the composition frame rate instead.
136 * Note: composition frame rates are usually lower than display frame rates
137 * so this will likely make your animation feel janky. However, it may be desirable
138 * for specific situations such as pixel art that are intended to have low frame rates.
139 */
140 suspend fun animate(
141 composition: LottieComposition?,
142 iteration: Int = this.iteration,
143 iterations: Int = this.iterations,
144 reverseOnRepeat: Boolean = this.reverseOnRepeat,
145 speed: Float = this.speed,
146 clipSpec: LottieClipSpec? = this.clipSpec,
147 initialProgress: Float = defaultProgress(composition, clipSpec, speed),
148 continueFromPreviousAnimate: Boolean = false,
149 cancellationBehavior: LottieCancellationBehavior = LottieCancellationBehavior.Immediately,
150 ignoreSystemAnimationsDisabled: Boolean = false,
151 useCompositionFrameRate: Boolean = false,
152 )
153 }
154
155 @Stable
156 private class LottieAnimatableImpl : LottieAnimatable {
157 override var isPlaying: Boolean by mutableStateOf(false)
158 private set
159
160 override val value: Float
161 get() = progress
162
163 override var iteration: Int by mutableStateOf(1)
164 private set
165
166 override var iterations: Int by mutableStateOf(1)
167 private set
168
169 override var reverseOnRepeat: Boolean by mutableStateOf(false)
170 private set
171
172 override var clipSpec: LottieClipSpec? by mutableStateOf(null)
173 private set
174
175 override var speed: Float by mutableStateOf(1f)
176 private set
177
178 override var useCompositionFrameRate: Boolean by mutableStateOf(false)
179 private set
180
181 /**
182 * Inverse speed value is used to play the animation in reverse when [reverseOnRepeat] is true.
183 */
184 private val frameSpeed: Float by derivedStateOf {
185 if (reverseOnRepeat && iteration % 2 == 0) -speed else speed
186 }
187
188 override var composition: LottieComposition? by mutableStateOf(null)
189 private set
190
191 private var progressRaw: Float by mutableStateOf(0f)
192
193 override var progress: Float by mutableStateOf(0f)
194 private set
195
196 override var lastFrameNanos: Long by mutableStateOf(AnimationConstants.UnspecifiedTime)
197 private set
198
199 private val endProgress: Float by derivedStateOf {
200 val c = composition
201 when {
202 c == null -> 0f
203 speed < 0 -> clipSpec?.getMinProgress(c) ?: 0f
204 else -> clipSpec?.getMaxProgress(c) ?: 1f
205 }
206 }
207
208 override val isAtEnd: Boolean by derivedStateOf { iteration == iterations && progress == endProgress }
209
210 private val mutex = MutatorMutex()
211
212 override suspend fun snapTo(
213 composition: LottieComposition?,
214 progress: Float,
215 iteration: Int,
216 resetLastFrameNanos: Boolean,
217 ) {
218 mutex.mutate {
219 this.composition = composition
220 updateProgress(progress)
221 this.iteration = iteration
222 isPlaying = false
223 if (resetLastFrameNanos) {
224 lastFrameNanos = AnimationConstants.UnspecifiedTime
225 }
226 }
227 }
228
229 override suspend fun animate(
230 composition: LottieComposition?,
231 iteration: Int,
232 iterations: Int,
233 reverseOnRepeat: Boolean,
234 speed: Float,
235 clipSpec: LottieClipSpec?,
236 initialProgress: Float,
237 continueFromPreviousAnimate: Boolean,
238 cancellationBehavior: LottieCancellationBehavior,
239 ignoreSystemAnimationsDisabled: Boolean,
240 useCompositionFrameRate: Boolean,
241 ) {
242 mutex.mutate {
243 this.iteration = iteration
244 this.iterations = iterations
245 this.reverseOnRepeat = reverseOnRepeat
246 this.speed = speed
247 this.clipSpec = clipSpec
248 this.composition = composition
249 updateProgress(initialProgress)
250 this.useCompositionFrameRate = useCompositionFrameRate
251 if (!continueFromPreviousAnimate) lastFrameNanos = AnimationConstants.UnspecifiedTime
252 if (composition == null) {
253 isPlaying = false
254 return@mutate
255 } else if (speed.isInfinite()) {
256 updateProgress(endProgress)
257 isPlaying = false
258 this.iteration = iterations
259 return@mutate
260 }
261
262 isPlaying = true
263 try {
264 val context = when (cancellationBehavior) {
265 LottieCancellationBehavior.OnIterationFinish -> NonCancellable
266 LottieCancellationBehavior.Immediately -> EmptyCoroutineContext
267 }
268 val parentJob = coroutineContext.job
269 withContext(context) {
270 while (true) {
271 val actualIterations = when (cancellationBehavior) {
272 LottieCancellationBehavior.OnIterationFinish -> {
273 if (parentJob.isActive) iterations else iteration
274 }
275 else -> iterations
276 }
277 if (!doFrame(actualIterations)) break
278 }
279 }
280 coroutineContext.ensureActive()
281 } finally {
282 isPlaying = false
283 }
284 }
285 }
286
287 private suspend fun doFrame(iterations: Int): Boolean {
288 return if (iterations == LottieConstants.IterateForever) {
289 // We use withInfiniteAnimationFrameNanos because it allows tests to add a CoroutineContext
290 // element that will cancel infinite transitions instead of preventing composition from ever going idle.
291 withInfiniteAnimationFrameNanos { frameNanos ->
292 onFrame(iterations, frameNanos)
293 }
294 } else {
295 withFrameNanos { frameNanos ->
296 onFrame(iterations, frameNanos)
297 }
298 }
299 }
300
301 private fun onFrame(iterations: Int, frameNanos: Long): Boolean {
302 val composition = composition ?: return true
303 val dNanos = if (lastFrameNanos == AnimationConstants.UnspecifiedTime) 0L else (frameNanos - lastFrameNanos)
304 lastFrameNanos = frameNanos
305
306 val minProgress = clipSpec?.getMinProgress(composition) ?: 0f
307 val maxProgress = clipSpec?.getMaxProgress(composition) ?: 1f
308
309 val dProgress = dNanos / 1_000_000 / composition.duration * frameSpeed
310 val progressPastEndOfIteration = when {
311 frameSpeed < 0 -> minProgress - (progressRaw + dProgress)
312 else -> progressRaw + dProgress - maxProgress
313 }
314 if (progressPastEndOfIteration < 0f) {
315 updateProgress(progressRaw.coerceIn(minProgress, maxProgress) + dProgress)
316 } else {
317 val durationProgress = maxProgress - minProgress
318 val dIterations = (progressPastEndOfIteration / durationProgress).toInt() + 1
319
320 if (iteration + dIterations > iterations) {
321 updateProgress(endProgress)
322 iteration = iterations
323 return false
324 }
325 iteration += dIterations
326 val progressPastEndRem = progressPastEndOfIteration - (dIterations - 1) * durationProgress
327 updateProgress(
328 when {
329 frameSpeed < 0 -> maxProgress - progressPastEndRem
330 else -> minProgress + progressPastEndRem
331 }
332 )
333 }
334
335 return true
336 }
337
338 private fun Float.roundToCompositionFrameRate(composition: LottieComposition?): Float {
339 composition ?: return this
340 val frameRate = composition.frameRate
341 val interval = 1 / frameRate
342 return this - this % interval
343 }
344
345 private fun updateProgress(progress: Float) {
346 this.progressRaw = progress
347 this.progress = if (useCompositionFrameRate) progress.roundToCompositionFrameRate(composition) else progress
348 }
349 }
350
defaultProgressnull351 private fun defaultProgress(composition: LottieComposition?, clipSpec: LottieClipSpec?, speed: Float): Float {
352 return when {
353 speed < 0 && composition == null -> 1f
354 composition == null -> 0f
355 speed < 0 -> clipSpec?.getMaxProgress(composition) ?: 1f
356 else -> clipSpec?.getMinProgress(composition) ?: 0f
357 }
358 }
359