Modificatori grafici

Oltre al componente componibile Canvas, Compose ha diverse grafiche utili Modifiers che aiutano a 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 Crea. In Crea esistono tre modificatori di disegno principali:

Il modificatore di base per il disegno è drawWithContent, dove puoi decidere l'ordine di disegno del tuo elemento componibile e i comandi di disegno emessi all'interno del modificatore. drawBehind è un wrapper pratico per drawWithContent che ha l'ordine di disegno impostato dietro i contenuti del componente componibile. drawWithCache chiama onDrawBehind o onDrawWithContent al suo interno e fornisce un meccanismo per memorizzare nella cache gli oggetti creati.

Modifier.drawWithContent: scegli l'ordine di disegno

Modifier.drawWithContent ti consente di eseguire operazioni DrawScope prima o dopo i contenuti del componente 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 contenuti vengano disegnati prima o dopo le operazioni di disegno personalizzato.

Ad esempio, se vuoi eseguire il rendering di un gradiente radiale sopra i tuoi contenuti per creare un effetto di serratura della torcia 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 elemento componibile per creare un'esperienza UI di tipo torcia.

Modifier.drawBehind: Disegno dietro un elemento componibile

Modifier.drawBehind ti consente di eseguire operazioni DrawScope dietro i contenuti componibili visualizzati sullo schermo. Se dai un'occhiata all'implementazione di Canvas, potresti notare che si tratta solo di un wrapper conveniente 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)
)

che produce il seguente risultato:

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 mantiene gli oggetti creati al suo interno memorizzati nella cache. Gli oggetti vengono memorizzati nella cache finché le dimensioni dell'area di disegno rimangono invariate o finché gli oggetti di stato letti non cambiano. 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 durante il disegno.

In alternativa, puoi memorizzare nella cache gli oggetti utilizzando remember, al di fuori del modificatore. Tuttavia, ciò non è sempre possibile in quanto non sempre hai accesso alla composizione. L'utilizzo di drawWithCache può essere più efficiente se gli oggetti vengono utilizzati solo per il disegno.

Ad esempio, se crei un Brush per disegnare un gradiente dietro un Text, utilizzando 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 Brush con drawWithCache

Modificatori grafici

Modifier.graphicsLayer: Applica trasformazioni ai composable

Modifier.graphicsLayer è un modificatore che inserisce il contenuto del composable in un livello di disegno. Un livello fornisce diverse funzioni, 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 ri-eseguire 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 istruzioni di disegno vengono eseguite e l'output viene acquisito in un buffer off-screen. 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 il ridimensionamento 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 rieseguire la lambda di disegno.

Modifier.graphicsLayer non modifica le dimensioni o il posizionamento misurati del componente, in quanto influisce solo sulla fase di disegno. Ciò significa che il tuo elemento componibile potrebbe sovrapporsi ad altri se viene disegnato al di fuori dei limiti del layout.

Con questo modificatore è possibile applicare le seguenti trasformazioni:

Scala: aumenta le dimensioni

scaleX e scaleY ingrandiscono o riducono i contenuti in direzione orizzontale o verticale, rispettivamente. Un valore pari a 1.0f indica che non è stata apportata alcuna modifica alla scala, mentre un valore pari a 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 elemento componibile Image
Traduzione

translationX e translationY possono essere modificati con graphicsLayer, translationX sposta il componente componibile a sinistra o a destra. translationY sposta il componente 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 la rotazione orizzontale, rotationY per la rotazione verticale e rotationZ per la rotazione 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: rotationX, rotationY e rotationZ impostati su Image by Modifier.graphicsLayer
Origin

È possibile specificare un transformOrigin. Viene quindi utilizzato come punto da cui avvengono le trasformazioni. Tutti gli esempi finora hanno utilizzato TransformOrigin.Center, che si trova a (0.5f, 0.5f). Se specifichi l'origine in (0f, 0f), le trasformazioni iniziano dall'angolo in alto a sinistra del componente componibile.

Se modifichi l'origine con una trasformazione rotationZ, puoi notare che l'elemento ruota attorno all'angolo in alto a sinistra del componente:

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

Forma specifica il contorno a cui vengono ritagliati i contenuti quando clip = true. In questo esempio, abbiamo impostato due caselle in modo che contengano due clip diversi: una utilizza la variabile clip graphicsLayer e l'altra il wrapper conveniente 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") vengono ritagliati in base alla forma del cerchio:

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

Se poi applichi un translationY al cerchio rosa in alto, vedrai che i limiti del composable sono ancora gli stessi, ma il cerchio viene disegnato sotto il cerchio in basso (e al di fuori dei suoi limiti).

Clip applicato con translationY 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 graphicsLayer
Figura 10: clip applicato sopra la trasformazione del livello di grafica

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 alfa applicato
Figura 11: immagine con alpha applicato

Strategia di composizione

Lavorare con alpha e trasparenza potrebbe non essere semplice come modificare un singolo valore alpha. Oltre a modificare un alpha, è possibile anche impostare un CompositingStrategy su un graphicsLayer. Un CompositingStrategy determina in che modo il contenuto del composable viene composto (assemblato) 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 off-screen se il valore alfa è inferiore a 1.0f o se è impostato un RenderEffect. Quando l'alpha è inferiore a 1f, viene creato automaticamente un livello di composizione per eseguire il rendering dei contenuti e poi disegnare questo buffer off-screen nella destinazione con l'alpha corrispondente. L'impostazione di un RenderEffect o di uno scorrimento eccessivo esegue sempre il rendering dei contenuti in un buffer fuori schermo, indipendentemente dal valore di CompositingStrategy impostato.

Fuori schermo

I contenuti del composable vengono sempre rasterizzati in una texture o bitmap off-screen prima del rendering nella destinazione. Questa funzionalità è utile per applicare operazioni di BlendMode per mascherare i contenuti e per migliorare le prestazioni durante il rendering di set complessi di istruzioni di disegno.

Un esempio di utilizzo di CompositingStrategy.Offscreen è con BlendModes. Dando un'occhiata all'esempio riportato di seguito, supponiamo che tu voglia rimuovere parti di un Image componibile emettendo 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 imposti CompositingStrategy su Offscreen, viene creata una texture off-screen per eseguire i comandi (applicando BlendMode solo ai contenuti di questo elemento componibile). e lo esegue 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, l'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 coinvolgono il canale alfa non funzioneranno come previsto senza un buffer off-screen. 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 circolare, con BlendMode.Clear e nessuna CompositingStrategy impostata

Per capire meglio: se l'app aveva uno sfondo della finestra traslucido e non hai utilizzato CompositingStrategy.Offscreen, BlendMode interagirebbe con l'intera app. Cancellerà tutti i pixel per mostrare l'app o lo sfondo sottostante, come in questo esempio:

Nessuna CompositingStrategy impostata e utilizzo di BlendMode.Clear con un'app con uno sfondo della finestra traslucido. Lo sfondo rosa è visibile nell'area intorno al cerchio di stato rosso.
Figura 14: nessuna impostazione di CompositingStrategy e utilizzo di BlendMode.Clear con un'app con uno sfondo della finestra traslucido. Nota come lo sfondo rosa sia visibile attraverso l'area intorno al cerchio di stato rosso.

È importante notare che quando si utilizza CompositingStrategy.Offscreen, viene creata e visualizzata sullo schermo una texture fuori schermo delle dimensioni dell'area di disegno. Per impostazione predefinita, tutti i comandi di disegno eseguiti con questa strategia vengono ritagliati in questa regione. Lo snippet di codice riportato di seguito illustra le differenze quando si passa all'utilizzo di texture off-screen:

@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 offscreen nella regione in cui non è presente l'opzione automatica
Figura 15: CompositingStrategy.Auto vs CompositingStrategy.Offscreen - offscreen clips to the region, where auto doesn’t
ModulateAlpha

Questa strategia di composizione modula l'alpha per ciascuna delle istruzioni di disegno registrate all'interno di graphicsLayer. Non creerà un buffer off-screen per l'alpha inferiore a 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 i contenuti sovrapposti. Per i casi d'uso in cui è noto in anticipo che i contenuti non si sovrappongono, questa opzione 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: l'applicazione di valori alfa diversi a parti diverse dei componenti 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 l'alpha impostato a ogni singolo comando di disegno
Figura 16: ModulateAlpha applica l'alpha impostato a ogni singolo comando di disegno

Scrivere i contenuti di un elemento componibile in una bitmap

Un caso d'uso comune è la creazione di un Bitmap da un composable. 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 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 un modificatore personalizzato, implementa l'interfaccia DrawModifier. In questo modo, hai accesso a un ContentDrawScope, che è lo stesso di quello esposto quando utilizzi Modifier.drawWithContent(). Puoi quindi estrarre operazioni di disegno comuni in modificatori di disegno personalizzati per pulire il codice e fornire wrapper pratici; ad esempio, Modifier.background() è un DrawModifier pratico.

Ad esempio, se vuoi implementare un Modifier che capovolge verticalmente i contenuti, puoi crearne uno come segue:

class FlippedModifier : DrawModifier {
    override fun ContentDrawScope.draw() {
        scale(1f, -1f) {
            this@draw.drawContent()
        }
    }
}

fun Modifier.flipped() = this.then(FlippedModifier())

Quindi utilizza questo modificatore invertito applicato a Text:

Text(
    "Hello Compose!",
    modifier = Modifier
        .flipped()
)

Modificatore invertito personalizzato sul testo
Figura 17: modificatore personalizzato invertito sul testo

Risorse aggiuntive

Per altri esempi di utilizzo di graphicsLayer e del disegno personalizzato, consulta le seguenti risorse: