Gráficos en Compose

Muchas apps deben poder controlar con precisión lo que se dibuja en la pantalla. Puede ser algo tan simple 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.

Dibujo básico con modificadores y DrawScope

La forma principal de dibujar algo personalizado en Compose es con modificadores, como Modifier.drawWithContent, Modifier.drawBehind y Modifier.drawWithCache.

Por ejemplo, para dibujar algo detrás de tu elemento componible, puedes usar el modificador drawBehind para comenzar a ejecutar comandos de dibujo:

Spacer(
    modifier = Modifier
        .fillMaxSize()
        .drawBehind {
            // this = DrawScope
        }
)

Si todo lo que necesitas es un elemento componible que dibuje, puedes usar el elemento componible Canvas. El elemento componible Canvas es un wrapper conveniente alrededor de Modifier.drawBehind. 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.

Todos los modificadores de dibujo exponen un DrawScope, un entorno de dibujo con alcance 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 de DrawScope.

Para dibujar algo, puedes usar una de las muchas funciones de dibujo en DrawScope. Por ejemplo, el siguiente código dibuja un rectángulo en la esquina superior izquierda de la pantalla:

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasQuadrantSize = size / 2F
    drawRect(
        color = Color.Magenta,
        size = canvasQuadrantSize
    )
}

Rectángulo rosa dibujado sobre un fondo blanco que ocupa un cuarto de la pantalla
Figura 1: Rectángulo dibujado con Canvas en Compose.

Para obtener más información sobre los diferentes modificadores de dibujo, consulta la documentación sobre modificadores de gráficos.

Sistema de coordenadas

Para dibujar algo en la pantalla, debes conocer el desplazamiento (x y y) y el tamaño del elemento. Con muchos de los métodos de dibujo en DrawScope, la posición y el tamaño los proporcionan los valores de parámetros predeterminados. Los parámetros predeterminados suelen posicionar el elemento en el punto [0, 0] sobre el lienzo y proporcionan un valor predeterminado de size que cubre toda el área de dibujo, como en el ejemplo anterior. Puedes ver que el rectángulo se encuentra en la parte superior izquierda. Para ajustar el tamaño y la posición de tu elemento, debes comprender el sistema de coordenadas en Compose.

El origen del sistema de coordenadas ([0,0]) se encuentra en el píxel superior izquierdo en el área de dibujo. x aumenta a medida que se mueve hacia la derecha y y aumenta a medida que se mueve hacia abajo.

Cuadrícula que muestra el sistema de coordenadas, que muestra la parte superior izquierda [0, 0] y la parte inferior derecha [ancho, altura]
Figura 2: Sistema de coordenadas de dibujo/cuadrícula de dibujo

Por ejemplo, si deseas dibujar una línea diagonal desde la esquina superior derecha del área del lienzo hasta la esquina inferior izquierda, puedes usar la función DrawScope.drawLine() y especificar un desplazamiento inicial y final con las posiciones x e y correspondientes:

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
    )
}

Transformaciones básicas

DrawScope ofrece transformaciones para cambiar dónde o cómo se ejecutan los comandos de dibujo.

Escala

Usa DrawScope.scale() para aumentar el tamaño de las operaciones de dibujo por un factor. Las operaciones como scale() se aplican a todas las operaciones de dibujo dentro de la expresión lambda correspondiente. Por ejemplo, el siguiente código aumenta el scaleX 10 veces y el scaleY 15 veces:

Canvas(modifier = Modifier.fillMaxSize()) {
    scale(scaleX = 10f, scaleY = 15f) {
        drawCircle(Color.Blue, radius = 20.dp.toPx())
    }
}

Un círculo escalado de manera no uniforme
Figura 3: Aplicación de una operación de escala a un círculo en Canvas

Trasladar

Usa DrawScope.translate() para mover tus operaciones de dibujo hacia arriba, hacia abajo, hacia la izquierda o hacia la derecha. Por ejemplo, el siguiente código mueve el dibujo 100 px hacia la derecha y 300 px hacia arriba:

Canvas(modifier = Modifier.fillMaxSize()) {
    translate(left = 100f, top = -300f) {
        drawCircle(Color.Blue, radius = 200.dp.toPx())
    }
}

Un círculo que se movió fuera del centro
Figura 4: Aplicación de una operación de traslación a un círculo en Canvas

Rotar

Usa DrawScope.rotate() para rotar tus operaciones de dibujo alrededor de un punto de pivote. Por ejemplo, el siguiente código rota un rectángulo 45 grados:

Canvas(modifier = Modifier.fillMaxSize()) {
    rotate(degrees = 45F) {
        drawRect(
            color = Color.Gray,
            topLeft = Offset(x = size.width / 3F, y = size.height / 3F),
            size = size / 3F
        )
    }
}

Un teléfono con un rectángulo rotado 45 grados en el centro de la pantalla
Figura 5: Usamos rotate() para aplicar una rotación al alcance de dibujo actual, que rota el rectángulo 45 grados.

Inserción

Usa DrawScope.inset() para ajustar los parámetros predeterminados del DrawScope actual, cambiar los límites del dibujo y trasladar los dibujos según corresponda:

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasQuadrantSize = size / 2F
    inset(horizontal = 50f, vertical = 30f) {
        drawRect(color = Color.Green, size = canvasQuadrantSize)
    }
}

Este código agrega relleno de forma efectiva a los comandos de dibujo:

Un rectángulo que está relleno a su alrededor
Figura 6: Aplicación de una inserción a los comandos de dibujo

Transformaciones múltiples

Para aplicar varias transformaciones a tus dibujos, usa la función DrawScope.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, el siguiente código aplica una traslación y una rotación al rectángulo:

Canvas(modifier = Modifier.fillMaxSize()) {
    withTransform({
        translate(left = size.width / 5F)
        rotate(degrees = 45F)
    }) {
        drawRect(
            color = Color.Gray,
            topLeft = Offset(x = size.width / 3F, y = size.height / 3F),
            size = size / 3F
        )
    }
}

Un teléfono con un rectángulo rotado hacia un lado de la pantalla
Figura 7: Aquí usamos withTransform para aplicar una rotación y una traslación, lo que rota el rectángulo y lo desplaza hacia la izquierda.

Operaciones de dibujo comunes

Cómo dibujar texto

Para dibujar texto en Compose, generalmente puedes usar el elemento componible Text. Sin embargo, si estás en un DrawScope o quieres dibujar el texto de forma manual con personalización, puedes usar el método DrawScope.drawText().

Para dibujar texto, crea un TextMeasurer usando rememberTextMeasurer y llama a drawText con el medidor:

val textMeasurer = rememberTextMeasurer()

Canvas(modifier = Modifier.fillMaxSize()) {
    drawText(textMeasurer, "Hello")
}

Se muestra un saludo dibujado en Canvas
Figura 8: Cómo dibujar texto en Canvas

Cómo medir texto

El diseño del texto funciona de manera diferente a otros comandos de dibujo. Normalmente, se asigna al comando de dibujo el tamaño (ancho y alto) para dibujar la forma o la imagen. Con el texto, hay algunos parámetros que controlan el tamaño del texto procesado, como el tamaño de la fuente, las fuentes, las ligaduras y el espacio entre las letras.

Con Compose, puedes usar un objeto TextMeasurer para obtener acceso al tamaño medido del texto, según los factores anteriores. Si deseas dibujar un fondo detrás del texto, puedes usar la información medida para obtener el tamaño del área que ocupa el texto:

val textMeasurer = rememberTextMeasurer()

Spacer(
    modifier = Modifier
        .drawWithCache {
            val measuredText =
                textMeasurer.measure(
                    AnnotatedString(longTextSample),
                    constraints = Constraints.fixedWidth((size.width * 2f / 3f).toInt()),
                    style = TextStyle(fontSize = 18.sp)
                )

            onDrawBehind {
                drawRect(pinkColor, size = measuredText.size.toSize())
                drawText(measuredText)
            }
        }
        .fillMaxSize()
)

Este fragmento de código genera un fondo rosa en el texto:

Texto de varias líneas que ocupa 2⁄3 del área completa, con un rectángulo de fondo
Figura 9: Texto de varias líneas que ocupa 2⁄3 del área completa, con un rectángulo de fondo

Cuando se ajustan las restricciones, el tamaño de la fuente o cualquier propiedad que afecte el tamaño medido, la acción resulta en un tamaño nuevo. Puedes configurar un tamaño fijo para width y height, y el texto luego seguirá el conjunto TextOverflow. Por ejemplo, el siguiente código renderiza texto en 1⁄3 de la altura y 1⁄3 del ancho del área componible, y establece TextOverflow en TextOverflow.Ellipsis:

val textMeasurer = rememberTextMeasurer()

Spacer(
    modifier = Modifier
        .drawWithCache {
            val measuredText =
                textMeasurer.measure(
                    AnnotatedString(longTextSample),
                    constraints = Constraints.fixed(
                        width = (size.width / 3f).toInt(),
                        height = (size.height / 3f).toInt()
                    ),
                    overflow = TextOverflow.Ellipsis,
                    style = TextStyle(fontSize = 18.sp)
                )

            onDrawBehind {
                drawRect(pinkColor, size = measuredText.size.toSize())
                drawText(measuredText)
            }
        }
        .fillMaxSize()
)

El texto ahora se dibuja en las restricciones con una elipsis al final:

Texto dibujado sobre un fondo rosa con puntos suspensivos cortados.
Figura 10: TextOverflow.Ellipsis con restricciones fijas en la medición de texto

Cómo dibujar una imagen

Para dibujar un objeto ImageBitmap con DrawScope, carga la imagen con ImageBitmap.imageResource() y, luego, llama a drawImage:

val dogImage = ImageBitmap.imageResource(id = R.drawable.dog)

Canvas(modifier = Modifier.fillMaxSize(), onDraw = {
    drawImage(dogImage)
})

Una imagen de un perro dibujado en Canvas
Figura 11: Dibujo de un ImageBitmap en Canvas

Cómo dibujar formas básicas

Hay muchas funciones de dibujo con formas en DrawScope. Para dibujar una forma, usa una de las funciones de dibujo predefinidas, como drawCircle:

val purpleColor = Color(0xFFBA68C8)
Canvas(
    modifier = Modifier
        .fillMaxSize()
        .padding(16.dp),
    onDraw = {
        drawCircle(purpleColor)
    }
)

API

Resultado

drawCircle()

dibujar círculo

drawRect()

dibujar rectángulo

drawRoundedRect()

dibujar rectángulo redondeado

drawLine()

dibujar línea

drawOval()

dibujar óvalo

drawArc()

dibujar arco

drawPoints()

dibujar puntos

Cómo dibujar una ruta

Una ruta es una serie de instrucciones matemáticas que dan como resultado un dibujo una vez ejecutado. DrawScope puede dibujar una ruta con el método DrawScope.drawPath().

Por ejemplo, supongamos que deseas dibujar un triángulo. Puedes generar una ruta con funciones como lineTo() y moveTo() usando el tamaño del área de trazado. Luego, llama a drawPath() con esta ruta recién creada para obtener un triángulo.

Spacer(
    modifier = Modifier
        .drawWithCache {
            val path = Path()
            path.moveTo(0f, 0f)
            path.lineTo(size.width / 2f, size.height / 2f)
            path.lineTo(size.width, 0f)
            path.close()
            onDrawBehind {
                drawPath(path, Color.Magenta, style = Stroke(width = 10f))
            }
        }
        .fillMaxSize()
)

Triángulo con forma de ruta invertida de color púrpura dibujado en Compose
Figura 12: Creación y dibujo de un Path en Compose

Cómo acceder al objeto Canvas

Con DrawScope, no tienes acceso directo a un objeto Canvas. Puedes usar DrawScope.drawIntoCanvas() para obtener acceso al objeto Canvas en el que puedes llamar a funciones.

Por ejemplo, si tienes un Drawable personalizado que quieres dibujar en el lienzo, puedes acceder al lienzo, llamar a Drawable#draw() y pasar el objeto Canvas:

val drawable = ShapeDrawable(OvalShape())
Spacer(
    modifier = Modifier
        .drawWithContent {
            drawIntoCanvas { canvas ->
                drawable.setBounds(0, 0, size.width.toInt(), size.height.toInt())
                drawable.draw(canvas.nativeCanvas)
            }
        }
        .fillMaxSize()
)

Un objeto ShapeDrawable negro ovalado que ocupa el tamaño original
Figura 13: Acceso al lienzo para dibujar un Drawable

Más información

Para obtener más información sobre cómo dibujar en Compose, consulta los siguientes recursos: