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

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

  1. Композиция элементов
  2. Макет элементов
  3. Рисование элементов

Compose преобразует состояние в пользовательский интерфейс с помощью композиции, макета и рисования.

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

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

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

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

Компонуемые функции – это базовый строительный блок Compose. Composable-функция – это функция, которая генерирует Unit, описывающий часть пользовательского интерфейса. Функция принимает некоторые входные данные и генерирует то, что показано на экране. Дополнительную информацию о composable-функциях можно найти в документации Ментальная модель 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-функцию, чтобы объединить эти макеты в более сложный макет, подходящий для вашего приложения.

Сравнивает три простых компонуемых макета: столбец, строку и поле.

Чтобы задать положение дочерних элементов в 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, вы можете использовать модификаторы для украшения или дополнения ваших составных элементов. Модификаторы необходимы для настройки макета. Например, здесь мы объединяем несколько модификаторов, чтобы настроить 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 предоставляет большое количество composable-функций на основе Material Design с зависимостью androidx.compose.material:material (включается при создании проекта Compose в Android Studio), что упрощает создание пользовательского интерфейса. Предоставляются такие элементы, как Drawer, FloatingActionButton и TopAppBar.

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

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

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

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

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

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