androidkotlinandroid-jetpack-composeandroid-appbarlayout

Collapsible header box/column jetpack compose


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

enter image description here

I want when scrolling up, red image header should collapse upto to three menu options like this

enter image description here

Any Suggestions !


Solution

  • 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:

    Screen Recording

    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.