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