I can't find any doc on the matter, is there something similar to a CollapsingToolbar in Compose?
All I found was a mention of it here, but nothing on how to set it up
Jetpack Compose implementation of Material Design 3 includes 4 types of Top App Bars (https://m3.material.io/components/top-app-bar/implementation):
CenterAlignedTopAppBarSmallTopAppBarMediumTopAppBarLargeTopAppBarhttps://developer.android.com/reference/kotlin/androidx/compose/material3/package-summary
They all have a scrollBehavior parameter, which can be used for collapsing the toolbar. There are 3 basic types of scroll behavior in the library:
TopAppBarDefaults.pinnedScrollBehaviorTopAppBarDefaults.enterAlwaysScrollBehaviorTopAppBarDefaults.exitUntilCollapsedScrollBehaviorhttps://developer.android.com/reference/kotlin/androidx/compose/material3/TopAppBarDefaults
Note: This API is annotated as experimental at the moment.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Test() {
val scrollBehavior = remember { TopAppBarDefaults.enterAlwaysScrollBehavior() }
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
MediumTopAppBar(
title = { Text(text = "Scroll Behavior Test") },
navigationIcon = {
IconButton(onClick = { /*TODO*/ }) {
Icon(imageVector = Icons.Default.Menu, contentDescription = "")
}
},
scrollBehavior = scrollBehavior
)
}
) {
LazyColumn(modifier = Modifier.fillMaxWidth()) {
items((1..50).toList()) { item ->
Text(modifier = Modifier.padding(8.dp), text = "Item $item")
}
}
}
}
Compose-collapsing-toolbar A simple implementation of CollapsingToolbarLayout for Jetpack Compose
You can follow the example in the docs to create a toolbar which expands/collapses on every scroll up/down.
To create a toolbar which expands only when the list is scrolled to the top, you can make a slight adaptation to the original example:
val toolbarHeight = 48.dp
val toolbarHeightPx = with(LocalDensity.current) { toolbarHeight.roundToPx().toFloat() }
var toolbarOffsetHeightPx by remember { mutableStateOf(0f) }
var totalScrollOffsetPx = remember { 0f }
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.y
totalScrollOffsetPx += delta
if (totalScrollOffsetPx in -toolbarHeightPx..0f) {
toolbarOffsetHeightPx = totalScrollOffsetPx
}
return Offset.Zero
}
}
}
By doing so, you have a flexibility which would enable you to create your own CollapsibleScaffold which could accept params like scrollBehaviour, appBarLayout and list composables etc.
That way, for instance, you could also programmatically calculate the height of the app bar and get rid of the high amount of boilerplate, making the code used in your screens neat and clean.
You can use the compose-collapsing-toolbar library.
Instalation : implementation "me.onebone:toolbar-compose:2.1.0"
Here are some gif images from the Readme.md of the library:

In the Jetpack Compose, I implemented collapsing toolbar by the following code:
@Composable
fun MainScreen(){
val scrollState = rememberScrollState()
// parallax effect by offset
val imageOffset = (-scrollState.value * 0.18f).dp
Box {
Image(
painter = painterResource(id = R.drawable.bg_header),
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier
.graphicsLayer { translationY = imageOffset }
.height(240.dp)
.fillMaxWidth()
)
Column(
Modifier
.verticalScroll(scrollState)
.padding(top = 200.dp)
.background(
MaterialTheme.colors.surface,
RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)
)
.fillMaxHeight()
.fillMaxWidth()
.padding(all = 16.dp)
) {
// Main content here
}
}
}
You can replace Column with LazyColumn
and val scrollState = rememberLazyListState()
I found this in Android docs, I think the documentation you linked in the question is talking about doing it like this with nested scrolling.
val toolbarHeight = 48.dp
val toolbarHeightPx = with(LocalDensity.current) { toolbarHeight.roundToPx().toFloat() }
val toolbarOffsetHeightPx = remember { mutableStateOf(0f) }
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.y
val newOffset = toolbarOffsetHeightPx.value + delta
toolbarOffsetHeightPx.value = newOffset.coerceIn(-toolbarHeightPx, 0f)
return Offset.Zero
}
}
}
Box(
Modifier
.fillMaxSize()
.nestedScroll(nestedScrollConnection)
) {
LazyColumn(contentPadding = PaddingValues(top = toolbarHeight)) {
items(100) { index ->
Text("I'm item $index", modifier = Modifier
.fillMaxWidth()
.padding(16.dp))
}
}
TopAppBar(
modifier = Modifier
.height(toolbarHeight)
.offset { IntOffset(x = 0, y = toolbarOffsetHeightPx.value.roundToInt()) },
title = { Text("toolbar offset is ${toolbarOffsetHeightPx.value}") }
)
}
I found a solution created by Samir Basnet (from Kotlin Slack Channel) which was useful for me, I hope it helps someone else...
@Composable
fun CollapsingEffectScreen() {
val items = (1..100).map { "Item $it" }
val lazyListState = rememberLazyListState()
var scrolledY = 0f
var previousOffset = 0
LazyColumn(
Modifier.fillMaxSize(),
lazyListState,
) {
item {
Image(
painter = painterResource(id = R.drawable.recife),
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier
.graphicsLayer {
scrolledY += lazyListState.firstVisibleItemScrollOffset - previousOffset
translationY = scrolledY * 0.5f
previousOffset = lazyListState.firstVisibleItemScrollOffset
}
.height(240.dp)
.fillMaxWidth()
)
}
items(items) {
Text(
text = it,
Modifier
.background(Color.White)
.fillMaxWidth()
.padding(8.dp)
)
}
}
}
Here is the result: