MotionLayout AppBar in Jetpack Compose

Note: MotionLayout is a subclass of ConstraintLayout, so it is important to first understand how ConstraintLayout works. If you aren’t familiar with ConstraintLayout, please learn it first and then comeback to read the rest of this article.
The full code for this tutorial is available at https://github.com/gideondev/MotionLayoutAppBarExample
The premise of MotionLayout is quite simple, we give the MotionLayout start and end positions of all child elements, and it will figure out how to animate from start to end positions.
To use MotionLayout in code, first, we have to add the required dependencies. Since MotionLayout comes as a part of the ConstraintLayout library, we will add the ConstraintLayout library as a dependency.
dependencies {
implementation("androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha02")
}
Concepts
1. Start and Final states
Before we can proceed any further, let’s take a good look at the initial and final states of the AppBar.


MotionLayout automatically animates the elements to go from what’s represented in the start state to what’s represented in the end state.
2. MotionLayout in Compose — Start, End, Progress
Here’s a simplified function definition of MotionLayout composable (Yes, all layouts in Jetpack Compose are composables).
inline fun MotionLayout(
start: ConstraintSet,
end: ConstraintSet,
progress: Float,
crossinline content: @Composable MotionLayoutScope.() -> Unit
) {
// Code}
As we can see from the function definition, we need to provide the start and end constraint sets. What is progress you ask? Well, it is a float value (between 0.0 and 1.0) representing a particular time in the total timeline of animation.
0.0f represents the start state, and 1.0f represents the end state. Likewise, 0.5f represents the time when animation is exactly halfway between start and end states.
Implementation
1. Break down of the design
To code the design, let’s identify each of the elements in the AppBar:

- Back Button
- Title
- Subtitle
- Box for Background
To avoid typos and human errors, we will use an enum class to identify each of these elements:
private enum class MotionLayoutAppBarItem {
BACK_BUTTON,
TITLE,
SUBTITLE,
BACKGROUND_BOX;
}
Hurray! So far so good.
2. Our MotionLayoutAppBar
Let’s call out component MotionLayoutAppBar. To maximize the customization, we will take as many parameters as possible in the top-level composable. Then we will pass these parameters on to the inner composable functions.
@Composable
fun MotionLayoutAppBar(
title: String,
subTitle: String,
modifier: Modifier = Modifier,
onBackPressed: () -> Unit = {},
elevation: Dp = 4.dp,
backgroundColor: Color = MaterialTheme.colors.primary,
contentColor: Color = MaterialTheme.colors.onPrimary,
progress: Float = 0.0f
) {
// Elements of the AppBar
}
3. Elements of AppBar
The direct child element of our MotionLayoutAppBar will be MotionLayout. Inside that MotionLayout, we put all of our elements and give them ids. To give a specific composable an id, we simply use layoutId(id: Any) method on its Modifier.
@OptIn(ExperimentalMotionApi::class)
@Composable
fun MotionLayoutAppBar(
title: String,
subTitle: String,
modifier: Modifier = Modifier,
onBackPressed: () -> Unit = {},
elevation: Dp = 4.dp,
backgroundColor: Color = MaterialTheme.colors.primary,
contentColor: Color = MaterialTheme.colors.onPrimary,
progress: Float = 0.0f
) {
MotionLayout(
modifier = modifier.fillMaxWidth(),
start = startConstraintSet(), // Not yet available
end = endConstraintSet(), // Not yet available
progress = progress
) {
Surface(
modifier = Modifier.layoutId(MotionLayoutAppBarItem.BACKGROUND_BOX),
elevation = elevation,
color = backgroundColor,
content = {}
)
IconButton(
modifier = Modifier.layoutId(MotionLayoutAppBarItem.BACK_BUTTON),
onClick = {
onBackPressed()
}
) {
Icon(
Icons.Default.ArrowBack,
"Back Button",
tint = contentColor
)
}
Text(
modifier = Modifier.layoutId(MotionLayoutAppBarItem.TITLE),
text = title,
style = MaterialTheme.typography.h6,
color = contentColor
)
Text(
modifier = Modifier.layoutId(MotionLayoutAppBarItem.SUBTITLE),
text = subTitle,
style = MaterialTheme.typography.subtitle1,
color = contentColor
)
}
}
4. Constraint sets
We define the start and end constraint sets that will be applied to elements of MotionLayoutAppBar.
a. Start state constraints:
private fun startConstraintSet() = ConstraintSet {
val backButton = createRefFor(MotionLayoutAppBarItem.BACK_BUTTON)
val title = createRefFor(MotionLayoutAppBarItem.TITLE)
val subtitle = createRefFor(MotionLayoutAppBarItem.SUBTITLE)
val backgroundBox = createRefFor(MotionLayoutAppBarItem.BACKGROUND_BOX)
constrain(backButton) {
top.linkTo(parent.top, 16.dp)
start.linkTo(parent.start, 16.dp)
bottom.linkTo(parent.bottom, 16.dp)
}
constrain(title) {
top.linkTo(parent.top, 16.dp)
start.linkTo(backButton.end, 16.dp)
}
constrain(subtitle) {
top.linkTo(title.bottom, 4.dp)
start.linkTo(title.start)
bottom.linkTo(parent.bottom, 16.dp)
}
constrain(backgroundBox) {
width = Dimension.matchParent
height = Dimension.fillToConstraints
top.linkTo(parent.top)
start.linkTo(parent.start)
end.linkTo(parent.end)
bottom.linkTo(parent.bottom)
}
}
b. End state constraints:
private fun endConstraintSet() = ConstraintSet {
val backButton = createRefFor(MotionLayoutAppBarItem.BACK_BUTTON)
val title = createRefFor(MotionLayoutAppBarItem.TITLE)
val subtitle = createRefFor(MotionLayoutAppBarItem.SUBTITLE)
val backgroundBox = createRefFor(MotionLayoutAppBarItem.BACKGROUND_BOX)
constrain(backButton) {
top.linkTo(parent.top, 16.dp)
start.linkTo(parent.start, 16.dp)
}
constrain(title) {
top.linkTo(backButton.bottom, 16.dp)
start.linkTo(backButton.start, 16.dp)
}
constrain(subtitle) {
top.linkTo(title.bottom, 8.dp)
start.linkTo(title.start)
bottom.linkTo(parent.bottom, 16.dp)
}
constrain(backgroundBox) {
width = Dimension.matchParent
height = Dimension.fillToConstraints
top.linkTo(parent.top)
start.linkTo(parent.start)
end.linkTo(parent.end)
bottom.linkTo(parent.bottom)
}
}
Umm.. Pretty much that’s enough to make our MotionLayoutAppBar work. whenever progress value is update, the elements will be arranged according to that.
Writing Previews
It is common to write previews to composables. Let’s write a bunch of them for our MotionLayoutAppBar. I already added the output images above, so not including them here again.
1. For the initial state:
@Preview
@Composable
fun InitialStatePreview() {
MotionLayoutAppBar(
title = "Title",
subTitle = "Subtitle",
backgroundColor = Color(0xFF214561),
progress = 0.0f
)
}
2. For the final state:
@Preview
@Composable
fun FinalStatePreview() {
MotionLayoutAppBar(
title = "Title",
subTitle = "Subtitle",
backgroundColor = Color(0xFF214561),
progress = 1.0f
)
}
3. Finally, my favorite. An infinite animation preview:
@Preview
@Composable
fun PreviewMotionLayoutAppBar() {
val motionLayoutProgress = remember { Animatable(0.0f) }
LaunchedEffect(Unit) {
motionLayoutProgress.animateTo(
1.0f,
animationSpec = infiniteRepeatable(
animation = tween(
delayMillis = 1000,
durationMillis = 1000,
easing = LinearEasing
)
)
)
}
MotionLayoutAppBar(
title = "Title",
subTitle = "Subtitle",
backgroundColor = Color(0xFF214561),
progress = motionLayoutProgress.value
)
}
Conclusion
Phew! We covered a lot of ground. So what to do with this component now?
We can use it as AppBar in our screens we will expand or collapse the AppBar based on the user scroll position. The below example animates our MotionLayoutAppBar every time user crosses the 150px scroll position.
@Preview
@Composable
fun MotionLayoutAppBarDemo() {
// As soon as this amount of scroll is reached, AppBar should expand.
val threshold = 150f
val scrollState = rememberScrollState() // Smoothly animate the MotionLayoutAppBar progress.
val progress by animateFloatAsState(
targetValue = if (scrollState.value > threshold) 1f else 0f,
tween(500, easing = LinearOutSlowInEasing)
)
Column(
modifier = Modifier.verticalScroll(scrollState)
) {
// Dummy screen content
val sectionColors = listOf(
Color(0XFFBBF6F3),
Color(0XFFF6F3B5),
Color(0XFFFFDCBE),
Color(0XFFFDB9C9)
)
sectionColors.forEach { backgroundColor ->
Box(
modifier = Modifier
.fillMaxWidth()
.height(400.dp)
.background(backgroundColor)
)
}
}
MotionLayoutAppBar(
title = "Title",
subTitle = "Subtitle",
progress = progress
)
}
Cheers!