Elementi grafici in Scrivi

Molte app devono poter controllare con precisione ciò che viene disegnato sullo schermo. Potrebbe trattarsi di un piccolo spazio, come posizionare un riquadro o un cerchio sullo schermo nel posto giusto, o 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 è tramite modificatori come Modifier.drawWithContent, Modifier.drawBehind e Modifier.drawWithCache.

Ad esempio, per disegnare qualcosa dietro il componibile, puoi usare il modificatore drawBehind per iniziare a eseguire comandi di disegno:

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

Se ti serve solo un componibile che disegna, puoi utilizzare il componibile Canvas. Il componibile Canvas è un utile involucro intorno a Modifier.drawBehind. Posiziona Canvas nel layout come faresti con qualsiasi altro elemento dell'interfaccia utente di Compose. All'interno dell'Canvas, puoi disegnare elementi con un controllo preciso su stile e posizione.

Tutti i modificatori di disegno espongono DrawScope, un ambiente di disegno con ambito che mantiene il proprio stato. Ciò consente di impostare i parametri per un gruppo di elementi grafici. DrawScope fornisce diversi campi utili, come size, un oggetto Size che specifica le dimensioni correnti di DrawScope.

Per disegnare qualcosa, puoi usare una delle numerose funzioni di disegno disponibili 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 con Canvas in Scrivi.

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 metodi di disegno su DrawScope, la posizione e le dimensioni sono fornite dai valori predefiniti dei parametri. I parametri predefiniti generalmente posizionano l'elemento nel punto [0, 0] del canvas e forniscono un valore size predefinito che riempie l'intera area di disegno, come nell'esempio sopra. Come puoi vedere il rettangolo è posizionato in alto a sinistra. Per regolare le dimensioni e la posizione dell'elemento, devi comprendere il sistema di coordinate di Compose.

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

Una griglia che mostra il sistema di coordinate in alto a sinistra [0, 0] e in basso a destra [larghezza, altezza]
Figura 2. Disegno del sistema di coordinate e della griglia di 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 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 cambiare dove o come vengono eseguiti i comandi di disegno.

Scala

Utilizza DrawScope.scale() per aumentare di un fattore le dimensioni delle operazioni di disegno. Operazioni come scale() si applicano a tutte le operazioni di disegno all'interno della funzione 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 in alto, in 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 fuori 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 pivot. 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
        )
    }
}

Un telefono 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'attuale DrawScope, modificando i limiti del 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 efficacemente spaziatura interna ai comandi di disegno:

Un rettangolo imbottito tutto intorno
Figura 6. Applicazione di un riquadro ai comandi di disegno.

Trasformazioni multiple

Per applicare più trasformazioni ai disegni, utilizza la funzione DrawScope.withTransform(), che crea e applica un'unica trasformazione che combina tutte le modifiche desiderate. L'utilizzo di withTransform() è più efficiente dell'esecuzione di chiamate nidificate a singole trasformazioni, perché tutte le trasformazioni vengono eseguite insieme in una singola operazione, invece di dover calcolare e salvare ciascuna trasformazione nidificata da parte di Compose.

Ad esempio, il seguente codice applica sia una traslazione 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
        )
    }
}

Un telefono con un rettangolo ruotato verso la parte laterale dello schermo
Figura 7. Usa withTransform per applicare sia una rotazione sia una traslazione, ruotando il rettangolo e spostandolo verso sinistra.

Operazioni di disegno comuni

Disegna testo

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

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

val textMeasurer = rememberTextMeasurer()

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

Mostrare un Hello disegnato su Canvas
Figura 8. Disegna testo su Canvas.

Misurare il testo

Il disegno del testo funziona in modo un po' diverso dagli altri comandi di disegno. Normalmente, assegna al comando di disegno le dimensioni (larghezza e altezza) per disegnare la forma/l'immagine. Con il testo, ci sono alcuni parametri che controllano la dimensione del testo visualizzato, come dimensione del carattere, carattere, legature e spaziatura tra le lettere.

Con Scrivi, puoi utilizzare una TextMeasurer per ottenere l'accesso alle dimensioni misurate del testo, in base ai fattori elencati in precedenza. Se vuoi tracciare 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 nel testo:

Testo su più righe che occupa 2⁄3 dell'intera area, con un rettangolo di sfondo
Figura 9. Testo su più righe che occupa 2⁄3 dell'intera area, con un rettangolo di sfondo.

Se modifichi i vincoli, le dimensioni del carattere o qualsiasi proprietà che influisca sulle dimensioni misurate, viene riportata una nuova dimensione. Puoi impostare una dimensione fissa per width e height; il testo segue quindi il set TextOverflow. Ad esempio, il seguente codice esegue il rendering del testo a 1⁄3 dell'altezza e a 1⁄3 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, il cui testo è tagliato da dei puntini di sospensione.
Figura 10. TextOverflow.Ellipsis con vincoli fissi sulla misurazione del testo.

Disegna immagine

Per disegnare una ImageBitmap con DrawScope, carica l'immagine utilizzando ImageBitmap.imageResource(), quindi chiama drawImage:

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

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

L'immagine di un cane disegnato su tela
Figura 11. Disegna una ImageBitmap su Canvas.

Disegnare forme di base

In DrawScope sono disponibili 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

Uscita

drawCircle()

disegna cerchio

drawRect()

tracciare un rettangolo

drawRoundedRect()

traccia rettangolo arrotondato

drawLine()

traccia linea

drawOval()

disegna ovale

drawArc()

arco di disegno

drawPoints()

disegnare punti

Disegna percorso

Un percorso è una serie di istruzioni matematiche che, una volta eseguito, generano un disegno. DrawScope può tracciare 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 con percorso viola capovolto disegnato su Scrivi
Figura 12. Creazione e disegno di un elemento Path in Compose.

Accesso a Canvas oggetto in corso...

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

Ad esempio, se vuoi disegnare un elemento Drawable personalizzato sul canvas, puoi accedere al 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 oggetto ShapeDrawable ovale nero che occupa la grandezza originale
Figura 13. Accesso al canvas per disegnare un Drawable.

Scopri di più

Per saperne di più su Disegno in Compose, dai un'occhiata alle seguenti risorse: