Además del elemento componible Canvas
, Compose tiene varios gráficos útiles Modifiers
que ayudan a dibujar contenido personalizado. Estos modificadores son útiles porque se pueden aplicar a cualquier elemento componible.
Modificadores de dibujo
Todos los comandos de dibujo se realizan con un modificador de dibujo en Compose. Existen tres modificadores de dibujos principales en Compose:
El modificador base para dibujar es drawWithContent
, en el que puedes decidir el orden de dibujo del elemento componible y los comandos de dibujo que se emiten dentro del modificador. drawBehind
es un wrapper práctico alrededor de drawWithContent
que tiene el orden de dibujo configurado detrás del contenido del elemento componible. drawWithCache
llama a onDrawBehind
o onDrawWithContent
dentro suyo. Además, proporciona un mecanismo para almacenar en caché los objetos creados en ellos.
Modifier.drawWithContent
: Elige el orden de dibujo
Modifier.drawWithContent
te permite ejecutar operaciones DrawScope
antes o después del contenido del elemento componible. Asegúrate de llamar a drawContent
para renderizar el contenido real del elemento componible. Con este modificador, puedes decidir el orden de las operaciones si deseas que el contenido se dibuje antes o después de tus operaciones de dibujo personalizadas.
Por ejemplo, si deseas renderizar un gradiente radial sobre tu contenido para crear un efecto de linterna en la IU, puedes hacer lo siguiente:
var pointerOffset by remember { mutableStateOf(Offset(0f, 0f)) } Column( modifier = Modifier .fillMaxSize() .pointerInput("dragging") { detectDragGestures { change, dragAmount -> pointerOffset += dragAmount } } .onSizeChanged { pointerOffset = Offset(it.width / 2f, it.height / 2f) } .drawWithContent { drawContent() // draws a fully black area with a small keyhole at pointerOffset that’ll show part of the UI. drawRect( Brush.radialGradient( listOf(Color.Transparent, Color.Black), center = pointerOffset, radius = 100.dp.toPx(), ) ) } ) { // Your composables here }
Modifier.drawBehind
: Dibujo detrás de un elemento componible
Modifier.drawBehind
permite realizar operaciones de DrawScope
detrás del contenido componible que se dibuja en pantalla. Si observas la implementación de Canvas
, quizás notes que se trata de un wrapper práctico alrededor de Modifier.drawBehind
.
Para dibujar un rectángulo redondeado detrás de Text
, haz lo siguiente:
Text( "Hello Compose!", modifier = Modifier .drawBehind { drawRoundRect( Color(0xFFBBAAEE), cornerRadius = CornerRadius(10.dp.toPx()) ) } .padding(4.dp) )
Esto produce el siguiente resultado:
Modifier.drawWithCache
: Cómo dibujar y almacenar en caché objetos de dibujo
Modifier.drawWithCache
mantiene almacenados en caché los objetos que se crean en su interior. Los objetos se almacenan en caché siempre y cuando el tamaño del área de trazado sea igual o no se hayan modificado los objetos de estado leídos. Este modificador es útil para mejorar el rendimiento de las llamadas de dibujo, ya que evita la necesidad de reasignar objetos (como Brush, Shader, Path
, etc.) que se crean con el dibujo.
Como alternativa, también puedes almacenar objetos en caché mediante remember
, fuera del modificador. Sin embargo, esto no siempre es posible, ya que no siempre tienes acceso a la composición. Puede ser más eficaz usar drawWithCache
si los objetos solo se usan para dibujar.
Por ejemplo, si creas un Brush
para dibujar un gradiente detrás de un Text
, con drawWithCache
se almacena en caché el objeto Brush
hasta que cambie el tamaño del área de trazado:
Text( "Hello Compose!", modifier = Modifier .drawWithCache { val brush = Brush.linearGradient( listOf( Color(0xFF9E82F0), Color(0xFF42A5F5) ) ) onDrawBehind { drawRoundRect( brush, cornerRadius = CornerRadius(10.dp.toPx()) ) } } )
Modificadores de gráficos
Modifier.graphicsLayer
: Aplica transformaciones a elementos componibles
Modifier.graphicsLayer
es un modificador que convierte el contenido del elemento componible en una capa de dibujo. Una capa proporciona algunas funciones diferentes, como las siguientes:
- Aislamiento para las instrucciones de dibujo (similar a
RenderNode
): La canalización de renderización puede volver a emitir las instrucciones de dibujo capturadas como parte de una capa de manera eficiente sin volver a ejecutar el código de la aplicación. - Transformaciones que se aplican a todas las instrucciones de dibujo incluidas en una capa.
- Rasterización para capacidades de composición: Cuando una capa se rasteriza, se ejecutan sus instrucciones de dibujo y el resultado se captura en un búfer fuera de pantalla. La composición de un búfer para marcos posteriores es más rápida que ejecutar las instrucciones individuales, pero se comportará como un mapa de bits cuando se apliquen transformaciones como el escalamiento o la rotación.
Transformaciones
Modifier.graphicsLayer
proporciona aislamiento para sus instrucciones de dibujo. Por ejemplo, se pueden aplicar varias transformaciones con Modifier.graphicsLayer
.
Estas se pueden animar o modificar sin necesidad de volver a ejecutar la expresión lambda del dibujo.
Modifier.graphicsLayer
no cambia el tamaño ni la posición de medición del elemento componible, ya que solo afecta la fase de dibujo. Eso significa que el elemento componible puede superponerse a otros si dibuja fuera de los límites de su diseño.
Las siguientes transformaciones se pueden aplicar con este modificador:
Aumento del tamaño con Scale
scaleX
y scaleY
aumentan o reducen el contenido en la dirección horizontal o vertical, respectivamente. Un valor de 1.0f
indica que no hay cambios en la escala; un valor de 0.5f
representa la mitad de la dimensión.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.scaleX = 1.2f this.scaleY = 0.8f } )
Elementos translation
translationX
y translationY
se pueden cambiar con graphicsLayer
, mientras que translationX
mueve el elemento componible hacia la izquierda o la derecha. translationY
mueve el elemento componible hacia arriba o abajo.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.translationX = 100.dp.toPx() this.translationY = 10.dp.toPx() } )
Rotación
Configura rotationX
para rotar de forma horizontal, rotationY
para rotar de forma vertical y rotationZ
para rotar en el eje Z (rotación estándar). Este valor se especifica en grados (0-360).
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.rotationX = 90f this.rotationY = 275f this.rotationZ = 180f } )
Origen
Se puede especificar un transformOrigin
. Luego, se usa como el punto desde el que se realizan las transformaciones. Todos los ejemplos hasta ahora utilizaron TransformOrigin.Center
, que está en (0.5f, 0.5f)
. Si especificas el origen en (0f, 0f)
, las transformaciones comienzan desde la esquina superior izquierda del elemento componible.
Si cambias el origen con una transformación rotationZ
, podrás ver que el elemento rota alrededor de la parte superior izquierda del elemento componible:
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.transformOrigin = TransformOrigin(0f, 0f) this.rotationX = 90f this.rotationY = 275f this.rotationZ = 180f } )
Recorte y forma
La forma es el contorno al que se recorta el contenido cuando clip = true
. En este ejemplo, configuramos dos cuadros para tener dos recortes diferentes: uno con la variable de recorte graphicsLayer
y el otro con el wrapper práctico Modifier.clip
.
Column(modifier = Modifier.padding(16.dp)) { Box( modifier = Modifier .size(200.dp) .graphicsLayer { clip = true shape = CircleShape } .background(Color(0xFFF06292)) ) { Text( "Hello Compose", style = TextStyle(color = Color.Black, fontSize = 46.sp), modifier = Modifier.align(Alignment.Center) ) } Box( modifier = Modifier .size(200.dp) .clip(CircleShape) .background(Color(0xFF4DB6AC)) ) }
El contenido del primer cuadro (el texto que dice "Hello Compose") se recorta con la forma de un círculo:
Si luego aplicas translationY
al círculo rosa superior, verás que los márgenes del elemento componible siguen siendo los mismos, pero el círculo se dibuja debajo del círculo inferior (y fuera de sus límites).
Para recortar el elemento componible conforme a la región en la que se dibuja, puedes agregar otro Modifier.clip(RectangleShape)
al comienzo de la cadena de modificadores. El contenido permanece dentro de los márgenes originales.
Column(modifier = Modifier.padding(16.dp)) { Box( modifier = Modifier .clip(RectangleShape) .size(200.dp) .border(2.dp, Color.Black) .graphicsLayer { clip = true shape = CircleShape translationY = 50.dp.toPx() } .background(Color(0xFFF06292)) ) { Text( "Hello Compose", style = TextStyle(color = Color.Black, fontSize = 46.sp), modifier = Modifier.align(Alignment.Center) ) } Box( modifier = Modifier .size(200.dp) .clip(RoundedCornerShape(500.dp)) .background(Color(0xFF4DB6AC)) ) }
Alfa
Se puede usar Modifier.graphicsLayer
para establecer un valor alpha
(opacidad) para toda la capa. 1.0f
es completamente opaco y 0.0f
es invisible.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "clock", modifier = Modifier .graphicsLayer { this.alpha = 0.5f } )
Estrategia de composición
Trabajar con alfa y transparencia podría no ser tan simple como cambiar un solo valor alfa. Además de cambiar un valor alfa, también existe la opción de establecer una CompositingStrategy
en una graphicsLayer
. Una CompositingStrategy
determina cómo está compuesto el contenido del elemento componible (cómo está unido) con el resto del contenido que ya se dibujó en la pantalla.
Las diferentes estrategias son las siguientes:
Automática (configuración predeterminada)
La estrategia de composición se determina mediante el resto de los parámetros de graphicsLayer
. Renderiza la capa en un búfer fuera de pantalla si el valor alfa es menor que 1.0f o si se configura un RenderEffect
. Cuando el valor alfa es menor que 1f, se crea automáticamente una capa compuesta para renderizar el contenido y, luego, se dibuja este búfer fuera de pantalla en el destino con el valor alfa correspondiente. Si se establece un RenderEffect
o un sobredesplazamiento, siempre se renderiza el contenido en un búfer fuera de pantalla, sin importar la configuración de CompositingStrategy
.
Fuera de pantalla
El contenido del elemento componible siempre se rasteriza en una textura o un mapa de bits fuera de la pantalla antes de renderizarlos en el destino. Esto es útil para aplicar operaciones BlendMode
a fin de enmascarar contenido y mejorar el rendimiento cuando se renderizan conjuntos complejos de instrucciones de dibujo.
Un ejemplo del uso de CompositingStrategy.Offscreen
es con BlendModes
. En el siguiente ejemplo, supongamos que deseas quitar partes de un elemento componible Image
con un comando de dibujo que usa BlendMode.Clear
. Si no configuras compositingStrategy
como CompositingStrategy.Offscreen
, BlendMode
interactúa con todo el contenido debajo de él.
Image( painter = painterResource(id = R.drawable.dog), contentDescription = "Dog", contentScale = ContentScale.Crop, modifier = Modifier .size(120.dp) .aspectRatio(1f) .background( Brush.linearGradient( listOf( Color(0xFFC5E1A5), Color(0xFF80DEEA) ) ) ) .padding(8.dp) .graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen } .drawWithCache { val path = Path() path.addOval( Rect( topLeft = Offset.Zero, bottomRight = Offset(size.width, size.height) ) ) onDrawWithContent { clipPath(path) { // this draws the actual image - if you don't call drawContent, it wont // render anything this@onDrawWithContent.drawContent() } val dotSize = size.width / 8f // Clip a white border for the content drawCircle( Color.Black, radius = dotSize, center = Offset( x = size.width - dotSize, y = size.height - dotSize ), blendMode = BlendMode.Clear ) // draw the red circle indication drawCircle( Color(0xFFEF5350), radius = dotSize * 0.8f, center = Offset( x = size.width - dotSize, y = size.height - dotSize ) ) } } )
Cuando se establece CompositingStrategy
en Offscreen
, se crea una textura fuera de pantalla para ejecutar los comandos (se aplica BlendMode
solo al contenido de este elemento componible). Luego, lo renderiza sobre lo que ya se renderizó en la pantalla, sin afectar el contenido ya dibujado.
Si no usaste CompositingStrategy.Offscreen
, como resultado de aplicar BlendMode.Clear
se borrarán todos los píxeles del destino, independientemente de lo que se haya establecido, lo que dejará visible el búfer de renderización de la ventana (negro). Muchos de los BlendModes
que usan alfa no funcionarán como se espera sin un búfer fuera de la pantalla. Observa el anillo negro alrededor del indicador de círculo rojo:
Para facilitar la comprensión: Si la app tiene un fondo de ventana translúcida y no usas CompositingStrategy.Offscreen
, entonces BlendMode
interactuará con toda la app. Se borrarán todos los píxeles para mostrar la app o el fondo de pantalla debajo, como se ve en este ejemplo:
Ten en cuenta que, cuando usas CompositingStrategy.Offscreen
, se crea una textura fuera de pantalla del tamaño del área de trazado y se renderiza en la pantalla. De forma predeterminada, los comandos de dibujo que se realizan con esta estrategia, se ajustan a esta región al recortarse. En el siguiente fragmento de código, se muestran las diferencias al usar texturas fuera de pantalla:
@Composable fun CompositingStrategyExamples() { Column( modifier = Modifier .fillMaxSize() .wrapContentSize(Alignment.Center) ) { // Does not clip content even with a graphics layer usage here. By default, graphicsLayer // does not allocate + rasterize content into a separate layer but instead is used // for isolation. That is draw invalidations made outside of this graphicsLayer will not // re-record the drawing instructions in this composable as they have not changed Canvas( modifier = Modifier .graphicsLayer() .size(100.dp) // Note size of 100 dp here .border(2.dp, color = Color.Blue) ) { // ... and drawing a size of 200 dp here outside the bounds drawRect(color = Color.Magenta, size = Size(200.dp.toPx(), 200.dp.toPx())) } Spacer(modifier = Modifier.size(300.dp)) /* Clips content as alpha usage here creates an offscreen buffer to rasterize content into first then draws to the original destination */ Canvas( modifier = Modifier // force to an offscreen buffer .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) .size(100.dp) // Note size of 100 dp here .border(2.dp, color = Color.Blue) ) { /* ... and drawing a size of 200 dp. However, because of the CompositingStrategy.Offscreen usage above, the content gets clipped */ drawRect(color = Color.Red, size = Size(200.dp.toPx(), 200.dp.toPx())) } } }
ModulateAlpha
Esta estrategia de composición modula el valor alfa de cada una de las instrucciones de dibujo registradas en graphicsLayer
. No creará un búfer fuera de pantalla para un valor alfa inferior a 1.0f, a menos que se establezca un RenderEffect
, por lo que puede ser más eficiente para la renderización de alfa. Sin embargo, puede proporcionar resultados diferentes para el contenido superpuesto. Para los casos de uso en los que se sabe de antemano que el contenido no se superpone, esto puede proporcionar un mejor rendimiento que CompositingStrategy.Auto
con valores alfa menores que 1.
A continuación, se incluye otro ejemplo de estrategias de composición; la aplicación de valores alfa distintos a partes diferentes de los elementos componibles, y la aplicación de una estrategia Modulate
:
@Preview @Composable fun CompositingStrategy_ModulateAlpha() { Column( modifier = Modifier .fillMaxSize() .padding(32.dp) ) { // Base drawing, no alpha applied Canvas( modifier = Modifier.size(200.dp) ) { drawSquares() } Spacer(modifier = Modifier.size(36.dp)) // Alpha 0.5f applied to whole composable Canvas( modifier = Modifier .size(200.dp) .graphicsLayer { alpha = 0.5f } ) { drawSquares() } Spacer(modifier = Modifier.size(36.dp)) // 0.75f alpha applied to each draw call when using ModulateAlpha Canvas( modifier = Modifier .size(200.dp) .graphicsLayer { compositingStrategy = CompositingStrategy.ModulateAlpha alpha = 0.75f } ) { drawSquares() } } } private fun DrawScope.drawSquares() { val size = Size(100.dp.toPx(), 100.dp.toPx()) drawRect(color = Red, size = size) drawRect( color = Purple, size = size, topLeft = Offset(size.width / 4f, size.height / 4f) ) drawRect( color = Yellow, size = size, topLeft = Offset(size.width / 4f * 2f, size.height / 4f * 2f) ) } val Purple = Color(0xFF7E57C2) val Yellow = Color(0xFFFFCA28) val Red = Color(0xFFEF5350)
Cómo escribir el contenido de un elemento componible en un mapa de bits
Un caso de uso común es crear un Bitmap
a partir de un elemento componible. Para copiar el contenido de tu elemento componible a un Bitmap
, crea un GraphicsLayer
con rememberGraphicsLayer()
.
Redirecciona los comandos de dibujo a la nueva capa con drawWithContent()
y graphicsLayer.record{}
. Luego, dibuja la capa en el lienzo visible con drawLayer
:
val coroutineScope = rememberCoroutineScope() val graphicsLayer = rememberGraphicsLayer() Box( modifier = Modifier .drawWithContent { // call record to capture the content in the graphics layer graphicsLayer.record { // draw the contents of the composable into the graphics layer this@drawWithContent.drawContent() } // draw the graphics layer on the visible canvas drawLayer(graphicsLayer) } .clickable { coroutineScope.launch { val bitmap = graphicsLayer.toImageBitmap() // do something with the newly acquired bitmap } } .background(Color.White) ) { Text("Hello Android", fontSize = 26.sp) }
Puedes guardar el mapa de bits en el disco y compartirlo. Para obtener más detalles, consulta el fragmento de ejemplo completo. Asegúrate de verificar los permisos del dispositivo antes de intentar guardar en el disco.
Modificador de dibujo personalizado
Para crear tu propio modificador personalizado, implementa la interfaz DrawModifier
. De esta manera, tendrás acceso a un ContentDrawScope
, que es lo mismo que se expone cuando usas Modifier.drawWithContent()
. Luego, puedes extraer operaciones comunes de dibujo en modificadores personalizados de dibujo para limpiar el código y proporcionar wrappers prácticos. Por ejemplo, Modifier.background()
es un DrawModifier
conveniente.
Por ejemplo, si deseas implementar un Modifier
que gire contenido de forma vertical, puedes crear uno de la siguiente manera:
class FlippedModifier : DrawModifier { override fun ContentDrawScope.draw() { scale(1f, -1f) { this@draw.drawContent() } } } fun Modifier.flipped() = this.then(FlippedModifier())
Luego, aplica este modificador invertido en Text
:
Text( "Hello Compose!", modifier = Modifier .flipped() )
Recursos adicionales
Para obtener más ejemplos del uso de graphicsLayer
y los dibujos personalizados, consulta los siguientes recursos:
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
- Gráficos en Compose
- Cómo personalizar una imagen {:#customize-image}
- Kotlin para Jetpack Compose