英文:
Jetpack compose - Building a column with children separated by a divider
问题
我试图构建一个自定义的Column
,其子元素之间由提供的分隔符分隔开。分隔符应仅应用于实际呈现的子元素之间。
我最初考虑尝试复制Column
使用的Arrangement.separatedBy()
,但对于我的用例似乎不可行。最终,我采用了自定义的可组合方法,并提出了以下实现,但在测量分隔符时遇到了问题。
任何帮助/指针将不胜感激。
@Composable
fun ColumnWithChildrenSeparatedByDivider(
modifier: Modifier = Modifier,
divider: @Composable () -> Unit,
content: @Composable () -> Unit,
) {
Layout(
modifier = modifier,
contents = listOf(content, divider),
) { measurables, constraints ->
val contentPlaceables = measurables.first().map { measurable ->
measurable.measure(constraints)
}
// 仅考虑将实际呈现的子元素
val contentToRenderCount = contentPlaceables.map { it.width > 0 }.count()
// 这会导致崩溃,因为无法多次测量相同的可测量对象
val dividerPlaceables = List(contentToRenderCount - 1) { measurables[1].first().measure(constraints) }
layout(constraints.maxWidth, constraints.maxHeight) {
var yPosition = 0
var dividerIndex = 0
for (contentPlaceable in contentPlaceables) {
if (contentPlaceable.height <= 0) {
continue
}
// 放置子元素
contentPlaceable.place(x = 0, y = yPosition)
yPosition += contentPlaceable.height
// 放置分隔符
val dividerPlaceable = dividerPlaceables[dividerIndex++]
dividerPlaceable.place(x = 0, y = yPosition)
yPosition += dividerPlaceable.height
}
}
}
}
@Composable
fun Divider() {
// 可以是任何内容
}
请注意,这是代码的翻译版本。
英文:
I'm trying to build a custom Column
whose children are separated by a divider that's provided to it. The divider should only be applied between children that are actually rendered.
I initially thought of trying to replicate Arrangement.separatedBy()
that Column
uses, but it doesn't seem possible for my use case. I ended up going with the custom composable approach, and came up with the following implementation, but ran into an issue with measuring the dividers.
Any help/pointers would be appreciated.
@Composable
fun ColumnWithChildrenSeparatedByDivider(
modifier: Modifier = Modifier,
divider: @Composable () -> Unit,
content: @Composable () -> Unit,
) {
Layout(
modifier = modifier,
contents = listOf(content, divider),
) { measurables, constraints ->
val contentPlaceables = measurables.first().map { measurable ->
measurable.measure(constraints)
}
// Only take into account children that will actually be rendered
val contentToRenderCount = contentPlaceables.map { it.width > 0 }.count()
// This crashes, since I can't measure the same measurable more than once
val dividerPlaceables = List(contentToRenderCount - 1) { measurables[1].first().measure(constraints) }
layout(constraints.maxWidth, constraints.maxHeight) {
var yPosition = 0
var dividerIndex = 0
for (contentPlaceable in contentPlaceables) {
if (contentPlaceable.height <= 0) {
continue
}
// Place child
contentPlaceable.place(x = 0, y = yPosition)
yPosition += contentPlaceable.height
// Place divider
val dividerPlaceable = dividerPlaceables[dividerIndex++]
dividerPlaceable.place(x = 0, y = yPosition)
yPosition += dividerPlaceable.height
}
}
}
}
@Composable
fun Divider() {
// Could be anything
}
答案1
得分: 1
以下是代码的翻译:
即使您能够多次使用以下代码进行测量:
measurables[1].first().measure(constraints)
您也无法放置已经放置的相同可放置对象。
您可以将分隔符的数量乘以以匹配内容大小,与可用内容大小的初始最大数量相匹配,比如,我在演示中使用了一个固定的数字。
@Composable
fun ColumnWithChildrenSeparatedByDivider(
modifier: Modifier = Modifier,
divider: @Composable () -> Unit,
content: @Composable () -> Unit,
) {
val dividers = @Composable {
repeat(15) {
divider()
}
}
Layout(
modifier = modifier,
contents = listOf(content, dividers),
) { measurables, constraints ->
val contentPlaceables = measurables.first().map { measurable ->
measurable.measure(constraints)
}
// 只考虑实际呈现的子元素
val contentToRenderCount = contentPlaceables.map { it.width > 0 }.count()
val dividerPlaceables = measurables[1].take(contentToRenderCount).map { measurable ->
measurable.measure(constraints)
}
// 使用 Constraints maxHeight 也不会修改修饰符的布局,以覆盖父级的大小。最好检查修饰符是否具有固定高度和有界高度,如果是,则使用高度的总和,否则使用约束的最大高度。
val hasFixedHeight = constraints.hasFixedHeight
val hasBoundedHeight = constraints.hasBoundedHeight
val height = if (hasFixedHeight && hasBoundedHeight) {
constraints.maxHeight
} else contentPlaceables.sumOf { it.height } + dividerPlaceables.sumOf { it.height }
layout(constraints.maxWidth, height) {
var yPosition = 0
var dividerIndex = 0
for (contentPlaceable in contentPlaceables) {
if (contentPlaceable.height <= 0) {
continue
}
// 放置子元素
contentPlaceable.place(x = 0, y = yPosition)
yPosition += contentPlaceable.height
// 放置分隔符
val dividerPlaceable = dividerPlaceables[dividerIndex++]
dividerPlaceable.place(x = 0, y = yPosition)
yPosition += dividerPlaceable.height
}
}
}
}
使用:
@Preview
@Composable
private fun Test() {
ColumnWithChildrenSeparatedByDivider(modifier = Modifier
.fillMaxWidth()
.border(2.dp, Color.Red),
content = {
Text(text = "Hello World")
Text(text = "Hello World")
Text(text = "Hello World")
Text(text = "Hello World")
Box(modifier = Modifier.width(0.dp))
Box(modifier = Modifier.width(0.dp))
Text(text = "Hello")
},
divider = {
Divider(
modifier = Modifier
.fillMaxWidth()
.height((1.dp))
)
}
)
}
另一个选项是使用单个参数 content: @Composable () -> Unit,然后为每个内容和分隔符分配 Modifier.layoutId(),并检查它们,或者使用索引的偶数和奇数位置的模数来匹配具有匹配分隔符的非零宽度内容。
@Composable
fun ColumnWithChildrenSeparatedByDivider(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
val measurePolicy = remember {
MeasurePolicy { measurables, constraints ->
val contentPlaceables = hashMapOf<Int, Placeable>()
measurables.filter {
it.layoutId == "content"
}.mapIndexed { index, measurable ->
contentPlaceables[index] = measurable.measure(
constraints.copy(minWidth = 0)
)
}
val contentPlaceablesMap = contentPlaceables.filterValues {
it.width > 0
}
val contentList = contentPlaceablesMap.values.toList()
val dividerPlaceables = measurables.filter {
it.layoutId == "divider"
}.map {
it.measure(constraints)
}.filterIndexed { index, _ ->
contentPlaceablesMap.contains(index)
}
val hasFixedHeight = constraints.hasFixedHeight
val hasBoundedHeight = constraints.hasBoundedHeight
val height = if (hasFixedHeight && hasBoundedHeight) {
constraints.maxHeight
} else contentList.sumOf { it.height } + dividerPlaceables.sumOf { it.height }
layout(constraints.maxWidth, height) {
var yPosition = 0
var dividerIndex = 0
for (contentPlaceable in contentList) {
if (contentPlaceable.height <= 0) {
continue
}
// 放置子元素
contentPlaceable.place(x = 0, y = yPosition)
yPosition += contentPlaceable.height
// 放置分隔符
val dividerPlaceable = dividerPlaceables[dividerIndex++]
dividerPlaceable.place(x = 0, y = yPosition)
yPosition += dividerPlaceable.height
}
}
}
}
Layout(
modifier = modifier,
content = content,
measurePolicy = measurePolicy
)
}
使用:
@Preview
@Composable
private fun Test() {
val content = mutableListOf<@Composable () -> Unit>(
{ Text(text = "Hello1", modifier = Modifier.layoutId("content")) },
{ Text(text = "Hello2", modifier = Modifier.layoutId("content")) },
{ Text(text = "Hello3", modifier = Modifier.layoutId("content")) },
{ Text(text = "Hello4", modifier = Modifier.layoutId("content")) },
{ Box(modifier = Modifier.width(0.dp).layoutId("content")) },
{ Box(modifier = Modifier.width(0.dp).layoutId("content")) },
{ Text(text = "Hello5", modifier = Modifier.layoutId("content")) }
)
ColumnWithChildrenSeparatedByDivider(
modifier = Modifier.fillMaxWidth()
) {
content.forEach {
it()
Divider(
modifier = Modifier
.layoutId("divider")
.fillMaxWidth(),
color = Color.Red,
thickness = 3.dp
)
}
}
}
英文:
Even if you were able to measure multiple times with
measurables[1].first().measure(constraints)
you wouldn't be able to place same placable that is placed already.
You can either multiply the number of Dividers to match content size with initial number max of available content size such as, i used a fixed number for demonstration.
@Composable
fun ColumnWithChildrenSeparatedByDivider(
modifier: Modifier = Modifier,
divider: @Composable () -> Unit,
content: @Composable () -> Unit,
) {
val dividers = @Composable {
repeat(15) {
divider()
}
}
Layout(
modifier = modifier,
contents = listOf(content, dividers),
) { measurables, constraints ->
val contentPlaceables = measurables.first().map { measurable ->
measurable.measure(constraints)
}
// Only take into account children that will actually be rendered
val contentToRenderCount = contentPlaceables.map { it.width > 0 }.count()
val dividerPlaceables = measurables[1].take(contentToRenderCount).map { measurable ->
measurable.measure(constraints)
}
// Also using Constraints maxHeight results no modifier
// layouts to cover size of parent as well. It's better to check if
// modifier has fixed height and finite height if so use sum of heights else max
// height from constraints.
val hasFixedHeight = constraints.hasFixedHeight
val hasBoundedHeight = constraints.hasBoundedHeight
val height = if (hasFixedHeight && hasBoundedHeight) {
constraints.maxHeight
} else contentPlaceables.sumOf { it.height } + dividerPlaceables.sumOf { it.height }
layout(constraints.maxWidth, height) {
var yPosition = 0
var dividerIndex = 0
for (contentPlaceable in contentPlaceables) {
if (contentPlaceable.height <= 0) {
continue
}
// Place child
contentPlaceable.place(x = 0, y = yPosition)
yPosition += contentPlaceable.height
// Place divider
val dividerPlaceable = dividerPlaceables[dividerIndex++]
dividerPlaceable.place(x = 0, y = yPosition)
yPosition += dividerPlaceable.height
}
}
}
}
Usage
@Preview
@Composable
private fun Test() {
ColumnWithChildrenSeparatedByDivider(modifier = Modifier
.fillMaxWidth()
.border(2.dp, Color.Red),
content = {
Text(text = "Hello World")
Text(text = "Hello World")
Text(text = "Hello World")
Text(text = "Hello World")
Box(modifier = Modifier.width(0.dp))
Box(modifier = Modifier.width(0.dp))
Text(text = "Hello")
},
divider = {
Divider(
modifier = Modifier
.fillMaxWidth()
.height((1.dp))
)
}
)
}
Other option is to use single param content: @Composable () -> Unit
then either give Modifier.layoutId() to each content and divider and check those or use modulus for even and odd positions with indexing to match non zero width content with matching divider.
@Composable
fun ColumnWithChildrenSeparatedByDivider(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
val measurePolicy = remember {
MeasurePolicy { measurables, constraints ->
val contentPlaceables = hashMapOf<Int, Placeable>()
measurables.filter {
it.layoutId == "content"
}.mapIndexed { index, measurable ->
contentPlaceables[index] = measurable.measure(
constraints.copy(minWidth = 0)
)
}
val contentPlaceablesMap = contentPlaceables.filterValues {
it.width > 0
}
val contentList = contentPlaceablesMap.values.toList()
val dividerPlaceables = measurables.filter {
it.layoutId == "divider"
}.map {
it.measure(constraints)
}.filterIndexed { index, _ ->
contentPlaceablesMap.contains(index)
}
val hasFixedHeight = constraints.hasFixedHeight
val hasBoundedHeight = constraints.hasBoundedHeight
val height = if (hasFixedHeight && hasBoundedHeight) {
constraints.maxHeight
} else contentList.sumOf { it.height } + dividerPlaceables.sumOf { it.height }
layout(constraints.maxWidth, height) {
var yPosition = 0
var dividerIndex = 0
for (contentPlaceable in contentList) {
if (contentPlaceable.height <= 0) {
continue
}
// Place child
contentPlaceable.place(x = 0, y = yPosition)
yPosition += contentPlaceable.height
// Place divider
val dividerPlaceable = dividerPlaceables[dividerIndex++]
dividerPlaceable.place(x = 0, y = yPosition)
yPosition += dividerPlaceable.height
}
}
}
}
Layout(
modifier = modifier,
content = content,
measurePolicy = measurePolicy
)
}
Usage
@Preview
@Composable
private fun Test() {
val content = mutableListOf<@Composable () -> Unit>(
{ Text(text = "Hello1", modifier = Modifier.layoutId("content")) },
{ Text(text = "Hello2" , modifier = Modifier.layoutId("content")) },
{ Text(text = "Hello3", modifier = Modifier.layoutId("content")) },
{ Text(text = "Hello4", modifier = Modifier.layoutId("content")) },
{ Box(modifier = Modifier.width(0.dp).layoutId("content")) },
{ Box(modifier = Modifier.width(0.dp).layoutId("content")) },
{ Text(text = "Hello5", modifier = Modifier.layoutId("content")) }
)
ColumnWithChildrenSeparatedByDivider(
modifier = Modifier.fillMaxWidth()
) {
content.forEach {
it()
Divider(
modifier = Modifier
.layoutId("divider")
.fillMaxWidth(),
color = Color.Red,
thickness = 3.dp
)
}
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论