diff --git a/paparazzi-gradle-plugin/src/test/projects/compose/build.gradle b/paparazzi-gradle-plugin/src/test/projects/compose/build.gradle index 1b31a0e543..28c255cccb 100644 --- a/paparazzi-gradle-plugin/src/test/projects/compose/build.gradle +++ b/paparazzi-gradle-plugin/src/test/projects/compose/build.gradle @@ -25,5 +25,6 @@ android { dependencies { implementation libs.composeUi.material + implementation libs.composeUi.uiTooling } diff --git a/paparazzi-gradle-plugin/src/test/projects/compose/src/main/java/app/cash/paparazzi/plugin/test/SimpleAnimation.kt b/paparazzi-gradle-plugin/src/test/projects/compose/src/main/java/app/cash/paparazzi/plugin/test/SimpleAnimation.kt new file mode 100644 index 0000000000..a7f9142032 --- /dev/null +++ b/paparazzi-gradle-plugin/src/test/projects/compose/src/main/java/app/cash/paparazzi/plugin/test/SimpleAnimation.kt @@ -0,0 +1,74 @@ +package app.cash.paparazzi.plugin.test + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +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.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Preview +@Composable +fun SimpleAnimation() { + Box(Modifier.fillMaxSize()) { + var visible by remember { + mutableStateOf(false) + } + + LaunchedEffect(Unit) { + visible = true + } + + val infiniteTransition = rememberInfiniteTransition(label = "infinite-transition") + val boxColor by infiniteTransition.animateColor( + initialValue = Color.Red, + targetValue = Color.Blue, + animationSpec = infiniteRepeatable(tween(200, 300), RepeatMode.Reverse), + label = "color" + ) + + AnimatedVisibility( + visible = visible, + enter = fadeIn(tween(delayMillis = 200)), + exit = fadeOut(tween(delayMillis = 100)) + ) { + Box( + Modifier + .size(100.dp) + .background(boxColor) + ) + } + + val scale by + infiniteTransition.animateFloat( + initialValue = 0.2f, + targetValue = 1f, + animationSpec = infiniteRepeatable(tween(200, 100), RepeatMode.Reverse), + label = "scale" + ) + + Text( + modifier = Modifier.scale(scale), + text = "Hello, Paparazzi" + ) + } +} diff --git a/paparazzi-gradle-plugin/src/test/projects/compose/src/test/java/app/cash/paparazzi/plugin/test/ComposeTest.kt b/paparazzi-gradle-plugin/src/test/projects/compose/src/test/java/app/cash/paparazzi/plugin/test/ComposeTest.kt index 828fc348fa..ee200ac9d9 100644 --- a/paparazzi-gradle-plugin/src/test/projects/compose/src/test/java/app/cash/paparazzi/plugin/test/ComposeTest.kt +++ b/paparazzi-gradle-plugin/src/test/projects/compose/src/test/java/app/cash/paparazzi/plugin/test/ComposeTest.kt @@ -1,5 +1,6 @@ package app.cash.paparazzi.plugin.test +import androidx.compose.ui.platform.ComposeView import app.cash.paparazzi.Paparazzi import org.junit.Rule import org.junit.Test @@ -14,4 +15,21 @@ class ComposeTest { HelloPaparazzi() } } + + @Test + fun animation() { + val view = ComposeView(paparazzi.context).apply { + setContent { SimpleAnimation() } + } + + paparazzi.gif(view, fps = 120) + paparazzi.gif(view, name = "start-end", fps = 2, end = 500) + paparazzi.gif(view, name = "middle-anim", start = 200, fps = 60) + paparazzi.snapshot(view = view, offsetMillis = 1, name = "1ms") + paparazzi.snapshot(view = view, offsetMillis = 100, name = "100ms") + paparazzi.snapshot(view = view, offsetMillis = 200, name = "200ms") + paparazzi.snapshot(view = view, offsetMillis = 300, name = "300ms") + paparazzi.snapshot(view = view, offsetMillis = 400, name = "400ms") + paparazzi.snapshot(view = view, offsetMillis = 500, name = "500ms") + } } diff --git a/paparazzi-gradle-plugin/src/test/projects/compose/src/test/snapshots/images/app.cash.paparazzi.plugin.test_ComposeTest_animation_100ms.png b/paparazzi-gradle-plugin/src/test/projects/compose/src/test/snapshots/images/app.cash.paparazzi.plugin.test_ComposeTest_animation_100ms.png new file mode 100644 index 0000000000..9779d64bd1 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/compose/src/test/snapshots/images/app.cash.paparazzi.plugin.test_ComposeTest_animation_100ms.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/compose/src/test/snapshots/images/app.cash.paparazzi.plugin.test_ComposeTest_animation_1ms.png b/paparazzi-gradle-plugin/src/test/projects/compose/src/test/snapshots/images/app.cash.paparazzi.plugin.test_ComposeTest_animation_1ms.png new file mode 100644 index 0000000000..9779d64bd1 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/compose/src/test/snapshots/images/app.cash.paparazzi.plugin.test_ComposeTest_animation_1ms.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/compose/src/test/snapshots/images/app.cash.paparazzi.plugin.test_ComposeTest_animation_200ms.png b/paparazzi-gradle-plugin/src/test/projects/compose/src/test/snapshots/images/app.cash.paparazzi.plugin.test_ComposeTest_animation_200ms.png new file mode 100644 index 0000000000..485f75c999 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/compose/src/test/snapshots/images/app.cash.paparazzi.plugin.test_ComposeTest_animation_200ms.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/compose/src/test/snapshots/images/app.cash.paparazzi.plugin.test_ComposeTest_animation_300ms.png b/paparazzi-gradle-plugin/src/test/projects/compose/src/test/snapshots/images/app.cash.paparazzi.plugin.test_ComposeTest_animation_300ms.png new file mode 100644 index 0000000000..348a94fadb Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/compose/src/test/snapshots/images/app.cash.paparazzi.plugin.test_ComposeTest_animation_300ms.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/compose/src/test/snapshots/images/app.cash.paparazzi.plugin.test_ComposeTest_animation_400ms.png b/paparazzi-gradle-plugin/src/test/projects/compose/src/test/snapshots/images/app.cash.paparazzi.plugin.test_ComposeTest_animation_400ms.png new file mode 100644 index 0000000000..e9859c118c Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/compose/src/test/snapshots/images/app.cash.paparazzi.plugin.test_ComposeTest_animation_400ms.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/compose/src/test/snapshots/images/app.cash.paparazzi.plugin.test_ComposeTest_animation_500ms.png b/paparazzi-gradle-plugin/src/test/projects/compose/src/test/snapshots/images/app.cash.paparazzi.plugin.test_ComposeTest_animation_500ms.png new file mode 100644 index 0000000000..b2e764fef3 Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/compose/src/test/snapshots/images/app.cash.paparazzi.plugin.test_ComposeTest_animation_500ms.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/compose/src/test/snapshots/videos/app.cash.paparazzi.plugin.test_ComposeTest_animation.png b/paparazzi-gradle-plugin/src/test/projects/compose/src/test/snapshots/videos/app.cash.paparazzi.plugin.test_ComposeTest_animation.png new file mode 100644 index 0000000000..478f2f20da Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/compose/src/test/snapshots/videos/app.cash.paparazzi.plugin.test_ComposeTest_animation.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/compose/src/test/snapshots/videos/app.cash.paparazzi.plugin.test_ComposeTest_animation_middle-anim.png b/paparazzi-gradle-plugin/src/test/projects/compose/src/test/snapshots/videos/app.cash.paparazzi.plugin.test_ComposeTest_animation_middle-anim.png new file mode 100644 index 0000000000..8b58d18fcb Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/compose/src/test/snapshots/videos/app.cash.paparazzi.plugin.test_ComposeTest_animation_middle-anim.png differ diff --git a/paparazzi-gradle-plugin/src/test/projects/compose/src/test/snapshots/videos/app.cash.paparazzi.plugin.test_ComposeTest_animation_start-end.png b/paparazzi-gradle-plugin/src/test/projects/compose/src/test/snapshots/videos/app.cash.paparazzi.plugin.test_ComposeTest_animation_start-end.png new file mode 100644 index 0000000000..07e6a8125f Binary files /dev/null and b/paparazzi-gradle-plugin/src/test/projects/compose/src/test/snapshots/videos/app.cash.paparazzi.plugin.test_ComposeTest_animation_start-end.png differ diff --git a/paparazzi/src/main/java/app/cash/paparazzi/PaparazziSdk.kt b/paparazzi/src/main/java/app/cash/paparazzi/PaparazziSdk.kt index ff6141b06c..f5e1597670 100644 --- a/paparazzi/src/main/java/app/cash/paparazzi/PaparazziSdk.kt +++ b/paparazzi/src/main/java/app/cash/paparazzi/PaparazziSdk.kt @@ -289,16 +289,30 @@ public class PaparazziSdk @JvmOverloads constructor( } viewGroup.addView(modifiedView) + + /** + * Compose animation tracks a startTime to ensure animations run correctly. + * We need to ensure the startTime is 0 so when we using the startNanos for animation position works correctly. + * + * Multiple render calls needed for [androidx.compose.animation.core.Transition] like the one used by [androidx.compose.animation.AnimatedVisibility] to work properly. + */ + if (recomposer != null && startNanos > 0) { + withTime(0) { + renderSession.render(false) + } + withTime(0) { + renderSession.render(false) + } + } + for (frame in 0 until frameCount) { val nowNanos = (startNanos + (frame * 1_000_000_000.0 / fps)).toLong() // If we have pendingTasks run recomposer to ensure we get the correct frame. var hasPendingWork = false withTime(nowNanos) { - val result = renderSession.render(true) - if (result.status == ERROR_UNKNOWN) { - throw result.exception - } + renderForResult() + if (hasComposeRuntime && recomposer != null) { // If we have pending tasks, we need to trigger it within the context of the first frame. if (frame == 0 && (recomposer as Recomposer).hasPendingWork) { @@ -309,10 +323,7 @@ public class PaparazziSdk @JvmOverloads constructor( if (hasPendingWork) { withTime(nowNanos) { - val result = renderSession.render(true) - if (result.status == ERROR_UNKNOWN) { - throw result.exception - } + renderForResult() } val recomposerInstance = recomposer as Recomposer @@ -346,6 +357,13 @@ public class PaparazziSdk @JvmOverloads constructor( } } + private fun renderForResult() { + val result = renderSession.render(true) + if (result.status == ERROR_UNKNOWN) { + throw result.exception + } + } + private fun withTime( timeNanos: Long, block: () -> Unit