MotionLayout AppBar in Jetpack Compose

MotionLayoutAppBar
dependencies {
implementation("androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha02")
}

Concepts

1. Start and Final states

Initial State of the AppBar
Final State of the AppBar

2. MotionLayout in Compose — Start, End, Progress

inline fun MotionLayout(
start: ConstraintSet,
end: ConstraintSet,
progress: Float,
crossinline content: @Composable MotionLayoutScope.() -> Unit
) {
// Code
}

Implementation

1. Break down of the design

Break up of the AppBar elements
  1. Back Button
  2. Title
  3. Subtitle
  4. Box for Background
private enum class MotionLayoutAppBarItem {
BACK_BUTTON,
TITLE,
SUBTITLE,
BACKGROUND_BOX;
}

2. Our MotionLayoutAppBar

@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

@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

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

Writing Previews

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

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

--

--

--

Just trying my hand at writing. I am a guitarist, computer geek and a biker. I love trying new things and being in nature.

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Advance Android Rat Written in Java And Php With More Features:

Now in Android #31

Dynamic Configurations for Your Flutter App

Speed Boost Power Up

Flutter — Effectively scale UI according to different screen sizes

Now in Android #19

Manual ViewModel dependency injection via navigation component

Navigation Component: Dialog Destinations

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Gideon Paul

Gideon Paul

Just trying my hand at writing. I am a guitarist, computer geek and a biker. I love trying new things and being in nature.

More from Medium

Android — Spring & fling animation in Jetpack compose

Lab Notes: An Introduction to Android Jetpack Compose

Lazy layouts in Compose recap(GoogleIO 2022)

Android Kotlin Retrofit With Coroutine