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 di disegno
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 Composable e i comandi di disegno emessi all'interno del modificatore. drawBehind
è un pratico wrapper per drawWithContent
che ha il
ordine di disegno impostato su 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 composable. Assicurati di chiamare drawContent
per visualizzare i contenuti effettivi del composable. 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 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 esaminate l'implementazione di Canvas
, potreste notare che si tratta solo di un pratico 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 di oggetti Draw
Modifier.drawWithCache
mantiene in cache gli oggetti
che vengono creati al suo interno. Gli oggetti vengono memorizzati nella cache purché le dimensioni
dell'area di disegno rimangano invariate o gli oggetti di stato letti non
siano stati modificati. Questo modificatore è utile per migliorare le prestazioni delle chiamate di disegno, in quanto consente di evitare 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, ciò non è sempre possibile perché non hai sempre accesso alla composizione. L'utilizzo di drawWithCache
può essere più efficace 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 all'interno di un livello possono essere riassegnate 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 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. Il compositing 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 ridimensionamento o 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 liamma di disegno.
Modifier.graphicsLayer
non modifica le dimensioni o il posizionamento misurati del composable, in quanto influisce solo sulla fase di disegno. Ciò significa che il composable potrebbe sovrapporsi ad altri se finisce per essere disegnato 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 sostituiti con graphicsLayer
.translationX
sposta il composable verso sinistra o verso 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 ruotare orizzontalmente, rotationY
per ruotare verticalmente e
rotationZ
per ruotare 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 un'alfa, è possibile impostare anche un
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 offscreen se alpha è inferiore a 1.0f o se è impostato un valore 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 degli BlendModes
che coinvolgono l'alpha non funzioneranno come previsto senza un buffer offscreen. 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:
È importante notare che, quando utilizzi CompositingStrategy.Offscreen
, viene creata e visualizzata sullo schermo una texture offscreen delle dimensioni dell'area di disegno. 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, questo approccio può offrire un rendimento migliore rispetto a
CompositingStrategy.Auto
con valori alfa inferiori a 1.
Di seguito è riportato un altro esempio di diverse strategie di composizione: applicazione di valori alpha diversi a parti diverse dei composabili e 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 maggiori dettagli, consulta lo snippet di esempio completo. Assicurati di controllare le autorizzazioni sul dispositivo prima di provare a salvare sul disco.
Modificatore di 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 ripulire il codice e fornire wrapper pratici; ad esempio, Modifier.background()
è un praticoDrawModifier
.
Ad esempio, se vuoi implementare un Modifier
che capovolga 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 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