Jetpack Compose: Modificadores De Apariencia

En este tutorial aprenderás a usar los modificadores de apariencia en Compose, con el fin de modificar el tamaño, posición, alineamiento, bordes y demás características de diseño visual de tus elementos componibles.


¿Qué Es Un Modificador?

Como vimos en Agregar Jetpack Compose A Un Proyecto, los modificadores son objetos que decoran a tus funciones componibles para:

  • Cambiar su comportamiento y apariencia
  • Procesar entradas del usuario
  • Permitir la detección de gestos (click, deslizamientos, arrastres y scrolling)
  • Añadir descripciones para que los servicios de accesibilidad y el framework de pruebas entiendan la composición

Estos son representados por la interfaz Modifier, la cual provee múltiples funciones de extensión que usan la función Modifier.then(), para la concatenación de modificadores:

// Archivo Modifier.kt de Jetpack Compose
infix fun then(other: Modifier): Modifier =
        if (other === Modifier) this else CombinedModifier(this, other)

//...

// Combinando modificadores
Box(
    Modifier
        .scale(0.5f)
        .size(size)
        .background(yellow)
)

Android Studio te facilita la visualización de todos los modificadores asociados a un contexto cuando digitas «Modifier.» en el atributo modifier de la función composable en edición:

Modificadores de Compose En Android Studio

Lectura: Este tutorial hace parte de la guía de Jetpack Compose de Develou. Si aún no estás familiarizado con estas librerías, te recomiendo leer «Agregar Jetpack Compose A Un Proyecto Android Studio«.

Ejemplo De Modificadores De Apariencia

Para esta sección crearemos un nuevo módulo llamado p3_modificadores e incluiremos diferentes archivos Kotlin donde ubicaremos a las funciones componibles que resulten de los ejemplos de cada modificador

Ejemplos de modificadores de Compose

La idea es probar el comportamiento de cada modificador con elementos Box sin contenido y así comprender el efecto que se produce desde la vista previa. Descarga el proyecto Android Studio desde el siguiente enlace:

Con esto en mente, comencemos por el primer grupo de modificadores de apariencia.


Modificadores De Tamaño

Cuando añades un elemento a la pantalla sus dimensiones son ajustadas por defecto al contenido de este. Para cambiar el tamaño a un valor personalizado usa los modificadores del archivo Size.kt.

En Compose los valores de las métricas de dimensiones son apoyados por propiedades de extensión como dp y sp. Por ejemplo:

import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

val padding = 16.dp
val textSize = 12.sp

Con estos valores podrás modificar el tamaño con los modificadores que veremos a continuación.

Modificadores De Ancho

Usa los siguiente modificadores para cambiar el ancho de un elemento:

  • width(width: Dp): Especifica el ancho fijo de un componente en el valor de width
  • fillMaxWidth(): Al ancho rellena todo el espacio disponible del padre. También puedes especificar el porcentaje que deseas que el elemento ocupe
  • requiredWidth(width: Dp): Declara el ancho del contenido para que sea igual a width. Si el padre tiene menor tamaño, entonces se centrará el elemento.

Por ejemplo:

@Composable
@Preview
fun WidthExample() {
    Column(
        Modifier.width(300.dp)
    ) {
        Text(
            "width() -> 100dp",
            Modifier
                .width(100.dp)
                .height(100.dp)
                .background(yellow)
        )
        Text(
            "fillMaxWidth() -> 300dp",
            Modifier
                .fillMaxWidth()
                .height(100.dp)
                .background(blue)
        )
        Text(
            "requiredWidth() -> 400dp",
            Modifier
                .requiredWidth(400.dp)
                .height(100.dp)
                .background(red)
        )
    }
}

El resultado en la previsualización será:

Puntos a destacar del ejemplo anterior:

  • Es necesario especificar la altura con el modificador heigth() para que las cajas muestren su rectángulo de contenido
  • La columna tiene un ancho de 300dp y su alto al no ser especificado, se ajusta al contenido de sus hijos
  • El tercer bloque requería 400dp de ancho, se ha centrado al superar el ancho de su padre. La previsualización de Compose muestra una línea punteada indicando cual es su tamaño declarado.

Modificadores De Alto

La dimensión de altura tiene modificadores equivalentes al ancho: height(), fillMaxHeight() y requireHeight().

Por lo que es la misma dinámica para su especificación. Veamos un ejemplo dentro de una fila:

@Composable
@Preview
fun HeightExamples() {
    Row(
        Modifier.height(300.dp)
    ) {
        Text(
            "100dp",
            Modifier
                .weight(1.0f)
                .height(100.dp)
                .background(yellow)
        )
        Text(
            "MAX",
            Modifier
                .weight(1.0f)
                .fillMaxHeight()
                .background(blue)
        )
        Text(
            "MAX/2",
            Modifier
                .weight(1.0f)
                .fillMaxHeight(0.5f)
                .background(red)
        )
        Text(
            "400dp",
            Modifier
                .weight(1.0f)
                .requiredHeight(400.dp)
                .background(green)
        )
    }
}

La previsualización de estas funciones componibles será:

Modificadores de alto de Compose

Esta vez distribuimos equitativamente el ancho de cada bloque en la columna con weight() y establecimos las alturas en 100dp, 300dp, 150dp y 400dp.

Tanto fillMaxWidth() y fillMaxHeight() pueden recibir alternativamente un factor que especifique el porcentaje del máximo que se desea ocupar. En este caso fue 0.5f para representar a la mitad del alto máximo.

Modificar Ancho Y Alto A La Vez

También es posible declarar el tamaño de ambas dimensiones a partir de los siguiente modificadores:

  • size(Dp): Su primera variación recibe el valor en dp para generar un contenido cuadrado
  • size(width, height): Crea un contenido rectangular de width×height
  • requiredSize(): Declara el tamaño en dp requerido para ambas dimensiones

Veamos ejemplos de uso:

@Composable
@Preview
fun SizeExamples() {
    Box(Modifier.size(300.dp)) {
        Box(
            Modifier
                .fillMaxSize()
                .background(yellow)
        )
        Box(
            Modifier
                .size(50.dp)
                .align(Alignment.BottomCenter)
                .background(blue)
        )
        Box(
            Modifier
                .size(50.dp, 100.dp)
                .background(red)
        )
        Box(
            Modifier
                .requiredSize(100.dp)
                .align(Alignment.CenterEnd)
                .background(green)
        )
    }
}

El código anterior crea una caja con otras cuatro en su interior:

Modificador size() de Compose

Modificadores De Posición

Mueve el contenido de un elemento hacia una posición relativa con los modificadores de offset:

  • offset(x, y): Modifica la distancia del elemento con respecto a los ejes
  • absoluteOffset(x, y): Es igual que offset() solo que no contempla la dirección del layout

Por ejemplo, creemos una caja de 100x100dp, desplazemos su contenido 10dp en el eje x y 15dp en el y luego pintemos su fondo de azul:

@Composable
@Preview
fun OffsetExample() {
    Box(
        Modifier
            .size(100.dp)
            .offset(10.dp, 15.dp)
            .background(blue)
    )
}

En la previsualización el offset se proyectará así:

Modificador offset() de Compose

Como ves, las restricciones del tamaño no se vieron afectadas por el offset establecido.


Modificadores De Borde

Si deseas establecer el estilo para los bordes de los cuatro lados de un elemento usa el modificador border() y cualquiera de sus variaciones.

  • border(border: BorderStroke, shape: Shape)
  • border(width: Dp, color: Color, shape: Shape)
  • border(width: Dp, brush: Brush, shape: Shape)

Por ejemplo, si deseamos aplicar una línea de contorno a un elemento, la definición del borde sería:

@Composable
@Preview
fun BorderExamples() {
    Column {

        Box(
            Modifier
                .size(100.dp)
                .border(3.dp, Color.DarkGray)
        )

        Box(
            Modifier
                .size(100.dp)
                .border(
                    border = BorderStroke(3.dp, green),
                    shape = CutCornerShape(5.dp)
                )
        )

        Box(
            Modifier
                .size(100.dp)
                .border(
                    width = 3.dp,
                    brush = Brush.horizontalGradient(
                        listOf(yellow, blue, red)
                    ),
                    shape = RectangleShape
                )
        )
    }
}

La clase BorderStroke representa la línea con la que se dibujará el borde y Brush.horizontalGradient() nos permite crear un gradiente a partir de una lista de colores.

En la previa tendremos:

Modificador border() en Compose

Modificadores De Relleno

El relleno o padding es el espacio entre el contenido del elemento y su borde:

  • padding(start, top, end, bottom): Aplica el relleno en los cuatro lados correspondientes. No obstante, al ser argumentos nombrados, es posible pasar valores solo para los que necesites
  • padding(horizontal, vertical): Aplica el mismo valor de relleno para el lado izquierdo y derecho con horizontal. Igual para el lado superior e inferior a partir de vertical
  • padding(all): Aplicar el mismo relleno a todos los bordes
  • paddingFromBaseline(top, bottom): Aplica padding entre la línea base del texto y las partes superior e inferior del layout

Tomemos como ejemplo el aumento del relleno de varios textos:

@Composable
@Preview
fun PaddingExample1() {
    Box(Modifier.background(blue)) {
        Text(
            "Top = 32dp y Start = 32dp",
            modifier = Modifier
                .padding(top = 32.dp, start = 32.dp)
                .background(yellow)
        )

    }
}

@Composable
@Preview
fun PaddingExample2() {
    Box(Modifier.background(blue)) {
        Text(
            "Horizontal = 32dp",
            modifier = Modifier
                .padding(horizontal = 32.dp)
                .background(yellow)
        )
    }

}

@Composable
@Preview
fun PaddingExample3() {
    Box(Modifier.background(blue)) {
        Text(
            "All = 32dp",
            modifier = Modifier
                .padding(32.dp)
                .background(yellow)
        )
    }

}

@Composable
@Preview
fun PaddingExample4() {
    Box(Modifier.background(blue)) {
        Text(
            "Baseline = 32dp",
            modifier = Modifier
                .paddingFromBaseline(top = 32.dp)
                .background(yellow)
        )
    }
}

Al previsualizar tendremos:

Modificador padding() de Compose

Modificadores De Dibujo

Los modificadores de dibujo te permiten cambiar el procedimiento de dibujo del contenido de tus nodos gráficos en la UI.

Veamos algunos de ellos.

Modificar Canal Alfa

Usa el modificador alpha() para dibujar el contenido con el canal alfa modificado por un valor flotante entre 0.0 y 1.0.

Por ejemplo:

@Composable
@Preview
fun AlphaExample() {
    Box(Modifier.size(150.dp, 100.dp)) {
        Box(
            Modifier
                .size(100.dp)
                .alpha(0.5f)
                .background(blue, CircleShape)
                .border(2.dp, blue, CircleShape)
        )
        Box(
            Modifier
                .size(100.dp)
                .alpha(0.5f)
                .background(red, CircleShape)
                .border(2.dp, red, CircleShape)
                .align(Alignment.CenterEnd)
        )
    }
}

El código anterior establece un 50% de transparencia para ambos círculos dibujados en la caja.

Ejemplo de modificador alpha() de Compose

Modificar Fondo

Aplica el modificador background() para aplicar un color sólido sobre el contenido del nodo gráfico:

  • background(color: Color, shape: Shape)
  • background(brush: Brush, shape: Shape, alpha: Float)

Veamos tres ejemplos para colorear el fondo:

val brush = Brush.horizontalGradient(
    listOf(green, red)
)

@Composable
@Preview
fun BackgroundExample() {
    Column {
        Box(
            Modifier
                .size(100.dp)
                .background(yellow)
        )
        Box(
            Modifier
                .size(100.dp)
                .background(brush = brush)
        )
    }
}

En el primero tan solo aplicamos el color amarillo de la variable yellow.

Y el segundo usando la clase Brush para añadir complejidad al pintado con un gradiente horizontal entre verde y rojo.

El resultado visual de estos ejemplo es:

Modificador background() de Compose

Modificar Forma Del Contenido

Con el modificador clip puedes recortar el contenido tomando como referencia una instancia de tipo Shape. Esta interfaz define la figura a trazar.

Compose nos provee dos propiedades de nivel superior para representar círculos y rectángulos: CircleShape y RectangleShape. Además de dos clases para producir figuras con redondeo y corte de esquinas: RoundedCornerShape y CutCornerShape.

Veamos un ejemplo de estos elementos:

@Composable
@Preview
fun ClipExample() {
    Row(Modifier.padding(16.dp)) {
        Column {
            Box(
                Modifier
                    .size(100.dp)
                    .clip(CircleShape)
                    .background(blue)

            )

            Box(
                Modifier
                    .padding(vertical = 29.dp)
                    .size(100.dp, 50.dp)
                    .clip(RoundedCornerShape(50))
                    .background(blue)

            )
            Box(
                Modifier
                    .size(100.dp)
                    .clip(RoundedCornerShape(topStart = 50.dp))
                    .background(blue)

            )

        }
        Spacer(Modifier.size(8.dp))
        Column {
            Box(
                Modifier
                    .size(100.dp)
                    .clip(RectangleShape)
                    .background(blue)

            )

            Box(
                Modifier
                    .padding(vertical = 4.dp)
                    .size(100.dp)
                    .clip(CutCornerShape(50))
                    .background(blue)

            )
            Box(
                Modifier
                    .size(100.dp)
                    .clip(CutCornerShape(topEnd = 25.dp))
                    .background(blue)

            )
        }
    }

}

Las funciones de fabricación CutCornerShape() y RoundedCornerShape() están escritas en varias versiones para recibir diferentes parámetros. Puedes especificar el tamaño en dps para el nombre de bordes (como topEnd) específicos, o aplicar a todos los cuatro. Además de usar un entero para el porcentaje aplicado si así lo deseas.

Las cajas anteriores están divididas en dos columnas. La primera tiene las figuras con borde redondeado y la segunda los elementos con bordes rectos:

Modificador clip() de Compose

Modificadores De Transformación

También disponemos de modificadores para transformaciones lineales de los elementos sobre el plano.

Escalado

Usa el modificador scale() para aplicar escalado al contenido. Este recibe un flotante que actúa como el factor de estiramiento o contracción.

@Composable
@Preview
fun ScaleExample() {
    val size = 100.dp
    Row(
        Modifier.size(400.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {

        Box(
            Modifier
                .scale(0.5f)
                .size(size)
                .background(yellow)
        )
        Box(
            Modifier
                .scale(1f)
                .size(size)
                .background(blue)
        )
        Box(
            Modifier
                .scale(2f)
                .size(size)
                .background(red)
        )
    }
}

El código anterior toma como referencia una caja de 100x100dp para alargar sus dimensiones por dos. También creamos una caja que representa la compresión de la mitad de la original con el factor 0.5f.

Modificador scale() de Compose

Como ves, el escalado es uniforme en ambos ejes, lo que significa que las medidas se mueven desde el centro de cada caja y no desde su origen.

Rotación

Aplica el modificador rotate() para rotar el componible en la cantidad de grados que se recibe como argumento. Dicha transformación se realiza tomando al centro como punto de origen, donde los valores positivos significan rotación a favor de las manecillas del reloj y los negativos rotación en contra.

@Composable
@Preview
fun RotateExample() {
    Row(modifier = Modifier.padding(16.dp)) {
        RotableCircle(0f)
        RotableCircle(45f)
        RotableCircle(90f)
        RotableCircle(135f)
    }
}

@Composable
fun RotableCircle(degrees: Float) {
    Box(
        Modifier
            .rotate(degrees)
            .size(50.dp)
            .background(yellow, CircleShape)

    ) {
        Box(
            Modifier
                .size(15.dp)
                .background(blue, CircleShape)
                .align(Alignment.TopCenter)
        )
    }
}

Las rotaciones anteriores giran el círculo externo hacia 45, 90 y 135 grados. El círculo interno mostrará este cambio de la siguiente forma:

Modificador rotate() de Compose

Encadenar Modificadores

Compose nos permite concatenar el efecto de los modificadores a partir de la invocación continua de cada función de extensión que los representan.

Debido a que cada función afecta la instancia Modifier retornada por la anterior, el orden en que invoques los efectos puede alterar el resultado final.

Por ejemplo, supongamos que creamos una caja a la cual le aplicaremos un borde externo rojo, padding coloreado de azul y el fondo del contenido coloreado de amarillo interno como se muestra en la siguiente imagen:

Encadenar modificadores en Compose

La cadena de modificadores que llegaría a este contenido sería la siguiente:

@Composable
@Preview
fun ChainingExample() {  
    Box(
        Modifier
            .border(2.dp, red)
            .background(blue)
            .padding(16.dp)
            .background(yellow)
            .size(100.dp)
    )
}

No obstante, ¿qué pasaría si alteramos el orden de la siguiente forma?:

  1. Fijar el tamaño del componible
  2. Pintar el fondo del contenido con amarillo
  3. Añadir el borde
  4. Aplicar padding
  5. Pintar el fondo restante
@Composable
@Preview
fun ChainingExample() {
    Row {
        Box(
            Modifier
                .border(2.dp, red)
                .background(blue)
                .padding(16.dp)
                .background(yellow)
                .size(100.dp)
        )

        Box(
            Modifier
                .size(100.dp)
                .background(yellow)
                .border(2.dp, red)
                .padding(16.dp)
                .background(blue)

        )
    }
}

El resultado cambiará totalmente hacia la siguiente presentación:

El orden de los modificadores importa

Como ves, al fijar las dimensiones, el tamaño se redujo drásticamente, ya que definir el borde y el padding antes de size() usa espacio adicional. Y el color del fondo se invierte debido a que el espacio del padding es interno.

Con este sistema de concatenación de modificadores obtenemos un control más estricto en la forma en que se dibuja el contenido. Sin embargo, debes tener cuidado el orden al invocarlos.

Hay modificadores que son definitivos, es decir, una vez invocados, restringen el contenido. Por ejemplo size(), si intentas crear una cadena donde lo llamas dos veces, solo la primera llamada se aplicará.

Por otro lado, hay modificadores que pueden ser rellamados y seguirán teniendo efecto en el resultado actual de la cadena como lo es border().

Ú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!