Support completable animations in Compose tests (#2051)
The change in #1987 to always use `withInfiniteAnimationFrameNanos` prevents non-infinite animations from completing in Compose tests.
I added a simple check to use `withFrameNanos` or `withInfiniteAnimationFrameNanos` based on the number of iterations. This fixes tests that wait for animations to complete, see sample `WalkthroughAnimationTest`. And `InfiniteAnimationTest` still passes.
Let me know if I'm missing some other use case.
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 16863f6..6403bca 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
@@ -10,6 +10,7 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import androidx.compose.runtime.withFrameNanos
import com.airbnb.lottie.LottieComposition
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.NonCancellable
@@ -255,10 +256,22 @@
}
}
- // We use withInfiniteAnimationFrameNanos because it allows tests to add a CoroutineContext
- // element that will cancel infinite transitions instead of preventing composition from ever going idle.
- private suspend fun doFrame(iterations: Int): Boolean = withInfiniteAnimationFrameNanos { frameNanos ->
- val composition = composition ?: return@withInfiniteAnimationFrameNanos true
+ private suspend fun doFrame(iterations: Int): Boolean {
+ return if (iterations == LottieConstants.IterateForever) {
+ // We use withInfiniteAnimationFrameNanos because it allows tests to add a CoroutineContext
+ // element that will cancel infinite transitions instead of preventing composition from ever going idle.
+ withInfiniteAnimationFrameNanos { frameNanos ->
+ onFrame(iterations, frameNanos)
+ }
+ } else {
+ withFrameNanos { frameNanos ->
+ onFrame(iterations, frameNanos)
+ }
+ }
+ }
+
+ private fun onFrame(iterations: Int, frameNanos: Long): Boolean {
+ val composition = composition ?: return true
val dNanos = if (lastFrameNanos == AnimationConstants.UnspecifiedTime) 0L else (frameNanos - lastFrameNanos)
lastFrameNanos = frameNanos
@@ -279,7 +292,7 @@
if (iteration + dIterations > iterations) {
progress = endProgress
iteration = iterations
- return@withInfiniteAnimationFrameNanos false
+ return false
}
iteration += dIterations
val progressPastEndRem = progressPastEndOfIteration - (dIterations - 1) * durationProgress
@@ -289,7 +302,7 @@
}
}
- true
+ return true
}
}
diff --git a/sample-compose/src/androidTest/java/com/airbnb/lottie/samples/WalkthroughAnimationTest.kt b/sample-compose/src/androidTest/java/com/airbnb/lottie/samples/WalkthroughAnimationTest.kt
new file mode 100644
index 0000000..ec62397
--- /dev/null
+++ b/sample-compose/src/androidTest/java/com/airbnb/lottie/samples/WalkthroughAnimationTest.kt
@@ -0,0 +1,46 @@
+package com.airbnb.lottie.samples
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import com.airbnb.lottie.LottieCompositionFactory
+import com.airbnb.lottie.compose.LottieAnimation
+import com.airbnb.lottie.compose.animateLottieCompositionAsState
+import com.airbnb.lottie.sample.compose.ComposeActivity
+import com.airbnb.lottie.sample.compose.R
+import org.junit.Rule
+import org.junit.Test
+
+class WalkthroughAnimationTest {
+
+ @get:Rule
+ val composeTestRule = createAndroidComposeRule(ComposeActivity::class.java)
+
+ @Test
+ fun testWalkthroughCompletes() {
+ val composition = LottieCompositionFactory.fromRawResSync(composeTestRule.activity, R.raw.walkthrough).value!!
+ var animationCompleted = true
+
+ composeTestRule.setContent {
+ val progress by animateLottieCompositionAsState(composition, iterations = 1)
+
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ ) {
+ LottieAnimation(
+ composition,
+ progress,
+ )
+ }
+
+ if (progress == 1f) {
+ animationCompleted = true
+ }
+ }
+
+ composeTestRule.mainClock.advanceTimeUntil { animationCompleted }
+ }
+}
\ No newline at end of file