Jetpack Compose:如何实现动态高度的自定义部分折叠布局?
自定义动态高度折叠布局解决方案(Jetpack Compose)
针对你遇到的动态高度、固定顶部布局及状态差异化UI问题,以下是完整的实现方案:
1. 自定义滚动行为类(支持动态高度)
首先实现一个支持动态测量展开高度的滚动行为类,用于处理滚动偏移和折叠进度计算:
import androidx.compose.foundation.ScrollState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.unit.Dp import kotlinx.coroutines.CoroutineScope class DynamicCollapsingScrollBehavior( val collapsedHeight: Dp, val coroutineScope: CoroutineScope, val isScrollEnabled: Boolean = true ) { // 动态记录展开内容的实际高度 private var _expandedHeight: Float = collapsedHeight.value var expandedHeight: Float get() = _expandedHeight set(value) { _expandedHeight = value.coerceAtLeast(collapsedHeight.value) } val scrollState = ScrollState(0) // 折叠进度:0=完全展开,1=完全折叠 val collapseProgress: Float get() { val availableScroll = expandedHeight - collapsedHeight.value return if (availableScroll <= 0) 1f else (scrollState.value / availableScroll).coerceIn(0f, 1f) } // 嵌套滚动连接,实现列表与折叠布局的联动 val nestedScrollConnection = object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { if (!isScrollEnabled) return Offset.Zero val delta = available.y val newScrollValue = (scrollState.value - delta).coerceIn(0f, expandedHeight - collapsedHeight.value) val consumed = scrollState.value - newScrollValue scrollState.value = newScrollValue return Offset(0f, consumed) } } }
2. 自定义折叠布局组件
使用SubcomposeLayout实现动态高度测量,同时根据折叠进度动态调整UI:
import androidx.compose.foundation.layout.SubcomposeLayout import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @Composable fun DynamicCollapsingLayout( modifier: Modifier = Modifier, scrollBehavior: DynamicCollapsingScrollBehavior, content: @Composable (collapseProgress: Float) -> Unit ) { SubcomposeLayout(modifier = modifier) { constraints -> // 第一步:测量展开状态下的内容高度 val expandedPlaceable = subcompose("ExpandedContent") { content(0f) }.first().measure(constraints.copy(minHeight = 0)) // 更新滚动行为中的展开高度 scrollBehavior.expandedHeight = expandedPlaceable.height.toFloat() val collapsedHeightPx = scrollBehavior.collapsedHeight.toPx().toInt() val currentLayoutHeight = (scrollBehavior.expandedHeight - scrollBehavior.scrollState.value).toInt() .coerceAtLeast(collapsedHeightPx) // 第二步:根据折叠进度布局内容 layout(constraints.maxWidth, currentLayoutHeight) { val progress = scrollBehavior.collapseProgress val contentPlaceable = subcompose("MainContent") { content(progress) }.first().measure(constraints.copy(minHeight = 0)) // 计算内容偏移,实现折叠时向上滑动的效果 val offsetY = (scrollBehavior.scrollState.value).toInt() .coerceAtMost(expandedPlaceable.height - collapsedHeightPx) contentPlaceable.place(0, -offsetY) } } }
3. 使用示例(完整Demo)
以下是包含动态高度内容、列表联动及状态差异化UI的完整示例:
import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @Composable fun CollapsingLayoutDemo() { val coroutineScope = rememberCoroutineScope() val scrollBehavior = remember { DynamicCollapsingScrollBehavior( collapsedHeight = 64.dp, coroutineScope = coroutineScope ) } val listState = rememberLazyListState() Box(Modifier.fillMaxSize()) { // 滚动列表 LazyColumn( state = listState, modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) ) { // 占位:让列表顶部与折叠布局衔接 item { Spacer(Modifier.height(scrollBehavior.expandedHeight.toDp())) } // 列表内容 items(50) { index -> Text( "列表项 $index", modifier = Modifier .fillMaxWidth() .padding(16.dp), fontSize = 16.sp ) } } // 自定义折叠布局 DynamicCollapsingLayout( modifier = Modifier.fillMaxWidth(), scrollBehavior = scrollBehavior ) { progress -> Column( modifier = Modifier .fillMaxWidth() .background(Color.White) .padding(16.dp) ) { // 标题:折叠时缩小字号、加深字重 val titleSize = 24.sp - (6.sp * progress) val titleWeight = if (progress > 0.5f) FontWeight.Bold else FontWeight.Normal Text( "自定义折叠标题", fontSize = titleSize, fontWeight = titleWeight, modifier = Modifier.alpha(1f - progress * 0.1f) ) // 动态内容:折叠到一半时隐藏 AnimatedVisibility(visible = progress < 0.5f) { Column(modifier = Modifier.padding(top = 8.dp)) { Text("动态内容行1:支持任意高度") Text("动态内容行2:无需提前设置高度") Text("动态内容行3:适配各种复杂布局") } } } } } }
核心解决点说明
- 动态高度支持:通过
SubcomposeLayout先测量展开状态下的内容高度,无需提前指定固定值 - 折叠后固定顶部:折叠布局通过
Box层级覆盖在列表上方,列表顶部的占位Spacer保证内容衔接,滚动到折叠状态时布局高度固定为collapsedHeight,自然停留在顶部 - 状态差异化UI:通过
collapseProgress参数(0到1的浮点值),可以灵活控制文字大小、字重、透明度、内容显示/隐藏等UI变化,实现完全自定义的折叠效果
内容的提问来源于stack exchange,提问作者testivanivan
相关产品推荐
相关产品推荐

