Каждый раз, когда вызывается родительский компонуемый объект, он отвечает за управление размером и расположением всех своих дочерних элементов. Положение дочернего элемента определяется с использованием координат x и y относительно положения родителя. Что касается размера, родительский элемент накладывает ограничения, которые определяют максимально и минимально допустимые размеры высоты и ширины дочернего элемента. В зависимости от конфигурации размер родительского компонента может быть либо фиксированным (например, с помощью модификатора size()), либо рассчитан на основе размера и расположения его дочерних элементов. Все встроенные контейнеры - Box, Row и Column содержат логику, которая измеряет каждого дочернего компонента и вычисляет, как расположить каждый из них. И мы также можем использовать для создания собственных контейнеров все те же методы, которые применяются встроенными контейнерами.
Кастомные контейнеры компоновки можно разделить на две категории:
В самой базовой форме пользовательский контейнер может быть реализован как модификатор компоновк, который можно применить к одному элементу пользовательского интерфейса
(что-то похожее на стандартный модификатор padding()
)
Второй способ представляет создание своего объекта Layout, который будет применяться ко всем дочерним компонентам (аналогично Box, Column и Row)
Общий синтаксис реализации кастомного модификатора компоновки выглядит следующим образом:
fun Modifier.имя_модификатора (необязательные_параметры) = layout { measurable, constraints -> // код настройки позиции и размера компонента }
Концевой лямбде передаются два параметра с именами measurable
и constraints
. Параметр measurable
представляет дочерний компонент, для
которого был вызван модификатор, а параметр constraints
содержит максимальную и минимальную ширину и высоту, разрешенные для дочернего компонента.
Когда модификатор размещает дочерний компонент, ему необходимо знать размеры этого компонента, чтобы убедиться, что он соответствует ограничениям constraints
,
переданным в лямбда-выражение. Эти значения можно получить, вызвав у объекта measurable
метод measure()
, в который передается параметр constraints
:
val placeable = measurable.measure(constraints)
Этот вызов вернет объект класса Placeable, который в свойствах width
и height
содержит значения высоты и ширины.
Тип Placeable предоставляет ряд методов для установки новой позиции компонента в контейнере:
place()
placeRelative()
placeRelativeWithLayer()
Эти методы имеют множество различных версий, но все они помещают компонент на определенную позицию внутри контейнера.
Например определим модификатор компоновки, который помещает компонент на определенную позицию с координатами X и Y относительно позиции по умолчанию:
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.runtime.Composable import androidx.compose.ui.layout.layout class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent{ Box(Modifier.fillMaxSize()) { MyBox(Modifier.positionLayout(100, 50).background(Color.DarkGray)) } } } } fun Modifier.positionLayout(x: Int, y: Int) = layout { measurable, constraints -> val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { placeable.placeRelative(x, y) } } @Composable fun MyBox(modifier: Modifier) { Box(Modifier.size(200.dp).then(modifier)) }
Здесь определен кастомный модификатор компоновки positionLayout
(название произвольное). Пусть в качестве параметров он принимает координаты верхнего левого угла компонента относительно
позиции по умолчанию (если компонент один, то это будет позиция верхнего левого угла контейнера )- параметры x и y.
fun Modifier.positionLayout(x: Int, y: Int) = layout { measurable, constraints -> val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { placeable.placeRelative(x, y) } }
В концевой лямбде получаем объект Placeable
val placeable = measurable.measure(constraints)
Затем вызывается функция layout():
layout(placeable.width, placeable.height) { placeable.placeRelative(x, y) }
Эта функция получает высоту и ширину объекта Placeable. В качестве последнего параметра в функцию layout передается лямбда-выражение, которое собственно и выполняет позиционирование компонента.
В частности, для этого мы используем метод placeable.placeRelative()
, который принимает координаты x и y, по которым надо поместить компонент (координаты относительно верхнего левого угла контейнера).
Для примера здесь определяется кастомный компонент MyBox, который по сути является оберткой над Box:
@Composable fun MyBox(modifier: Modifier) { Box(Modifier.size(200.dp).then(modifier)) }
К Box применяется модификатор, который устанавливает размер прямоугольной области в 200 пикселей и добавляет функции модификаторов, которые передаются в компонент MyBox через единственный параметр.
ПРи вызове метода setContent() применяем наш модификатор positionLayout:
MyBox(Modifier.positionLayout(100, 50).background(Color.DarkGray))
ТО есть в данном случае компонент будет позиционироваться на координату с x=100 и y=50. И кроме того, к нему применяется темно-серый фон:
Причем опять же подчеркну, что позиция устанавливается относительно точки по умолчанию (в данном случае верхний левый угол контейнера). И если наш контейнер MyBox помещается в другой контейнер, то к MyBox применяются также отступы родительского контейнера. Например:
Box(Modifier.padding(10.dp, 40.dp)) { MyBox(Modifier.positionLayout(100, 50).background(Color.DarkGray)) }
В данном случае реальная позиция MyBox будет на точке x=(10+100)=110 и y=(40+50)=90.
Также при разработке своих макетов компоновки следует помнить, что дочерний компонент необходимо измерять только один раз при каждом вызове модификатора. Это так называемое однократное измерение (single-pass measurement) необходимо для обеспечения быстрой и эффективной визуализации иерархий дерева пользовательского интерфейса.
Другой пример - кастомный модификатор margin()
, который устанавливает внешние отступы, но его мезаника будет аналогична:
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.runtime.Composable import androidx.compose.ui.layout.layout class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent{ Box(Modifier.fillMaxSize()) { MyBox(Modifier.margin(200).background(Color.DarkGray)) } } } } fun Modifier.margin(all:Int) = layout { measurable, constraints -> val placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { placeable.placeRelative(all, all) } } @Composable fun MyBox(modifier: Modifier) { Box(Modifier.size(200.dp).then(modifier)) }