Modificatori grafici

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
}

Figura 1: Modifier.drawWithContent utilizzato sopra un componibile per creare un'esperienza UI di tipo torcia.

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:

Testo e uno sfondo disegnati con Modifier.drawfound
Figura 2: testo e sfondo disegnati con Modifier.drawfound

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())
                )
            }
        }
)

Memorizzazione nella cache dell'oggetto Brush con disegnoWithCache
Figura 3: memorizzazione nella cache dell'oggetto Brush con disegnoWithCache

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
        }
)

Figura 4: scaleX e scaleY applicate a un'immagine componibile
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()
        }
)

Figura 5: TranslationX e translationY applicate all'immagine con Modifier.graphicslayer
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
        }
)

Figura 6: rotazioneX, rotazione Y e rotazioneZ impostate su Immagine da Modifier.graphicslayer
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
        }
)

Figura 7: rotazione applicata con TransformOrigin impostato su 0f, 0f

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:

Clip applicata a Box componibile
Figura 8: clip applicato a Box componibile

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).

Clip applicata con traduzioneY e bordo rosso per i contorni
Figura 9: clip applicato con traslazione Y e bordo rosso per i contorni

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))
    )
}

Clip applicato sopra la trasformazione di Graphic Layer
Figura 10: clip applicato sopra la trasformazione di graphiclayer

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
        }
)

Immagine con alfa applicata
Figura 11: immagine con alfa applicata

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.

Modifier.drawWithContent su un'immagine che mostra un'indicazione a cerchio, con l'opzione BlendMode.Clear all'interno dell'app
Figura 12: Modifier.drawWithContent su un'immagine che mostra un'indicazione di un cerchio, con i valori BlendMode.Clear e CompositingStrategy.Offscreen all'interno dell'app

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:

Modifier.drawWithContent su un'immagine che mostra un'indicazione circolare, con BlendMode.Clear e nessuna CompositingStrategy impostata
Figura 13: Modifier.drawWithContent su un'immagine che mostra un'indicazione di un cerchio, con BlendMode.Clear e nessuna CompositingStrategy impostata

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:

Nessuna strategia di CompositingStrategy impostata e l'utilizzo di BlendMode.Clear con un'app con uno sfondo di finestra traslucido. Lo sfondo rosa viene mostrato attraverso l'area intorno al cerchio di stato rosso.
Figura 14: nessuna strategia di CompositingStrategy impostata e l'utilizzo di BlendMode.Clear con un'app con uno sfondo traslucido della finestra. Osserva come lo sfondo rosa viene mostrato attraverso l'area intorno al cerchio di stato rosso.

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()))
       }
   }
}

CompositingStrategy.Auto vs CompositingStrategy.Offscreen: clip fuori schermo nella regione, dove auto no
Figura 15: CompositingStrategy.Auto rispetto a CompositingStrategy.Offscreen: clip fuori schermo nella regione, dove Auto no
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)

ModulateAlpha applica il set alfa a ogni singolo comando di disegno
Figura 16: ModulateAlpha applica il set alpha a ogni singolo comando di disegno

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()
)

Modificatore capovolto personalizzato nel testo
Figura 17: modificatore capovolto personalizzato del testo

Risorse aggiuntive

Per altri esempi sull'utilizzo di graphicsLayer e dei disegni personalizzati, consulta le seguenti risorse: