Fundamentos de Android: Diseño con ConstraintLayout

Fundamentos de Android: Diseño con ConstraintLayout

Hasta ahora en nuestros diseños hemos utilizado LinearLayout como contenedor de elementos. En esta entrada vamos a utilizar ConstraintLayout para realizar el diseño de una aplicación sencilla, además de repasar otros conceptos ya conocidos como, por ejemplo, el manejo de recursos de distinto tipo (dimensiones, texto, estilos, fuentes...) y la interacción del usuario que puedes revisar en entradas previas.

Un ConstraintLayout es un contenedor de elementos o ViewGroup (grupo de vistas), disponible a partir de API 9, que permite posicionar y dimensionar otros elementos o vistas (views) de manera flexible. Este tipo de contenedor permite crear diseños complejos sin otros grupos anidados. Además, es muy fácil de usar directamente desde el editor de diseño, arrastrando los elementos sin necesidad de editar el código XML.

Podemos entender un constraint (quizá en este contexto se puede traducir como fijación o sujeción) como una conexión o alineación entre dos elementos en la interfaz de usuario; y cada una conecta o alinea un elemento con otro, con el elemento padre o con una guía no visible. En este tipo de diseño, posicionamos cada elemento definiendo al menos una referencia horizontal y vertical respecto a otros elementos.

Vamos a practicar con este diseño creando un nuevo proyecto en Android Studio a partir de una actividad vacía (Empty Activity). Como sabemos, esta plantilla utiliza en el diseño un ConstraintLayout como grupo raíz por defecto (además de un TextView con el texto "Hello World!"):



Abrimos layout/activity_main.xml en la vista Design. Como vamos a ajustar el diseño manualmente, debemos desactivar la conexión automática de elementos; nos aseguramos que está desactivada desde un botón en la barra de herramientas como se ve en la imagen:



Junto al botón Autoconnect está el botón para establecer los márgenes por defecto (Default Margins) cuyo valor predeterminado es 8dp; lo ajustamos a 16dp, para que a partir de ahora se aplique a los nuevos constraints que utilicemos.



Seleccionamos el TextView y nos fijamos que en el panel de atributos vemos el inspector de elementos (view inspector), que solo está disponible para los elementos contenidos en un ConstraintLayout. Este inspector incluye controles para ajustar atributos del diseño como los constraints, los tipos de constraint, el descentramiento o desviación horizontal y vertical (constraint bias) y los márgenes.

Inspector de elementos (view inspector)

Vamos a practicar con el inspector de elementos o vistas. Por ejemplo, el atributo de desviación (constraint bias) posiciona el elemento a lo largo de los ejes horizontal y vertical (por defecto todo elemento se centra con un valor del 50%). Para cambiar los valores debemos arrastrar los controles deslizantes sobre los ejes y comprobamos que simultáneamente cambia la posición del TextView en los ejes de la pantalla.

Observamos en el inspector de vistas que el valor para los cuatro márgenes (izquierdo, derecho, superior e inferior) es 0; desde el desplegable, a los tres primeros lo cambiamos a 16 (que es 16dp). Podemos comprobar en la vista Text que se han añadido estos valores:
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
android:layout_marginRight="16dp"

En el inspector de vistas, las flechas dentro del cuadrado representan un tipo de constraint, con tres tipos posibles que cambian sucesivamente al pulsar sobre ellas:
  • Wrap Content: el elemento se expande solo lo necesario para ajustarse a su contenido.
  • Fixed: se especifica una dimensión fija para los márgenes del elemento.
  • Match Constraints: el elemento se expande tanto como sea posible (teniendo en cuenta los márgenes propios). Este tipo es muy flexible ya que permite que el diseño se adapte a diferentes tamaños y orientaciones de pantalla.

En nuestra aplicación, cambiamos izquierda y derecha a Match Constraints:



En el inspector de vistas, eliminamos la fijación inferior pulsando en el punto Delete Bottom Constraint. Ahora pasamos a la vista Text, y en el atributo android:layout_marginLeft extraemos el recurso de dimensión con el nombre margen_ancho:



Y lo aplicamos a los márgenes superior y derecho:
android:layout_marginLeft="@dimen/margen_ancho"
android:layout_marginTop="@dimen/margen_ancho"
android:layout_marginRight="@dimen/margen_ancho"

Ahora, desde el panel de atributos, le podemos agregar una tipografía desplegando el campo fontFamily y seleccionando More Fonts para abrir la ventana Resources; buscamos roboto y seleccionamos Roboto -> Regular; y la añadimos al proyecto:



Esto agrega la carpeta res/font que contiene el archivo de fuente roboto.ttf. Además, en la vista Text tenemos el atributo android:fontFamily="@font/roboto". En el atributo textSize le asignamos un valor de 24sp para el tamaño de la fuente y con el botón de los tres puntos creamos un nuevo recurso de dimensión:



Ahora en values/styles.xml podemos añadir un estilo personalizado como éste (fondo y texto de colores determinados, alineado al centro, de tamaño establecido en la dimensión box_text_size, negrita y fuente establecida en font/roboto):
<style name="whiteBox">
   <item name="android:background">@android:color/holo_green_light</item>
   <item name="android:textAlignment">center</item>
   <item name="android:textSize">@dimen/box_text_size</item>
   <item name="android:textStyle">bold</item>
   <item name="android:textColor">@android:color/white</item>
   <item name="android:fontFamily">@font/roboto</item>
</style>

Ahora en el atributo text (el que no tiene el icono de llave inglesa) pulsamos sobre los tres puntos para abrir la ventana Resources; y agregamos un nuevo recurso de texto:



Volvemos al TextView en vista de Design, y le aplicamos el estilo whiteBox:



Puesto que este estilo ya contiene el atributo android:fontFamily con el valor Roboto, en el TextView, en modo Text, podemos eliminar este atributo. Volvemos a la vista Design y pulsamos en el botón Device for preview (D) que por defecto muestra Pixel, para desplegar una lista de dispositivos, y seleccionamos algunos para ver cómo el TextView se adapta a las distintas pantallas. Podremos ver algo así:



Bien, ahora añadimos otro TextView bajo el que ya tenemos (arrastrándolo desde la paleta directamente a la vista previa) y lo alineamos manualmente al margen izquierdo. En el editor de diseño, con el nuevo TextView seleccionado, nos situamos sobre el punto superior, que es el controlador de sujeción (constraint handle), y entonces se vuelve verde y parpadea:



Entonces creamos un Top Constraint conectando la parte superior del TextView con la parte inferior del otro TextView:



Al crear la conexión entre ambos elementos, automáticamente el margen superior se ajusta a 16dp porque es el valor por defecto que configuramos antes. Repetimos el mismo proceso para crear una fijación en el lado izquierdo respecto al elemento raíz. Ten en cuenta que además de utilizar directamente el elemento desde el editor de diseño, también podemos crear fijaciones desde el inspector de vistas:



Desde el panel de atributos, ajustamos algunos valores así:
  • ID: box_dos_txt
  • layout_height: 130dp
  • layout_width: 130dp
  • text: @string/box_dos
  • style: @style/whiteBox



Ten en cuenta que estamos utilizando dimensiones fijas para la altura y anchura del TextView, pero suele ser más recomendable usar fijaciones flexibles y relativas, por ejemplo match_constraint o wrap_content, puesto que en general cuanto más elementos de tamaño fijo tiene una aplicación menos adaptable será su diseño para diferentes configuraciones de pantalla.

Otra forma de crear fijaciones es con una cadena de elementos (chain), esto es, agrupando o encadenando elementos vinculados entre sí con sujeciones bidireccionales. Por ejemplo, el siguiente diagrama muestra dos elementos que están fijados entre sí, lo que crea una cadena horizontal:



El primer elemento de una cadena se conoce como cabeza o cabecera de la cadena (head of the chain), y sus atributos controlan, posicionan y distribuyen todas los elementos en la cadena. Para cadenas horizontales, la cabeza es la vista más a la izquierda mientras que para cadenas verticales, la cabeza es la vista superior.

Se pueden aplicar distintos estilos a las cadenas para definir la forma en que los elementos encadenados se extienden y alinean:
  1. Spread: Después de tener en cuenta los márgenes, los elementos se distribuyen uniformemente en el espacio disponible (es el estilo por defecto).
  2. Spread inside: El primer y el último elementos se adjuntan al elemento padre en cada extremo de la cadena, mientras que el resto de elementos se distribuyen uniformemente en el espacio disponible.
  3. Weighted: Los elementos cambian de tamaño para llenar todo el espacio en función de los valores establecidos en los atributos layout_constraintHorizontal_weight o layout_constraintVertical_weight. De manera predeterminada, el espacio se distribuye uniformemente entre cada elemento según match constraints (0dp), pero se puede asignar un valor a cada elemento para ocupar más o menos espacio.
  4. Packed: Los elementos se agrupan (después de tener en cuenta los márgenes) y luego se puede ajustar la posición de toda la cadena cambiando la desviación (bias) de la cabeza.



Podemos aplicar uno de estos estilos a una cadena desde el editor de diseño en el atributo layout_constraintHorizontal_chainStyle o en el atributo layout_constraintVertical_chainStyle para la cabeza de la cadena, o bien desde el código XML, por ejemplo:
// Horizontal spread chain
app:layout_constraintHorizontal_chainStyle="spread"

// Vertical spread inside chain
app:layout_constraintVertical_chainStyle="spread_inside"

// Horizontal packed chain
app:layout_constraintHorizontal_chainStyle="packed"

Para crear una cadena vertical en nuestra aplicación, en el archivo activity_main.xml, vista Design, creamos otros tres elementos TextView arrastrándolos a la derecha del TextView box_dos_txt:



Como hicimos antes con los otros TextView, ajustamos sus atributos de id, text y style. Observa que para aplicar el mismo estilo a los tres (@style/whiteBox), desde el árbol de componentes puedes seleccionarlos conjuntamente y aplicarlo de una vez.

Ahora, para crear la cadena, desde el editor de diseño seleccionamos los tres elementos y con el botón secundario pulsamos en Chains > Create Vertical Chain. Esto crea una cadena vertical que se extiende desde Box Uno hasta el final del diseño.

Seleccionamos el elemento Box Tres y creamos una sujeción desde su parte superior hasta la parte superior del Cuadro Dos (esta nueva sujeción reemplaza la sujeción superior existente por lo que no es necesario eliminarla explícitamente):



Creamos otra sujeción desde la parte inferior del Box Cinco hasta la parte inferior del Box Dos:



También creamos una sujeción desde el lado izquierdo del Box Tres al lado derecho del Box Dos. Repetimos para los Boxes Cuatro y Cinco. Además creamos sujeciones del lado derecho de estos tres elementos al lado derecho del diseño, y para los tres cambiamos el atributo layout_width a match_constraint (que es lo mismo que poner el valor 0dp).



Para separar estos tres elementos entre sí, agregamos márgenes asignando al atributo Layout_Margin el valor que tenemos guardado en el recurso de dimensiones llamado margen_ancho; para los tres elementos lo aplicamos a Layout_Margin start y end y para el Box Cuatro también a top y bottom (y eliminamos el resto de márgenes de estos elementos).

Podemos comprobar que el diseño se adapta a cambios en la pantalla ajustando la orientación a Landscape (horizontal) de la vista previa desde el icono Orientation for Preview (O) de la barra de herramientas:



Ahora vamos a añadir cierta interacción del usuario para que cuando toque algunos elementos cambien de color. Para ello primero cambiaremos el color de todos los elementos de texto a blanco, desde values/styles.xml, en el estilo whiteBox cambiamos el color de fondo (android:background) a blanco:
<item name="android:background">@android:color/white</item>

En MainActivity.kt, después de la función onCreate(), escribimos una función llamada pintarView con un View como parámetro que será el elemento que cambiará de color:
private fun pintarView(view: View) { }

Dentro de esta función podemos utilizar una instrucción when con el id de recurso del elemento utilizado como argumento, de modo que cuando el bloque when verifique el id del elemento se aplicará el cambio de color de fondo para ese elemento con el método setBackgroundColor() y como argumento constantes de la clase Color (requiere import android.graphics.Color):
private fun pintarView(view: View) {
    when (view.id) {
        R.id.box_uno_txt -> view.setBackgroundColor(Color.DKGRAY)
        R.id.box_dos_txt -> view.setBackgroundColor(Color.GRAY)
        R.id.box_tres_txt -> view.setBackgroundColor(Color.BLUE)
        R.id.box_cuatro_txt -> view.setBackgroundColor(Color.MAGENTA)
        R.id.box_cinco_txt -> view.setBackgroundColor(Color.BLUE)
    }
}

También queremos que si el usuario toca el fondo, éste cambie a un tono de gris claro que revelará los contornos de los elementos de texto, lo que orientará al usuario sobre dónde tocar. Para ello, si ninguna de las condiciones de when se verifica, sabemos que el usuario ha tocado el fondo, por lo que añadimos una instrucción else con un color gris:
else -> view.setBackgroundColor(Color.LTGRAY)

Otra posible manera de escribir lo mismo es:
val color = when (view.id) {
    R.id.box_uno_txt -> Color.DKGRAY
    R.id.box_dos_txt -> Color.GRAY
    R.id.box_tres_txt -> Color.BLUE
    R.id.box_cuatro_txt -> Color.MAGENTA
    R.id.box_cinco_txt -> Color.BLUE
    else -> Color.LTGRAY
}
view.setBackgroundColor(color)

También debemos añadir una identificación (id) al elemento raíz del diseño para cambiar su color; para ello desde activity_main.xml, asignamos un valor al ID del ConstraintLayout.

Ahora nos falta detectar los clics, y para ello escribimos la función clicView() para que pueda invocar a la función pintarView para cada elemento pulsado. Esta función es invocada desde onCreate:
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    clicView()
}

private fun clicView() { }

En la función clicView declaramos una lista con los elementos que nos interesa que tengan una acción al ser pulsados:
val clickableViews = listOf(box_uno_txt, box_dos_txt, box_tres_txt, box_cuatro_txt, box_cinco_txt, constraint_layout)

Así, con un bucle for podemos crear rápidamente un controlador de clic para cada elemento:
for (item in clickableViews) {
    item.setOnClickListener { pintarView(it) }
}

Ejecuta la aplicación en el emulador para comprobar que funciona. De momento el código de MainActivity.tk queda así:
package com.android.colores

import android.graphics.Color
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        clicView()
    }

    private fun clicView() {
        val clickableViews =
            listOf(box_uno_txt, box_dos_txt, box_tres_txt, box_cuatro_txt, box_cinco_txt, constraint_layout)

        for (item in clickableViews) {
            item.setOnClickListener { pintarView(it) }
        }
    }

    private fun pintarView(view: View) {
        /*when (view.id) {
            R.id.box_uno_txt -> view.setBackgroundColor(Color.DKGRAY)
            R.id.box_dos_txt -> view.setBackgroundColor(Color.GRAY)
            R.id.box_tres_txt -> view.setBackgroundColor(Color.BLUE)
            R.id.box_cuatro_txt -> view.setBackgroundColor(Color.MAGENTA)
            R.id.box_cinco_txt -> view.setBackgroundColor(Color.BLUE)
            else -> view.setBackgroundColor(Color.LTGRAY)
        }*/

        val color = when (view.id) {
            R.id.box_uno_txt -> Color.DKGRAY
            R.id.box_dos_txt -> Color.GRAY
            R.id.box_tres_txt -> Color.BLUE
            R.id.box_cuatro_txt -> Color.MAGENTA
            R.id.box_cinco_txt -> Color.BLUE
            else -> Color.LTGRAY
        }
        view.setBackgroundColor(color)
    }
}

Otra posibilidad es usar imágenes en lugar de colores y texto, de modo que cuando el usuario toca los elementos se muestran las imágenes; en ese caso se utilizaría la función setBackgroundResource() para establecer una imagen como fondo del elemento.

Aunque funciona (hace lo que se supone que debe hacer), estaría bien añadir unas mínimas instrucciones que indiquen al usuario cómo usar la aplicación. Para ello, vamos a crear dos TextView bajo el resto de elementos, uno con la etiqueta 'Cómo jugar' y el otro con la información. El primero lo creamos con estos atributos:
  • ID: etiqueta_info
  • text: @string/etiqueta_info
  • fontFamily: @font/roboto
  • textSize: @dimen/box_text_size
  • textStyle: B (bold)

Y creamos una sujeción desde su lado izquierdo hasta el elemento raíz, y a Layout_Margin start le asignamos el valor del recurso @dimen/margen_ancho.
<TextView
    android:id="@+id/etiqueta_info"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="@dimen/margen_ancho"
    android:fontFamily="@font/roboto"
    android:text="@string/etiqueta_info"
    android:textSize="@dimen/box_text_size"
    android:textStyle="bold"
    app:layout_constraintStart_toStartOf="parent"
    tools:layout_editor_absoluteY="235dp" />

Vemos que el último atributo difiere en el prefijo del espacio de nombres (tools): son los conocidos como atributos de tiempo de diseño (design-time attributes). Estos atributos solo se usan y aplican durante el plan del diseño pero no en tiempo de ejecución; así, cuando se ejecuta la aplicación esos atributos son ignorados. En este caso se ha agregado este atributo porque todavía no hemos especificado una sujeción vertical para el elemento y, como sabemos, todos los elementos contenidos en un ConstraintLayout deben fijarse horizontal y verticalmente (de lo contrario, los elementos saltan a un borde del elemento padre cuando ejecuta la aplicación, y por eso el Editor de Diseño agrega atributos de tiempo de diseño para mantener el elemento en su lugar durante el diseño, y los elimina en el momento que se crea la sujeción que falta).

Creamos otro TextView a la derecha y bajo el anterior con estos atributos:
  • ID: info_text
  • layout_width: match_constraint
  • text: @string/text_info
  • fontFamily: @font/roboto

Creamos una sujeción desde su lado derecho hasta el borde derecho del elemento padre y otra desde su lado izquierdo hasta la derecha de etiqueta_info.



Ahora podemos alinear los textos de ambos TextView utilizando la sujeción de línea base (Baseline constraint), lo que suele resultar especialmente útil con elementos de texto con fuentes de distinto tamaño. Para ello, al seleccionar el elemento etiqueta_info y mantener el ratón sobre él, aparece el icono de Edit Baseline; lo pulsamos y aparece una línea verde parpadeante bajo el texto. Pulsamos sobre ella y la arrastramos hasta la linea verde de debajo del texto del elemento info_text (para eliminarla volvemos a pulsar sobre la línea, ahora roja, bajo el texto).

Como vimos, nos faltaba especificar sujeciones verticales a los nuevos elementos de texto; creamos sujeciones para el info_text: desde su parte inferior hasta la parte inferior del elemento padre y desde su parte superior hasta la parte inferior del Box Dos (como vemos, no hace falta especificar una sujeción vertical para etiqueta_info puesto que este elemento está fijado a info_text, que sí está fijado verticalmente).

Para terminar, vamos a agregar una cadena de botones. Arrastramos tres botones desde la paleta hasta la parte inferior del diseño y a cada uno le asignamos un id y un recurso de texto. Alineamos verticalemente las etiquetas de los botones entre sí creando una sujeción de línea base (Baseline constraint) desde el texto de cada botón lateral al texto del botón central. Seleccionamos los tres botones y creamos una cadena horizontal (Chains > Create Horizontal chain). Ajustamos los márgenes: el margen izquierdo del botón izquierdo, los márgenes derecho e izquierdo del botón central y el márgen derecho del botón derecho con un valor de 16dp (@dimen/margen_ancho).

Y ajustamos las sujecciones del botón central: desde su parte superior a la parte inferior del info_text y desde su parte inferior hasta la parte inferior del diseño. Cambiamos también su desviación vertical (vertical bias) a 100 para situar los botones en la parte inferior del diseño.
<Button
    android:id="@+id/boton_rojo"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="@dimen/margen_ancho"
    android:text="@string/boton_rojo"
    app:layout_constraintBaseline_toBaselineOf="@+id/boton_amarillo"
    app:layout_constraintEnd_toStartOf="@+id/boton_amarillo"
    app:layout_constraintHorizontal_bias="0.5"
    app:layout_constraintStart_toStartOf="parent" />

<Button
    android:id="@+id/boton_amarillo"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginLeft="@dimen/margen_ancho"
    android:layout_marginTop="16dp"
    android:layout_marginRight="@dimen/margen_ancho"
    android:layout_marginBottom="16dp"
    android:text="@string/boton_amarillo"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toStartOf="@+id/boton_verde"
    app:layout_constraintHorizontal_bias="0.5"
    app:layout_constraintStart_toEndOf="@+id/boton_rojo"
    app:layout_constraintTop_toBottomOf="@+id/info_text"
    app:layout_constraintVertical_bias="1.0" />

<Button
    android:id="@+id/boton_verde"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginEnd="@dimen/margen_ancho"
    android:text="@string/boton_verde"
    app:layout_constraintBaseline_toBaselineOf="@+id/boton_amarillo"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintHorizontal_bias="0.5"
    app:layout_constraintStart_toEndOf="@+id/boton_amarillo" />

Probamos el diseño en diferentes dispositivos y orientaciones. Ya solo nos queda añadir los controladores para los botones, para que cuando sean pulsados cambie el color de los TextView. Primero añadimos algunos colores al archivo values/colors.xml:
<color name="verde">#12C700</color>
<color name="rojo">#E54304</color>
<color name="amarillo">#FFFF00</color>

En MainActivity.kt, en la lista de elementos clickableViews añadimos los botones a través de sus atributos id:
val clickableViews = listOf(
    box_uno_txt, box_dos_txt, box_tres_txt, box_cuatro_txt, box_cinco_txt,
    constraint_layout,
    boton_rojo, boton_amarillo, boton_verde
)

Y en la función pintarView() dentro de la expresión when (antes de la instrucción else) escribimos el código para cambiar los colores cuando se pulsan los botones:
private fun pintarView(view: View) {
    when (view.id) {
        R.id.box_uno_txt -> view.setBackgroundColor(Color.DKGRAY)
        R.id.box_dos_txt -> view.setBackgroundColor(Color.GRAY)
        R.id.box_tres_txt -> view.setBackgroundColor(Color.BLUE)
        R.id.box_cuatro_txt -> view.setBackgroundColor(Color.MAGENTA)
        R.id.box_cinco_txt -> view.setBackgroundColor(Color.BLUE)

        R.id.boton_rojo -> box_tres_txt.setBackgroundResource(R.color.rojo)
        R.id.boton_amarillo -> box_cuatro_txt.setBackgroundResource(R.color.amarillo)
        R.id.boton_verde -> box_cinco_txt.setBackgroundResource(R.color.verde)

        else -> view.setBackgroundColor(Color.LTGRAY)
    }
}

Comentarios

Entradas populares

I/O: entrada y salida de datos en consola

Recursos gratis para aprender Kotlin

Lectura y escritura de archivos