Skip to content

Commit

Permalink
Proposed fix to better time Compose UI animations
Browse files Browse the repository at this point in the history
  • Loading branch information
geoff-powell committed Oct 18, 2024
1 parent 0dfd588 commit 3079a30
Show file tree
Hide file tree
Showing 13 changed files with 119 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ android {

dependencies {
implementation libs.composeUi.material
implementation libs.composeUi.uiTooling
}

Original file line number Diff line number Diff line change
@@ -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"
)
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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")
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 26 additions & 8 deletions paparazzi/src/main/java/app/cash/paparazzi/PaparazziSdk.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 3079a30

Please sign in to comment.