ConstraintLayout en Compose

ConstraintLayout es un diseño que te permite colocar elementos componibles en relación con otros del mismo tipo en la pantalla. Es una alternativa al uso de varios Row, Column, Box y otros elementos de diseño personalizados anidados. ConstraintLayout es útil cuando se implementan diseños más grandes con requisitos de alineación más complejos.

Considera usar ConstraintLayout en las siguientes situaciones:

  • A fin de evitar anidar varios Column y Row para posicionar elementos en la pantalla a fin de mejorar la legibilidad del código.
  • Para posicionar elementos de componibilidad en relación con otros elementos que admiten composición, o bien posicionarlos según lineamientos, barreras o cadenas.

Nota: En el sistema de View, ConstraintLayout era la forma recomendada para crear diseños grandes y complejos, ya que una jerarquía de vistas plana era mejor, en términos de rendimiento, que las vistas anidadas. Sin embargo, eso no es problema en Compose, ya que puede procesar de manera eficiente jerarquías de diseño detalladas.

Para comenzar con ConstraintLayout

Para usar ConstraintLayout en Compose, debes agregar esta dependencia en tu build.gradle (además de la configuración de Compose):

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

En Compose, ConstraintLayout funciona de la siguiente manera con una DSL:

  • Crea referencias para cada elemento componible en ConstraintLayout mediante createRefs() o createRefFor().
  • Las restricciones se proporcionan mediante el modificador constrainAs(), que toma la referencia como parámetro y te permite especificar sus restricciones en la expresión lambda del cuerpo.
  • Las restricciones se especifican mediante linkTo() o algún otro método útil.
  • parent es una referencia existente que se puede usar para especificar restricciones hacia el mismo elemento ConstraintLayout.

En este ejemplo vemos uno de esos elementos que usa un 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)
            }
        )
    }
}

Este código restringe la parte superior del Button al elemento principal, con un margen de 16.dp, y un Text a la parte inferior del Button, también con un margen de 16.dp.

Muestra un botón y un elemento de texto organizados en un ConstraintLayout

API desacoplada

En el ejemplo de ConstraintLayout, las restricciones se especifican de forma intercalada, con un modificador en el elemento que admite composición al que se aplican. Sin embargo, hay situaciones en las que es preferible desacoplar las restricciones de los diseños a los que se aplican. Por ejemplo, quizás querrías cambiar las restricciones en función de la configuración de la pantalla o agregar una animación entre dos conjuntos de restricciones.

En casos como esos, puedes usar ConstraintLayout de otro modo:

  1. Pasa un ConstraintSet como parámetro a ConstraintLayout.
  2. Asigna referencias creadas en el ConstraintSet a los elementos componibles con el 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)
        }
    }
}

Luego, cuando necesites cambiar las restricciones, simplemente puedes pasar un ConstraintSet diferente.

Conceptos de ConstraintLayout

ConstraintLayout contiene conceptos, como lineamientos, barreras y cadenas, que pueden ayudarte a posicionar elementos dentro del elemento de componibilidad.

Guías

Las guías son pequeñas ayudas visuales para crear diseños. Los elementos de componibilidad pueden estar limitados a un guía. Las guías son útiles para posicionar elementos en un cierto dp o percentage dentro del elemento componible superior.

Existen dos tipos de guías: verticales y horizontales. Las dos horizontales son top y bottom, y las dos verticales son start y 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 crear una guía, usa createGuidelineFrom* con el tipo requerido. Esto crea una referencia que se puede usar en el bloque Modifier.constrainAs().

Barreras

Las barreras hacen referencia a varios elementos componibles para crear un lineamiento virtual basado en el widget más extremo del lado especificado.

Para crear una barrera, usa createTopBarrier() (o createBottomBarrier(), createEndBarrier(), createStartBarrier()) y proporciona las referencias que la conformarán.

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

        val topBarrier = createTopBarrier(button, text)
    }
}

La barrera se puede usar en un bloque Modifier.constrainAs().

Cadenas

Las cadenas proporcionan un comportamiento similar al del grupo en un solo eje (horizontal o vertical). El otro eje se puede restringir de forma independiente.

Para crear una cadena, usa createVerticalChain o 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)
    }
}

La cadena se puede usar en el bloque Modifier.constrainAs().

Se puede configurar una cadena con diferentes ChainStyles, que deciden cómo administrar el espacio que rodea un elemento componible, por ejemplo:

  • ChainStyle.Spread: El espacio se distribuye de manera uniforme entre todos los elementos de componibilidad, incluidos los espacios libres antes del primer elemento de componibilidad y después del último.
  • ChainStyle.SpreadInside: El espacio se distribuye de manera uniforme entre todos los elementos de componibilidad, sin ningún espacio libre antes del primer elemento que admite composición ni después del último.
  • ChainStyle.Packed: El espacio se distribuye antes del primero y después del último elemento de componibilidad, que se empaquetan sin espacio entre sí.

Más información

Obtén más información sobre ConstraintLayout en Compose a partir del funcionamiento de las APIs en los ejemplos de Compose que usan ConstraintLayout.