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) } }
}
...
}