hey, i'm looking for help regarding custom animati...
# compose
z
hey, i'm looking for help regarding custom animations using Compose.
🧡 2
I'm trying to animate like this:
Copy code
modifier.then(
                                    when (stateChange.direction) {
                                        StateChange.FORWARD -> Modifier.graphicsLayer(translationX = fullWidth + (-1) * fullWidth * animationProgress)
                                        StateChange.BACKWARD -> Modifier.graphicsLayer(translationX = -1 * fullWidth + fullWidth * animationProgress)
                                        else /* REPLACE */ -> Modifier.graphicsLayer(alpha = 0 + animationProgress)
                                    }
                            )
and it's based on this code:
Copy code
var isAnimating by remember { mutableStateOf(true) } // true renders previous initially for fullWidth

            val scope = rememberCoroutineScope()

            val lerping = Animatable(0.0f, Float.VectorConverter, 1.0f) // DO NOT REMEMBER!

            var animationProgress by remember { mutableStateOf(0.0f) }

            var fullWidth by remember { mutableStateOf(0) }
            var fullHeight by remember { mutableStateOf(0) }

            val measurePolicy = MeasurePolicy { measurables, constraints ->
                val placeables = measurables.fastMap { it.measure(constraints) }
                val maxWidth = placeables.fastMaxBy { it.width }?.width ?: 0
                val maxHeight = placeables.fastMaxBy { it.height }?.height ?: 0

                if (fullWidth == 0) {
                    fullWidth = maxWidth
                }

                if (fullHeight == 0) {
                    fullHeight = maxHeight
                }

                layout(maxWidth, maxHeight) {
                    placeables.fastForEach { placeable ->
                        placeable.place(0, 0)
                    }
                }
            }

            Layout(
                    content = {
                        if (fullWidth > 0 && fullHeight > 0) {
                            key(topNewKey) { topNewKey.RenderComposable(modifier) }
                        }
                    },
                    measurePolicy = measurePolicy,
                    modifier = when {
                        !isAnimating -> modifier
                        else -> animationConfiguration.customComposableTransitions.newComposableTransition.animateNewComposable(modifier, stateChange, fullWidth, fullHeight, animationProgress)
                    }
            )

            Layout(
                    content = {
                        if (isAnimating) {
                            key(topPreviousKey) {
                                topPreviousKey.RenderComposable(modifier)
                            }
                        }
                    },
                    measurePolicy = measurePolicy,
                    modifier = when {
                        !isAnimating -> modifier
                        else -> animationConfiguration.customComposableTransitions.previousComposableTransition.animatePreviousComposable(modifier, stateChange, fullWidth, fullHeight, animationProgress)
                    }
            )

            DisposableEffect(key1 = completionCallback, effect = {
                scope.launch {
                    isAnimating = true
                    animationProgress = 0.0f
                    lerping.animateTo(1.0f, animationConfiguration.animationSpec) {
                        animationProgress = this.value
                    }
                    isAnimating = false
                    completionCallback.stateChangeComplete()
                }

                onDispose {
                    // do nothing
                }
            })
but for whatever reason it causes the "composable being animated in" to flicker in before it actually is rendered "elsewhere" by the graphicsLayer. what am I missing? πŸ€”
alternately, people tend to say that animation in Compose is trivial and i'm not seeing it, so hopefully i'm actually missing something πŸ˜‚
z
I wonder if it has something to do with the fact that
DisposableEffect
won’t execute until after the composition is committed, which means the first frame will be composed without
isAnimating == true
?
a
Likely. In more detail, the composition will commit and the effect will run before layout and drawing, but the launched coroutine will be dispatched and scheduled to start running after the frame draws.
Either way, an effect can't influence the composition by way of feedback in the same frame.
You'll want to set up your initial state as part of the composition itself; look to your initial state remember blocks.
You probably also want to use a LaunchedEffect rather than a DisposableEffect that launches; as-is that code can run concurrent with itself if the keys change. You're already using rememberUpdatedState for the key, you probably don't want both.
z
@Zach Klippenstein (he/him) [MOD] even if i force the
isAnimating == true
it flickers πŸ€” just checked now
Copy code
@Composable
        fun RenderScreen(modifier: Modifier = Modifier) {
            val stateChange = stateChange ?: return
            val callback = callback ?: return

            var completionCallback by remember { mutableStateOf<StateChanger.Callback?>(null) }

            var isAnimating by remember { mutableStateOf(true) } // true renders previous initially for fullWidth

            if (completionCallback !== callback) {
                isAnimating = true
                completionCallback = callback
            }
okay i've updated the code as per https://github.com/Zhuinden/simple-stack-compose-integration/blob/1e659374bc51da57[…]en/simplestackcomposeintegration/core/ComposeIntegrationCore.kt and now am using
LaunchedEffect
instead of
DisposableEffect
+
rememberCoroutineScope
and it looks like this
Copy code
LaunchedEffect(key1 = completionCallback, block = {
                animationProgress = 0.0f
                isAnimating = true
                lerping.animateTo(1.0f, animationConfiguration.animationSpec) {
                    animationProgress = this.value
                }
                isAnimating = false
                completionCallback.stateChangeComplete()
            })
the flicker is still not gone 😩
Either way, an effect can't influence the composition by way of feedback in the same frame.
i'm actually not sure what you mean here and i'm also not sure where the first render is coming from. I've been punching this thing for about 10 hours now lol
how am i supposed to trigger an animation then
z
Copy code
isAnimating = true
animationProgress = 0.0f
I think these should be called directly from your composable function, before reading them in your layouts, so that the first frame that is composed reads the correct initial values.
z
I will try that asap thanks
d
Not `remember`ing the Animatable could cause issues as well, there's no guarantee that recomposition won't happen during the animation, and when it does you get a new
Animatable
(with a new value) if you don't remember it.
βž• 1
I'd log the
animationProgress
after
Copy code
animationProgress = this.value
to inspect whether there's any suspicious jump in
animationProgress
.
z
remembering caused problems but maybe the problems were just a symptom
i'll check the recommendations now, thanks πŸ‘
πŸ‘ 1
interesting, i think setting isAnimating = true and animationProgress = 0.0f BEFORE the Layout {s helped πŸ‘ ❀️
it's not perfect though. there is still a flicker, but at least it's not showing the new view on top of the previous view for a moment
d
Curious what the flicker looks like, if you don't mind sharing a video. πŸ™‚
z
so new code is:
Copy code
val stateChange = stateChange ?: return
            val callback = callback ?: return

            var completionCallback by remember { mutableStateOf<StateChanger.Callback?>(null) }

            val topNewKey = stateChange.topNewKey<DefaultComposeKey>()
            val topPreviousKey = stateChange.topPreviousKey<DefaultComposeKey>()

            var isAnimating by remember { mutableStateOf(true) }

            val lerping = remember { Animatable(0.0f, Float.VectorConverter, 1.0f) }

            var animationProgress by remember { mutableStateOf(0.0f) }

            if (completionCallback !== callback) {
                completionCallback = callback

                if (topPreviousKey != null) {
                    animationProgress = 0.0f
                    isAnimating = true
                }
            }

            if (topPreviousKey == null) {
                key(topNewKey) {
                    topNewKey.RenderComposable(modifier)
                }

                DisposableEffect(key1 = completionCallback, effect = {
                    completionCallback!!.stateChangeComplete()

                    onDispose {
                        // do nothing
                    }
                })

                return
            }

            var fullWidth by remember { mutableStateOf(0) }
            var fullHeight by remember { mutableStateOf(0) }

            val measurePolicy = MeasurePolicy { measurables, constraints ->
                val placeables = measurables.fastMap { it.measure(constraints) }
                val maxWidth = placeables.fastMaxBy { it.width }?.width ?: 0
                val maxHeight = placeables.fastMaxBy { it.height }?.height ?: 0

                if (fullWidth == 0 && maxWidth != 0) {
                    fullWidth = maxWidth
                }

                if (fullHeight == 0 && maxHeight != 0) {
                    fullHeight = maxHeight
                }

                layout(maxWidth, maxHeight) {
                    placeables.fastForEach { placeable ->
                        placeable.place(0, 0)
                    }
                }
            }

            Layout(
                    content = {
                        if (fullWidth > 0 && fullHeight > 0) {
                            key(topNewKey) {
                                topNewKey.RenderComposable(modifier)
                            )
                        }
                    },
                    measurePolicy = measurePolicy,
                    modifier = when {
                        !isAnimating -> modifier
                        else -> animationConfiguration.customComposableTransitions.newComposableTransition.animateNewComposable(modifier, stateChange, fullWidth, fullHeight, animationProgress)
                    }
            )

            Layout(
                    content = {
                        if (isAnimating) {
                            key(topPreviousKey) {
                                topPreviousKey.RenderComposable(modifier)
                            }
                        }
                    },
                    measurePolicy = measurePolicy,
                    modifier = when {
                        !isAnimating -> modifier
                        else -> animationConfiguration.customComposableTransitions.previousComposableTransition.animatePreviousComposable(modifier, stateChange, fullWidth, fullHeight, animationProgress)
                    }
            )

            LaunchedEffect(key1 = completionCallback, block = {
                lerping.animateTo(1.0f, animationConfiguration.animationSpec) {
                    animationProgress = this.value
                }
                isAnimating = false
                lerping.snapTo(0f)
                completionCallback!!.stateChangeComplete()
            })
        }
it's as if the composable was removed and re-added πŸ€” I'll get a vid
d
I just noticed that the
Animatable
in the snippet above has a
VisibilityThreshold
/ precision of 1.0f. That means it'll consider itself done when it's 1.0f away from the target. That's likely not what you want. I'd recommend using the factory method
Animatable(0f)
, or setting the
visibilityThreshold
to a much smaller number (
0.01f
perhaps).
z
it is now `
Copy code
val lerping = remember { Animatable(0.0f) }`
the flicker in the video is still there though
i'm not sure what causes this one πŸ˜„ at least the incorrectly rendered new view is gone haha
maybe the keys should be `rememberUpdatedState`'d
nope, no effect 😩
maybe it is technically because the new key becomes the old key, and there is a new key πŸ€”
so it makes sense for it to be "swapped out", but i figured
key()
lets me wrap that via structural equality
oh the dog list key is not data class, that could easily cause problems πŸ‘€
still flickers πŸ˜‚
d
One observation on the flicker: I'm seeing images popping in as they get loaded. Not sure if that's intended to be handled with a fade. So when it's a full page of images, and they pop in all at once it causes a flicker. There might be other flickers too.
This is what I see, a few frames apart, before vs. after the images are loaded
z
yeah it appears that the switch of the screen from being
newKey.RenderComposable()
to become
previousKey.RenderComposable()
makes it become a "new composable"
I'm not sure how to force them to be the same
a
The position of the call in the function body is part of the identity
For something to have the same identity it must be the same call, e.g.:
Copy code
val myKey = when { ... }
myKey.RenderComposable() // same call identity, even when myKey changes
πŸ‘ 1
z
oh i get it. I'll figure something out then thanks
z
okay i am now ensuring that the given key is rendered in the same position within the same layout and it still flickers πŸ˜‚
Copy code
val stateChange = stateChange ?: return
            val callback = callback ?: return

            var completionCallback by remember { mutableStateOf<StateChanger.Callback?>(null) }

            val topNewKey by rememberUpdatedState(newValue = stateChange.topNewKey<DefaultComposeKey>())
            val topPreviousKey by rememberUpdatedState(newValue = stateChange.topPreviousKey<DefaultComposeKey>())

            var isAnimating by remember { mutableStateOf(false) }

            val lerping = remember { Animatable(0.0f) }

            var animationProgress by remember { mutableStateOf(0.0f) }

            var initialization by remember { mutableStateOf(true) }

            if (completionCallback !== callback) {
                completionCallback = callback

                if (topPreviousKey != null) {
                    initialization = false

                    animationProgress = 0.0f
                    isAnimating = true
                } else {
                    initialization = true
                }
            }

            var fullWidth by remember { mutableStateOf(0) }
            var fullHeight by remember { mutableStateOf(0) }

            val measurePolicy = MeasurePolicy { measurables, constraints ->
                val placeables = measurables.fastMap { it.measure(constraints) }
                val maxWidth = placeables.fastMaxBy { it.width }?.width ?: 0
                val maxHeight = placeables.fastMaxBy { it.height }?.height ?: 0

                if (fullWidth == 0 && maxWidth != 0) {
                    fullWidth = maxWidth
                }

                if (fullHeight == 0 && maxHeight != 0) {
                    fullHeight = maxHeight
                }

                layout(maxWidth, maxHeight) {
                    placeables.fastForEach { placeable ->
                        placeable.place(0, 0)
                    }
                }
            }

            val keySlot1 by rememberUpdatedState(when {
                initialization -> topNewKey
                else -> topPreviousKey
            })

            val keySlot2 by rememberUpdatedState(when {
                initialization -> topPreviousKey
                else -> topNewKey
            })

            Layout(
                    content = {
                        if (initialization || isAnimating) {
                            keySlot1?.RenderComposable(modifier)
                        }
                    },
                    measurePolicy = measurePolicy,
                    modifier = when {
                        !isAnimating || initialization -> modifier
                        else -> animationConfiguration.customComposableTransitions.previousComposableTransition.animateComposable(
                            modifier,
                            stateChange,
                            fullWidth,
                            fullHeight,
                            animationProgress,
                        )
                    }
            )

            Layout(
                    content = {
                        if (!initialization || isAnimating) {
                            keySlot2?.RenderComposable(modifier)
                        }
                    },
                    measurePolicy = measurePolicy,
                    modifier = when {
                        !isAnimating || initialization -> modifier
                        else -> animationConfiguration.customComposableTransitions.newComposableTransition.animateComposable(
                            modifier,
                            stateChange,
                            fullWidth,
                            fullHeight,
                            animationProgress,
                        )
                    }
            )

            LaunchedEffect(key1 = completionCallback, block = {
                if (topPreviousKey != null) {
                    lerping.animateTo(1.0f, animationConfiguration.animationSpec) {
                        animationProgress = this.value
                    }
                    isAnimating = false
                    lerping.snapTo(0f)
                }
                completionCallback!!.stateChangeComplete()
            })
maybe the problem is that i have 2 layout tags instead of putting them in a list with
key {
to render them each with a modifier πŸ˜”
yes black 1
hmm
i thought Crossfade was less applicable than AnimatedVisibilityImpl but maybe you're onto something, I'll try to figure out how to merge my current behavior into this thing
d
Crossfade is definitely more applicable to your use case than AnimatedVisibilityImpl. πŸ™‚
As Adam said, the position of the call is a part of the composable identify. You need to make sure the same composable is invoked in the same position (in your code) to not be treated as a new one.
or at least i think i am doing that now πŸ€”
the very first transition actually works
maybe i should log if they still get swapped or something after it's !initialization
agh i really am switching them up still
rip
i'm going to need a whiteboard for this
i have to redesign this entirely πŸ˜‚
thank you everyone, thanks adam + zach + doris ❀️
πŸ‘ 4
s
@zhuinden Did you manage to solve this issue I have a similar issue with Image Flicking
z
yes i solved it with correct order of the saved state provider + key
and the for loop