You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 08:55:59