Elementi grafici in Scrivi

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

Disegno di base con modificatori e DrawScope

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

Ad esempio, per disegnare qualcosa dietro il tuo componibile, 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 componibile che disegni, puoi utilizzare il Canvas componibile. Il componibile Canvas è un wrapper pratico per Modifier.drawBehind. Inserisci Canvas nel layout come faresti con qualsiasi altro elemento dell'interfaccia utente di Compose. All'interno di Canvas, puoi disegnare elementi con un controllo preciso sul loro stile e sulla loro 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 un quarto dello schermo
Figura 1. Rettangolo disegnato utilizzando Canvas in Compose.

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 vengono fornite dai valori dei parametri predefiniti. I parametri predefiniti in genere posizionano l'elemento nel punto [0, 0] sul canvas e forniscono una size predefinita che riempie l'intera area di disegno, come nell'esempio precedente: puoi vedere 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]) è il pixel più in alto a sinistra nell'area di disegno. x aumenta man mano che si sposta a 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 di disegno / griglia di disegno.

Ad esempio, se vuoi disegnare una linea diagonale dall'angolo in alto a destra di l'area del canvas all'angolo in basso a sinistra, puoi utilizzare la DrawScope.drawLine() funzione e specificare un offset di inizio e fine 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 la posizione o la modalità di esecuzione dei comandi di disegno.

Scala

Utilizza DrawScope.scale() per aumentare le dimensioni delle operazioni di disegno di un fattore. Le 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 scala a un cerchio su Canvas.

Traduci

Utilizza DrawScope.translate() per spostare le operazioni di disegno verso l'alto, verso il basso, a sinistra o a destra. Ad esempio, il seguente codice sposta il disegno di 100 px a destra e di 300 px verso l'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

Utilizza DrawScope.rotate() per ruotare le operazioni di disegno attorno 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.

Interno

Utilizza DrawScope.inset() per regolare i parametri predefiniti dell'oggetto corrente DrawScope, modificando i limiti di disegno e traducendo 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 effettivamente 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 DrawScope.withTransform() funzione, che crea e applica una singola trasformazione che combina tutte le modifiche desiderate. 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é dover calcolare e salvare ogni trasformazione nidificata.

Ad esempio, il seguente codice applica sia una traduzione sia 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 sia una traduzione, ruotando il rettangolo e spostandolo a sinistra.

Operazioni di disegno comuni

Disegna testo

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

Per disegnare testo, crea un TextMeasurer utilizzando rememberTextMeasurer e chiama drawText con il misuratore:

val textMeasurer = rememberTextMeasurer()

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

Mostra un saluto disegnato su Canvas
Figura 8. Disegno di testo su Canvas.

Misura il testo

Il disegno del testo funziona in modo leggermente diverso rispetto ad altri comandi di disegno. In genere, al comando di disegno vengono assegnate le dimensioni (larghezza e altezza) con cui disegnare la forma/l'immagine. Con il testo, esistono alcuni parametri che controllano le dimensioni del testo sottoposto a rendering, come le dimensioni del carattere, il carattere, le legature e la 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 su più righe che occupa 2/3 dell'area completa, con un rettangolo di sfondo
Figura 9. Testo su più righe che occupa ⅔ delle dimensioni dell'intera area, con un rettangolo di sfondo.

La regolazione 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 e height, dopodiché il testo segue l'oggetto TextOverflow impostato. Ad esempio, il seguente codice esegue il rendering del testo in ⅓ dell'altezza e ⅓ della larghezza dell'area del 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.

Disegna forme di base

Esistono molte funzioni di disegno di forme su DrawScope. 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

Disegna percorso

Un percorso è una serie di istruzioni matematiche che generano un disegno una volta eseguite. 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 ottenere accesso all'oggetto Canvas stesso su cui puoi chiamare le funzioni.

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

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. Accesso al canvas per disegnare un Drawable.

Scopri di più

Per ulteriori informazioni sul disegno in Compose, consulta le seguenti risorse: