Oltre al composable Canvas
, Compose offre diversi elementi grafici utiliModifiers
che consentono di disegnare contenuti personalizzati. Questi modificatori sono utili
perché possono essere applicati a qualsiasi composable.
Modificatori dei disegni
Tutti i comandi di disegno vengono eseguiti con un modificatore di disegno in Componi. In Compose esistono tre modificatori di disegno principali:
Il modificatore di base per il disegno è drawWithContent
, che ti consente di decidere l'ordine di disegno del tuo Composable e i comandi di disegno emessi all'interno del modificatore. drawBehind
è un comodo wrapper attorno a drawWithContent
in cui l'ordine di disegno è impostato dietro il contenuto dell'elemento componibile. 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 del disegno
Modifier.drawWithContent
consente di eseguire operazioni DrawScope
prima o dopo il contenuto del componibile. Assicurati di chiamare drawContent
per visualizzare i contenuti effettivi del composable. Con questo modificatore puoi decidere l'ordine delle operazioni se vuoi che i tuoi contenuti vengano tracciati prima o dopo le operazioni di disegno personalizzato.
Ad esempio, se vuoi applicare un gradiente radiale sopra i contenuti per creare un effetto di luce del buco della serratura nell'interfaccia utente, 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 composibili disegnati sullo schermo. Se
esamina l'implementazione di Canvas
, potresti notare che
si tratta solo di un pratico wrapper attorno a 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
conserva nella cache gli oggetti
creati al suo interno. Gli oggetti vengono memorizzati nella cache purché la dimensione dell'area di disegno sia la stessa o se gli oggetti di stato letti non hanno subito modifiche. Questo modificatore è utile per migliorare le prestazioni delle chiamate di disegno, in quanto evita la necessità di riassegnare 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, non sempre è possibile: non sempre hai accesso alla composizione. Può essere più efficace 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 di grafica
Modifier.graphicsLayer
: applica le trasformazioni ai composabili
Modifier.graphicsLayer
è un modificatore che trasforma i contenuti del disegno componibile 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 sue istruzioni di disegno vengono eseguite e l'output viene acquisito in un buffer offscreen. La composizione di un buffer di questo tipo per i frame successivi è più veloce 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 istruzioni di disegno. Ad esempio, è possibile applicare varie trasformazioni utilizzando Modifier.graphicsLayer
.
Questi possono essere animati o modificati senza dover eseguire nuovamente il disegno lambda.
Modifier.graphicsLayer
non modifica le dimensioni o il posizionamento misurati del composable, in quanto influisce solo sulla fase di disegno. Ciò significa che il tuo componibile potrebbe sovrapporsi ad altri se finisca per tracciare al di fuori dei limiti del layout.
Con questo modificatore è possibile applicare le seguenti trasformazioni:
Scala - aumenta dimensioni
scaleX
e scaleY
consentono di ingrandire o ridurre 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
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 l'elemento componibile a sinistra o a destra. translationY
consente di spostare 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 la rotazione orizzontalmente, rotationY
per la rotazione verticale e
rotationZ
per la rotazione sull'asse Z (rotazione standard). Questo valore viene 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 transformOrigin
. Viene poi utilizzato come punto di partenza per 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 alla parte 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 e forma
La forma specifica il contorno a cui vengono ritagliati i contenuti quando clip = true
. In questo esempio, abbiamo impostato due caselle con due clip diversi: uno che utilizza la variabile clip graphicsLayer
e l'altro che utilizza 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 "Un saluto da Scrivi") vengono ritagliati in base alla forma del cerchio:
Se applichi un translationY
al cerchio rosa in alto, vedrai che i limiti del composable rimangono invariati, ma il cerchio viene disegnato sotto il cerchio in basso (e al di fuori dei suoi limiti).
Per ritagliare il composable nella regione in cui è 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 composizione
L'utilizzo di alpha e trasparenza potrebbe non essere semplice come modificare un singolo valore alpha. Oltre a modificare una versione alpha, esiste anche la possibilità di impostare una CompositingStrategy
su un graphicsLayer
. Un CompositingStrategy
determina in che modo 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 composizione è determinata dal resto dei parametri graphicsLayer
. Esegue il rendering del livello in un buffer fuori schermo se alpha è inferiore a 1.0f o è impostato un RenderEffect
. Ogni volta che l'alpha è inferiore a 1f, viene creato automaticamente un livello di composizione per eseguire il rendering dei contenuti e poi disegnare questo buffer offscreen nella destinazione con l'alpha corrispondente. L'impostazione di un valore RenderEffect
o di scorrimento eccessivo rende sempre i contenuti in un buffer offscreen, indipendentemente dal valore impostato per CompositingStrategy
.
Fuori schermo
I contenuti del composable vengono sempre rasterizzati in una texture o bitmap offscreen prima del rendering nella destinazione. Questa operazione è utile per applicare operazioni BlendMode
per mascherare i contenuti e per il rendimento 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 composable Image
emettendo un comando draw 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 imposti CompositingStrategy
su Offscreen
, viene creata una texture offscreen su cui eseguire i comandi (applicando BlendMode
solo ai contenuti di questo composable). Viene poi visualizzato 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 della destinazione, indipendentemente da ciò che è stato già impostato, lasciando visibile il buffer di rendering della finestra (nero). Molti
BlendModes
che coinvolgono la versione alpha non funzioneranno come previsto senza
un buffer fuori schermo. Nota l'anello nero attorno all'indicatore del cerchio rosso:
Per comprendere meglio: se l'app avesse uno sfondo della finestra traslucido e non utilizzassi CompositingStrategy.Offscreen
, BlendMode
interagirebbe con l'intera app. Cancellerebbe tutti i pixel per mostrare l'app o lo sfondo sottostante, come in questo esempio:
È opportuno notare che, quando utilizzi CompositingStrategy.Offscreen
, viene creata una texture fuori schermo
delle dimensioni dell'area di disegno, che viene visualizzata di nuovo
sullo schermo. Per impostazione predefinita, tutti i comandi di disegno eseguiti con questa strategia vengono tagliati in base a questa regione. Lo snippet di codice seguente illustra le differenze quando si passa all'utilizzo di texture offscreen:
@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 del graphicsLayer
. Non verrà creato un buffer offscreen per valori alpha inferiori a 1.0f, a meno che non sia impostato un valore RenderEffect
, pertanto può essere più efficiente per il rendering dell'alfa. Tuttavia, può fornire risultati diversi per i contenuti in sovrapposizione. Per i casi d'uso in cui è noto in anticipo che i contenuti
non si sovrappongono, ciò può fornire prestazioni migliori rispetto a
CompositingStrategy.Auto
con valori alpha inferiori a 1.
Di seguito è riportato un altro esempio di strategie di composizione diverse, ovvero l'applicazione di diversi elementi alfa a parti diverse 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) ) } val Purple = Color(0xFF7E57C2) val Yellow = Color(0xFFFFCA28) val Red = Color(0xFFEF5350)
Scrivere i contenuti di un composable in una bitmap
Un caso d'uso comune è la creazione di un Bitmap
da un composable. Per copiare i contenuti del composable in un Bitmap
, crea un Bitmap
utilizzando rememberGraphicsLayer()
.GraphicsLayer
Reindirizza i comandi di disegno al nuovo livello utilizzando drawWithContent()
e
graphicsLayer.record{}
. Quindi, 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 ulteriori dettagli, consulta lo snippet di esempio completo. Assicurati di controllare le autorizzazioni sul dispositivo prima di provare a salvare sul disco.
Modificatore disegno personalizzato
Per creare il tuo modificatore personalizzato, implementa l'interfaccia DrawModifier
. In questo modo, hai accesso a un ContentDrawScope
, che è lo stesso visualizzato quando utilizzi Modifier.drawWithContent()
. Puoi quindi estrarre le operazioni di disegno comuni in modificatori di disegno personalizzati per pulire il codice e fornire wrapper pratici; ad esempio, Modifier.background()
è un pratico DrawModifier
.
Ad esempio, se vuoi implementare un Modifier
che capovolga
i contenuti verticalmente, 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())
Poi utilizza questo modificatore capovolto applicato a Text
:
Text( "Hello Compose!", modifier = Modifier .flipped() )
Risorse aggiuntive
Per altri esempi di utilizzo di graphicsLayer
e dei disegni personalizzati, consulta le seguenti risorse:
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Grafica in Compose
- Personalizzare un'immagine {:#customize-image}
- Kotlin per Jetpack Compose