В этой статье вспоминаем, как вообще работают лейауты в Compose, обсуждаем изменение лейаута отдельного компонента, смотрим, как создавать кастомные Layout и LazyLayout и учимся откладывать композицию. И всё это на примерах, в том числе из дизайн-системы Авито.
var imageHeightPx by remember { mutableStateOf(0) }Image(modifier = Modifier.onSizeChanged { size ->imageHeightPx = size.height})Text()
Text(modifier = Modifier.offset(y = imageHeightPx.toDp(),))
Такой подход, конечно, сработает, но вот отрисовываться лейаут будет за 2 фрейма из-за смены состояния и рекомпозиции.
Давайте разберем чуть подробнее, как работает Layout в Jetpack Compose.
Modifier в Compose — это объект, который используется для изменения или дополнения поведения и внешнего вида UI-элемента — composable-функции.
Modifier на самом деле — это тоже LayoutNode, то есть часть UI-дерева. Модификаторы оборачивают те composable-функции, к которым их применяют. На первом уровне ноды находится тот Modifier, который был применен первым. Он оборачивает следующий за ним модификатор и так далее. Composable-функция находится в самом низу.
Constraints — границы размера LayoutNode, то есть минимальное и максимальное значения, которое может принять LayoutNode. Constraints передаются от родителя к дочерним элементам, а размеры возвращаются наверх по мере их измерения.
fun Modifier.width(width: Dp) = this.then(Modifier.layout { measurable: Measurable, constraints: Constraints ->})
val newConstraints = constraints.copy(minWidth = width,maxWidth = width,)
val placeable = measurable.measure(newConstraints)
interface Measurable {fun measure(constraints: Constraints): Placeable
abstract class Placeable : Measured {var width: Intvar height: Int}
interface MeasureScope {fun layout(width: Int,height: Int,alignmentLines: Map<AlignmentLine, Int> = emptyMap(),placementBlock: Placeable.PlacementScope.() -> Unit): MeasureResult}
layout(placeable.width, placeable.height) {// PlacementScopeplaceable.place(0, 0)}
fun Modifier.width(width: Dp) = this.then(Modifier.layout { measurable: Measurable, constraints: Constraints ->val newConstraints = constraints.copy( minWidth = width,maxWidth = width,)val placeable = measurable.measure(newConstraints) // (1) измеряем дочерние нодыlayout(placeable.width, placeable.height) { // (2) определяем собственный размерplaceable.place(0, 0) // (3) располагаем дочерние ноды}})
class LayoutModifierImpl(var measureBlock: MeasureScope.(Measurable, Constraints) -> MeasureResult) : LayoutModifierNode {override fun MeasureScope.measure(measurable: Measurable,constraints: Constraints) = measureBlock(measurable, constraints)}
class TextStringSimpleNode(...) : LayoutModifierNode, DrawModifierNode {override fun MeasureScope.measure(measurable: Measurable,constraints: Constraints) { ... }override fun ContentDrawScope.draw() { ... }}
@Composablefun Column() {Layout(...)}
Layout(content = {Image()Text()},)
measurePolicy = { measurables: List<Measurable>, constraints: Constraints ->...}
val placeables = measurables.map {it.measure(constraints)}
layout(width = width, height = height) {placeables.forEach { placeable ->placeable.placeRelative(x, y)}}
@Composablefun Column() {Layout( // вызываем кастомный Layoutcontent = { // передаем набор composable-функций, которые будут отображаться в лейаутеImage()Text()},measurePolicy = { measurables: List<Measurable>, constraints: Constraints -> ...val placeables = measurables.map { // измеряем каждый Measurableit.measure(constraints)}...layout(width = width, height = height) {placeables.forEach { placeable ->placeable.placeRelative(x, y) // размещаем на экране}}},)}
Layout(content = {Image(Modifier.layoutId(ImageId))Text(Modifier.layoutId(TextId))}) { measurables, constraints ->val imageMeasurable = measurables.firstOrNull { it.layoutId == ImageId }...}
interface Measurable {val parentData: Any?...}
class ParentDataModifierImpl() : ParentDataModifier {override fun Density.modifyParentData(parentData: Any?) { ... }}parentData затем можно считать в лейауте:Layout(content = content,) { measurables, constraints ->measurables.forEach {it.parentData?.let { ... }}}
val Measurable.layoutId: Any?
get() = (parentData as? LayoutIdParentData)?.layoutId