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:
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:
width(width: Dp)
: Especifica el ancho fijo de un componente en el valor dewidth
fillMaxWidth()
: Al ancho rellena todo el espacio disponible del padre. También puedes especificar el porcentaje que deseas que el elemento ocuperequiredWidth(width: Dp)
: Declara el ancho del contenido para que sea igual awidth
. 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á:
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 cuadradosize(width, height)
: Crea un contenido rectangular de width×heightrequiredSize()
: 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:
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 ejesabsoluteOffset(x, y)
: Es igual queoffset()
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í:
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:
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 necesitespadding(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 devertical
padding(all)
: Aplicar el mismo relleno a todos los bordespaddingFromBaseline(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:
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:
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:
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?:
- Fijar el tamaño del componible
- Pintar el fondo del contenido con amarillo
- Añadir el borde
- Aplicar padding
- 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()
.
Ú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!