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

Jetpack Compose:如何实现LazyVerticalGrid单个网格项点击展开

在Jetpack Compose中实现单个可展开的LazyVerticalGrid项

我想要实现一个基于LazyVerticalGrid的UI布局,点击单个网格元素时,仅展开该元素并显示其原本隐藏的子项详情列表。

核心问题:如何实现点击效果仅作用于被点击的项,而非其所在列的所有项?


失败尝试

我曾用LazyColumn实现,但chunked方法将数据按3个元素一组拆分为二维列表,导致只能按行管理展开状态,且无法准确定位单个项的索引,最终出现点击一个元素时,所在列的所有元素同时展开的问题。

当时的实现代码如下:

@Composable
fun ExpandableGridUsingLazyColumn() {
    val items = listOf(
        CollapsableSection(
            title = "电子与家电",
            rows = listOf("电子产品", "笔记本配件", "手机配件")
        ),
        CollapsableSection(
            title = "服饰",
            rows = listOf("男装", "女装")
        ),
        CollapsableSection(
            title = "家具与家居装饰",
            rows = listOf("家具", "家居与园艺")
        ),
        CollapsableSection(
            title = "食品与饮料",
            rows = listOf("饮料", "乳制品与烘焙", "杂货与主食", "零食与品牌食品", "蔬菜与水果")
        ),
        CollapsableSection(
            title = "健康与美妆",
            rows = listOf("洗浴用品", "男士护理", "个人护理")
        ),
        CollapsableSection(
            title = "家居清洁用品",
            rows = listOf("清洁产品", "家居必需品")
        ),
        CollapsableSection(
            title = "汽车配件",
            rows = listOf()
        ),
        CollapsableSection(
            title = "运动与健身",
            rows = listOf()
        ),
        CollapsableSection(
            title = "玩具、书籍与爱好",
            rows = listOf("书籍与文具", "玩具")
        )
    ) // 示例数据
    val columns = 3 // 列数
    val rows = remember(items) { items.chunked(columns) }
    val collapsedState = remember { rows.map { true }.toMutableStateList() }
    var itemIndex = 0

    LazyColumn(
        modifier = Modifier.fillMaxSize(),
        contentPadding = PaddingValues(8.dp)
    ) {
        rows.forEachIndexed { i, rowItems ->
            item {
                Column {
                    // 标题行
                    Row(
                        modifier = Modifier.fillMaxWidth(),
                        horizontalArrangement = Arrangement.spacedBy(8.dp)
                    ) {
                        rowItems.forEachIndexed { index, item ->
                            Box(
                                modifier = Modifier
                                    .weight(1f)
                                    .aspectRatio(1f)
                                    .background(Color.LightGray, shape = RoundedCornerShape(8.dp))
                                    .clickable {
                                        itemIndex = index
                                        collapsedState[index] = !collapsedState[index]
                                    },
                                contentAlignment = Alignment.Center
                            ) {
                                Text(
                                    text = item.title,
                                    style = MaterialTheme.typography.bodyMedium
                                )
                            }
                        }
                        // 填充行内剩余空白
                        repeat(columns - rowItems.size) {
                            Spacer(modifier = Modifier.weight(1f))
                        }
                    }

                    Spacer(modifier = Modifier.height(8.dp)) // 标题行之间的间距

                    // 展开内容
                    if (!collapsedState[itemIndex]) {
                        Column(
                            modifier = Modifier
                                .fillMaxWidth()
                                .padding(8.dp)
                        ) {
                            rowItems[itemIndex].rows.forEach { item ->
                                Text(
                                    text = "$item 详情",
                                    modifier = Modifier
                                        .padding(vertical = 4.dp)
                                        .background(
                                            Color(0xFFEFEFEF),
                                            shape = RoundedCornerShape(4.dp)
                                        )
                                        .padding(8.dp)
                                )
                            }
                        }
                    }
                }
            }
        }
    }
}

// 补充数据类
data class CollapsableSection(val title: String, val rows: List<String>)

正确实现方案:使用LazyVerticalGrid+独立项状态

要实现单个网格项的独立展开/折叠,核心是给每个网格项维护独立的展开状态,而非按行或列维护。直接使用LazyVerticalGrid替代LazyColumn+chunked的方式,就能避免行级状态的干扰。

完整代码

@Composable
fun ExpandableLazyVerticalGrid() {
    val items = listOf(
        CollapsableSection(
            title = "电子与家电",
            rows = listOf("电子产品", "笔记本配件", "手机配件")
        ),
        CollapsableSection(
            title = "服饰",
            rows = listOf("男装", "女装")
        ),
        CollapsableSection(
            title = "家具与家居装饰",
            rows = listOf("家具", "家居与园艺")
        ),
        CollapsableSection(
            title = "食品与饮料",
            rows = listOf("饮料", "乳制品与烘焙", "杂货与主食", "零食与品牌食品", "蔬菜与水果")
        ),
        CollapsableSection(
            title = "健康与美妆",
            rows = listOf("洗浴用品", "男士护理", "个人护理")
        ),
        CollapsableSection(
            title = "家居清洁用品",
            rows = listOf("清洁产品", "家居必需品")
        ),
        CollapsableSection(
            title = "汽车配件",
            rows = listOf()
        ),
        CollapsableSection(
            title = "运动与健身",
            rows = listOf()
        ),
        CollapsableSection(
            title = "玩具、书籍与爱好",
            rows = listOf("书籍与文具", "玩具")
        )
    )

    // 为每个项维护独立的展开状态,初始全部折叠
    val expandedStates = remember {
        mutableStateMapOf<Int, Boolean>().apply {
            items.indices.forEach { put(it, false) }
        }
    }

    LazyVerticalGrid(
        columns = GridCells.Fixed(3),
        modifier = Modifier.fillMaxSize(),
        contentPadding = PaddingValues(8.dp),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        itemsIndexed(items) { index, section ->
            Column(
                modifier = Modifier
                    .background(Color.LightGray, RoundedCornerShape(8.dp))
                    .clickable {
                        // 切换当前项的展开状态
                        expandedStates[index] = !(expandedStates[index] ?: false)
                    }
            ) {
                // 网格项标题区域
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .aspectRatio(1f),
                    contentAlignment = Alignment.Center
                ) {
                    Text(
                        text = section.title,
                        style = MaterialTheme.typography.bodyMedium,
                        textAlign = TextAlign.Center,
                        modifier = Modifier.padding(8.dp)
                    )
                }

                // 展开的详情列表,仅当当前项展开时显示
                if (expandedStates[index] == true) {
                    Column(
                        modifier = Modifier
                            .fillMaxWidth()
                            .background(Color(0xFFEFEFEF), RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp))
                            .padding(8.dp)
                    ) {
                        section.rows.forEach { item ->
                            Text(
                                text = "$item",
                                modifier = Modifier.padding(vertical = 4.dp)
                            )
                        }
                    }
                }
            }
        }
    }
}

data class CollapsableSection(val title: String, val rows: List<String>)

关键实现要点

  • 独立状态管理:使用mutableStateMapOf<Int, Boolean>,以每个项的索引为key存储展开状态,确保每个项的状态互不干扰。
  • 直接使用LazyVerticalGrid:通过GridCells.Fixed(3)指定3列布局,无需手动拆分数据,每个网格项都是独立的Composable。
  • 点击事件绑定到单个项:点击事件直接作用于当前网格项的Column,切换对应索引的状态,仅影响当前项的展开/折叠。

内容的提问来源于stack exchange,提问作者Tirumal Phutane

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 17:40:53