Skip to content

Commit

Permalink
[Compose] Respect system animator scale (#2000)
Browse files Browse the repository at this point in the history
This will only automatically work for animateLottieCompositionAsState(...) because accessing the system animator scale requires a Context and the coroutines LottieAnimatable functions don't have access to that. Apps will need to manually handle the scale if they aren't using animateLottieCompositionAsState(...).

When animations are disabled, the speed becomes infinite and Lottie will jump to the end of the animation.

Fixes #1906
  • Loading branch information
gpeal authored Jan 17, 2022
1 parent ba067ca commit a226747
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -126,16 +126,19 @@ 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?,
iteration: Int = this.iteration,
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,
)
}

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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(
Expand All @@ -41,18 +46,22 @@ 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." }

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) {
Expand All @@ -64,7 +73,7 @@ fun animateLottieCompositionAsState(
animatable.animate(
composition,
iterations = iterations,
speed = speed,
speed = actualSpeed,
clipSpec = clipSpec,
initialProgress = animatable.progress,
continueFromPreviousAnimate = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit a226747

Please sign in to comment.