Основы макетов Compose

Jetpack Compose значительно упрощает разработку пользовательского интерфейса приложения. Compose преобразует состояние в элементы интерфейса с помощью:

  1. Состав элементов
  2. Макет элементов
  3. Рисование элементов

Как передавать состояние преобразования в интерфейс с помощью композиции, макета и рисования

В этом документе рассказывается о том, как размещать элементы интерфейса, и объясняется, как использовать некоторые из строительных блоков, которые предоставляет Compose.

Цели макетов в Compose

Система макетов в Jetpack Compose создана с двумя основными целями:

Основы composable-функций

Компонуемые функции – это основные строительные блоки Compose. Компонуемая функция – это функция, которая создает Unit, описывающий часть интерфейса. Функция принимает входные данные и генерирует то, что показывается на экране. Дополнительную информацию о композициях можно найти в документации по ментальной модели Compose.

Composable-функция может создавать несколько элементов пользовательского интерфейса. Однако, если вы не укажете, как их расположить, Compose может разместить элементы не так, как вам нужно. Например, этот код создает два текстовых элемента:

@Composable
fun ArtistCard() {
    Text("Alfred Sisley")
    Text("3 minutes ago")
}

Если не указать, как именно нужно расположить элементы, Compose разместит их друг над другом, и текст будет нечитаемым:

Два текстовых элемента, наложенных друг на друга, из-за чего текст нечитаем

Compose предоставляет набор готовых макетов, которые помогут вам расположить элементы интерфейса. Кроме того, вы можете легко создавать собственные, более специализированные макеты.

Стандартные компоненты макета

Во многих случаях можно просто использовать стандартные элементы макета Compose.

Используйте Column, чтобы разместить элементы на экране по вертикали.

@Composable
fun ArtistCardColumn() {
    Column {
        Text("Alfred Sisley")
        Text("3 minutes ago")
    }
}

Два текстовых элемента, расположенных в столбце, чтобы текст был читабельным.

Аналогично, чтобы разместить объекты на экране по горизонтали, используйте тег Row. Элементы Column и Row поддерживают настройку выравнивания содержащихся в них элементов.

@Composable
fun ArtistCardRow(artist: Artist) {
    Row(verticalAlignment = Alignment.CenterVertically) {
        Image(bitmap = artist.image, contentDescription = "Artist image")
        Column {
            Text(artist.name)
            Text(artist.lastSeenOnline)
        }
    }
}

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

Используйте Box, чтобы разместить элементы поверх друг друга. Box также поддерживает настройку выравнивания элементов, которые он содержит.

@Composable
fun ArtistAvatar(artist: Artist) {
    Box {
        Image(bitmap = artist.image, contentDescription = "Artist image")
        Icon(Icons.Filled.Check, contentDescription = "Check mark")
    }
}

Показывает два элемента, расположенных друг над другом

Часто этих блоков достаточно. Вы можете написать собственную composable-функцию, чтобы объединить эти макеты в более сложный макет, подходящий для вашего приложения.

Сравнение трех простых макетов: column, row и box

Чтобы задать позицию дочерних элементов в объекте Row, укажите аргументы horizontalArrangement и verticalAlignment. Для Column задайте аргументы verticalArrangement и horizontalAlignment:

@Composable
fun ArtistCardArrangement(artist: Artist) {
    Row(
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.End
    ) {
        Image(bitmap = artist.image, contentDescription = "Artist image")
        Column { /*...*/ }
    }
}

Объекты выровнены по правому краю.

Модель макета

В модели макета дерево интерфейса создается за один проход. Каждый узел сначала должен измерить себя, а затем рекурсивно измерить все дочерние узлы, передавая ограничения размера вниз по дереву. Затем узлы-листья получают размеры и размещаются, а полученные размеры и инструкции по размещению передаются обратно вверх по дереву.

Родительские элементы измеряются до дочерних, но размещаются после них.

Рассмотрим следующую функцию SearchResult.

@Composable
fun SearchResult() {
    Row {
        Image(
            // ...
        )
        Column {
            Text(
                // ...
            )
            Text(
                // ...
            )
        }
    }
}

Эта функция создает следующее дерево пользовательского интерфейса.

SearchResult
  Row
    Image
    Column
      Text
      Text

В примере SearchResult макет дерева интерфейса выглядит следующим образом:

  1. Корневой узел Row должен выполнить измерение.
  2. Корневой узел Row просит свой первый дочерний узел Image выполнить измерение.
  3. Image – это конечный узел (то есть у него нет дочерних узлов), поэтому он сообщает размер и возвращает инструкции по размещению.
  4. Корневой узел Row просит второй дочерний узел Column выполнить измерение.
  5. Узел Column просит дочерний узел Text выполнить измерение.
  6. Первый узел Text является конечным, поэтому он сообщает размер и возвращает инструкции по размещению.
  7. Узел Column просит дочерний узел Text выполнить измерение.
  8. Второй узел Text является конечным, поэтому он сообщает размер и возвращает инструкции по размещению.
  9. Теперь, когда узел Column измерил, разместил и задал размеры дочерних элементов, он может определить собственный размер и местоположение.
  10. Теперь, когда корневой узел Row измерил, разместил и задал размеры дочерних узлов, он может определить собственные размеры и положение.

Порядок измерения, определения размера и размещения в дереве интерфейса результатов поиска

Эффективность

Compose обеспечивает высокую производительность, поскольку собирает данные о детях только один раз. Однопроходное измерение позволяет Compose эффективно обрабатывать глубокие деревья интерфейса. Если элемент дважды измерил свой дочерний элемент, а тот, в свою очередь, дважды измерил каждый из своих дочерних элементов и так далее, то одна попытка разместить весь интерфейс потребует много работы, что может негативно сказаться на производительности приложения.

Если вам нужно несколько измерений, в Compose есть специальная система – внутренние измерения. Подробнее о встроенных измерениях в макетах Compose…

Поскольку измерение и размещение – это разные этапы прохода по макету, любые изменения, которые влияют только на размещение элементов, а не на измерение, можно выполнять отдельно.

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

Как описано в разделе Модификаторы Compose, вы можете использовать модификаторы, чтобы украшать или дополнять свои функции, созданные с помощью Compose. Модификаторы необходимы для настройки раскладки. Например, в приведенном ниже коде мы объединили несколько модификаторов, чтобы настроить ArtistCard:

@Composable
fun ArtistCardModifiers(
    artist: Artist,
    onClick: () -> Unit
) {
    val padding = 16.dp
    Column(
        Modifier
            .clickable(onClick = onClick)
            .padding(padding)
            .fillMaxWidth()
    ) {
        Row(verticalAlignment = Alignment.CenterVertically) { /*...*/ }
        Spacer(Modifier.size(padding))
        Card(
            elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
        ) { /*...*/ }
    }
}

Ещё более сложный макет, в котором с помощью модификаторов можно менять расположение графики и определять, какие области реагируют на ввод данных пользователем.

В приведенном выше коде вы можете увидеть, как используются разные функции-модификаторы.

  • clickable делает composable-функцию реагирующей на ввод данных пользователем и показывает эффект волны.
  • padding добавляет вокруг элемента пустое пространство.
  • fillMaxWidth позволяет composable-функции заполнить всю ширину, заданную родительским элементом.
  • size() – предпочтительная ширина и высота элемента.

Прокручиваемые макеты

Подробнее о макетах с возможностью прокрутки можно узнать из документации по жестам Compose.

Информацию о списках и ленивых списках можно найти в документации по Compose.

Адаптивные макеты

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

Ограничения

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

@Composable
fun WithConstraintsComposable() {
    BoxWithConstraints {
        Text("My minHeight is $minHeight while my maxWidth is $maxWidth")
    }
}

Макеты на основе рекламных мест

Compose предоставляет множество композиций на основе Material Design с зависимостью androidx.compose.material:material (включена при создании проекта Compose в Android Studio), чтобы упростить создание интерфейса. Предоставляются все элементы, например Drawer, FloatingActionButton и TopAppBar.

В компонентах Material активно используются API слотов – шаблон, представленный в Compose, чтобы добавить уровень настройки поверх композиций. Такой подход делает компоненты более гибкими, поскольку они принимают дочерний элемент, который может настраивать себя сам, а не раскрывать каждый параметр конфигурации дочернего элемента. Слоты оставляют в интерфейсе пустое место, которое разработчик может заполнить по своему усмотрению. Например, в TopAppBar можно настроить следующие слоты:

Диаграмма, на которой показаны доступные слоты в панели приложения Material Components

Компонуемые функции обычно принимают компонуемую лямбда-функцию content ( content: @Composable () -> Unit). API слотов предоставляют несколько параметров content для определенных целей. Например, TopAppBar позволяет указать контент для title, navigationIcon и actions.

Например, Scaffold позволяет реализовать интерфейс с базовой структурой макета Material Design. Scaffold предоставляет слоты для наиболее распространенных компонентов Material Design верхнего уровня, таких как TopAppBar, BottomAppBar, FloatingActionButton и Drawer. С помощью Scaffold можно легко убедиться, что эти компоненты правильно расположены и работают вместе.

Пример приложения JetNews, в котором для размещения нескольких элементов используется Scaffold

@Composable
fun HomeScreen(/*...*/) {
    ModalNavigationDrawer(drawerContent = { /* ... */ }) {
        Scaffold(
            topBar = { /*...*/ }
        ) { contentPadding ->
            // ...
        }
    }
}