Modificatori grafici

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
}

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

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:

Testo e sfondo disegnati utilizzando Modifier.drawBehind
Figura 2: testo e sfondo disegnati utilizzando Modifier.drawBehind

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

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

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

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

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

Figura 6: rotationX, rotationY e rotationZ impostati su Image da Modifier.graphicsLayer
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
        }
)

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

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:

Clip applicato al composable Box
Figura 8: clip applicato al composable Box

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

Clip applicato con la translazione Y e bordo rosso per il contorno
Figura 9: clip applicato con traslazione Y e bordo rosso per il contorno

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

Clip applicato sopra la trasformazione del livello grafico
Figura 10: clip applicato sopra la trasformazione graphiclayer

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

Immagine con alpha applicato
Figura 11: immagine con alpha applicato

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.

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

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:

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

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:

Nessuna strategia CompositingStrategy impostata e utilizzo di MergeMode.Clear con un'app che ha uno sfondo di una finestra traslucida. La carta da parati rosa viene mostrata nell'area attorno al cerchio di stato rosso.
Figura 14: nessuna strategia di composizione impostata e utilizzo di BlendMode.Clear con un'app con uno sfondo della finestra traslucido. Notare come lo sfondo rosa viene visualizzato nell'area attorno al cerchio di stato rosso.

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

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

ModulateAlpha applica il valore alpha impostato a ogni singolo comando draw
Figura 16: ModulateAlpha applica il valore alpha impostato a ogni singolo comando di disegno

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

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

Risorse aggiuntive

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