Иногда для вёрстки сложных экранов не хватает 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 - это самое правое и нижнее занятые места соответственно.
Результат:

