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

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 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:

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

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

Memorizzazione nella cache dell'oggetto Pennello 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 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
        }
)

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

Figura 5: translationX e translationY applicati a Image con Modifier.graphicsLayer
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
        }
)

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 translationY 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 del livello grafico

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

Modifier.drawWithContent su un'immagine che mostra un'indicazione circolare, 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 degli BlendModes che coinvolgono l'alpha non funzioneranno come previsto senza un buffer offscreen. 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 di composizione impostata e utilizzo di BlendMode.Clear con un'app con uno sfondo della finestra traslucido. 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 che ha uno sfondo della finestra traslucido. Notare come lo sfondo rosa viene visualizzato nell'area attorno al cerchio di stato rosso.

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

CompositingStrategy.Auto e CompositingStrategy.Offscreen: i clip fuori campo vengono inseriti nella regione, mentre non avviene con la modalità automatica
Figura 15: CompositingStrategy.Auto e CompositingStrategy.Offscreen: i clip offscreen vengono inseriti nella regione, mentre non avviene con l'auto
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)

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

Modificatore personalizzato capovolto sul 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: