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

能否使用Jetpack Compose实现嵌套粘性头部?

Jetpack Compose 实现嵌套粘性头部

Jetpack Compose 官方提供的stickyHeader仅支持单层粘性头部,要实现多层嵌套的粘性效果,需要通过监听滚动状态+自定义悬浮头部的方式来实现。以下是具体实现方案:

1. 定义数据结构

先定义对应层级的数据模型,方便构建列表内容:

data class Category(
    val title: String,
    val subCategories: List<SubCategory>
)

data class SubCategory(
    val title: String,
    val items: List<String>
)

2. 核心实现代码

通过LazyColumn构建列表,结合滚动状态监听动态更新悬浮的嵌套头部:

@Composable
fun NestedStickyHeaderScreen() {
    // 模拟测试数据
    val categories = listOf(
        Category(
            "一级分类A",
            listOf(
                SubCategory("二级分类A1", listOf("项目1", "项目2", "项目3")),
                SubCategory("二级分类A2", listOf("项目4", "项目5"))
            )
        ),
        Category(
            "一级分类B",
            listOf(
                SubCategory("二级分类B1", listOf("项目6", "项目7")),
                SubCategory("二级分类B2", listOf("项目8", "项目9", "项目10"))
            )
        )
    )

    val listState = rememberLazyListState()
    // 记录当前需要悬浮的一级/二级头部
    val currentStickyCategory = remember { mutableStateOf<Category?>(null) }
    val currentStickySubCategory = remember { mutableStateOf<SubCategory?>(null) }

    // 监听滚动位置,更新悬浮头部
    LaunchedEffect(listState) {
        snapshotFlow { listState.firstVisibleItemIndex }
            .collect { index ->
                var currentIndex = index
                var targetCategory: Category? = null
                var targetSubCategory: SubCategory? = null

                // 遍历数据,匹配当前滚动位置对应的层级头部
                for (cat in categories) {
                    if (currentIndex == 0) {
                        targetCategory = cat
                        break
                    }
                    currentIndex--

                    for (subCat in cat.subCategories) {
                        if (currentIndex == 0) {
                            targetCategory = cat
                            targetSubCategory = subCat
                            break
                        }
                        currentIndex--

                        if (currentIndex < subCat.items.size) {
                            targetCategory = cat
                            targetSubCategory = subCat
                            break
                        }
                        currentIndex -= subCat.items.size
                    }
                    if (targetCategory != null) break
                }

                currentStickyCategory.value = targetCategory
                currentStickySubCategory.value = targetSubCategory
            }
    }

    Box {
        // 主列表
        LazyColumn(state = listState) {
            categories.forEach { category ->
                // 一级头部项
                item(key = "category_${category.title}") {
                    CategoryHeader(category.title)
                }

                category.subCategories.forEach { subCategory ->
                    // 二级头部项
                    item(key = "subcategory_${subCategory.title}") {
                        SubCategoryHeader(subCategory.title)
                    }
                    // 子列表项
                    items(subCategory.items) { item ->
                        ListItemContent(item)
                    }
                }
            }
        }

        // 悬浮的嵌套粘性头部
        Column {
            currentStickyCategory.value?.let {
                CategoryHeader(it.title)
            }
            currentStickySubCategory.value?.let {
                SubCategoryHeader(it.title)
                    .padding(start = 16.dp) // 缩进区分层级
            }
        }
    }
}

// 一级头部样式
@Composable
private fun CategoryHeader(title: String) {
    Text(
        text = title,
        fontSize = 20.sp,
        fontWeight = FontWeight.Bold,
        modifier = Modifier
            .fillMaxWidth()
            .background(Color.LightGray)
            .padding(16.dp)
    )
}

// 二级头部样式
@Composable
private fun SubCategoryHeader(title: String) {
    Text(
        text = title,
        fontSize = 16.sp,
        fontWeight = FontWeight.SemiBold,
        modifier = Modifier
            .fillMaxWidth()
            .background(Color.Gray.copy(alpha = 0.8f))
            .padding(horizontal = 16.dp, vertical = 12.dp)
    )
}

// 列表项内容
@Composable
private fun ListItemContent(text: String) {
    Text(
        text = text,
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
    )
}

3. 进阶优化:头部顶起动画

如果需要实现演示中"二级头部顶起一级头部"的平滑动画,可以通过记录每个头部的位置坐标,计算偏移量来调整悬浮头部的位置:

  • 给每个头部添加onGloballyPositioned监听,记录其在列表中的Y轴坐标
  • 在滚动监听中计算当前悬浮头部的偏移值,通过Modifier.offset(y = ...)实现动态位移
  • 利用animateDpAsState实现平滑的过渡动画

内容的提问来源于stack exchange,提问作者Akshdeep Singh

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 07:55:08