Макеты Flow в Compose

FlowRow и FlowColumn – это composable-функции, похожие на Row и Column, но отличающиеся тем, что элементы переносятся на следующую строку, когда в контейнере заканчивается место. Это создаёт несколько строк или столбцов. Количество элементов в строке также можно контролировать, задав maxItemsInEachRow или maxItemsInEachColumn. Часто для создания адаптивных макетов можно использовать FlowRow и FlowColumn – контент не будет обрезан, если элементы слишком велики для одного измерения, а использование комбинации maxItemsInEach* с Modifier.weight(weight) может помочь в создании макетов, которые при необходимости заполняют/расширяют ширину строки или столбца.

Типичный пример – чип или интерфейс фильтрации:

5 фишек в FlowRow, показывающих переполнение следующей строки, когда больше нет свободного места.
Рисунок 1. Пример FlowRow

Основные функции

Чтобы использовать FlowRow или FlowColumn, создайте эти composable-функции и поместите в них элементы, которые должны следовать стандартному потоку:

@Composable
private fun FlowRowSimpleUsageExample() {
    FlowRow(modifier = Modifier.padding(8.dp)) {
        ChipItem("Price: High to Low")
        ChipItem("Avg rating: 4+")
        ChipItem("Free breakfast")
        ChipItem("Free cancellation")
        ChipItem("£50 pn")
    }
}

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

Особенности макета с плавающим размещением

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

Расположение по главной оси: горизонтальное или вертикальное.

Главная ось – это ось, на которой размещаются элементы (например, в FlowRow элементы расположены горизонтально). Параметр horizontalArrangement в FlowRow определяет, как свободное пространство распределяется между элементами.

В таблице ниже показаны примеры настройки horizontalArrangement для товаров в FlowRow.

Для горизонтального расположения установлено значение FlowRow

Результат

Arrangement.Start (Default)

Объекты выровнены по началу

Arrangement.SpaceBetween

Расположение элементов с пробелом между ними

Arrangement.Center

Товары расположены в центре

Arrangement.End

Элементы расположены в конце

Arrangement.SpaceAround

Объекты, расположенные с пространством вокруг них

Arrangement.spacedBy(8.dp)

Элементы, разделенные определенным количеством dp

Для FlowColumn доступны аналогичные варианты с verticalArrangement, по умолчанию – Arrangement.Top.

Расположение по поперечной оси

Поперечная ось – это ось, перпендикулярная главной. Например, на FlowRow это вертикальная ось. Чтобы изменить расположение всего содержимого контейнера по поперечной оси, используйте verticalArrangement для FlowRow и horizontalArrangement для FlowColumn.

В таблице ниже показано, как задать разные значения verticalArrangement для товаров в FlowRow.

Для вертикального выравнивания установлено значение FlowRow

Результат

Arrangement.Top (Default)

Раскладка в контейнере со съемкой сверху

Arrangement.Bottom

Расположение контейнера внизу

Arrangement.Center

Выравнивание контейнера по центру

Для FlowColumn похожие варианты доступны в horizontalArrangement. По умолчанию используется Arrangement.Start.

Выравнивание отдельных элементов

Возможно, вы захотите расположить отдельные элементы в строке с разным выравниванием. Это свойство отличается от verticalArrangement и horizontalArrangement тем, что выравнивает элементы внутри текущей строки. Вы можете применить это с помощью Modifier.align().

Например, если элементы в FlowRow имеют разную высоту, строка принимает высоту самого большого элемента и применяет к элементам Modifier.align(alignmentOption):

Вертикальное выравнивание установлено на FlowRow

Результат

Alignment.Top (Default)

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

Alignment.Bottom

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

Alignment.CenterVertically

Объекты выровнены по центру

Для FlowColumn доступны похожие варианты. По умолчанию используется выравнивание Alignment.Start.

Максимальное количество элементов в строке или столбце

Параметры maxItemsInEachRow или maxItemsInEachColumn определяют максимальное количество элементов на главной оси, которое может быть в одной строке, прежде чем произойдет перенос на следующую. Значение по умолчанию – Int.MAX_INT. В этом случае в строку помещается максимально возможное количество элементов.

Например, если задать maxItemsInEachRow, в начальном макете будет только три элемента:

Максимальное значение не задано

maxItemsInEachRow = 3

Максимальное значение не задано в строке потока Максимальное количество объектов в строке

Вес товаров

Вес увеличивает элемент на основе его коэффициента и доступного пространства на строке, в которой он был размещен. Важно отметить, что FlowRow и Row по-разному используют веса для расчета ширины элемента. Для Rows вес рассчитывается на основе всех элементов в Row. При использовании FlowRow вес определяется на основе элементов в строке, в которой размещен элемент, а не всех элементов в контейнере FlowRow.

Например, если у вас есть четыре объекта, которые находятся на одной линии и имеют разные веса 1f, 2f, 1f и 3f, то общий вес будет равен 7f. Оставшееся место в строке или столбце будет разделено на 7f. Ширина каждого элемента будет рассчитываться по формуле weight * (remainingSpace / totalWeight).

Вы можете использовать сочетание Modifier.weight и максимального количества элементов с FlowRow или FlowColumn, чтобы создать макет в виде сетки. Этот подход полезен для создания адаптивных макетов, которые подстраиваются под размер вашего устройства.

Ниже приведены примеры того, как можно использовать весовые коэффициенты. Например, это может быть сетка, в которой все элементы имеют одинаковый размер, как показано ниже:

Сетка создана со строкой потока
Рисунок 2. Создание сетки с помощью FlowRow

Чтобы создать сетку с элементами одинакового размера, выполните следующие действия:

val rows = 3
val columns = 3
FlowRow(
    modifier = Modifier.padding(4.dp),
    horizontalArrangement = Arrangement.spacedBy(4.dp),
    maxItemsInEachRow = rows
) {
    val itemModifier = Modifier
        .padding(4.dp)
        .height(80.dp)
        .weight(1f)
        .clip(RoundedCornerShape(8.dp))
        .background(MaterialColors.Blue200)
    repeat(rows * columns) {
        Spacer(modifier = itemModifier)
    }
}

Важно: если вы добавите ещё один элемент и повторите его 10 раз вместо 9, последний элемент займет весь последний столбец, так как общий вес для всей строки равен 1f:

Последний элемент в сетке в полном размере
Рисунок 3. Использование FlowRow для создания сетки, в которой последний элемент занимает всю ширину

Вы можете комбинировать веса с другими Modifiers, например Modifier.width(exactDpAmount), Modifier.aspectRatio(aspectRatio) или Modifier.fillMaxWidth(fraction). Все эти модификаторы работают вместе, позволяя адаптивно изменять размер элементов в FlowRow (или FlowColumn).

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

Чередующаяся сетка с плавающей строкой
Рисунок 4. FlowRow с чередующимися размерами строк

Для этого используйте следующий код:

FlowRow(
    modifier = Modifier.padding(4.dp),
    horizontalArrangement = Arrangement.spacedBy(4.dp),
    maxItemsInEachRow = 2
) {
    val itemModifier = Modifier
        .padding(4.dp)
        .height(80.dp)
        .clip(RoundedCornerShape(8.dp))
        .background(Color.Blue)
    repeat(6) { item ->
        // if the item is the third item, don't use weight modifier, but rather fillMaxWidth
        if ((item + 1) % 3 == 0) {
            Spacer(modifier = itemModifier.fillMaxWidth())
        } else {
            Spacer(modifier = itemModifier.weight(0.5f))
        }
    }
}

Дробные размеры

С помощью Modifier.fillMaxWidth(fraction) можно указать размер контейнера, который должен занимать элемент. Это отличается от того, как Modifier.fillMaxWidth(fraction) работает с Row или Column, поскольку элементы Row/Column занимают процент от оставшейся ширины, а не от ширины всего контейнера.

Например, следующий код дает разные результаты при использовании FlowRow и Row:

FlowRow(
    modifier = Modifier.padding(4.dp),
    horizontalArrangement = Arrangement.spacedBy(4.dp),
    maxItemsInEachRow = 3
) {
    val itemModifier = Modifier
        .clip(RoundedCornerShape(8.dp))
    Box(
        modifier = itemModifier
            .height(200.dp)
            .width(60.dp)
            .background(Color.Red)
    )
    Box(
        modifier = itemModifier
            .height(200.dp)
            .fillMaxWidth(0.7f)
            .background(Color.Blue)
    )
    Box(
        modifier = itemModifier
            .height(200.dp)
            .weight(1f)
            .background(Color.Magenta)
    )
}

FlowRow: средний элемент, занимающий 0,7 ширины контейнера.

Дробная ширина с плавающей строкой

Row: средний элемент занимает 0,7 % от оставшейся ширины Row.

Дробная ширина со строкой

fillMaxColumnWidth() и fillMaxRowHeight()

Применение Modifier.fillMaxColumnWidth() или Modifier.fillMaxRowHeight() к элементу внутри FlowColumn или FlowRow гарантирует, что элементы в том же столбце или строке будут иметь ту же ширину или высоту, что и самый большой элемент в столбце/строке.

Например, в этом примере используется FlowColumn для отображения списка десертов Android. Вы можете увидеть разницу в ширине каждого элемента, когда к ним применяется Modifier.fillMaxColumnWidth(), по сравнению с тем, когда это не так и элементы переносятся.

FlowColumn(
    Modifier
        .padding(20.dp)
        .fillMaxHeight()
        .fillMaxWidth(),
    horizontalArrangement = Arrangement.spacedBy(8.dp),
    verticalArrangement = Arrangement.spacedBy(8.dp),
    maxItemsInEachColumn = 5,
) {
    repeat(listDesserts.size) {
        Box(
            Modifier
                .fillMaxColumnWidth()
                .border(1.dp, Color.DarkGray, RoundedCornerShape(8.dp))
                .padding(8.dp)
        ) {

            Text(
                text = listDesserts[it],
                fontSize = 18.sp,
                modifier = Modifier.padding(3.dp)
            )
        }
    }
}

К каждому элементу применено Modifier.fillMaxColumnWidth()

fillMaxColumnWidth

Ширина не меняется (элементы переносятся)

Не задана максимальная ширина столбца без заполнения