Elementi grafici in Scrivi

Molte app devono essere in grado di controllare con precisione cosa viene disegnato sullo schermo. Potrebbe trattarsi di un semplice riquadro o cerchio sullo schermo posizionato nel punto giusto oppure di una complessa disposizione di elementi grafici in molti stili diversi.

Disegno di base con modificatori e DrawScope

Il modo principale per disegnare qualcosa di personalizzato in Compose è con i modificatori, come Modifier.drawWithContent, Modifier.drawBehind e Modifier.drawWithCache.

Ad esempio, per disegnare qualcosa dietro il composable, puoi utilizzare il modificatore drawBehind per iniziare a eseguire i comandi di disegno:

Spacer(
    modifier = Modifier
        .fillMaxSize()
        .drawBehind {
            // this = DrawScope
        }
)

Se ti serve solo un composable che disegni, puoi utilizzare il composable Canvas. Il componente componibile Canvas è un wrapper pratico per Modifier.drawBehind. Posiziona Canvas nel layout come faresti con qualsiasi altro elemento dell'interfaccia utente Compose. All'interno di Canvas, puoi disegnare elementi con un controllo preciso su stile e posizione.

Tutti i modificatori di disegno espongono un DrawScope, un ambiente di disegno con ambito che mantiene il proprio stato. In questo modo puoi impostare i parametri per un gruppo di elementi grafici. DrawScope fornisce diversi campi utili, come size, un oggetto Size che specifica le dimensioni attuali di DrawScope.

Per disegnare qualcosa, puoi utilizzare una delle tante funzioni di disegno su DrawScope. Ad esempio, il seguente codice disegna un rettangolo nell'angolo in alto a sinistra dello schermo:

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasQuadrantSize = size / 2F
    drawRect(
        color = Color.Magenta,
        size = canvasQuadrantSize
    )
}

Rettangolo rosa disegnato su uno sfondo bianco che occupa un quarto dello schermo
Figura 1. Rettangolo disegnato utilizzando Canvas in Composizione.

Per scoprire di più sui diversi modificatori di disegno, consulta la documentazione relativa ai modificatori grafici.

Sistema di coordinate

Per disegnare qualcosa sullo schermo, devi conoscere l'offset (x e y) e le dimensioni dell'elemento. Con molti dei metodi di disegno su DrawScope, la posizione e le dimensioni sono fornite dai valori predefiniti dei parametri. I parametri predefiniti in genere posizionano l'elemento nel punto [0, 0] del canvas e forniscono un size predefinito che riempie l'intera area di disegno, come nell'esempio precedente. Puoi notare che il rettangolo è posizionato in alto a sinistra. Per regolare le dimensioni e la posizione dell'elemento, devi comprendere il sistema di coordinate in Compose.

L'origine del sistema di coordinate ([0,0]) si trova nel pixel più in alto a sinistra dell'area di disegno. x aumenta man mano che si sposta verso destra e y aumenta man mano che si sposta verso il basso.

Una griglia che mostra il sistema di coordinate con l'angolo in alto a sinistra [0, 0] e l'angolo in basso a destra [larghezza, altezza]
Figura 2. Sistema di coordinate del disegno / griglia del disegno.

Ad esempio, se vuoi tracciare una linea diagonale dall'angolo in alto a destra dell'area del canvas all'angolo in basso a sinistra, puoi utilizzare la funzione DrawScope.drawLine() e specificare un offset iniziale e finale con le posizioni x e y corrispondenti:

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasWidth = size.width
    val canvasHeight = size.height
    drawLine(
        start = Offset(x = canvasWidth, y = 0f),
        end = Offset(x = 0f, y = canvasHeight),
        color = Color.Blue
    )
}

Trasformazioni di base

DrawScope offre trasformazioni per modificare dove o come vengono eseguiti i comandi di disegno.

Diffusione

Utilizza DrawScope.scale() per aumentare le dimensioni delle operazioni di disegno di un fattore. Operazioni come scale() si applicano a tutte le operazioni di disegno all'interno della lambda corrispondente. Ad esempio, il seguente codice aumenta scaleX di 10 volte e scaleY di 15 volte:

Canvas(modifier = Modifier.fillMaxSize()) {
    scale(scaleX = 10f, scaleY = 15f) {
        drawCircle(Color.Blue, radius = 20.dp.toPx())
    }
}

Un cerchio scalato in modo non uniforme
Figura 3. Applicazione di un'operazione di ridimensionamento a un cerchio su Canvas.

Traduci

Utilizza DrawScope.translate() per spostare le operazioni di disegno verso l'alto, il basso, sinistra o destra. Ad esempio, il seguente codice sposta il disegno di 100 px a destra e di 300 px in alto:

Canvas(modifier = Modifier.fillMaxSize()) {
    translate(left = 100f, top = -300f) {
        drawCircle(Color.Blue, radius = 200.dp.toPx())
    }
}

Un cerchio che si è spostato dal centro
Figura 4. Applicazione di un'operazione di traduzione a un cerchio su Canvas.

Ruota

Usa DrawScope.rotate() per ruotare le operazioni di disegno intorno a un punto di rotazione. Ad esempio, il seguente codice ruota un rettangolo di 45 gradi:

Canvas(modifier = Modifier.fillMaxSize()) {
    rotate(degrees = 45F) {
        drawRect(
            color = Color.Gray,
            topLeft = Offset(x = size.width / 3F, y = size.height / 3F),
            size = size / 3F
        )
    }
}

Uno smartphone con un rettangolo ruotato di 45 gradi al centro dello schermo
Figura 5. Utilizziamo rotate() per applicare una rotazione all'ambito di disegno corrente, che ruota il rettangolo di 45 gradi.

Inset

Utilizza DrawScope.inset() per regolare i parametri predefiniti dell'attuale DrawScope, modificando i limiti di disegno e traslando i disegni di conseguenza:

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasQuadrantSize = size / 2F
    inset(horizontal = 50f, vertical = 30f) {
        drawRect(color = Color.Green, size = canvasQuadrantSize)
    }
}

Questo codice aggiunge in modo efficace il padding ai comandi di disegno:

Un rettangolo con un riempimento tutto intorno
Figura 6. Applicazione di un rientro ai comandi di disegno.

Più trasformazioni

Per applicare più trasformazioni ai disegni, utilizza la funzione DrawScope.withTransform(), che crea e applica una singola trasformazione che combina tutte le modifiche che vuoi apportare. L'utilizzo di withTransform() è più efficiente rispetto all'esecuzione di chiamate nidificate a singole trasformazioni, perché tutte le trasformazioni vengono eseguite insieme in una singola operazione, anziché Compose che deve calcolare e salvare ciascuna delle trasformazioni nidificate.

Ad esempio, il seguente codice applica sia una traslazione che una rotazione al rettangolo:

Canvas(modifier = Modifier.fillMaxSize()) {
    withTransform({
        translate(left = size.width / 5F)
        rotate(degrees = 45F)
    }) {
        drawRect(
            color = Color.Gray,
            topLeft = Offset(x = size.width / 3F, y = size.height / 3F),
            size = size / 3F
        )
    }
}

Uno smartphone con un rettangolo ruotato spostato sul lato dello schermo
Figura 7. Utilizza withTransform per applicare sia una rotazione che una traslazione, ruotando il rettangolo e spostandolo a sinistra.

Operazioni di disegno comuni

Disegna testo

Per disegnare il testo in Compose, in genere puoi utilizzare il composable Text. Tuttavia, se ti trovi in un DrawScope o vuoi disegnare il testo manualmente con la personalizzazione, puoi utilizzare il metodo DrawScope.drawText().

Per disegnare il testo, crea un TextMeasurer utilizzando rememberTextMeasurer e chiama drawText con lo strumento di misurazione:

val textMeasurer = rememberTextMeasurer()

Canvas(modifier = Modifier.fillMaxSize()) {
    drawText(textMeasurer, "Hello")
}

Un saluto disegnato su Canvas
Figura 8. Disegno di testo su Canvas.

Misura del testo

Il testo del disegno funziona in modo leggermente diverso rispetto ad altri comandi di disegno. Normalmente, dai al comando di disegno le dimensioni (larghezza e altezza) con cui disegnare la forma/l'immagine. Con il testo, esistono alcuni parametri che controllano le dimensioni del testo di rendering, come dimensioni del carattere, carattere, legature e spaziatura tra le lettere.

Con Compose, puoi utilizzare un TextMeasurer per accedere alle dimensioni misurate del testo, a seconda dei fattori sopra indicati. Se vuoi disegnare uno sfondo dietro il testo, puoi utilizzare le informazioni misurate per ottenere le dimensioni dell'area occupata dal testo:

val textMeasurer = rememberTextMeasurer()

Spacer(
    modifier = Modifier
        .drawWithCache {
            val measuredText =
                textMeasurer.measure(
                    AnnotatedString(longTextSample),
                    constraints = Constraints.fixedWidth((size.width * 2f / 3f).toInt()),
                    style = TextStyle(fontSize = 18.sp)
                )

            onDrawBehind {
                drawRect(pinkColor, size = measuredText.size.toSize())
                drawText(measuredText)
            }
        }
        .fillMaxSize()
)

Questo snippet di codice produce uno sfondo rosa sul testo:

Testo multilinea che occupa i 2/3 della superficie totale, con un rettangolo di sfondo
Figura 9. Testo multilinea che occupa 2/3 dell'area completa, con un rettangolo di sfondo.

La modifica dei vincoli, delle dimensioni del carattere o di qualsiasi proprietà che influisce sulle dimensioni misurate comporta la segnalazione di una nuova dimensione. Puoi impostare una dimensione fissa sia per width che per height e il testo segue poi la TextOverflow impostata. Ad esempio, il seguente codice esegue il rendering del testo in ⅓ dell'altezza e ⅓ della larghezza dell'area componibile e imposta TextOverflow su TextOverflow.Ellipsis:

val textMeasurer = rememberTextMeasurer()

Spacer(
    modifier = Modifier
        .drawWithCache {
            val measuredText =
                textMeasurer.measure(
                    AnnotatedString(longTextSample),
                    constraints = Constraints.fixed(
                        width = (size.width / 3f).toInt(),
                        height = (size.height / 3f).toInt()
                    ),
                    overflow = TextOverflow.Ellipsis,
                    style = TextStyle(fontSize = 18.sp)
                )

            onDrawBehind {
                drawRect(pinkColor, size = measuredText.size.toSize())
                drawText(measuredText)
            }
        }
        .fillMaxSize()
)

Il testo viene ora disegnato nei vincoli con un'ellissi alla fine:

Testo disegnato su sfondo rosa, con puntini di sospensione che tagliano il testo.
Figura 10. TextOverflow.Ellipsis con vincoli fissi sulla misurazione del testo.

Disegna immagine

Per disegnare un ImageBitmap con DrawScope, carica l'immagine utilizzando ImageBitmap.imageResource() e poi chiama drawImage:

val dogImage = ImageBitmap.imageResource(id = R.drawable.dog)

Canvas(modifier = Modifier.fillMaxSize(), onDraw = {
    drawImage(dogImage)
})

Un'immagine di un cane disegnato su Canvas
Figura 11. Disegno di un ImageBitmap su Canvas.

Disegnare forme di base

DrawScope offre molte funzioni di disegno di forme. Per disegnare una forma, utilizza una delle funzioni di disegno predefinite, ad esempio drawCircle:

val purpleColor = Color(0xFFBA68C8)
Canvas(
    modifier = Modifier
        .fillMaxSize()
        .padding(16.dp),
    onDraw = {
        drawCircle(purpleColor)
    }
)

API

Output

drawCircle()

disegna cerchio

drawRect()

draw rect

drawRoundedRect()

draw rounded rect

drawLine()

draw line

drawOval()

disegna ovale

drawArc()

disegna arco

drawPoints()

punti di estrazione

Percorso di disegno

Un percorso è una serie di istruzioni matematiche che, una volta eseguite, danno vita a un disegno. DrawScope può disegnare un percorso utilizzando il metodo DrawScope.drawPath().

Ad esempio, supponiamo che tu voglia disegnare un triangolo. Puoi generare un percorso con funzioni come lineTo() e moveTo() utilizzando le dimensioni dell'area di disegno. Poi chiama drawPath() con questo percorso appena creato per ottenere un triangolo.

Spacer(
    modifier = Modifier
        .drawWithCache {
            val path = Path()
            path.moveTo(0f, 0f)
            path.lineTo(size.width / 2f, size.height / 2f)
            path.lineTo(size.width, 0f)
            path.close()
            onDrawBehind {
                drawPath(path, Color.Magenta, style = Stroke(width = 10f))
            }
        }
        .fillMaxSize()
)

Un triangolo viola rovesciato disegnato su Crea
Figura 12. Creazione e disegno di un Path in Compose.

Accesso all'oggetto Canvas

Con DrawScope, non hai accesso diretto a un oggetto Canvas. Puoi utilizzare DrawScope.drawIntoCanvas() per accedere all'oggetto Canvas stesso su cui puoi chiamare le funzioni.

Ad esempio, se hai un Drawable personalizzato che vuoi disegnare sulla tela, puoi accedere alla tela e chiamare Drawable#draw(), passando l'oggetto Canvas:

val drawable = ShapeDrawable(OvalShape())
Spacer(
    modifier = Modifier
        .drawWithContent {
            drawIntoCanvas { canvas ->
                drawable.setBounds(0, 0, size.width.toInt(), size.height.toInt())
                drawable.draw(canvas.nativeCanvas)
            }
        }
        .fillMaxSize()
)

Un ShapeDrawable nero ovale che occupa l'intera dimensione
Figura 13. Accedere al canvas per disegnare un Drawable.

Scopri di più

Per ulteriori informazioni su Disegno in Compose, consulta le seguenti risorse: