I have this scenario: Upon screen transition I wan...
# compose
d
I have this scenario: Upon screen transition I want to animate an arbitrary amount of composables. I don’t want the other screen to show until all the composables have finished their exit animations. How would I go about knowing when the animations all complete? I am using
AnimatedVisibilty
for pretty much everything. But that composable does not have a callback, perhaps I could use a
LaunchedEffect
or
DisposableEffect
for that piece? I am not sure if this is an architectural question or a compose question. It’s kind of a cross cutting thing.
d
At the moment, the only way to get the finished signal from
AnimatedVisibility
is to put a
DisposableEffect
in its
content
composable and rely on the
onDisposed
callback. You could consider doing that. Alternatively, if the enter/exit transition that you have for
AnimatedVisibility
can be easily expressed via an animation as a part of the
Transition
, (i.e. fading, sliding that doesn't depend on the size, etc) you can rewrite the animations and wait for the
Transition
to finish all its animations. Currently I'm working on an idea that allows
AnimatedVisibility
to be created as a part of the
Transition
:
Transition<T>.AnimatedVisibility(visible: (T) -> Boolean, enter:.., exit:...)
. Seems like it would help accomplish what you are trying to do. 🙂 Sounds like you are building a sequential exit and then enter transition?
d
@Doris Liu Exactly! I was trying to choreograph some animations across multiple and arbitrary Composables. I ended up not using
AnimatedVisiblity
and restructured the code to use the
animateXAsState()
functions. My main use case was screen transition choreography.
d
Do you have a mock of the end results you are going for? I would love to take a look to make sure the APIs we are building now would make that use case straightforward to implement. 🙂
d
I used almost the exact same code on each composable I could post that if you like? I will draw up a mock file with the basic pattern I am using to get the job done. I am sure we could figure out how to make it a bit more elegant or robust.
d
A mock file would be wonderful.🙏 I'm particularly curious about the choreography of the different animations. ☺️
d
@Doris Liu Here is a rough mockup of what I am doing as of right now. The actual code I have has more encapsualation then this but I think this demonstrates the ideas well.
If you paste that into a working Compose project it should run all by itself.
@Doris Liu Here is a video of the app I am working on.
Please let me know what you think.
I have some ideas floating around for making things more generic.
d
Thanks for sharing the code. Love the staggered enter in the video. 😄 Have you tried the
updateTransition(transitionState: MutableTransitionState)
API? (API reference can be found here). With the
MutableTransitionState
, you could skip the
onShown
and
onHidden
callbacks, as the animation will be updating that state for you. If you have more than one animation for each screen, I'd recommend trying out the Transition API. It should make things easier to manage if you had one transition for each screen, and adding all the enter/exit animations in that screen to the same transition.
d
Thanks @Doris Liu This is sweet!! The docs say this:
Copy code
// currentState will be updated to targetState when the transition is finished, so
// it can be used as a signal to start the next transition.
How do I do this? Do I used a
LaunchedEffect
keyed on the current state?
Copy code
LaunchedEffect(
        key1 = state.currentState
    ) {
        onTransitionCompleted(state.currentState)
    }
Like this?
d
If you were to start another transition in a composable function, you could simply do
if(transitionState.currentState == transitionState.targetState && transitionState.currentState ==...) { // start another transition }
currentState
itself is backed by a
MutableState
, so if you read it in composition, you'll get a recomposition when it changes.
Alternatively, if you only care about when
currentState
changes, you could create a
snapShotFlow
, and collect from it. 🙂
d
I think I got it!
snapShotFlow
!!!! mind blown
🤣 1
d
The snippet above works, except you don't really need pass the
onTransitionCompleted
to the card, since you can observe that
TransitionState
wherever you have a reference to it.
d
How would I do that from a ViewModel? Or anywhere outside a Composable? I am not sure the pieces are connecting for me here.
Copy code
@Composable
internal fun WindowFocusObserver(onWindowFocusChanged: (isWindowFocused: Boolean) -> Unit) {
    val windowInfo = LocalWindowInfo.current
    val callback = rememberUpdatedState(onWindowFocusChanged)
    LaunchedEffect(windowInfo) {
        snapshotFlow { windowInfo.isWindowFocused }.collect { callback.value(it) }
    }
}
WindowInfo does it like this.
d
Yes, that's same as what I'd recommend for observing the value change via
snapshotFlow
. You can consider creating the
TransitionState
in the ViewModel, and derive other states from it
d
except you don’t really need pass the 
onTransitionCompleted
 to the card
Ok but I still need to pass some kind of callback into a Composable somewhere in order to get it called correct?
You are just implying that it could be some higher up Composable?
d
Ok but I still need to pass some kind of callback into a Composable somewhere in order to get it called correct?
That's an imperative way of thinking. 😛 I'd declare the
text
as a
derivedStateOf
the
TransitionState
You are just implying that it could be some higher up Composable?
That's indeed what I was implying. 🙂 Though I often find the onFinished callback not necessary in Compose, especially if all it does is trigger another state change.
d
https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary#snapshotflow Ok so this example shows how to react to the state from outside a Composable.
Copy code
data class ButtonUIState(
    val text: State<String> = mutableStateOf(""),
    val onClick: () -> Unit = {},
)

private val buttonState = ButtonUIState(
        text = derivedStateOf { if (topBarState.isVisible.value) state2Text else state1Text },
        onClick = ::transitionStates,
    )
Copy code
val state  = remember {
        MutableTransitionState(false)
    }

    val text by remember {
        derivedStateOf {  if (state.currentState) "Lowered" else "Raised" }
    }

    val isEnabled by remember {
        derivedStateOf { state.currentState == state.targetState }
    }

    Column(modifier = Modifier.fillMaxSize(),verticalArrangement = Arrangement.SpaceEvenly, horizontalAlignment = Alignment.CenterHorizontally) {
        PoppingInCard(
            state = state
        )

        Button(
            onClick = { state.targetState = !state.currentState},
            enabled = isEnabled
        ) {
            Text(text = "Transition to $text")
        }
    }
Thanks to your help I think I am finally cooking with gas (maybe even jetpack fuel!) This way the state can even be further hoisted into the model!
d
Yup. Also it'll be a lot easier to manage multiple enter/exit animations, if they are a part of the same transition, as opposed to checking their own animation state independently. 🙂