Jetpack Compose中如何设置列表项的自适应宽度或换行?

26

我需要实现下一个UI元素:

enter image description here

  • 字符串列表大小未知
  • 任何项都应该是自适应内容。
  • 如果一个项不能适应行,它将在下一行中。
  • 所有的列表/网格都居中对齐
5个回答

47

您可以使用来自accompanist-flowlayoutFlowRow来实现此功能。它会将其子项水平渲染(类似于Row),但如果它们不适合现有行,则通过转到新行来包装它们。它还允许配置项目之间的水平和垂直间距。

为了很好地处理非常长的字符串(它们本身不适合单行),您可以在Text上设置overflow = TextOverflow.EllipsismaxLines = 1

@Composable
fun HashTagList(hashTags: List<String>) {
    FlowRow(
        modifier = Modifier.padding(8.dp),
        mainAxisAlignment = MainAxisAlignment.Center,
        mainAxisSize = SizeMode.Expand,
        crossAxisSpacing = 12.dp,
        mainAxisSpacing = 8.dp
    ) {
        hashTags.forEach { hashTag ->
            Text(
                text = hashTag,
                modifier = Modifier
                    .background(
                        color = colorForHashTag(hashTag),
                        shape = RoundedCornerShape(4.dp)
                    )
                    .padding(8.dp),
                overflow = TextOverflow.Ellipsis,
                maxLines = 1
            )
        }
    }
}

在此输入图片描述


6
FlowRow和FlowColumn现已弃用,建议使用自定义布局(Layout)代替。详情请参见:https://android-review.googlesource.com/c/platform/frameworks/support/+/1521704 - Some random IT boy
你有什么想法可以让这个组的添加/删除项目动画化吗? - Matt Wolfe

30

更新:如果需要一个现成的解决方案,请查看Accompanist库中的Jetpack Compose Flow Layouts。如果需要自定义解决方案,请参考下面的答案。


自从Compose版本1.0.0-alpha10起,FlowRow和FlowColumn已经被弃用。希望未来会有一些内置的解决方案,但现在你可以像弃用说明建议的那样使用自定义布局。以下是一个示例:

@Composable
fun TagRow(tags: Collection<String>) {
    SimpleFlowRow(
        verticalGap = 8.dp,
        horizontalGap = 8.dp,
        alignment = Alignment.CenterHorizontally,
        modifier = Modifier.padding(16.dp)
    ) {
        for (tag in tags) {
            Text(
                text = "#$tag",
                maxLines = 1,
                overflow = TextOverflow.Ellipsis,
                modifier = Modifier
                    .background(Color.LightGray, RoundedCornerShape(4.dp))
                    .padding(4.dp)
            )
        }
    }
}

@Composable
fun SimpleFlowRow(
    modifier: Modifier = Modifier,
    alignment: Alignment.Horizontal = Alignment.Start,
    verticalGap: Dp = 0.dp,
    horizontalGap: Dp = 0.dp,
    content: @Composable () -> Unit
) = Layout(content, modifier) { measurables, constraints ->
    val hGapPx = horizontalGap.roundToPx()
    val vGapPx = verticalGap.roundToPx()

    val rows = mutableListOf<MeasuredRow>()
    val itemConstraints = constraints.copy(minWidth = 0)

    for (measurable in measurables) {
        val lastRow = rows.lastOrNull()
        val placeable = measurable.measure(itemConstraints)

        if (lastRow != null && lastRow.width + hGapPx + placeable.width <= constraints.maxWidth) {
            lastRow.items.add(placeable)
            lastRow.width += hGapPx + placeable.width
            lastRow.height = max(lastRow.height, placeable.height)
        } else {
            val nextRow = MeasuredRow(
                items = mutableListOf(placeable),
                width = placeable.width,
                height = placeable.height
            )

            rows.add(nextRow)
        }
    }

    val width = rows.maxOfOrNull { row -> row.width } ?: 0
    val height = rows.sumBy { row -> row.height } + max(vGapPx.times(rows.size - 1), 0)

    val coercedWidth = width.coerceIn(constraints.minWidth, constraints.maxWidth)
    val coercedHeight = height.coerceIn(constraints.minHeight, constraints.maxHeight)

    layout(coercedWidth, coercedHeight) {
        var y = 0

        for (row in rows) {
            var x = when(alignment) {
                Alignment.Start -> 0
                Alignment.CenterHorizontally -> (coercedWidth - row.width) / 2
                Alignment.End -> coercedWidth - row.width

                else -> throw Exception("unsupported alignment")
            }

            for (item in row.items) {
                item.place(x, y)
                x += item.width + hGapPx
            }

            y += row.height + vGapPx
        }
    }
}

private data class MeasuredRow(
    val items: MutableList<Placeable>,
    var width: Int,
    var height: Int
)

结果如下: 输入图像描述

您可以在 GitHub上 找到完整的示例代码。


2
这看起来很不错,但我注意到所有项目都同时加载。如果有1000多个带图像的项目,这可能会成为问题。是否有一种方法可以延迟加载项目? - tiagocarvalho92

5

如果您想使用流式布局,那么Accompanist中的Flow layouts就是您所需要的。

您可以在此处查找文档:https://google.github.io/accompanist/flowlayout/

具体来说,FlowRow将允许您在示例中实现所需的效果。


FlowRow不能垂直对齐项目,例如Alignment.CenterVertically。 - Barrufet
我想我刚刚明白了“cross”是什么意思:D - Barrufet

2

对于那些希望使用 Alignment.Vertical 控制高度较小的项目对齐的实现的人,这里是 Valeriy 答案的修改版本:


@Composable
fun FlowRow(
    modifier: Modifier = Modifier,
    alignment: Alignment.Horizontal = Alignment.Start,
    verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
    verticalGap: Dp = 0.dp,
    horizontalGap: Dp = 0.dp,
    content: @Composable () -> Unit
) = Layout(content, modifier) { measurables, constraints ->
    val hGapPx = horizontalGap.roundToPx()
    val vGapPx = verticalGap.roundToPx()

    val rows = mutableListOf<MeasuredRow>()
    val itemConstraints = constraints.copy(minWidth = 0)

    for (measurable in measurables) {
        val lastRow = rows.lastOrNull()
        val placeable = measurable.measure(itemConstraints)

        if (lastRow != null && lastRow.width + hGapPx + placeable.width <= constraints.maxWidth) {
            lastRow.items.add(placeable)
            lastRow.width += hGapPx + placeable.width
            lastRow.height = max(lastRow.height, placeable.height)
        } else {
            val nextRow = MeasuredRow(
                items = mutableListOf(placeable),
                width = placeable.width,
                height = placeable.height
            )

            rows.add(nextRow)
        }
    }

    val width = rows.maxOfOrNull { row -> row.width } ?: 0
    val height = rows.sumBy { row -> row.height } + max(vGapPx.times(rows.size - 1), 0)

    val coercedWidth = width.coerceIn(constraints.minWidth, constraints.maxWidth)
    val coercedHeight = height.coerceIn(constraints.minHeight, constraints.maxHeight)

    layout(coercedWidth, coercedHeight) {
        var y = 0

        for (row in rows) {
            var x = when(alignment) {
                Alignment.Start -> 0
                Alignment.CenterHorizontally -> (coercedWidth - row.width) / 2
                Alignment.End -> coercedWidth - row.width

                else -> throw Exception("unsupported alignment")
            }

            for (item in row.items) {
                var localY = 0
                if (item.height < row.height) {
                    localY = when (verticalAlignment) {
                        Alignment.Start -> y
                        Alignment.CenterVertically -> y + ((row.height + vGapPx) / 2 - item.height / 2)
                        Alignment.End -> y + row.height
                        else -> throw Exception("unsupported alignment")
                    }
                } else localY = y
                item.place(x, localY)
                x += item.width + hGapPx
            }

            y += row.height + vGapPx
        }
    }
}

private data class MeasuredRow(
    val items: MutableList<Placeable>,
    var width: Int,
    var height: Int
)

不再工作了,当我设置 Alignment.CenterVertically 时,较小的项在底部。 - Barrufet

1

您可以使用 LazyVerticalGrid 并使用网格单元来选择如何显示子项。更多信息请参见 https://alexzh.com/jetpack-compose-building-grids/

val data = listOf("Item 1", "Item 2", "Item 3", "Item 4", "Item 5")

LazyVerticalGrid(
    cells = GridCells.Fixed(3),
    contentPadding = PaddingValues(8.dp)
) {
    items(data) { item ->
        Card(
            modifier = Modifier.padding(4.dp),
            backgroundColor = Color.LightGray
        ) {
            Text(
                text = item,
                fontSize = 24.sp,
                textAlign = TextAlign.Center,
                modifier = Modifier.padding(24.dp)
            )
        }
    }
}

结果

这是一个实验性的功能,但它运行良好。在导入项目时要小心。


这并不适合用户的需求,因为它只允许固定大小。如果使用自适应大小,效果会更好,但扩展性不太好。 - tiagocarvalho92

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接