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
}
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
)
}
}
}