Jetpack Compose позволяет создавать собственные компоненты компоновки с полным контролем над размером и расположением всех дочерних компонентов. Для создания контейнеров компоновки применяется встроенная функция Layout, которая предоставляет способ измерения и позиционирования дочерних компонентов. Функция Layout имеет ряд версий, которые похожи. Возьмем одну из них:
@UiComposable @Composable inline fun Layout( content: @Composable @UiComposable () -> Unit, modifier: Modifier = Modifier, measurePolicy: MeasurePolicy ): Unit
В общем случае функция-компонент Layout принимает три параметра:
content
: содержимое
modifier
: функции-модификаторы
measurePolicy
: политика компоновки вложенного содержимого в виде объекта MeasurePolicy
В начале рассмотрим общий принцип создания контейнера компоновки. Для демонстрации возьмем самое просто определение контейнера:
@Composable fun MyLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit){ Layout(modifier = modifier, content = content) { measurables, constraints -> val placeables = measurables.map { measurable -> // измеряем каждый дочерний компонент measurable.measure(constraints) } layout(constraints.maxWidth, constraints.maxHeight) { placeables.forEach { placeable -> placeable.placeRelative(x = 0, y = 0) } } } }
В данном случае контейнер называется MyLayout. Он принимает два параметра - модификаторы и функцию установки содержимого. Это наиболее часто применяемые параметры, но естественно при необходимости можно определять и другие параметры.
Далее функция MyLayout вызывает компонент Layout, в который передаются модификаторы и содержимое. Третий параметр функции Layout в данном случае представляет концевую лямбду, которая принимает
два параметра: measurables
и constraints
. Параметр measurables
содержит все дочерние компоненты, которые передаются в Layout через content
(список объектов
Measurable
). А параметр constraints
содержит максимальную и минимальную ширину и высоту, допустимую для дочерних компонентов.
Layout(modifier = modifier, content = content) { measurables, constraints -> ..................
Далее выполняется измерение дочерних компонентов, а результаты помещаются в список объектов Placeable:
val placeables = measurables.map { measurable -> // измеряем каждый дочерний компонент measurable.measure(constraints) }
Здесь метод map()
проходит по всем дочерним компонентам (объектам Measurable) и для каждого из них выполняет метод measure(), который, в свою очередь,
измеряет дочерний компонент. Результатом является список экземпляров Placeable (по одному для каждого дочернего компонента).
В конце вызывается функция layout()
, которой передаются максимальные значения высоты и ширины, разрешенные родительским элементом:
layout(constraints.maxWidth, constraints.maxHeight) { placeables.forEach { placeable -> placeable.placeRelative(x = 0, y = 0) } }
Концевая лямбда перебирает все дочерние компоненты (которые теперь представляют объекты Placeable) с помощью функции placeables.forEach()
и позиционирует их
относительно позиции по умолчанию, назначенной родительским элементом. В данном случае для простоты позиционирование производится на точку с координатами x=0 и y=0.
Рассмотрим применение своего контейнера компоновки:
package com.example.helloapp import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.tooling.preview.Preview import androidx.compose.runtime.Composable import androidx.compose.ui.layout.Layout class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent{ Box(Modifier.fillMaxSize()) { MyLayout(spacing = 20){ Box(Modifier.size(100.dp).background(Color.DarkGray)) Box(Modifier.size(100.dp).background(Color.Red)) Box(Modifier.size(100.dp).background(Color.Blue)) Box(Modifier.size(100.dp).background(Color.Magenta)) Box(Modifier.size(100.dp).background(Color.Green)) } } } } } @Composable fun MyLayout(modifier: Modifier = Modifier, spacing: Int = 0, content: @Composable () -> Unit){ Layout(modifier = modifier,content = content) { measurables, constraints -> val placeables = measurables.map { measurable -> measurable.measure(constraints) } layout(constraints.maxWidth, constraints.maxHeight) { var yCoord = 0 var xCoord = 0 placeables.forEach { placeable -> // размещаем текущий дочерний компонент placeable.placeRelative(x = xCoord, y = yCoord) // устанавливаем x-координату для следующего компонента xCoord += placeable.width + spacing // если дошли до края контейнера if((xCoord + placeable.width) > constraints.maxWidth) { // увеличиваем y-координату (переход на следующую строку) yCoord += placeable.height + spacing // и сбрасываем x-координату xCoord = 0 } } } } }
Здесь наш кастомный контейнер MyLayout принимает три параметра:
@Composable fun MyLayout(modifier: Modifier = Modifier, spacing: Int = 0, content: @Composable () -> Unit){
Кроме стандартных параметров типа модификатора и содержимого контейнер также принимает параметр spacing - отступ между вложенными компонентами. По умолчанию он равен 0, соответственно параметр является необязательным для установки.
Внутри MyLayout в компоненте Layout
выполняем размещение каждого вложенного компонента с помощью следующего кода:
layout(constraints.maxWidth, constraints.maxHeight) { var yCoord = 0 var xCoord = 0 placeables.forEach { placeable -> placeable.placeRelative(x = xCoord, y = yCoord) xCoord += placeable.width + spacing if((xCoord + placeable.width) > constraints.maxWidth) { yCoord += placeable.height + spacing xCoord = 0 } } }
Для установки координат x и y здесь определены дополнительные переменные xCoord и yCoord соответственно. ПРи переборе каждого вложенного компонента вначале устанавливаем его позицию на основании переменных xCoord и yCoord:
placeable.placeRelative(x = xCoord, y = yCoord)
Затем увеличиваем значение координаты x на ширину размещенного компонента плюс отступ:
xCoord += placeable.width + spacing
Если координата x вышла за диапазон допустимой ширины, то сбрасываем переменную xCoord и увеличиваем переменную yCoord на высоту размещенного компонента плюс отступ. ТО есть таким образом мы перейдем на следующую строку:
if((xCoord + placeable.width) > constraints.maxWidth) { yCoord += placeable.height + spacing xCoord = 0 }
Для установки следующего компонента, таким образом, будут применяться уже измененные значения xCoord и yCoord.
ПРименение нашего контейнера компоновки аналогично другим компонентам. В данном случае устанавливаем параметр spacing и размещаем внутри несколько типовых компонентов:
MyLayout(spacing = 20){ Box(Modifier.size(100.dp).background(Color.DarkGray)) Box(Modifier.size(100.dp).background(Color.Red)) Box(Modifier.size(100.dp).background(Color.Blue)) Box(Modifier.size(100.dp).background(Color.Magenta)) Box(Modifier.size(100.dp).background(Color.Green)) }