I have been trying implementing the collapsible toolbar in my app. I have the following composables
ModalNavigationDrawer(
drawerContent = {
ModalDrawerSheet(drawerContainerColor = Color.White) {
// my menu items
}
}, drawerState = drawerState, content = { }, modifier = Modifier
.background(
colorResource(id = R.color.white)
)
){
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.background(colorResource(id = R.color.white))
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
HeaderTile("Good Morning", coroutineScope, drawerState);
OptionsTile(
modifier = Modifier.fillMaxWidth(),
)
}
}
@Composable
fun HeaderTile(
greetMessage: String,
coroutineScope: CoroutineScope,
drawerState: DrawerState
) {
val transparentRed = Color.Red.copy(alpha = 0.8f) // 50% transparent red
val transparentBlue = Color.Black.copy(alpha = 0.8f)
val brush = Brush.verticalGradient(listOf(transparentRed, transparentBlue))
Box(
modifier = Modifier
.height(240.dp)
.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = R.drawable.header_image),
contentDescription = null,
contentScale = ContentScale.Crop, // Adjust as needed
modifier = Modifier.fillMaxSize()
)
Box(
modifier = Modifier
.fillMaxSize()
.background(
brush = brush
) // Bottom-right corner
)
Box(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.TopStart)
.padding(top = 5.dp)
.wrapContentHeight(),
contentAlignment = Alignment.TopStart
) {
androidx.compose.material3.IconButton(onClick = { coroutineScope.launch { drawerState.open() } },
content = {
androidx.compose.material3.Icon(
imageVector = Icons.Default.Menu, contentDescription = null,
Modifier.size(30.dp),
tint = colorResource(id = R.color.white)
)
})
Column(
modifier = Modifier
.fillMaxWidth(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
androidx.compose.material3.Text(
text = "#Hello Everyone",
color = White,
style = androidx.compose.material3.MaterialTheme.typography.headlineSmall,
fontStyle = FontStyle.Italic,
)
}
}
}
}
@Composable
fun OptionsTile(
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.offset(y = -(50).dp)
.padding(start = 15.dp, end = 15.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
for (i in 1..3) {
Card(
colors = CardDefaults.cardColors(
containerColor = White
),
modifier = Modifier
.height(height = 100.dp)
.weight(1f),
elevation = CardDefaults.cardElevation(defaultElevation = 5.dp),
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
}
}
}
}
}
This will give me layout like this
I want when scrolling up, red image header should collapse upto to three menu options like this
Any Suggestions !
You can use a NestedScrollConnection
to achieve this. Override the onPreScroll
function, and when the header is not fully expanded / collapsed, consume the scroll event to increase / decrease the height
of the header.
Output:
Code:
@Composable
fun MainComposable() {
val coroutineScope = rememberCoroutineScope()
val density = LocalDensity.current
var minHeaderHeightPx by remember { mutableFloatStateOf(100.dp.dpToPx(density)) }
var maxHeaderHeightPx by remember { mutableFloatStateOf(250.dp.dpToPx(density)) }
var currentHeaderHeightPx by remember { mutableFloatStateOf(250.dp.dpToPx(density)) }
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.y
val newHeaderHeightPx = currentHeaderHeightPx + delta
currentHeaderHeightPx = newHeaderHeightPx.coerceIn(minHeaderHeightPx, maxHeaderHeightPx)
val unconsumedPx = newHeaderHeightPx - currentHeaderHeightPx
return Offset(x = 0f, y = delta - unconsumedPx)
}
}
}
Column(
modifier = Modifier
.background(colorResource(id = R.color.white))
.fillMaxSize()
.nestedScroll(nestedScrollConnection),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
HeaderTile(
modifier = Modifier
.height(currentHeaderHeightPx.toInt().pxToDp(density)),
greetMessage = "Good Morning",
expansionProgress = currentHeaderHeightPx / maxHeaderHeightPx,
coroutineScope = coroutineScope
);
Spacer(modifier = Modifier.height(56.dp))
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
items(50) { item ->
Text("Item $item", modifier = Modifier.padding(16.dp))
}
}
}
}
@Composable
fun HeaderTile(
modifier: Modifier = Modifier,
greetMessage: String,
expansionProgress: Float,
coroutineScope: CoroutineScope,
) {
val transparentRed = Color.Red.copy(alpha = 0.8f) // 50% transparent red
val transparentBlue = Color.Black.copy(alpha = 0.8f)
val brush = Brush.verticalGradient(listOf(transparentRed, transparentBlue))
Box(
modifier = modifier
.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Image(
imageVector = Icons.Filled.LocalPolice,
contentDescription = null,
contentScale = ContentScale.Crop, // Adjust as needed
modifier = Modifier.fillMaxSize()
)
Box(
modifier = Modifier
.fillMaxSize()
.background(
brush = brush
) // Bottom-right corner
)
Box(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.TopStart)
.padding(top = 5.dp)
.wrapContentHeight(),
contentAlignment = Alignment.TopStart
) {
IconButton(onClick = {},
content = {
Icon(
imageVector = Icons.Default.Menu, contentDescription = null,
Modifier.size(30.dp),
tint = colorResource(id = R.color.white)
)
}
)
}
Column(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.Center)
.offset(x = 0.dp, y = -(32 * (1 - expansionProgress)).dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
modifier = Modifier.padding(8.dp),
text = greetMessage,
color = White,
style = MaterialTheme.typography.headlineSmall,
fontStyle = FontStyle.Italic,
)
}
OptionsTile(
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
@Composable
fun OptionsTile(
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.offset(y = 50.dp)
.padding(start = 15.dp, end = 15.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
for (i in 1..3) {
Card(
colors = CardDefaults.cardColors(
containerColor = White,
),
modifier = Modifier
.height(height = 100.dp)
.weight(1f),
elevation = CardDefaults.cardElevation(defaultElevation = 5.dp),
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
}
}
}
}
}
fun Dp.dpToPx(density: Density) = with(density) { this@dpToPx.toPx() }
fun Int.pxToDp(density: Density) = with(density) { this@pxToDp.toDp() }
Note:
I removed some redundant code related to the ModalNavigationDrawer
.