Skip to content

Commit

Permalink
Add reverseOnRepeat option to LottieAnimatable (#2128)
Browse files Browse the repository at this point in the history
Resolves issue #2125 - `LottieAnimation Composable: support LottieAnimationView (XML) repeatMode equivalent`.

This PR adds a `reverseOnRepeat` boolean option to `LottieAnimatable` (defaults to `false`). When set to true the effect is the same as using `app:lottie_repeatMode="reverse"` in the Android XML lottie implementation.
  • Loading branch information
brentwatson committed Oct 5, 2022
1 parent 7275e64 commit c553ad3
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ interface LottieAnimatable : LottieAnimationState {
composition: LottieComposition?,
iteration: Int = this.iteration,
iterations: Int = this.iterations,

This comment has been minimized.

Copy link
@dancartwright08

This comment has been minimized.

Copy link
@dancartwright08

dancartwright08 May 13, 2023

I agreed with terms and condition

reverseOnRepeat: Boolean = this.reverseOnRepeat,
speed: Float = this.speed,
clipSpec: LottieClipSpec? = this.clipSpec,
initialProgress: Float = defaultProgress(composition, clipSpec, speed),
Expand All @@ -160,12 +161,22 @@ private class LottieAnimatableImpl : LottieAnimatable {
override var iterations: Int by mutableStateOf(1)
private set

override var reverseOnRepeat: Boolean by mutableStateOf(false)
private set

override var clipSpec: LottieClipSpec? by mutableStateOf(null)
private set

override var speed: Float by mutableStateOf(1f)
private set

/**
* Inverse speed value is used to play the animation in reverse when [reverseOnRepeat] is true.
*/
private val frameSpeed: Float by derivedStateOf {
if (reverseOnRepeat && iteration % 2 == 0) -speed else speed
}

override var composition: LottieComposition? by mutableStateOf(null)
private set

Expand Down Expand Up @@ -206,6 +217,7 @@ private class LottieAnimatableImpl : LottieAnimatable {
composition: LottieComposition?,
iteration: Int,
iterations: Int,
reverseOnRepeat: Boolean,
speed: Float,
clipSpec: LottieClipSpec?,
initialProgress: Float,
Expand All @@ -216,6 +228,7 @@ private class LottieAnimatableImpl : LottieAnimatable {
mutex.mutate {
this.iteration = iteration
this.iterations = iterations
this.reverseOnRepeat = reverseOnRepeat
this.speed = speed
this.clipSpec = clipSpec
this.composition = composition
Expand Down Expand Up @@ -278,9 +291,9 @@ private class LottieAnimatableImpl : LottieAnimatable {
val minProgress = clipSpec?.getMinProgress(composition) ?: 0f
val maxProgress = clipSpec?.getMaxProgress(composition) ?: 1f

val dProgress = dNanos / 1_000_000 / composition.duration * speed
val dProgress = dNanos / 1_000_000 / composition.duration * frameSpeed
val progressPastEndOfIteration = when {
speed < 0 -> minProgress - (progress + dProgress)
frameSpeed < 0 -> minProgress - (progress + dProgress)
else -> progress + dProgress - maxProgress
}
if (progressPastEndOfIteration < 0f) {
Expand All @@ -297,7 +310,7 @@ private class LottieAnimatableImpl : LottieAnimatable {
iteration += dIterations
val progressPastEndRem = progressPastEndOfIteration - (dIterations - 1) * durationProgress
progress = when {
speed < 0 -> maxProgress - progressPastEndRem
frameSpeed < 0 -> maxProgress - progressPastEndRem
else -> minProgress + progressPastEndRem
}
}
Expand All @@ -313,4 +326,4 @@ private fun defaultProgress(composition: LottieComposition?, clipSpec: LottieCli
speed < 0 -> clipSpec?.getMaxProgress(composition) ?: 1f
else -> clipSpec?.getMinProgress(composition) ?: 0f
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ fun LottieAnimation(
applyOpacityToLayers: Boolean = false,
enableMergePaths: Boolean = false,
renderMode: RenderMode = RenderMode.AUTOMATIC,
reverseOnRepeat: Boolean = false,
maintainOriginalImageBounds: Boolean = false,
dynamicProperties: LottieDynamicProperties? = null,
alignment: Alignment = Alignment.Center,
Expand All @@ -188,6 +189,7 @@ fun LottieAnimation(
composition,
isPlaying,
restartOnPlay,
reverseOnRepeat,
clipSpec,
speed,
iterations,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ interface LottieAnimationState : State<Float> {

val iterations: Int

val reverseOnRepeat: Boolean

val clipSpec: LottieClipSpec?

val speed: Float
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import com.airbnb.lottie.utils.Utils
* is still playing.
* @param restartOnPlay If isPlaying switches from false to true, restartOnPlay determines whether
* the progress and iteration gets reset.
* @param reverseOnRepeat Defines what this animation should do when it reaches the end. This setting
* is applied only when [iterations] is either greater than 0 or [LottieConstants.IterateForever].
* Defaults to `false`.
* @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.
Expand All @@ -42,6 +45,7 @@ fun animateLottieCompositionAsState(
composition: LottieComposition?,
isPlaying: Boolean = true,
restartOnPlay: Boolean = true,
reverseOnRepeat: Boolean = false,
clipSpec: LottieClipSpec? = null,
speed: Float = 1f,
iterations: Int = 1,
Expand Down Expand Up @@ -73,6 +77,7 @@ fun animateLottieCompositionAsState(
animatable.animate(
composition,
iterations = iterations,
reverseOnRepeat = reverseOnRepeat,
speed = actualSpeed,
clipSpec = clipSpec,
initialProgress = animatable.progress,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,34 @@ class LottieAnimatableImplTest {
}
}

@Test
fun testReverseOnRepeat() = runTest {
val job = launch {
anim.animate(
composition,
reverseOnRepeat = true,
iterations = LottieConstants.IterateForever,
)
}
assertFrame(0, progress = 0f, iteration = 1, iterations = LottieConstants.IterateForever)

mapOf(
0L to 0.0f,
300L to 0.5f,
598L to 0.99f,
599L to 1.0f,
601L to 0.99f, // start reversing animation
899L to 0.5f,
1199L to 0.0f,
).forEach { (frameTime, expectedProgress) ->
clock.frameMs(frameTime)
assertEquals(
"Expecting progress $expectedProgress @ frame $frameTime, but was ${anim.progress}",
expectedProgress, anim.progress, 0.01f
)
}
job.cancel()
}

@Test
fun testNonCancellable() = runTest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ fun AnimatableExamplesPage() {
ExampleCard("Example 5", "Click to toggle playback") {
Example5()
}
ExampleCard("Example 6", "Reverse Animation on Repeat") {
Example6()
}
}
}
}
Expand Down Expand Up @@ -166,4 +169,19 @@ private fun Example5() {
modifier = Modifier
.clickable { shouldPlay = !shouldPlay }
)
}
}

@Composable
private fun Example6() {
val anim = rememberLottieAnimatable()
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
LaunchedEffect(composition) {
anim.animate(
composition,
iterations = LottieConstants.IterateForever,
reverseOnRepeat = true,
)
}
LottieAnimation(anim.composition, { anim.progress })
}

0 comments on commit c553ad3

Please sign in to comment.