Oltre all'Canvas
componibile, Compose ha diverse utili grafici
Modifiers
che sono utili per 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 Scrivi. In Scrivi ci sono tre modificatori di disegno principali:
Il modificatore di base per il disegno è drawWithContent
, che permette di decidere l'ordine di disegno del componibile e i comandi di disegno inviati all'interno del modificatore. drawBehind
è un pratico wrapper attorno a drawWithContent
in cui l'ordine di disegno è impostato dietro i contenuti del componibile. drawWithCache
chiama onDrawBehind
o onDrawWithContent
al suo interno e fornisce un meccanismo per memorizzare nella cache gli oggetti creati al suo interno.
Modifier.drawWithContent
: scegli l'ordine del disegno
Modifier.drawWithContent
consente di
eseguire le operazioni DrawScope
prima o dopo i contenuti
del componibile. Assicurati di chiamare drawContent
per visualizzare i contenuti effettivi
del componibile. Con questo modificatore, puoi decidere l'ordine delle operazioni, se vuoi che i contenuti vengano disegnati prima o dopo le operazioni di disegno personalizzato.
Ad esempio, se vuoi eseguire il rendering di un gradiente radiale nella parte superiore dei tuoi contenuti per creare un effetto buco della torcia nell'interfaccia utente, puoi procedere come segue:
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 componibile
Modifier.drawBehind
consente di eseguire
DrawScope
operazioni dietro i contenuti componibili disegnati sullo schermo. Se
esamina l'implementazione di Canvas
, potresti notare che
è solo un comodo wrapper intorno a Modifier.drawBehind
.
Per disegnare un rettangolo arrotondato dietro a Text
:
Text( "Hello Compose!", modifier = Modifier .drawBehind { drawRoundRect( Color(0xFFBBAAEE), cornerRadius = CornerRadius(10.dp.toPx()) ) } .padding(4.dp) )
che produce il seguente risultato:
Modifier.drawWithCache
: disegno e memorizzazione nella cache di oggetti di disegno
Modifier.drawWithCache
conserva nella cache gli oggetti
creati al suo interno. Gli oggetti vengono memorizzati nella cache purché le dimensioni dell'area di disegno siano le stesse oppure se gli oggetti di stato letti non sono stati modificati. 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 in fase di disegno.
In alternativa, puoi anche memorizzare nella cache gli oggetti utilizzando remember
, al di fuori del modificatore. Tuttavia, ciò non è sempre possibile perché non sempre hai accesso
alla composizione. Può essere più efficace usare drawWithCache
se gli oggetti vengono utilizzati solo per il disegno.
Ad esempio, se crei un elemento Brush
per tracciare un gradiente dietro un elemento Text
, utilizza
drawWithCache
per memorizzare 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 di grafica
Modifier.graphicsLayer
: applica le trasformazioni ai componibili
Modifier.graphicsLayer
è un modificatore che trasforma i contenuti del disegno componibile in un livello di disegno. Un livello offre alcune funzioni diverse, tra cui:
- Isolamento per le relative 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 dover eseguire nuovamente il codice dell'applicazione. - Trasformazioni che si applicano a tutte le istruzioni di disegno contenute in un livello.
- Rasterizzazione delle capacità di composizione. Quando un livello viene rasterizzato, vengono eseguite le relative istruzioni di disegno e l'output viene acquisito in un buffer fuori schermo. La composizione di un buffer per i frame successivi è più rapida rispetto all'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
.
Queste possono essere animate o modificate senza dover eseguire nuovamente
il lambda di disegno.
Modifier.graphicsLayer
non modifica le dimensioni misurate o il posizionamento del componibile, poiché influisce solo sulla fase di disegno. Ciò significa che l'elemento componibile potrebbe sovrapporsi agli altri se risulta al di fuori dei limiti del layout.
Con questo modificatore è possibile applicare le seguenti trasformazioni:
Scala - aumenta dimensioni
scaleX
e scaleY
ingrandiscono o riducono i contenuti rispettivamente in direzione orizzontale o verticale. Un valore 1.0f
indica l'assenza di variazioni nella scala, mentre un valore
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 componibile a destra o a sinistra. translationY
sposta il componibile 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 orizzontalmente, rotationY
per ruotare verticalmente 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 } )
Origin
È possibile specificare un valore transformOrigin
. Viene quindi utilizzato come punto
da cui avvengono le trasformazioni. Tutti gli esempi finora hanno utilizzato
TransformOrigin.Center
, che è a (0.5f, 0.5f)
. Se specifichi l'origine in (0f, 0f)
, le trasformazioni inizieranno dall'angolo in alto a sinistra del componibile.
Se modifichi l'origine con una trasformazione rotationZ
, puoi vedere che l'elemento
ruota intorno alla parte in alto a sinistra dell'elemento componibile:
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 e forma
L'opzione Forma specifica il contorno a cui vengono clip i contenuti quando clip = true
. In
questo esempio, abbiamo impostato due riquadri per avere due clip diversi: uno con la variabile di clip graphicsLayer
e l'altro con il pratico 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") sono ritagliati alla forma del cerchio:
Se poi applichi un elemento translationY
al cerchio rosa superiore, noti che i limiti dell'elemento componibile sono gli stessi, ma il cerchio viene disegnato sotto il cerchio inferiore (e fuori dai suoi limiti).
Per agganciare il componibile all'area in cui è raffigurato, puoi aggiungere un altro
Modifier.clip(RectangleShape)
all'inizio della catena di modificatori. Il contenuto rimane quindi entro i 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, mentre 0.0f
è invisibile.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "clock", modifier = Modifier .graphicsLayer { this.alpha = 0.5f } )
Strategia di composizione
Utilizzare le versioni alpha e trasparente potrebbe non essere così semplice come modificare un singolo valore alfa. Oltre a modificare una versione alpha, c'è anche la possibilità di impostare un
CompositingStrategy
su un graphicsLayer
. Un elemento CompositingStrategy
determina in che modo i
contenuti del componibile vengono compositi (messi insieme) con gli altri
contenuti già disegnati sullo schermo.
Le diverse strategie sono:
Automatica (opzione predefinita)
La strategia di composizione è determinata dal resto dei parametri graphicsLayer
. Esegue il rendering del livello in un buffer fuori schermo se alpha è inferiore a 1,0f o se è impostato un RenderEffect
. Ogni volta che il valore alpha è inferiore a 1f, viene creato automaticamente un livello di compositing per eseguire il rendering dei contenuti e quindi disegnare questo buffer fuori schermo nella destinazione con l'alpha corrispondente. L'impostazione di un valore
RenderEffect
o di overscroll consente sempre il rendering dei contenuti in un buffer fuori schermo,
indipendentemente dal set CompositingStrategy
.
Fuori schermo
I contenuti del componibile vengono sempre rasterizzati in una trama o in una bitmap fuori schermo prima di essere visualizzati nella destinazione. Ciò è utile per
applicare operazioni di 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
. Se diamo un'occhiata all'esempio di seguito, supponiamo che tu voglia rimuovere parti di un componibile Image
inviando un comando di disegno che utilizza BlendMode.Clear
. Se non imposti compositingStrategy
su CompositingStrategy.Offscreen
, BlendMode
interagisce con tutti i contenuti sottostanti.
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 ) ) } } )
Se il criterio CompositingStrategy
viene impostato su Offscreen
, viene creata una texture fuori schermo
per l'esecuzione dei comandi (applicando BlendMode
solo ai
contenuti di questo componibile). Poi esegue il rendering sopra ciò che viene già visualizzato
sullo schermo, senza influire sui contenuti già tracciati.
Se non hai utilizzato CompositingStrategy.Offscreen
, il risultato dell'applicazione di
BlendMode.Clear
cancella 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 prevedono l'alpha non funzionano come previsto senza un buffer offscreen. Nota l'anello nero intorno all'indicatore del cerchio rosso:
Per capire meglio questo aspetto: se l'app avesse uno sfondo traslucido della finestra e non utilizzi l'CompositingStrategy.Offscreen
, l'BlendMode
interagirebbe con l'intera app. Verranno cancellati tutti i pixel per mostrare l'app o lo sfondo sottostanti, come in questo esempio:
Vale la pena notare che quando utilizzi CompositingStrategy.Offscreen
, viene creata e visualizzata di nuovo una texture fuori schermo
delle dimensioni dell'area di disegno. Per impostazione predefinita, tutti i comandi di disegno eseguiti con questa strategia vengono associati a questa regione. Lo snippet di codice riportato di seguito 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 nel graphicsLayer
. Non creerà un buffer fuori schermo per alpha al di sotto di 1.0f a meno che non sia impostato un RenderEffect
, quindi può essere più efficiente per il rendering alpha. Tuttavia, può fornire risultati diversi
per contenuti che si sovrappongono. Per i casi d'uso in cui è noto in anticipo che i contenuti non si sovrappongono, questa opzione può fornire un rendimento migliore rispetto a CompositingStrategy.Auto
con valori alpha inferiori a 1.
Ecco un altro esempio di strategie di composizione diverse, con l'applicazione di alfabetiche diverse a diverse parti dei componibili e l'applicazione di una strategia Modulate
:
@Preview @Composable fun CompositingStratgey_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)
Scrivere i contenuti di un componibile in una bitmap
Un caso d'uso comune è la creazione di un elemento Bitmap
da un componibile. Per copiare i
contenuti del tuo 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 su disco e condividerla. Per ulteriori dettagli, consulta lo snippet di esempio completo. Assicurati di controllare le autorizzazioni del dispositivo prima di provare a salvare su disco.
Modificatore disegno personalizzato
Per creare il tuo modificatore personalizzato, implementa l'interfaccia DrawModifier
. In questo modo potrai accedere a un ContentDrawScope
, che equivale a ciò che viene esposto quando utilizzi Modifier.drawWithContent()
. Puoi quindi estrarre le operazioni di disegno comuni ai modificatori di disegno personalizzati per ripulire il codice e fornire pratici wrapper. Ad esempio, Modifier.background()
è un comodo DrawModifier
.
Ad esempio, se vuoi implementare un Modifier
che capovolge verticalmente i contenuti, puoi crearne uno nel seguente modo:
class FlippedModifier : DrawModifier { override fun ContentDrawScope.draw() { scale(1f, -1f) { this@draw.drawContent() } } } fun Modifier.flipped() = this.then(FlippedModifier())
Quindi usa questo modificatore capovolto applicato a Text
:
Text( "Hello Compose!", modifier = Modifier .flipped() )
Risorse aggiuntive
Per altri esempi sull'utilizzo di graphicsLayer
e dei disegni personalizzati, consulta le seguenti risorse:
Consigliato per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Elementi grafici in Compose
- Personalizzare un'immagine {:#customize-image}
- Kotlin per Jetpack Compose