diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimatable.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimatable.kt index 9d9467fa2a..16863f6ac7 100644 --- a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimatable.kt +++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimatable.kt @@ -126,6 +126,8 @@ interface LottieAnimatable : LottieAnimationState { * you will want it to cancel immediately. However, if you have a state based * transition and you want an animation to finish playing before moving on to * the next one then you may want to set this to [LottieCancellationBehavior.OnIterationFinish]. + * @param ignoreSystemAnimationsDisabled When set to true, the animation will animate even if animations are disabled at the OS level. + * Defaults to false. */ suspend fun animate( composition: LottieComposition?, @@ -133,9 +135,10 @@ interface LottieAnimatable : LottieAnimationState { iterations: Int = this.iterations, speed: Float = this.speed, clipSpec: LottieClipSpec? = this.clipSpec, - initialProgress: Float = defaultProgress(composition, clipSpec, speed), + initialProgress: Float = defaultProgress(composition, clipSpec, speed), continueFromPreviousAnimate: Boolean = false, cancellationBehavior: LottieCancellationBehavior = LottieCancellationBehavior.Immediately, + ignoreSystemAnimationsDisabled: Boolean = false, ) } @@ -207,9 +210,9 @@ private class LottieAnimatableImpl : LottieAnimatable { initialProgress: Float, continueFromPreviousAnimate: Boolean, cancellationBehavior: LottieCancellationBehavior, + ignoreSystemAnimationsDisabled: Boolean, ) { mutex.mutate { - require(speed.isFinite()) { "Speed must be a finite number. It is $speed." } this.iteration = iteration this.iterations = iterations this.speed = speed @@ -220,6 +223,11 @@ private class LottieAnimatableImpl : LottieAnimatable { if (composition == null) { isPlaying = false return@mutate + } else if (speed.isInfinite()) { + progress = endProgress + isPlaying = false + this.iteration = iterations + return@mutate } isPlaying = true diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieConstants.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieConstants.kt index 882fda0eb0..5962dd5003 100644 --- a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieConstants.kt +++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieConstants.kt @@ -1,5 +1,7 @@ package com.airbnb.lottie.compose +import android.content.Context + object LottieConstants { /** * Use this with [animateLottieCompositionAsState]'s iterations parameter to repeat forever. diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/animateLottieCompositionAsState.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/animateLottieCompositionAsState.kt index 9f7ef837d7..78211ea77f 100644 --- a/lottie-compose/src/main/java/com/airbnb/lottie/compose/animateLottieCompositionAsState.kt +++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/animateLottieCompositionAsState.kt @@ -1,12 +1,14 @@ package com.airbnb.lottie.compose import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext import com.airbnb.lottie.LottieComposition +import com.airbnb.lottie.utils.Utils /** * Returns a [LottieAnimationState] representing the progress of an animation. @@ -31,6 +33,9 @@ import com.airbnb.lottie.LottieComposition * you will want it to cancel immediately. However, if you have a state based * transition and you want an animation to finish playing before moving on to * the next one then you may want to set this to [LottieCancellationBehavior.OnIterationFinish]. + * @param ignoreSystemAnimatorScale By default, Lottie will respect the system animator scale set in developer options or set to 0 + * by things like battery saver mode. When set to 0, the speed will effectively become [Integer.MAX_VALUE]. + * Set this to false if you want to ignore the system animator scale and always default to normal speed. */ @Composable fun animateLottieCompositionAsState( @@ -41,6 +46,7 @@ fun animateLottieCompositionAsState( speed: Float = 1f, iterations: Int = 1, cancellationBehavior: LottieCancellationBehavior = LottieCancellationBehavior.Immediately, + ignoreSystemAnimatorScale: Boolean = false, ): LottieAnimationState { require(iterations > 0) { "Iterations must be a positive number ($iterations)." } require(speed.isFinite()) { "Speed must be a finite number. It is $speed." } @@ -48,11 +54,14 @@ fun animateLottieCompositionAsState( val animatable = rememberLottieAnimatable() var wasPlaying by remember { mutableStateOf(isPlaying) } + // Dividing by 0 correctly yields Float.POSITIVE_INFINITY here. + val actualSpeed = if (ignoreSystemAnimatorScale) speed else (speed / Utils.getAnimationScale(LocalContext.current)) + LaunchedEffect( composition, isPlaying, clipSpec, - speed, + actualSpeed, iterations, ) { if (isPlaying && !wasPlaying && restartOnPlay) { @@ -64,7 +73,7 @@ fun animateLottieCompositionAsState( animatable.animate( composition, iterations = iterations, - speed = speed, + speed = actualSpeed, clipSpec = clipSpec, initialProgress = animatable.progress, continueFromPreviousAnimate = false, diff --git a/lottie-compose/src/test/java/com/airbnb/lottie/compose/LottieAnimatableImplTest.kt b/lottie-compose/src/test/java/com/airbnb/lottie/compose/LottieAnimatableImplTest.kt index 7ef172e4fa..fa024d9077 100644 --- a/lottie-compose/src/test/java/com/airbnb/lottie/compose/LottieAnimatableImplTest.kt +++ b/lottie-compose/src/test/java/com/airbnb/lottie/compose/LottieAnimatableImplTest.kt @@ -278,6 +278,70 @@ class LottieAnimatableImplTest { assertFrame(450, progress = 1f, speed = 2f, isPlaying = false, isAtEnd = true) } + @Test + fun testInfiniteSpeed() { + val clipSpec = LottieClipSpec.Progress(0.33f, 0.57f) + runTest { + launch { + anim.animate(composition, clipSpec = clipSpec, speed = Float.POSITIVE_INFINITY, iterations = LottieConstants.IterateForever) + } + assertFrame( + 0, + progress = 0.57f, + isPlaying = false, + speed = Float.POSITIVE_INFINITY, + clipSpec = clipSpec, + isAtEnd = true, + iterations = LottieConstants.IterateForever, + iteration = LottieConstants.IterateForever, + lastFrameNanos = AnimationConstants.UnspecifiedTime, + ) + } + } + + @Test + fun testInfiniteSpeedWithIterations() { + val clipSpec = LottieClipSpec.Progress(0.33f, 0.57f) + runTest { + launch { + anim.animate(composition, clipSpec = clipSpec, speed = Float.POSITIVE_INFINITY, iterations = 3) + } + assertFrame( + 300, + progress = 0.57f, + isPlaying = false, + speed = Float.POSITIVE_INFINITY, + clipSpec = clipSpec, + isAtEnd = true, + iterations = 3, + iteration = 3, + lastFrameNanos = AnimationConstants.UnspecifiedTime, + ) + } + } + + @Test + fun testNegativeInfiniteSpeed() { + val clipSpec = LottieClipSpec.Progress(0.33f, 0.57f) + runTest { + launch { + anim.animate(composition, clipSpec = clipSpec, speed = Float.NEGATIVE_INFINITY, iterations = LottieConstants.IterateForever) + } + assertFrame( + 0, + progress = 0.33f, + isPlaying = false, + speed = Float.NEGATIVE_INFINITY, + clipSpec = clipSpec, + isAtEnd = true, + iterations = LottieConstants.IterateForever, + iteration = LottieConstants.IterateForever, + lastFrameNanos = AnimationConstants.UnspecifiedTime, + ) + } + } + + @Test fun testNonCancellable() = runTest { val job = launch {