Icono del sitio Develou

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:

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:


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

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:

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:

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á:

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:

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:


Modificadores De Posición

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

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í:

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.

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:


Modificadores De Relleno

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

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:


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.

Modificar Fondo

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

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:

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:


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.

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:


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:

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:

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().

Salir de la versión móvil