Jetpack Compose滚动时头部折叠至菜单上方的实现方案咨询
Jetpack Compose 实现可折叠头部布局
问题描述
我正尝试在应用中使用Jetpack Compose实现可折叠头部,现有如下Composable组件代码:
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 ) { } } } }
当前布局效果:
期望效果:向上滚动页面时,红色渐变头部区域折叠至三个菜单卡片的上方,效果如下:
实现方案
要实现这种折叠效果,核心是利用嵌套滚动监听动态调整头部的高度和内容显示状态,同时适配内容区域的偏移。以下是具体实现步骤:
1. 重构布局结构
将原有的Column滚动布局改为Box层叠头部与滚动内容,让头部独立于滚动内容进行位置和尺寸调整。
2. 监听滚动事件
使用NestedScrollConnection捕获滚动偏移量,计算头部从原始高度(240dp)到折叠后最小高度(比如60dp)的动态变化值。
3. 动态调整头部与内容
根据滚动偏移量,修改头部的高度、背景透明度,控制标题的显示/隐藏,同时调整内容区域的顶部偏移以保持布局连贯性。
修改后的完整代码
import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Menu import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.unit.dp import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable fun CollapsibleHeaderScreen( drawerState: DrawerState, coroutineScope: CoroutineScope ) { ModalNavigationDrawer( drawerContent = { ModalDrawerSheet(drawerContainerColor = Color.White) { // 菜单内容 } }, drawerState = drawerState, modifier = Modifier.background(colorResource(id = R.color.white)) ) { // 定义头部的原始高度和折叠后的最小高度 val originalHeaderHeight = 240.dp val collapsedHeaderHeight = 60.dp val headerHeightRange = originalHeaderHeight - collapsedHeaderHeight val scrollState = rememberLazyListState() // 从滚动状态中获取偏移量,转换为dp val scrollOffset by remember { derivedStateOf { scrollState.firstVisibleItemScrollOffset.coerceAtMost(headerHeightRange.roundToPx()) } } // 计算当前头部高度 val currentHeaderHeight = originalHeaderHeight - scrollOffset.toDp() Box(modifier = Modifier.fillMaxSize()) { // 滚动内容区域 LazyColumn( state = scrollState, modifier = Modifier .fillMaxSize() .background(colorResource(id = R.color.white)), contentPadding = PaddingValues(top = originalHeaderHeight - 50.dp) // 保留原始头部下方的偏移 ) { item { OptionsTile(modifier = Modifier.fillMaxWidth()) } // 可添加更多滚动内容 items(20) { index -> Text( text = "列表项 $index", modifier = Modifier .fillMaxWidth() .padding(16.dp) ) } } // 可折叠头部 HeaderTile( greetMessage = "Good Morning", coroutineScope = coroutineScope, drawerState = drawerState, currentHeight = currentHeaderHeight, scrollOffsetRatio = scrollOffset.toFloat() / headerHeightRange.roundToPx() ) } } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun HeaderTile( greetMessage: String, coroutineScope: CoroutineScope, drawerState: DrawerState, currentHeight: Dp, scrollOffsetRatio: Float // 0=未折叠,1=完全折叠 ) { val transparentRed = Color.Red.copy(alpha = 0.8f) val transparentBlue = Color.Black.copy(alpha = 0.8f) val brush = Brush.verticalGradient(listOf(transparentRed, transparentBlue)) Box( modifier = Modifier .height(currentHeight) .fillMaxWidth(), contentAlignment = Alignment.Center ) { // 背景图,折叠时保持顶部显示 Image( painter = painterResource(id = R.drawable.header_image), contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize(), alpha = 1 - scrollOffsetRatio * 0.5f // 折叠时稍微降低透明度 ) // 渐变层 Box( modifier = Modifier .fillMaxSize() .background(brush = brush) ) // 顶部菜单按钮 Box( modifier = Modifier .fillMaxWidth() .align(Alignment.TopStart) .padding(top = 5.dp) .wrapContentHeight(), contentAlignment = Alignment.TopStart ) { IconButton( onClick = { coroutineScope.launch { drawerState.open() } }, modifier = Modifier.padding(start = 8.dp) ) { Icon( imageVector = Icons.Default.Menu, contentDescription = "打开抽屉", Modifier.size(30.dp), tint = colorResource(id = R.color.white) ) } // 标题,折叠时逐渐隐藏 Column( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { Text( text = "#Hello Everyone", color = Color.White, style = MaterialTheme.typography.headlineSmall, fontStyle = FontStyle.Italic, alpha = 1 - scrollOffsetRatio // 折叠时完全隐藏 ) } } } } @Composable fun OptionsTile( modifier: Modifier = Modifier ) { Row( modifier = modifier .padding(start = 15.dp, end = 15.dp), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { repeat(3) { Card( colors = CardDefaults.cardColors(containerColor = Color.White), modifier = Modifier .height(100.dp) .weight(1f), elevation = CardDefaults.cardElevation(defaultElevation = 5.dp), ) { Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { // 卡片内容 } } } } }
关键代码解释
- 滚动状态监听:通过
rememberLazyListState和derivedStateOf实时获取滚动偏移量,计算头部当前高度。 - 动态高度调整:
currentHeaderHeight随滚动偏移从原始高度递减至最小高度,实现折叠效果。 - 内容偏移适配:
LazyColumn的contentPadding设置为原始头部高度减去卡片偏移量,确保初始布局与原效果一致。 - 平滑过渡:利用
scrollOffsetRatio控制标题透明度和背景图透明度,实现折叠过程的自然过渡。
内容的提问来源于stack exchange,提问作者Rajeev Kumar
相关产品推荐
相关产品推荐

