<@UNH9ZT3NZ> I have a question about shared elemen...
# compose
m
@Doris Liu I have a question about shared element transition with compose navigation. I'm using the
SharedToolBarDemo
sample here. Let's say I want my top app bar to contain a
LinearProgressIndicator
that animates from progress 0f to 1f upon navigation. Would that be possible? Typically, I would use
animateFloatAsState
, but my top bar composable would recompose immediately and the progress animation just jumps to 1f.
Something like this...
Copy code
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
private fun TopAppBarWithStuff(
    text: String,
    progress: Float,
    sharedTransitionScope: SharedTransitionScope,
    animatedVisibilityScope: AnimatedVisibilityScope
) {
    with(sharedTransitionScope) {
        val animatedProgress by animateFloatAsState(targetValue = progress, label = "progress")

        Column(
            modifier = Modifier.sharedElement(
                rememberSharedContentState(key = "appBar"),
                animatedVisibilityScope,
            ),
        ) {
            TopAppBar(title = { Text(text) })
            LinearProgressIndicator(
                progress = { animatedProgress }
            )
        }
    }
}
Copy code
val navController = rememberNavController()
    SharedTransitionLayout {
        NavHost(navController, startDestination = "first") {
            composable("first",
                enterTransition = { slideInHorizontally(initialOffsetX = { -it }) },
                exitTransition = { slideOutHorizontally(targetOffsetX = { it }) }
            ) {
                Column {
                    TopAppBarWithStuff(
                        text = "Text",
                        progress = 0f,
                        sharedTransitionScope = this@SharedTransitionLayout,
                        animatedVisibilityScope = this@composable
                    )
                    // content
                }
            }
            composable("second",
                enterTransition = { slideInHorizontally(initialOffsetX = { -it }) },
                exitTransition = { slideOutHorizontally(targetOffsetX = { it }) }
            ) {
                Column {
                    TopAppBarWithStuff(
                        progress = 1f,
                        text = "Cat",
                        sharedTransitionScope = this@SharedTransitionLayout,
                        animatedVisibilityScope = this@composable,
                    )
                    // content
                }
            }
        ....
s
There's no animation happening here since you replace one composable with the other composable completely. You can try perhaps having that shared UI in a movable content, so giving it a new state will in fact do the transition from 0f to 1f and vice versa, instead of just replacing the entire composable with a new one.
m
Thanks for the input!
movableContentOf
does sound helpful... I tried this, but I see the same behavior. The progress bar doesn't animate.
Copy code
SharedTransitionLayout {
        val topBarComposable: @Composable (String, Float, AnimatedVisibilityScope) -> Unit =
            { text, progress, animatedVisibilityScope ->
                TopAppBarWithStuff(
                    text = text,
                    progress = progress,
                    sharedTransitionScope = this@SharedTransitionLayout,
                    animatedVisibilityScope = animatedVisibilityScope
                )
            }
        
        val toolbar = remember(topBarComposable) {
            movableContentOf(topBarComposable)
        }

        NavHost(navController, startDestination = "first") {
            composable("first",
                enterTransition = { slideInHorizontally(initialOffsetX = { -it }) },
                exitTransition = { slideOutHorizontally(targetOffsetX = { it }) }
            ) {
                Column {
                    toolbar("Text", 0.33f, this@composable)
                    ...
                }
            }
            composable("second",
                enterTransition = { slideInHorizontally(initialOffsetX = { -it }) },
                exitTransition = { slideOutHorizontally(targetOffsetX = { it }) }
            ) {
                Column {
                    toolbar("Text 2", 0.66f, this@composable)
I'm basically trying to set up a pattern similar to what's described here. The ripple effect animation doesn't finish running all the way when tapping on the bottom bar item
d
MoveableContent only "moves" when it's invoked exactly once in a different location. In the example above, the MovableContent is composed twice, once in the start destination, and again in the new destination in the same frame. In that case, the
MoveableContent
is treated as two separate instances that do not share states. You'll need to make sure it is only invoked once by doing something like:
Copy code
composable("name", enter = ...) {
    if (transition.targetState == EnterExitState.Visible) { // invoke the MovableContent here }
}
Note that the
sharedElement
modifier can't be a part of the movable content, because we expect the
sharedElement
modifier to be present in both incoming and outgoing content to find a match. Alternatively, and more simply, you could consider creating an
Animatable
outside the NavHost. The two destinations could then share the animation state, and only the incoming content gets to set the animation target:
Copy code
val anim = remember { Animatable(0f) }
NavHost {
   composable("name", enter = ...) {
       if (transition.targetState == EnterExitState.Visible) { LaunchedEffect(Unit) { anim.animateTo(progress) } }
  }
...
}