Jetpack Compose facilita el trabajo con gráficos personalizados. Muchas apps deben poder controlar con precisión lo que se dibuja en la pantalla. Puede ser algo tan pequeño como colocar un cuadrado o un círculo en la pantalla en el lugar correcto, o podría ser una disposición compleja de elementos gráficos de muchos estilos diferentes. Con el enfoque declarativo de Compose, toda la configuración gráfica se produce en un solo lugar, en vez de dividirla entre una llamada de método y un objeto auxiliar Paint
. Compose se encarga de crear y actualizar los objetos necesarios de manera eficiente.
Gráficos declarativos con Compose
Compose amplía su enfoque declarativo respecto de cómo maneja los gráficos. El enfoque de Compose ofrece varias ventajas:
- Compose minimiza el estado en sus elementos gráficos, lo que te ayuda a evitar las dificultades de la programación de estados.
- Cuando dibujas algo, todas las opciones se muestran donde lo esperas: en la función de componibilidad.
- Las APIs de gráficos de Compose se encargan de crear y liberar objetos de manera eficiente.
Canvas
El elemento componible principal para gráficos personalizados es Canvas
.
Coloca el Canvas
en tu diseño de la misma manera que lo harías con cualquier otro elemento de la IU de Compose. Dentro de Canvas
, puedes dibujar elementos con un control preciso sobre su estilo y ubicación.
Por ejemplo, este código crea un elemento Canvas
componible y llena todo el espacio disponible en su elemento superior:
Canvas(modifier = Modifier.fillMaxSize()) {
}
Canvas
expone automáticamente un DrawScope
, un entorno de dibujo con alcance definido que mantiene su propio estado. Esto te permite establecer los parámetros para un grupo de elementos gráficos. DrawScope
proporciona varios campos útiles, como size
, un objeto Size
que especifica las dimensiones actuales y máximas de DrawScope
.
Por ejemplo, imagina que deseas dibujar una línea diagonal desde la esquina superior derecha del lienzo hasta la esquina inferior izquierda. Para ello, agrega un elemento drawLine
componible:
Canvas(modifier = Modifier.fillMaxSize()) { val canvasWidth = size.width val canvasHeight = size.height drawLine( start = Offset(x = canvasWidth, y = 0f), end = Offset(x = 0f, y = canvasHeight), color = Color.Blue ) }
Figura 1: Usa drawLine
para dibujar una línea a lo largo del lienzo. El código establece el color de la línea, pero usa el ancho predeterminado.
Puedes usar otros parámetros para personalizar el dibujo. Por ejemplo, de forma predeterminada, la línea se dibuja con un ancho delgado, que se muestra como un píxel de ancho, independientemente de la escala del dibujo. Puedes anular el valor predeterminado configurando un valor strokeWidth
:
Canvas(modifier = Modifier.fillMaxSize()) { val canvasWidth = size.width val canvasHeight = size.height drawLine( start = Offset(x = canvasWidth, y = 0f), end = Offset(x = 0f, y = canvasHeight), color = Color.Blue, strokeWidth = 5F ) }
Figura 2: Modifica la línea de la Figura 1 anulando el ancho predeterminado.
Hay muchas otras funciones de dibujo simples, como drawRect
y drawCircle
.
Por ejemplo, este código dibuja un círculo relleno en el medio del lienzo, con un diámetro que equivale a la mitad de la dimensión más corta del lienzo:
Canvas(modifier = Modifier.fillMaxSize()) {
val canvasWidth = size.width
val canvasHeight = size.height
drawCircle(
color = Color.Blue,
center = Offset(x = canvasWidth / 2, y = canvasHeight / 2),
radius = size.minDimension / 4
)
}
Figura 3: Usa drawCircle
para colocar un círculo en el centro del lienzo.
De forma predeterminada, drawCircle
dibuja un círculo relleno, por lo que no es necesario especificar esa configuración de forma explícita.
Las funciones de dibujo tienen parámetros predeterminados útiles. Por ejemplo, de forma predeterminada, drawRectangle()
completa todo el alcance de su elemento superior, y drawCircle()
tiene un radio que equivale a la mitad de la dimensión más corta de su elemento superior. Como siempre ocurre con Kotlin, puedes hacer que tu código sea mucho más simple y claro aprovechando los valores de parámetros predeterminados y estableciendo únicamente los parámetros que necesitas cambiar. Puedes aprovechar esto proporcionando parámetros explícitos a los métodos de dibujo de DrawScope
, ya que los elementos dibujados basarán su configuración predeterminada en la del alcance de su elemento superior.
DrawScope
Como se mencionó, cada Canvas
de Compose expone un DrawScope
, un entorno de dibujo con alcance definido, en el que emites tus comandos de dibujo.
Por ejemplo, el siguiente código dibuja un rectángulo en la esquina superior izquierda del lienzo:
Canvas(modifier = Modifier.fillMaxSize()) {
val canvasQuadrantSize = size / 2F
drawRect(
color = Color.Green,
size = canvasQuadrantSize
)
}
Puedes usar la función DrawScope.inset()
para ajustar los parámetros predeterminados del alcance actual, cambiar los límites del dibujo y traducir los dibujos según corresponda. Las operaciones como inset()
se aplican a todas las operaciones de dibujo dentro de la expresión lambda correspondiente:
val canvasQuadrantSize = size / 2F
inset(50F, 30F) {
drawRect(
color = Color.Green,
size = canvasQuadrantSize
)
}
DrawScope
ofrece otras transformaciones simples, como rotate()
.
Por ejemplo, este código dibuja un rectángulo que cubre el noveno medio del lienzo:
val canvasSize = size
val canvasWidth = size.width
val canvasHeight = size.height
drawRect(
color = Color.Gray,
topLeft = Offset(x = canvasWidth / 3F, y = canvasHeight / 3F),
size = canvasSize / 3F
)
Figura 4: Usa drawRect
para dibujar un rectángulo relleno en el medio de la pantalla.
Puedes rotar el rectángulo aplicando una rotación en su DrawScope
:
rotate(degrees = 45F) {
drawRect(
color = Color.Gray,
topLeft = Offset(x = canvasWidth / 3F, y = canvasHeight / 3F),
size = canvasSize / 3F
)
}
Figura 5: Usamos rotate()
para aplicar una rotación al alcance de dibujo actual, que rota el rectángulo 45 grados.
Si deseas aplicar varias transformaciones a tus dibujos, el mejor enfoque es no crear entornos DrawScope
anidados. En su lugar, debes usar la función withTransform()
, que crea y aplica una sola transformación que combina todos los cambios deseados. El uso de withTransform()
es más eficiente que realizar llamadas anidadas a transformaciones individuales, ya que todas las transformaciones se realizan juntas en una sola operación, en lugar de que Compose deba calcular y guardar cada una de las transformaciones anidadas.
Por ejemplo, este código aplica una traducción y una rotación al rectángulo:
withTransform({
translate(left = canvasWidth / 5F)
rotate(degrees = 45F)
}) {
drawRect(
color = Color.Gray,
topLeft = Offset(x = canvasWidth / 3F, y = canvasHeight / 3F),
size = canvasSize / 3F
)
}
Figura 6: Aquí usamos withTransform
para aplicar una rotación y una traducción, lo que rota el rectángulo y lo desplaza hacia la izquierda.