Miguel Kano
05/26/2024, 10:37 PMSharedToolBarDemo
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.Miguel Kano
05/26/2024, 10:38 PM@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 }
)
}
}
}
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
}
}
....
Stylianos Gakis
05/26/2024, 10:44 PMMiguel Kano
05/27/2024, 3:26 AMmovableContentOf
does sound helpful...
I tried this, but I see the same behavior. The progress bar doesn't animate.
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)
Miguel Kano
05/27/2024, 3:30 AMDoris Liu
05/28/2024, 10:14 PMMoveableContent
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:
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:
val anim = remember { Animatable(0f) }
NavHost {
composable("name", enter = ...) {
if (transition.targetState == EnterExitState.Visible) { LaunchedEffect(Unit) { anim.animateTo(progress) } }
}
...
}