Elementi grafici in Scrivi

Molte app devono essere in grado di controllare con precisione cosa viene disegnato sullo schermo. Può trattarsi di un'azione semplice come inserire una casella o un cerchio sullo schermo nel punto giusto o di un'elaborata 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 disegna, puoi utilizzare il composable Canvas. Il composable Canvas è un wrapper comodo per Modifier.drawBehind. Posiziona 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, ad esempio size, un oggetto Size che specifica le dimensioni attuali del DrawScope.

Per disegnare qualcosa, puoi utilizzare una delle numerose funzioni di disegno in 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 Scrittura.

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 draw in DrawScope, la posizione e le dimensioni vengono fornite dai valori predefiniti dei parametri. In genere, i parametri predefiniti posizionano l'elemento nel punto [0, 0] della tela e forniscono un valore size predefinito 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]) si trova nel pixel 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'origine in alto a sinistra [0, 0] e in basso a destra [larghezza, altezza]
Figura 2. Sistema di coordinate del disegno / griglia di disegno.

Ad esempio, se vuoi tracciare una linea diagonale dall'angolo in alto a destra dell'area della tela 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 funzione lambda corrispondente. Ad esempio, il seguente codice aumenta scaleX 10 volte e scaleY 15 volte:

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

Un cerchio con scala non uniforme
Figura 3. Applicazione di un'operazione di scala a un cerchio in 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 traslazione a un cerchio in 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.

Inset

Utilizza DrawScope.inset() per modificare i parametri predefiniti dell'DrawScope corrente, cambiando i confini dei disegni e traducendoli di conseguenza:

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

Questo codice aggiunge efficacemente spaziatura ai comandi di disegno:

Un rettangolo con spaziatura interna
Figura 6. Applicazione di un incavo 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 desiderate. L'utilizzo di withTransform() è più efficiente rispetto all'esecuzione di chiamate nidificate alle singole trasformazioni, perché tutte le trasformazioni vengono eseguite insieme in un unica operazione, anziché dover calcolare e salvare ciascuna delle trasformazioni nidificate.

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

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. Usa withTransform per applicare sia una rotazione che una traslazione, ruotando il rettangolo e spostandolo verso sinistra.

Operazioni di disegno comuni

Disegna testo

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

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

val textMeasurer = rememberTextMeasurer()

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

Viene mostrato un messaggio di saluto disegnato su Canvas
Figura 8. Disegno di testo su Canvas.

Misura il testo

Il testo disegnato funziona in modo leggermente diverso rispetto agli altri comandi di disegno. In genere, al comando di disegno vengono assegnate le dimensioni (larghezza e altezza) in cui disegnare la forma/l'immagine. Per il testo, esistono alcuni parametri che controllano le dimensioni del testo visualizzato, ad esempio le dimensioni del carattere, il carattere, le legature e la spaziatura tra le lettere.

Con Scrivi, puoi utilizzare un TextMeasurer per accedere alle dimensioni misurate del testo, in base ai fattori sopra indicati. Se vuoi disegnare un 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 genera uno sfondo rosa per il testo:

Testo multilinea che occupa ⅔ delle dimensioni dell'area intera, con un rettangolo di sfondo
Figura 9. Testo multilinea che occupa ⅔ delle dimensioni dell'area intera, con un rettangolo di sfondo.

La modifica dei vincoli, delle dimensioni dei caratteri o di qualsiasi proprietà che influisce sulle dimensioni misurate comporta la generazione di una nuova dimensione. Puoi impostare una dimensione fissa sia per width che per height, in modo che il testo segua l'impostazione TextOverflow. 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 tre puntini che ne interrompono la visualizzazione.
Figura 10. TextOverflow.Ellipsis con vincoli fissi per la 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 disegnata su Canvas
Figura 11. Disegno di un ImageBitmap su Canvas.

Disegnare forme di base

DrawScope offre molte funzioni per il 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()

disegna rettangolo arrotondato

drawLine()

disegna linea

drawOval()

disegna ovale

drawArc()

draw arc

drawPoints()

punti di disegno

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. Quindi, 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 con percorso capovolto disegnato in Componi
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 canvas, puoi accedere alla canvas 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 a grandezza originale
Figura 13. Accesso alla tela per disegnare un Drawable.

Scopri di più

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