Oltre al componibile Canvas, Compose include diversi Modifiers grafici utili che aiutano a disegnare contenuti personalizzati. Questi modificatori sono utili perché possono essere applicati a qualsiasi componibile.
Modificatori di disegno
Tutti i comandi di disegno vengono eseguiti con un modificatore di disegno in Compose. In Compose sono presenti tre modificatori di disegno principali:
Il modificatore di base per il disegno è drawWithContent, con cui puoi decidere l'ordine di disegno del tuo componibile e i comandi di disegno emessi all'interno del modificatore. drawBehind è un comodo wrapper di drawWithContent con l'ordine di disegno impostato dietro i contenuti del composable. drawWithCache
chiama onDrawBehind o onDrawWithContent al suo interno e fornisce un
meccanismo per memorizzare nella cache gli oggetti creati al loro interno.
Modifier.drawWithContent: scegli l'ordine di disegno
Modifier.drawWithContent ti consente di eseguire operazioni DrawScope prima o dopo i contenuti del componibile. Assicurati di chiamare drawContent per eseguire il rendering dei contenuti effettivi del composable. Con questo modificatore, puoi decidere l'ordine delle operazioni, se vuoi che i tuoi contenuti vengano disegnati prima o dopo le operazioni di disegno personalizzate.
Ad esempio, se vuoi eseguire il rendering di una sfumatura radiale sopra i tuoi contenuti per creare un effetto di luce di una torcia sulla UI, puoi procedere nel seguente modo:
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: disegno dietro un composable
Modifier.drawBehind ti consente di eseguire operazioni DrawScope dietro i contenuti componibili disegnati sullo schermo. Se
esamini l'implementazione di Canvas, potresti notare che si tratta
solo di un comodo wrapper per Modifier.drawBehind.
Per disegnare un rettangolo arrotondato dietro Text:
Text( "Hello Compose!", modifier = Modifier .drawBehind { drawRoundRect( Color(0xFFBBAAEE), cornerRadius = CornerRadius(10.dp.toPx()) ) } .padding(4.dp) )
Il risultato è il seguente:
Modifier.drawWithCache: disegno e memorizzazione nella cache degli oggetti di disegno
Modifier.drawWithCache mantiene memorizzati nella cache gli oggetti creati al suo interno. Gli oggetti vengono memorizzati nella cache finché le dimensioni dell'area di disegno rimangono invariate o finché gli oggetti di stato letti non cambiano. Questo modificatore è utile per migliorare le prestazioni delle chiamate di disegno, in quanto evita la necessità di riallocare gli oggetti (ad esempio Brush, Shader, Path e così via) creati durante il disegno.
In alternativa, puoi anche memorizzare nella cache gli oggetti utilizzando remember, al di fuori del modificatore. Tuttavia, questo non è sempre possibile, in quanto non sempre hai accesso alla composizione. Può essere più efficiente utilizzare drawWithCache se gli oggetti vengono utilizzati solo per il disegno.
Ad esempio, se crei un Brush per disegnare un gradiente dietro un Text, l'utilizzo di drawWithCache memorizza nella cache l'oggetto Brush finché le dimensioni dell'area di disegno non cambiano:
Text( "Hello Compose!", modifier = Modifier .drawWithCache { val brush = Brush.linearGradient( listOf( Color(0xFF9E82F0), Color(0xFF42A5F5) ) ) onDrawBehind { drawRoundRect( brush, cornerRadius = CornerRadius(10.dp.toPx()) ) } } )
Modificatori grafici
Modifier.graphicsLayer: applica le trasformazioni ai componibili
Modifier.graphicsLayer
è un modificatore che fa sì che i contenuti del composable vengano disegnati in un livello di disegno. Un livello fornisce alcune funzioni diverse, ad esempio:
- Isolamento per le istruzioni di disegno (simile a
RenderNode). Le istruzioni di disegno acquisite come parte di un livello possono essere riemesse in modo efficiente dalla pipeline di rendering senza rieseguire il codice dell'applicazione. - Trasformazioni che si applicano a tutte le istruzioni di disegno contenute in un livello.
- Rasterizzazione per le funzionalità di composizione. Quando un livello viene rasterizzato, le relative istruzioni di disegno vengono eseguite e l'output viene acquisito in un buffer fuori schermo. La composizione di un buffer di questo tipo per i frame successivi è più veloce dell'esecuzione delle singole istruzioni, ma si comporterà come una bitmap quando vengono applicate trasformazioni come la scalabilità o la rotazione.
Trasformazioni
Modifier.graphicsLayer fornisce l'isolamento per le sue istruzioni di disegno;
ad esempio, è possibile applicare varie trasformazioni utilizzando
Modifier.graphicsLayer. Questi possono essere animati o modificati senza dover eseguire nuovamente la funzione lambda di disegno.
Modifier.graphicsLayer non modifica le dimensioni o la posizione misurate del composable, poiché influisce solo sulla fase di disegno. Ciò significa che il tuo composable potrebbe sovrapporsi ad altri se finisce per disegnare al di fuori dei limiti del layout.
Con questo modificatore è possibile applicare le seguenti trasformazioni:
Scala: aumenta le dimensioni
scaleX e scaleY ingrandiscono o rimpiccioliscono i contenuti rispettivamente in direzione orizzontale o verticale. Un valore pari a 1.0f indica che non è stata apportata alcuna modifica alla scala, mentre un valore pari a 0.5f indica la metà della dimensione.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.scaleX = 1.2f this.scaleY = 0.8f } )
Traduzione
translationX e translationY possono essere modificati con graphicsLayer,
translationX sposta il composable a sinistra o a destra. translationY sposta il composable verso l'alto o verso il basso.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.translationX = 100.dp.toPx() this.translationY = 10.dp.toPx() } )
Rotazione
Imposta rotationX per ruotare in orizzontale, rotationY per ruotare in verticale e rotationZ per ruotare sull'asse Z (rotazione standard). Questo valore è specificato in gradi (0-360).
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.rotationX = 90f this.rotationY = 275f this.rotationZ = 180f } )
Origine
È possibile specificare un transformOrigin. Viene quindi utilizzato come punto da cui avvengono le trasformazioni. Tutti gli esempi finora hanno utilizzato TransformOrigin.Center, che si trova in (0.5f, 0.5f). Se specifichi l'origine in (0f, 0f), le trasformazioni iniziano dall'angolo in alto a sinistra del composable.
Se modifichi l'origine con una trasformazione rotationZ, puoi vedere che l'elemento ruota attorno all'angolo in alto a sinistra del composable:
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 } )
Clip and Shape
La forma specifica il contorno a cui i contenuti vengono ritagliati quando clip = true. In questo esempio, impostiamo due riquadri in modo che abbiano due clip diversi: uno utilizza la variabile clip graphicsLayer e l'altro utilizza il comodo wrapper 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)) ) }
I contenuti della prima casella (il testo "Hello Compose") vengono ritagliati in base alla forma del cerchio:
Se poi applichi un translationY al cerchio rosa in alto, vedrai che i limiti del componibile sono ancora gli stessi, ma il cerchio viene disegnato sotto il cerchio in basso (e al di fuori dei suoi limiti).
Per ritagliare il composable nell'area in cui viene disegnato, puoi aggiungere un altro Modifier.clip(RectangleShape) all'inizio della catena di modificatori. I contenuti rimangono quindi all'interno dei limiti originali.
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)) ) }
Alpha
Modifier.graphicsLayer può essere utilizzato per impostare un alpha (opacità) per l'intero livello. 1.0f è completamente opaco e 0.0f è invisibile.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "clock", modifier = Modifier .graphicsLayer { this.alpha = 0.5f } )
Strategia di compositing
Lavorare con l'alpha e la trasparenza non è sempre così semplice come modificare un singolo valore alpha. Oltre a modificare un valore alfa, è anche possibile impostare un CompositingStrategy su un graphicsLayer. Un CompositingStrategy determina il modo in cui i contenuti del composable vengono composti (messi insieme) con gli altri contenuti già disegnati sullo schermo.
Le diverse strategie sono:
Automatica (opzione predefinita)
La strategia di compositing è determinata dal resto dei parametri graphicsLayer. Esegue il rendering del livello in un buffer fuori schermo se l'alpha è inferiore a 1.0f o se è impostato un RenderEffect. Ogni volta che l'alpha è inferiore a 1f, viene creato automaticamente un livello di compositing per eseguire il rendering dei contenuti e poi disegnare questo buffer fuori schermo nella destinazione con l'alpha corrispondente. L'impostazione di un valore RenderEffect o di overscroll esegue sempre il rendering dei contenuti in un buffer fuori schermo, indipendentemente dal valore CompositingStrategy impostato.
Fuori schermo
I contenuti del composable vengono sempre rasterizzati in una texture o bitmap fuori schermo prima del rendering nella destinazione. Questo è utile per applicare operazioni BlendMode per mascherare i contenuti e per le prestazioni durante il rendering di insiemi complessi di istruzioni di disegno.
Un esempio di utilizzo di CompositingStrategy.Offscreen è con BlendModes.
Nell'esempio seguente, supponiamo che tu voglia rimuovere parti di un componibile Image eseguendo un comando di disegno che utilizza BlendMode.Clear. Se non imposti compositingStrategy su CompositingStrategy.Offscreen, BlendMode interagisce con tutti i contenuti in background.
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 ) ) } } )
Impostando CompositingStrategy su Offscreen, viene creata una texture fuori schermo per eseguire i comandi (applicando BlendMode solo ai contenuti di questo composable). Quindi, lo visualizza sopra ciò che è già visualizzato sullo schermo, senza influire sui contenuti già disegnati.
Se non hai utilizzato CompositingStrategy.Offscreen, i risultati dell'applicazione di BlendMode.Clear cancellano tutti i pixel nella destinazione, indipendentemente da ciò che era già impostato, lasciando visibile il buffer di rendering della finestra (nero). Molte delle BlendModes che coinvolgono l'alpha non funzioneranno come previsto senza un buffer fuori schermo. Nota l'anello nero intorno all'indicatore circolare rosso:
Per capire meglio: se l'app aveva uno sfondo della finestra traslucido e non hai utilizzato CompositingStrategy.Offscreen, BlendMode interagiva con l'intera app. Cancellava tutti i pixel per mostrare l'app o lo sfondo sottostante, come in questo esempio:
Vale la pena notare che, quando utilizzi CompositingStrategy.Offscreen, viene creata una texture fuori schermo delle dimensioni dell'area di disegno, che viene poi visualizzata di nuovo sullo schermo. Tutti i comandi di disegno eseguiti con questa strategia vengono ritagliati per impostazione predefinita in questa regione. Il seguente snippet di codice illustra le differenze quando si passa all'utilizzo di texture fuori schermo:
@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
Questa strategia di composizione modula l'alpha per ciascuna delle istruzioni di disegno registrate all'interno di graphicsLayer. Non creerà un buffer fuori schermo per un valore alfa inferiore a 1,0f, a meno che non sia impostato un RenderEffect, quindi può essere più efficiente per il rendering alfa. Tuttavia, può fornire risultati diversi per i contenuti sovrapposti. Per i casi d'uso in cui è noto in anticipo che i contenuti non si sovrappongono, questo può fornire prestazioni migliori rispetto a CompositingStrategy.Auto con valori alfa inferiori a 1.
L'esempio seguente mostra diverse strategie di composizione: l'applicazione di diversi valori alfa a diverse parti dei componibili e l'applicazione di una strategia 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) ) } @Preview @Composable fun ModifierGraphicsLayerOutsets() { Column( modifier = Modifier .background(Color(0xFF0A0E14)) .padding(48.dp), verticalArrangement = Arrangement.spacedBy(48.dp), horizontalAlignment = Alignment.CenterHorizontally ) { val pillShape = RoundedCornerShape(percent = 50) // Without outsets: alpha = 0.5f creates an offscreen buffer that clips the 24.dp glow Box( modifier = Modifier .size( width = 140.dp, height = 56.dp ) .graphicsLayer( alpha = 0.5f, clip = false ) .dropShadow( shape = pillShape, shadow = Shadow(radius = 24.dp, color = Color.Cyan) ) .background(color = Color(0xFF141824), shape = pillShape), contentAlignment = Alignment.Center ) { Text( text = "✨ Glow", color = Color.White, fontSize = 18.sp ) } Box( modifier = Modifier .size( width = 140.dp, height = 56.dp ) .graphicsLayer( alpha = 0.5f, outsets = LayerOutsets(24.dp), clip = false ) .dropShadow( shape = pillShape, shadow = Shadow(radius = 24.dp, color = Color.Cyan) ) .background(color = Color(0xFF141824), shape = pillShape), contentAlignment = Alignment.Center ) { Text( text = "✨ Glow", color = Color.White, fontSize = 18.sp ) } } } val Purple = Color(0xFF7E57C2) val Yellow = Color(0xFFFFCA28) val Red = Color(0xFFEF5350)
Margini esterni dei livelli
Quando un composable viene disegnato in un buffer fuori schermo (ad esempio, quando alpha
è inferiore a 1.0f, viene impostato un RenderEffect o viene utilizzato CompositingStrategy.Offscreen), la texture fuori schermo viene impostata per impostazione predefinita sui limiti del layout misurato del composable. Qualsiasi disegno o ombra all'interno del livello che si estende oltre i limiti del layout del composable (ad esempio un bagliore esterno disegnato con Modifier.dropShadow) viene ritagliato implicitamente al confine del buffer fuori schermo.
Anziché aggiungere Modifier.padding() (che sposta la misurazione del layout degli elementi UI adiacenti), passa LayerOutsets al parametro outsets di Modifier.graphicsLayer. In questo modo, i limiti visivi del buffer fuori schermo vengono espansi su ogni lato senza alterare le dimensioni del layout misurate del composable. Tieni presente che LayerOutsets non espande la forma di ritaglio o i limiti dell'ombra del livello, che rimangono basati sulle dimensioni misurate originali del componibile (quindi mantieni clip = false su graphicsLayer per evitare il ritaglio ai limiti originali):
Box( modifier = Modifier .size( width = 140.dp, height = 56.dp ) .graphicsLayer( alpha = 0.5f, outsets = LayerOutsets(24.dp), clip = false ) .dropShadow( shape = pillShape, shadow = Shadow(radius = 24.dp, color = Color.Cyan) ) .background(color = Color(0xFF141824), shape = pillShape), contentAlignment = Alignment.Center ) { Text( text = "✨ Glow", color = Color.White, fontSize = 18.sp ) }
LayerOutsets (in alto, ritagliato in base ai limiti del layout rettangolare del composable) e con LayerOutsets(24.dp) (in basso, non ritagliato)Scrivi i contenuti di un composable in una bitmap
Un caso d'uso comune è la creazione di un Bitmap da un componibile. Per copiare i contenuti del componibile in un Bitmap, crea un GraphicsLayer utilizzando rememberGraphicsLayer().
Reindirizza i comandi di disegno al nuovo livello utilizzando drawWithContent() e graphicsLayer.record{}. Poi disegna il livello nel canvas visibile utilizzando
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) }
Puoi salvare la bitmap sul disco e condividerla. Per maggiori dettagli, consulta lo snippet di esempio completo. Assicurati di controllare le autorizzazioni sul dispositivo prima di provare a salvare su disco.
Modificatore di disegno personalizzato
Per creare un modificatore personalizzato, implementa l'interfaccia DrawModifier. In questo modo, puoi accedere a un ContentDrawScope, che è lo stesso esposto quando utilizzi Modifier.drawWithContent(). Puoi quindi estrarre le operazioni di disegno comuni in modificatori di disegno personalizzati per ripulire il codice e fornire wrapper pratici; ad esempio, Modifier.background() è un pratico DrawModifier.
Ad esempio, se vuoi implementare un Modifier che capovolge verticalmente i contenuti, puoi crearne uno come segue:
class FlippedModifier : DrawModifier { override fun ContentDrawScope.draw() { scale(1f, -1f) { this@draw.drawContent() } } } fun Modifier.flipped() = this.then(FlippedModifier())
Quindi utilizza questo modificatore invertito applicato a Text:
Text( "Hello Compose!", modifier = Modifier .flipped() )
Risorse aggiuntive
Per altri esempi di utilizzo di graphicsLayer e disegno personalizzato, consulta le seguenti risorse:
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Grafica in Compose
- Personalizza un'immagine {:#customize-image}
- Kotlin per Jetpack Compose