Cadenas En El ConstraintLayout

Las cadenas en el ConstraintLayout, son restricciones que permiten distribuir linealmente un grupo de views que están conectados bidireccionalmente.

Apariencia de cadenas en el ConstraintLayout

Las restricciones de posición aplicadas al view en la cadena, son independientes del otro eje. Esto quiere decir que si un widget pertenece a una cadena vertical, no afectará la distribución de una cadena horizontal de la que también forme parte.

El objetivo de este tutorial es mostrarte como:

  • Crear una cadena (Chain) en Android Studio
  • Diferenciar los estilos de cadenas
  • Conocer los atributos del ConstraintLayout para cadenas
  • Saber cómo influyen las márgenes en las cadenas

Nota: Este tutorial es la tercera parte de la guía del ConstraintLayout en Android (todo). Te recomiendo leer los contenidos Añadir ConstraintLayout y Restricciones para seguir un orden secuencial y comprender conocimientos previos.


Ejemplo De Cadenas En El ConstraintLayout

Usaremos como ilustración la creación de un layout básico de una calculadora, cuya naturaleza pone a prueba la distribución de botones para números y operaciones:

Ejemplo de App Calculadora en Android con ConstraintLayout

Con este ejemplo podrás practicar a fondo el uso de cadenas, con el fin de crear una cuadrícula con diferentes dimensiones y alineaciones. Puedes descargar el ejemplo desde el siguiente botón (el layout final se llama p3_cadenas.xml):


Crear Cadenas

Para explorar la creación de cadenas, primero crearemos un nuevo recurso de layout llamado p3_cadenas.xml con un elemento raíz del tipo ConstraintLayout:

Crear layout con ConstraintLayout

Y luego añadiremos tres botones para representar la fila del teclado con los números 1, 2 y 3:

Crear Cadena Horizontal

Ahora bien, para crear una cadena horizontal desde el editor de layouts en Android Studio, selecciona los views que harán parte de ella, luego haz click derecho en alguno de ellos y selecciona la opción Chains>Create Horizontal Chain:

Create Horizontal Chain Android Studio
Chains>Create Horizontal Chains

Android Studio añadirá automáticamente dos extremos para el primer y último view con el fin de establecer el eje. Y luego añadirá el vínculo entre cada eslabón. La cadena no alineará el grupo, por lo que usa las restricciones de alineación para conseguir este efecto.

Los enlaces de las cadenas se representan por el vínculo bidireccional entre los botones. En XML esto se traduce al uso de las restricciones de posicionamiento en ambos sentidos.

Por ejemplo, el uso de End_toStart y Start_toEnd entre el botón 1 y el 2:

<Button
    android:id="@+id/button"...    
    app:layout_constraintEnd_toStartOf="@+id/button2"/>

<Button
    android:id="@+id/button2"...       
    app:layout_constraintStart_toEndOf="@+id/button"/>

Con esto en mente, añade los botones para las siguientes filas de botones y crea las cadenas horizontales correspondientes:

Cadenas horizontales de calculadora con ConstraintLayout

Crear Cadena Vertical

Crear una cadena vertical es exactamente igual. Selecciona los views que serán distribuidos verticalmente, pero esta vez selecciona la opción Create Vertical Chain. Por ejemplo, si conectamos verticalmente a los botones 7, 4, 1 y 0:

Chains > Create Vertical Chain
Chains > Create Vertical Chain

Cabezas De Cadenas

Al primer elemento de una cadena se le denomina cabeza. Este alberga un conjunto de atributos que hacen control de los demás eslabones en la cadena.

Cabezas de cadenas
Cabezas de cadenas

La cabeza en una cadena horizontal es el elemento más a la izquierda del grupo y en una vertical, será el elemento en la parte superior.

Por esta razón en la imagen anterior, el botón 4 es la cabeza de la cadena horizontal y el botón 7 el de la vertical.


Estilos De Cadenas

Es posible cambiar el comportamiento de distribución de espacio entre los elementos de una cadena a partir de la aplicación de los siguientes estilos:

Estilos de cadenas en ConstraintLayout

En XML los estilos se asignan a los atributos app:layout_constraintHorizontal_chainStyle y app:layout_constraintVertical_chainStyle.

Estilo Extendido (CHAIN_SPREAD)

Es el estilo por defecto cuando creas una cadena. Con él, los eslabones se despliegan uniformemente sobre el espacio disponible.

Aplícalo seleccionando la cadena y luego yendo a Chains > * Chain > spread:

Usa el valor spread en el atributo de estilo. En la ilustración anterior, Android Studio agregaría el valor a la definición horizontal:

<Button
    android:id="@+id/button1"...
    app:layout_constraintHorizontal_chainStyle="spread" />

Estilo Extendido En El Interior (CHAIN_SPREAD_INSIDE)

Es igual que la cadena extendida normal, solo que los extremos de la cadena no se esparcen. Se representa por la constante CHAIN_SPREAD_INSIDE en Kotlin y spread_inside en XML.

Estilo Ponderado (Weitghted Chain)

Si la cabeza está usando los modos CHAIN_SPREAD o CHAIN_SPREAD_INSIDE y las dimensiones de los views tienen a MATCH_CONSTRAINT. Es posible manipular el espacio que ocupará el elemento a partir de ponderaciones (pesos).

Este concepto es similar al atributo android:weight del LinearLayout, donde el espacio es distribuido según la proporción de cada view.

En la ilustración inicial le dimos pesos de 1, 2 y 3 respectivamente a cada botón. Por esta razón el botón 3 ocupa el 50% del espacio disponible, ya que 3 es la mitad del peso total 6.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/button"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="1"
        android:visibility="visible"
        app:layout_constraintEnd_toStartOf="@+id/button2"
        app:layout_constraintHorizontal_chainStyle="spread"
        app:layout_constraintHorizontal_weight="1"
        app:layout_constraintStart_toStartOf="parent" />

    <Button
        android:id="@+id/button2"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="2"
        app:layout_constraintHorizontal_weight="2"
        app:layout_constraintEnd_toStartOf="@+id/button3"
        app:layout_constraintStart_toEndOf="@+id/button" />

    <Button
        android:id="@+id/button3"
        android:layout_width="0dp"
        app:layout_constraintHorizontal_weight="3"
        android:layout_height="wrap_content"
        android:text="3"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/button2" />

</androidx.constraintlayout.widget.ConstraintLayout>

Asigna el peso con el atributo app:layout_constraintHorizontal_weight o Asigna el peso con el atributo app:layout_constraintVertical_weight.

Estilo Empaquetado

Los elementos de la cadena se empaquetan juntos. Lo que permite que el sesgo afecte a todo el grupo como una unidad.

El valor correspondiente en la cabeza es la constante CHAIN_PACKED o el valor packed:

<Button
    android:id="@+id/button1"... 
    app:layout_constraintHorizontal_bias="0.3"
    app:layout_constraintHorizontal_chainStyle="packed"/>

El valor del bias se asigna en la cabeza de la cadena. La definición anterior aplica un 30% de sesgo como se mostró en la imagen del inicio.


Márgenes En Cadenas

A diferencia de las márgenes aplicadas en restricciones de posicionamiento relativo, las márgenes en un vínculo bilateral son aditivas. Esto quiere decir, que no se toma la de mayor tamaño, si no que se calcula la suma de sus valores:

En el ejemplo anterior vemos como el botón tiene 16dp de margen en su lado derecho, al igual que el botón 2 en su lado izquierdo.

Cómo ves, el widget es tomado en cuenta con sus márgenes antes de calcular el espacio resultante para la distribución de espacios.


Completar Layout De Calculadora

Para finalizar, completa el layout de la calculadora con el uso de cadenas y el estilo que te parezca conveniente.

En mi caso añadiré un TextView al inicio para la superficie del resultado y luego usaré MATCH_CONSTRAINT para los botones en ambas dimensiones.

Ejemplo de ConstraintLayout para calculadora

Como ves, cada botón se expande con un peso equitativo entre las cadenas verticales y horizontales. Además de la existencia de una margen de 16dp con respecto al texto de resultados y el borde inferior del padre.

La definición XML final sería la siguiente:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/button2"
        style="@style/Widget.MaterialComponents.Button.TextButton"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:text="2"
        android:textSize="30sp"
        android:visibility="visible"
        app:layout_constraintBottom_toTopOf="@+id/dot"
        app:layout_constraintEnd_toStartOf="@+id/button3"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/button1"
        app:layout_constraintTop_toBottomOf="@+id/button5"
        tools:visibility="visible" />

    <Button
        android:id="@+id/button3"
        style="@style/Widget.MaterialComponents.Button.TextButton"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:text="3"
        android:textSize="30sp"
        android:visibility="visible"
        app:layout_constraintBottom_toTopOf="@+id/equals"
        app:layout_constraintEnd_toStartOf="@+id/subtraction"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/button2"
        app:layout_constraintTop_toBottomOf="@+id/button6"
        tools:visibility="visible" />

    <Button
        android:id="@+id/button5"
        style="@style/Widget.MaterialComponents.Button.TextButton"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:text="5"
        android:textSize="30sp"
        android:visibility="visible"
        app:layout_constraintBottom_toTopOf="@+id/button2"
        app:layout_constraintEnd_toStartOf="@+id/button6"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/button4"
        app:layout_constraintTop_toBottomOf="@+id/button8"
        tools:visibility="visible" />

    <Button
        android:id="@+id/button6"
        style="@style/Widget.MaterialComponents.Button.TextButton"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:text="6"
        android:textSize="30sp"
        android:visibility="visible"
        app:layout_constraintBottom_toTopOf="@+id/button3"
        app:layout_constraintEnd_toStartOf="@+id/multiplication"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/button5"
        app:layout_constraintTop_toBottomOf="@+id/button9"
        tools:visibility="visible" />

    <Button
        android:id="@+id/button7"
        style="@style/Widget.MaterialComponents.Button.TextButton"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginTop="16dp"
        android:text="7"
        android:textSize="30sp"
        android:visibility="visible"
        app:layout_constraintBottom_toTopOf="@+id/button4"
        app:layout_constraintEnd_toStartOf="@+id/button8"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/operations_text"
        app:layout_constraintVertical_chainStyle="packed"
        tools:visibility="visible" />

    <Button
        android:id="@+id/button4"
        style="@style/Widget.MaterialComponents.Button.TextButton"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:text="4"
        android:textSize="30sp"
        android:visibility="visible"
        app:layout_constraintBottom_toTopOf="@+id/button1"
        app:layout_constraintEnd_toStartOf="@+id/button5"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/button7"
        tools:visibility="visible" />

    <Button
        android:id="@+id/button1"
        style="@style/Widget.MaterialComponents.Button.TextButton"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:text="1"
        android:textSize="30sp"
        android:visibility="visible"
        app:layout_constraintBottom_toTopOf="@+id/button0"
        app:layout_constraintEnd_toStartOf="@+id/button2"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/button4"
        tools:visibility="visible" />

    <Button
        android:id="@+id/button0"
        style="@style/Widget.MaterialComponents.Button.TextButton"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginBottom="16dp"
        android:text="0"
        android:textSize="30sp"
        android:visibility="visible"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/dot"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/button1"
        tools:visibility="visible" />

    <Button
        android:id="@+id/button8"
        style="@style/Widget.MaterialComponents.Button.TextButton"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginTop="16dp"
        android:text="8"
        android:textSize="30sp"
        android:visibility="visible"
        app:layout_constraintBottom_toTopOf="@+id/button5"
        app:layout_constraintEnd_toStartOf="@+id/button9"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/button7"
        app:layout_constraintTop_toBottomOf="@+id/operations_text"
        app:layout_constraintVertical_chainStyle="packed"
        tools:visibility="visible" />

    <Button
        android:id="@+id/button9"
        style="@style/Widget.MaterialComponents.Button.TextButton"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginTop="16dp"
        android:text="9"
        android:textSize="30sp"
        android:visibility="visible"
        app:layout_constraintBottom_toTopOf="@+id/button6"
        app:layout_constraintEnd_toStartOf="@+id/division"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/button8"
        app:layout_constraintTop_toBottomOf="@+id/operations_text"
        app:layout_constraintVertical_chainStyle="packed"
        tools:visibility="visible" />

    <Button
        android:id="@+id/dot"
        style="@style/Widget.MaterialComponents.Button.TextButton"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginBottom="16dp"
        android:text="."
        android:textSize="30sp"
        android:visibility="visible"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/equals"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/button0"
        app:layout_constraintTop_toBottomOf="@+id/button2"
        tools:visibility="visible" />

    <Button
        android:id="@+id/equals"
        style="@style/Widget.MaterialComponents.Button.TextButton"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginBottom="16dp"
        android:text="="
        android:textSize="30sp"
        android:visibility="visible"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/addition"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/dot"
        app:layout_constraintTop_toBottomOf="@+id/button3"
        tools:visibility="visible" />

    <Button
        android:id="@+id/addition"
        style="@style/Widget.MaterialComponents.Button.TextButton"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginBottom="16dp"
        android:text="+"
        android:textSize="30sp"
        android:visibility="visible"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/equals"
        app:layout_constraintTop_toBottomOf="@+id/subtraction"
        tools:visibility="visible" />

    <Button
        android:id="@+id/subtraction"
        style="@style/Widget.MaterialComponents.Button.TextButton"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:text="−"
        android:textSize="30sp"
        android:visibility="visible"
        app:layout_constraintBottom_toTopOf="@+id/addition"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/button3"
        app:layout_constraintTop_toBottomOf="@+id/multiplication"
        tools:visibility="visible" />

    <Button
        android:id="@+id/multiplication"
        style="@style/Widget.MaterialComponents.Button.TextButton"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:text="×"
        android:textSize="30sp"
        android:visibility="visible"
        app:layout_constraintBottom_toTopOf="@+id/subtraction"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/button6"
        app:layout_constraintTop_toBottomOf="@+id/division"
        tools:visibility="visible" />

    <Button
        android:id="@+id/division"
        style="@style/Widget.MaterialComponents.Button.TextButton"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginTop="16dp"
        android:text="÷"
        android:textSize="30sp"
        android:visibility="visible"
        app:layout_constraintBottom_toTopOf="@+id/multiplication"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/button9"
        app:layout_constraintTop_toBottomOf="@+id/operations_text"
        app:layout_constraintVertical_chainStyle="packed"
        tools:visibility="visible" />

    <TextView
        android:id="@+id/operations_text"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="#E3F2FD"
        android:gravity="bottom|end"
        android:padding="16dp"
        android:text="0.0"
        android:textColor="@color/black"
        android:textSize="30sp"
        app:layout_constraintDimensionRatio="w,2:3"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Objetos De Ayuda Virtual

En este tutorial aprendiste sobre el uso de cadenas en el ConstraintLayout para agrupar elementos con la misión de distribuirlos linealmente. Viste cómo crearlas en el editor de layouts de Android Studio, los diferentes estilos existentes y el computo de sus márgenes.

Puedes continuar hacia el tutorial de objetos de ayuda virtual para estudiar el uso de líneas de guía, barreras y grupos.


Más Contenidos Android

Únete Al Discord De Develou

Si tienes problemas con el código de este tutorial, preguntas, recomendaciones o solo deseas discutir sobre desarrollo Android conmigo y otros desarrolladores, únete a la comunidad de Discord de Develou y siéntete libre de participar como gustes. ¡Te espero!