Jetpack compose — построение столбца с дочерними элементами, разделенными разделителем

Я пытаюсь создать обычай, чьи дочерние элементы разделены предоставленным ему разделителем. Разделитель следует применять только между дочерними элементами, которые фактически визуализируются.

Я сначала думал попробовать повторитьчтоиспользует, но это кажется невозможным для моего варианта использования. В итоге я выбрал пользовательский компонуемый подход и придумал следующую реализацию, но столкнулся с проблемой измерения разделителей.

Любая помощь/указатели будут оценены.

      
@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 ответ

Даже если бы вы могли измерить несколько раз, вы не смогли бы разместить то же самое место, которое уже размещено путем измерения.

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

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

Применение

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

Другой вариант — использовать содержимое с одним параметром: @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
                    }

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

Применение

      @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
            )
        }
    }
}
Другие вопросы по тегам