Собственные макеты

В Compose элементы пользовательского интерфейса представлены компонуемыми функциями, которые при вызове создают фрагмент пользовательского интерфейса, который затем добавляется в дерево пользовательского интерфейса, отображаемое на экране. У каждого элемента интерфейса есть один родительский элемент и может быть несколько дочерних. Каждый элемент также находится внутри родительского элемента, заданного в виде позиции (x, y) и размера, заданного в виде width и height.

Родительские элементы определяют ограничения для дочерних. Элемент должен определить свой размер в рамках этих ограничений. Ограничения задают минимальную и максимальную width и height элемента. Если у элемента есть дочерние элементы, он может измерить каждый из них, чтобы определить свой размер. После того как элемент определит и сообщит свой размер, он может определить, как разместить свои дочерние элементы относительно себя, как подробно описано в разделе Создание пользовательских макетов.

Размещение каждого узла в дереве пользовательского интерфейса состоит из трех этапов. Каждый узел должен:

  1. Измерить рост детей
  2. Определять размер самостоятельно
  3. Разместить дочерние элементы
Три этапа макета узла: измерение дочерних элементов, определение размера, размещение дочерних элементов
Рисунок 1. Три этапа макета узла: измерение дочерних элементов, определение размера и размещение дочерних элементов.

Области действия определяют, когда можно измерять и размещать дочерние элементы. Измерение макета может быть выполнено только во время проходов измерения и макета, а дочерний элемент может быть размещен только во время проходов макета (и только после того, как он был измерен). Благодаря областям видимости Compose, таким как MeasureScope и PlacementScope, это правило применяется во время компиляции.

Как использовать модификатор макета

Вы можете использовать модификатор layout, чтобы изменить способ измерения и размещения элемента. Модификатор принимает лямбда-выражение. Его параметры включают элемент, который вы можете измерить, передаваемый как measurable, и входящие ограничения этого составного элемента, передаваемые как constraints. Модификатор специального макета может выглядеть следующим образом:

fun Modifier.customLayoutModifier() =
    layout { measurable, constraints ->
        // ...
    }

Показывает на экране Text и позволяет управлять расстоянием от верхнего края до базовой линии первой строки текста. Именно это и делает модификатор paddingFromBaseline. Здесь вы реализуете его в качестве примера. Для этого используйте модификатор layout, чтобы вручную разместить компонуемый объект на экране. Вот как будет выглядеть результат, если для верхнего поля Text задано значение 24.dp:

Показана разница между обычным заполнением интерфейса, которое задает расстояние между элементами, и заполнением текста, которое задает расстояние от одной базовой линии до другой.
Рисунок 2. Применен текст с paddingFromBaseline.

Вот код, который позволяет задать такое расстояние:

fun Modifier.firstBaselineToTop(
    firstBaselineToTop: Dp
) = layout { measurable, constraints ->
    // Measure the composable
    val placeable = measurable.measure(constraints)

    // Check the composable has a first baseline
    check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
    val firstBaseline = placeable[FirstBaseline]

    // Height of the composable with padding - first baseline
    val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
    val height = placeable.height + placeableY
    layout(placeable.width, height) {
        // Where the composable gets placed
        placeable.placeRelative(0, placeableY)
    }
}

Вот что происходит в этом коде:

  1. В лямбда-параметре measurable вы измеряете Text, представленный измеряемым параметром, вызывая measurable.measure(constraints).
  2. Вы указываете размер composable-функции, вызывая метод layout(width, height), который также предоставляет лямбда-выражение, используемое для размещения обернутых элементов. В этом случае высота равна высоте Text плюс placeableY, то есть firstBaselineToTop минус первая базовая линия Text.
  3. Вы можете разместить элементы на экране, вызвав placeable.placeRelative(x, y). Если обернутые элементы не размещены, они не будут видны. При написании справа налево placeRelative зеркально отражает положение x. Подробнее о направлении макета… Позиция y – это firstBaselineToTop минус первая базовая линия текста, поэтому первая базовая линия находится на расстоянии firstBaselineToTop от верха.

Чтобы проверить, работает ли это, используйте модификатор в Text:

@Preview
@Composable
fun TextWithPaddingToBaselinePreview() {
    MyApplicationTheme {
        Text("Hi there!", Modifier.firstBaselineToTop(32.dp))
    }
}

@Preview
@Composable
fun TextWithNormalPaddingPreview() {
    MyApplicationTheme {
        Text("Hi there!", Modifier.padding(top = 32.dp))
    }
}

Несколько предварительных просмотров текстовых элементов; один показывает обычный отступ между элементами, другой показывает отступ от одной базовой линии до следующей.
Рисунок 3. Модификатор, примененный к composable-функции Text, и предварительный просмотр.

Как создавать собственные макеты

Модификатор layout изменяет только вызывающую composable-функцию. Чтобы измерить и разместить несколько composable-функций, используйте composable-функцию Layout. Эта компонуемая функция позволяет вручную измерять и размещать дочерние элементы. Все макеты более высокого уровня, такие как Column и Row, создаются с помощью composable-функции Layout.

В этом примере создается очень простая версия Column. Большинство специальных макетов создаются по следующему шаблону:

@Composable
fun MyBasicColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // measure and position children given constraints logic here
        // ...
    }
}

Как и в случае с модификатором layout, measurables – это список дочерних элементов, которые нужно измерить, а constraints – ограничения родительского элемента. Следуя той же логике, что и раньше, MyBasicColumn можно реализовать следующим образом:

@Composable
fun MyBasicColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // Don't constrain child views further, measure them with given constraints
        // List of measured children
        val placeables = measurables.map { measurable ->
            // Measure each children
            measurable.measure(constraints)
        }

        // Set the size of the layout as big as it can
        layout(constraints.maxWidth, constraints.maxHeight) {
            // Track the y co-ord we have placed children up to
            var yPosition = 0

            // Place children in the parent layout
            placeables.forEach { placeable ->
                // Position item on the screen
                placeable.placeRelative(x = 0, y = yPosition)

                // Record the y co-ord placed up to
                yPosition += placeable.height
            }
        }
    }
}

Дочерние composable-функции ограничены входящими ограничениями Layout и размещаются на основе yPosition предыдущей composable-функции.

Вот как будет использоваться эта пользовательская composable-функция:

@Composable
fun CallingComposable(modifier: Modifier = Modifier) {
    MyBasicColumn(modifier.padding(8.dp)) {
        Text("MyBasicColumn")
        Text("places items")
        Text("vertically.")
        Text("We've done it by hand!")
    }
}

Несколько текстовых элементов, расположенных друг над другом в столбце.
Рисунок 4. Специальная реализация Column.

Направление макета

Измените направление макета composable-функции, изменив локальную композицию LocalLayoutDirection.

Если вы размещаете composable-функции на экране вручную, LayoutDirection доступен в свойстве layoutDirection класса MeasureScope в модификаторе layout или composable-функции Layout.

При использовании layoutDirection размещайте composable-функции с помощью place. В отличие от метода placeRelative, метод place не зависит от направления макета (слева направо или справа налево).

Примеры использования собственных макетов

Подробнее о макетах и модификаторах рассказывается в статье Базовые макеты в Compose. Примеры специальных макетов можно найти в статье Примеры создания специальных макетов в Compose.

Подробнее…

Чтобы узнать больше о пользовательских макетах в Compose, ознакомьтесь со следующими дополнительными ресурсами.

Видео