Blog Infos
Author
Published
Topics
Published
Topics

Container transform is an animation pattern that transforms one container into another, typically using a shared element to connect two UI elements.

Container transform examples from Material Components for Android

This article is focused on the implementation of an animation that transforms a FAB into a full-screen page.

Shared element transitions are not yet available in Jetpack Compose; they are either in Backlog or In Focus, so we will have to leverage existing animation APIs.

The final animation 🌶 :

the enter transition is a little abrupt, but only those who get to the end will know why¹
Coding

To animate between two composables Jetpack Compose provides a convenient API — AnimatedContent.

AnimatedContent is a container that automatically animates its content when Transition.targetState changes. Its content for different target states is defined in a mapping between a target state and a composable function.

To implement a simple transition from one composable to another we can use the following code snippet:

@Composable
private fun FabContent(
modifier: Modifier = Modifier,
) {
var containerState by remember { mutableStateOf(ContainerState.Fab) }
AnimatedContent(
modifier = modifier,
targetState = containerState,
label = "container transform",
) { state ->
when (state) {
ContainerState.Fab -> Fab(
modifier = Modifier
.padding(end = 16.dp, bottom = 16.dp),
onClick = { containerState = ContainerState.Fullscreen }
)
ContainerState.Fullscreen -> AddContentScreen(
onBack = { containerState = ContainerState.Fab }
)
}
}
}
Based on the state we either render a FAB or AddContentScreen composable

By default, the AnimatedContent uses delayed fadeIn and scaleIn animations along with SizeTransform to animate size changes of the content.

The code above gets us very close to the desired result:

Transition from the FAB to the full-screen page with AnimatedContent

however, there are a few issues:

  • enter/exit transitions don’t follow an “expanding” pattern;
  • the fab flies diagonally instead of staying in the same place;
  • background color is not changing seamlessly.
Polishing

To fix the first two problems we need to have a shared shape animation between UI components which includes cornerRadiuselevation and padding. And as you might have guessed, to improve the color animation we need to have a shared backgroundColor animation.

Compose has many built-in animation mechanisms to animate primitive values like animateDpAsState , animateFloatAsState , animateColorAsState and many others including generic animateValueAsState where you can animate almost everything by providing your own TwoWayConverter<T, V>.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

No results found.

Jobs

animate*AsState — fire-and-forget animation function for *. This Composable function is overloaded for different parameter types such as FloatColorOffset, etc. When the provided targetValue is changed, the animation will run automatically.

To animate cornerRadius change we can use animateDpAsState as follows:

val cornerRadius: Dp by animateDpAsState(
  targetValue = when (containerState) {
    ContainerState.Fab -> 22.dp
    ContainerState.Fullscreen -> 0.dp
  }
)

The same needs to be done for paddingelevation, and backgroundColor, but to synchronize all our animations we need to use the Transition API.

var containerState by remember { mutableStateOf(ContainerState.Fab) }
val transition = updateTransition(targetState = containerState)
val cornerRadius by transition.animateDp { ... }
val backgroundColor by transition.animateColor { ... } 

Transition manages all the child animations on a state level. When the targetState changes, Transition will automatically start or adjust course for all its child animations to animate to the new target values defined for each animation.

Incorporating the API from above, the updated animation code is as follows:

@Composable
private fun FabContainer(
modifier: Modifier = Modifier,
) {
var containerState by remember { mutableStateOf(ContainerState.Fab) }
val transition = updateTransition(targetState = containerState)
val backgroundColor by transition.animateColor { state ->
when (state) {
ContainerState.Fab -> Colors.fabContainerColor
ContainerState.Fullscreen -> Colors.surface
}
}
val cornerRadius by transition.animateDp { state ->
when (state) {
ContainerState.Fab -> 22.dp
ContainerState.Fullscreen -> 0.dp
}
}
val elevation by transition.animateDp { ... }
val padding by transition.animateDp { ... }
transition.AnimatedContent(
modifier = modifier
// padding, shadow, and backgroundColor are shared between composables
.padding(end = padding, bottom = padding)
.shadow(
elevation = elevation,
shape = RoundedCornerShape(cornerRadius)
)
.drawBehind { drawRect(backgroundColor) }
) { state ->
when (state) {
ContainerState.Fab -> Fab()
ContainerState.Fullscreen -> AddContentScreen()
}
}
}
All child animations are managed using the Transition API

Executing the provided code, we achieve an animation with the container transform style:

FAB is transformed into a full-screen page with a “container transform” style

Furthermore, each animation can be configured to your liking by providing a transitionSpec. Let’s say you want cornerRadius to speed up quickly and slow down gradually:

val cornerRadius by transition.animateDp(
  transitionSpec = {
    tween(
      durationMillis = 500,
      easing = FastOutSlowInEasing,
    )
  }
) { ... }

More details about different Easing options (with nice GIFs!) can be found here and how to customize animations in general here.

Conclusion

The shared container technique with animated properties allows us to achieve pretty elegant animations, and the customization of individual child animations ​​lets you realize even the strangest whims of your designers. One of the downsides of this approach is that it doesn’t work with navigation frameworks, because you have to have a single shared parent for both composables.

The complete example shown here is available in the repository below:

https://github.com/rmyhal/container-transform-compose/?source=post_page—–98e5e74a15c9——————————–

[1] for those who read to the end and want to know why the enter animation on the gif at the top is a little fast, first of all, hello 👋!

And yes, it’s not only because it’s a gif, when I add an animated padding value to the AnimatedContent, the Compose ignores the SizeTransform that I pass to the AnimatedContent’s transitionSpec and uses some standard enter transition, although when I go back (exit transition) everything works as specified in the transitionSpec. The issue with more details is here, maybe I will update the article when there are updates.

Thanks for reading! 💚 Some useful links:

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
This is the second article in an article series that will discuss the dependency…
READ MORE
blog
Let’s suppose that for some reason we are interested in doing some tests with…
READ MORE
Menu