ConstraintLayout no Compose

O ConstraintLayout é um layout que permite posicionar elementos combináveis em relação a outros elementos na tela. Essa é uma alternativa ao uso de vários elementos Row, Column, Box aninhados e outros elementos de layout personalizados. O ConstraintLayout é útil para implementar layouts maiores com requisitos de alinhamento mais complicados.

Use ConstraintLayout nos seguintes cenários:

  • Para evitar o aninhamento de vários Columns e Rows quando for posicionar elementos na tela para melhorar a legibilidade do código.
  • Para posicionar elementos de composição em relação a outros ou para posicionar elementos com base em diretrizes, barreiras ou cadeias.

No sistema de visualização, ConstraintLayout era a maneira recomendada de criar layouts grandes e complexos, já que uma hierarquia de visualização plana era melhor para o desempenho do que as visualizações aninhadas. No entanto, isso não é um problema no Compose, que consegue processar hierarquias de layout profundas com eficiência.

Comece a usar o ConstraintLayout

Para usar o ConstraintLayout no Compose, é necessário adicionar essa dependência ao build.gradle, além da configuração do Compose:

implementation "androidx.constraintlayout:constraintlayout-compose:1.0.1"

No Compose, o ConstraintLayout funciona da seguinte maneira usando uma DSL:

  • Crie referências para cada elemento combinável no ConstraintLayout usando createRefs() ou createRefFor().
  • As restrições são fornecidas com o modificador constrainAs(), que usa a referência como um parâmetro e permite especificar as restrições no lambda do corpo.
  • As restrições são especificadas usando linkTo() ou outros métodos úteis.
  • parent é uma referência atual que pode ser usada para especificar restrições no próprio ConstraintLayout de composição.

Veja o exemplo de um elemento de composição usando 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)
            }
        )
    }
}

Esse código restringe o topo do Button ao pai com uma margem de 16.dp e um Text na parte inferior de Button, também com uma margem de 16.dp.

Mostra um botão e um elemento de texto organizados em um ConstraintLayout

API dissociada

No exemplo do ConstraintLayout, as restrições são especificadas inline, com um modificador na função de composição a que elas são aplicadas. No entanto, há situações em que é preferível dissociar as restrições dos layouts aos que elas se aplicam. Por exemplo, talvez você queira mudar as restrições com base na configuração da tela ou colocar uma animação entre dois conjuntos de restrições.

Para casos como esses, é possível usar ConstraintLayout de uma maneira diferente:

  1. Transmita um ConstraintSet como um parâmetro para ConstraintLayout.
  2. Atribua referências criadas no ConstraintSet aos elementos combináveis usando o modificador 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)
        }
    }
}

Assim, quando precisar mudar as restrições, basta transmitir um ConstraintSet diferente.

Conceitos de ConstraintLayout

O ConstraintLayout contém conceitos, como diretrizes, barreiras e cadeias, que podem ajudar a posicionar elementos dentro do elemento combinável.

Diretrizes

Diretrizes são pequenos auxiliares visuais para criar layouts. Os elementos de composição podem ser restritos a uma diretriz. Elas são úteis para posicionar elementos em uma determinada dp ou percentage dentro do elemento combinável pai.

Há dois tipos de diretrizes: vertical e horizontal. Os dois horizontais são top e bottom, e os dois verticais são start e 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)
}

Para criar uma diretriz, use o createGuidelineFrom* com o tipo de diretriz necessário. Isso cria uma referência que pode ser usada no bloco Modifier.constrainAs().

Barreiras

As barreiras são vários elementos combináveis quem criam uma diretriz virtual com base no widget mais extremo do lado especificado.

Para criar uma, use createTopBarrier() (ou: createBottomBarrier(), createEndBarrier(), createStartBarrier()) e forneça as referências que precisam compor a barreira.

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

        val topBarrier = createTopBarrier(button, text)
    }
}

A barreira pode ser usada em um bloco Modifier.constrainAs().

Cadeias

As cadeias fornecem comportamentos semelhantes a grupos em um único eixo (horizontal ou vertical). O outro eixo pode ser restrito de maneira independente.

Para criar uma cadeia, use createVerticalChain ou 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)
    }
}

A cadeia pode ser usada no bloco Modifier.constrainAs().

Uma cadeia pode ser configurada com diferentes ChainStyles, que decidem como lidar com o espaço ao redor de um elemento combinável, como:

  • ChainStyle.Spread: o espaço é distribuído uniformemente entre todos os elementos de composição, incluindo o espaço livre antes do primeiro elemento e após o último.
  • ChainStyle.SpreadInside: o espaço é distribuído uniformemente entre todos os elementos de composição, sem que haja espaço livre antes do primeiro elemento ou após o último.
  • ChainStyle.Packed: o espaço é distribuído antes do primeiro e depois do último elemento combinável. Esses elementos são compactados sem espaço entre si.

Saiba mais

Saiba mais sobre o ConstraintLayout no Compose usando as APIs em ação no Exemplos do Compose que usam ConstraintLayout (link em inglês).