ConstraintLayout в Compose

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

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

Настроить ConstraintLayout

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

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

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

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

Decoupled API

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

В таких случаях можно использовать ConstraintLayout следующим образом:

  1. Передайте ConstraintSet в качестве параметра в ConstraintLayout.
  2. Чтобы назначить ссылки, созданные в ConstraintSet, композициям, используйте модификатор 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 содержит такие концепции, как направляющие, барьеры и цепочки, которые помогают размещать элементы внутри композиции.

Правила

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

Существует два типа направляющих: вертикальные и горизонтальные. Горизонтальные стрелки – 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().

Барьеры

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

Чтобы создать барьер, используйте 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-функциями, включая свободное пространство до первой и после последней composable-функции.
  • ChainStyle.SpreadInside: пространство распределяется равномерно между всеми composable-функциями без свободного места до первой composable-функции и после последней composable-функции.
  • ChainStyle.Packed: пробел распределяется до первого и после последнего элемента, а сами элементы располагаются без пробелов между ними.