Иногда для вёрстки сложных экранов не хватает Row, Column, Box и других встроенных контейнеров, тогда нам приходится писать свои собственные. В этой статье мы напишем Row, который переносит дочерние элементы на следующую строку в случае недостатка места.

Эта статья поделена на 2 части: базовую и продвинутую.

Для создания собственных контейнеров в Compose используется элемент Layout:

@Composable
fun Layout(content: @Composable () -> Unit, modifier: Modifier = Modifier, measurePolicy: MeasurePolicy)
  • content - тело контейнера, содержащее все дочерние элементы.

  • measurePolicy - объект, отвечающий за расположение элементов внутри контейнера

Основной элемент будет выглядеть так:

@Composable
inline fun RowWithWrap(
    modifier: Modifier = Modifier,
    verticalSpacer: Dp = 0.dp,
    horizontalSpacer: Dp = 0.dp,
    content: @Composable () -> Unit
) {
    Box(modifier) {
        Layout(
            content = content,
            measurePolicy = rowWithWrapMesaurePolicy(verticalSpacer, horizontalSpacer)
        )
    }
}
  • verticalSpacer и horizontalSpacer - отступы между элементами по вертикали и горизонтали соответственно.

  • Box(modifier) - это небольшой костыль. Заставить Layout корректно обработать Modifier на уровне базовой статьи мы не можем. Это мы решим в продвинутой статье.

  • rowWithWrapMesaurePolicy создаёт политику расположения элементов исходя из отступов. Это так же понадобится в продвинутой статье

@Composable
fun rowWithWrapMesaurePolicy(
    verticalSpacer: Dp = 0.dp,
    horizontalSpacer: Dp = 0.dp
): MeasurePolicy = remember(verticalSpacer, horizontalSpacer) {
    MeasurePolicy { measurables: List<Measurable>, constraints: Constraints ->
        val positions = rowWithWrapRelativePositions(constraints, measurables, verticalSpacer, horizontalSpacer)
        val width = maxOf(positions.maxOf { it.maxXCoordinate }, constraints.minWidth)
        val height = minOf(maxOf(positions.maxOf { it.maxYCoordinate }, constraints.minHeight), constraints.maxHeight)
        layout(width, height) {
            for ((placeable, dx, dy) in positions) {
                placeable.placeRelative(dx, dy)
            }
        }
    }
}
  • Нам необходимо использование remember, чтобы не создавать лишних объектов каждую рекомпозицию.

  • MeasurePolicy - интерфейс с одним не реализованным, так что мы можем использовать лямбда выражение.

  • measurables - все дочерние элементы контейнера.

  • constraints - ограничения в размерах

  • Метод rowWithWrapRelativePositions вычисляет расположение всех элементов относительно верхнего левого угла контейнера. Возвращает наш дата-класс, но об этом далее.

  • layout(width, height) устанавливает размеры контейнера. Внутри него мы располагаем все элементы на вычисленных ранее местах.

private fun MeasureScope.rowWithWrapRelativePositions(
    constraints: Constraints,
    measurables: List<Measurable>,
    verticalSpacer: Dp,
    horizontalSpacer: Dp
): List<PlaceableRelativePosition> {
    val res = mutableListOf<PlaceableRelativePosition>()
    var x = 0
    var y = 0
    var maxHeight = -1

    for (measurable in measurables) {
        val placeable = measurable.measure(constraints)
        if (placeable.width + x > constraints.maxWidth) {
            y += maxHeight + verticalSpacer.roundToPx()
            x = 0
            maxHeight = -1
        }
        res += PlaceableRelativePosition(placeable, x, y)
        x += placeable.width + horizontalSpacer.roundToPx()
        maxHeight = maxOf(maxHeight, placeable.height)
    }

    return res
}

private data class PlaceableRelativePosition(val placable: Placeable, val dx: Int, val dy: Int)

private val PlaceableRelativePosition.maxXCoordinate: Int
    get() = dx + placable.width

private val PlaceableRelativePosition.maxYCoordinate: Int
    get() = dy + placable.height
  • Measurable::measure вычисляет размеры дочернего элемента исходя из внешних ограничений.

  • maxXCoordinate и maxYCoordinate - это самое правое и нижнее занятые места соответственно.

Результат:

Комментарии (4)


  1. Rusrst
    07.10.2022 08:40
    +1

    Прикольно:)

    А я помню на стажировке писал аналог flexboxlayout (custom viewgroup), весело это :)

    Пора бы уже и compose наверное во всю изучать...


  1. Firsto
    07.10.2022 09:00

    Эта статья поделена на 2 части: базовую и продвинутую.

    Лучше бы сразу в одной написать, а то на самом интересном месте прерывается. :-)


    1. GoogleTan Автор
      07.10.2022 11:01
      +1

      А что бы вы хотели видеть в продвинутой статье? Как я для вас оборвала статью? Буду очень благодарна


      1. Firsto
        07.10.2022 11:24

        Ожидал кастомизации для нестандартного использования, с чем не справился бы FlowRow из accompanist (как пример: там когда-то сложно было отцентрировать элементы по вертикали)

        Но всё равно буду ждать продолжения)