ConstraintLayout в Compose

ConstraintLayout – это макет, который позволяет размещать composable-функции относительно других composable-функций на экране. Это альтернатива использованию нескольких вложенных элементов Row, Column, Box и других настраиваемых элементов макета.

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

Настроить ConstraintLayout

Чтобы использовать ConstraintLayout в Compose, добавьте эту зависимость в файл build.gradle (в дополнение к настройке Compose):

implementation "androidx.constraintlayout:constraintlayout-compose:$constraintlayout_compose_version"

ConstraintLayout в Compose работает следующим образом с использованием DSL:

  • Создайте ссылки для каждого элемента composable в ConstraintLayout с помощью createRefs() или createRefFor().
  • Ограничения задаются с помощью модификатора constrainAs(), который принимает ссылку в качестве параметра и позволяет указать ограничения в теле лямбда-выражения.
  • Ограничения задаются с помощью linkTo() или других методов.
  • parent – это существующая ссылка, которую можно использовать для указания ограничений в отношении самого компонента ConstraintLayout.

Пример composable-функции с использованием ConstraintLayout:

@Composable
fun ConstraintLayoutContent() {
    ConstraintLayout {
        // Create references for the composables to constrain
        val (button, text) = createRefs()

        Button(
            onClick = { /* Do something */ },
            // Assign reference "button" to the Button composable
            // and constrain it to the top of the ConstraintLayout
            modifier = Modifier.constrainAs(button) {
                top.linkTo(parent.top, margin = 16.dp)
            }
        ) {
            Text("Button")
        }

        // Assign reference "text" to the Text composable
        // and constrain it to the bottom of the Button composable
        Text(
            "Text",
            Modifier.constrainAs(text) {
                top.linkTo(button.bottom, margin = 16.dp)
            }
        )
    }
}

Этот код привязывает верхнюю часть Button к родительскому элементу с полем 16.dp и Text к нижней части Button также с полем 16.dp.

Кнопка появляется над текстом.
Рисунок 1. Button и Text composable-функции, связанные друг с другом в ConstraintLayout.

Дезагрегированный API

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

В таких случаях вы можете использовать ConstraintLayout по-другому:

  1. Передайте ConstraintSet в качестве параметра в ConstraintLayout.
  2. Назначьте ссылки, созданные в ConstraintSet, composable-функциям с помощью модификатора layoutId.

@Composable
fun DecoupledConstraintLayout() {
    BoxWithConstraints {
        val constraints = if (minWidth < 600.dp) {
            decoupledConstraints(margin = 16.dp) // Portrait constraints
        } else {
            decoupledConstraints(margin = 32.dp) // Landscape constraints
        }

        ConstraintLayout(constraints) {
            Button(
                onClick = { /* Do something */ },
                modifier = Modifier.layoutId("button")
            ) {
                Text("Button")
            }

            Text("Text", Modifier.layoutId("text"))
        }
    }
}

private fun decoupledConstraints(margin: Dp): ConstraintSet {
    return ConstraintSet {
        val button = createRefFor("button")
        val text = createRefFor("text")

        constrain(button) {
            top.linkTo(parent.top, margin = margin)
        }
        constrain(text) {
            top.linkTo(button.bottom, margin)
        }
    }
}

Затем, когда вам нужно изменить ограничения, вы можете просто передать другой ConstraintSet.

ConstraintLayout концепция

В ConstraintLayout есть такие концепции, как направляющие, барьеры и цепочки, которые помогают позиционировать элементы внутри composable-функции.

Правила

Направляющие – это небольшие визуальные подсказки, которые помогают создавать макеты. Составные элементы могут быть привязаны к направляющей. Направляющие помогают размещать элементы в определенной позиции по оси dp или percentage внутри родительской composable-функции.

Существует два типа направляющих: вертикальные и горизонтальные. Горизонтальные – top и bottom, вертикальные – start и end.

ConstraintLayout {
    // Create guideline from the start of the parent at 10% the width of the Composable
    val startGuideline = createGuidelineFromStart(0.1f)
    // Create guideline from the end of the parent at 10% the width of the Composable
    val endGuideline = createGuidelineFromEnd(0.1f)
    //  Create guideline from 16 dp from the top of the parent
    val topGuideline = createGuidelineFromTop(16.dp)
    //  Create guideline from 16 dp from the bottom of the parent
    val bottomGuideline = createGuidelineFromBottom(16.dp)
}

Чтобы создать направляющую, используйте createGuidelineFrom* с нужным типом направляющей. Это создает ссылку, которую можно использовать в блоке Modifier.constrainAs().

Барьеры

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

Чтобы создать барьер, используйте createTopBarrier() (или createBottomBarrier(), createEndBarrier(), createStartBarrier()) и укажите ссылки, которые должны составлять барьер.

ConstraintLayout {
    val constraintSet = ConstraintSet {
        val button = createRefFor("button")
        val text = createRefFor("text")

        val topBarrier = createTopBarrier(button, text)
    }
}

Затем барьер можно использовать в блоке Modifier.constrainAs().

Сети

Цепочки обеспечивают групповое поведение по одной оси (горизонтальной или вертикальной). Другую ось можно ограничить независимо.

Чтобы создать сеть, используйте createVerticalChain или createHorizontalChain:

ConstraintLayout {
    val constraintSet = ConstraintSet {
        val button = createRefFor("button")
        val text = createRefFor("text")

        val verticalChain = createVerticalChain(button, text, chainStyle = ChainStyle.Spread)
        val horizontalChain = createHorizontalChain(button, text)
    }
}

Затем цепочку можно использовать в блоке Modifier.constrainAs().

Для цепочки можно настроить разные ChainStyles, которые определяют, как обрабатывать пространство вокруг composable-функции, например:

  • ChainStyle.Spread – пространство равномерно распределяется между всеми composable-функциями, включая свободное пространство перед первой и после последней функции.
  • ChainStyle.SpreadInside – пространство распределяется равномерно между всеми composable-функциями, без свободного пространства перед первой и после последней.
  • ChainStyle.Packed – свободное пространство распределяется до первой и после последней composable-функции, а между ними нет пробелов.