Questo codelab fa parte del corso Advanced Android in Kotlin. Per ottenere il massimo valore da questo corso, ti consigliamo di seguire i codelab in sequenza, ma non è obbligatorio. Tutti i codelab del corso sono elencati nella pagina di destinazione dei codelab Advanced Android in Kotlin.
Introduzione
In Android, hai a disposizione diverse tecniche per implementare grafica e animazioni 2D personalizzate nelle visualizzazioni.
Oltre a utilizzare risorse disegnabili, puoi creare disegni 2D utilizzando i metodi di disegno della classe Canvas. Canvas è una superficie di disegno 2D che fornisce metodi per disegnare. Questa opzione è utile quando l'app deve essere ridisegnata regolarmente, perché ciò che vede l'utente cambia nel tempo. In questo codelab, imparerai a creare e disegnare su un canvas visualizzato in un View.
I tipi di operazioni che puoi eseguire su un canvas includono:
- Riempi l'intera tela con il colore.
- Disegna forme, come rettangoli, archi e percorsi con lo stile definito in un oggetto
Paint. L'oggettoPaintcontiene le informazioni su stile e colore per disegnare geometrie (come linee, rettangoli, ovali e tracciati) o, ad esempio, il carattere del testo. - Applica trasformazioni, come traduzione, scalabilità o trasformazioni personalizzate.
- Ritaglia, ovvero applica una forma o un percorso al canvas per definirne le porzioni visibili.

Come puoi immaginare il disegno su Android (in modo molto semplificato)
Il disegno su Android o su qualsiasi altro sistema moderno è un processo complesso che include livelli di astrazioni e ottimizzazioni fino all'hardware. Il modo in cui Android disegna è un argomento affascinante su cui è stato scritto molto e i suoi dettagli vanno oltre l'ambito di questo codelab.
Nel contesto di questo codelab e della sua app che disegna su un canvas per la visualizzazione a schermo intero, puoi pensarlo nel seguente modo.

- Hai bisogno di una visualizzazione per mostrare ciò che stai disegnando. Potrebbe trattarsi di una delle visualizzazioni fornite dal sistema Android. In alternativa, in questo codelab crei una visualizzazione personalizzata che funge da visualizzazione dei contenuti per la tua app (
MyCanvasView). - Come tutte le visualizzazioni, anche questa ha una propria tela (
canvas). - Per il modo più semplice di disegnare sul canvas di una visualizzazione, esegui l'override del relativo metodo
onDraw()e disegna sul canvas. - Quando crei un disegno di un edificio, devi memorizzare nella cache ciò che hai disegnato in precedenza. Esistono diversi modi per memorizzare nella cache i dati. Uno è in una bitmap (
extraBitmap). Un altro è salvare una cronologia di ciò che hai disegnato come coordinate e istruzioni. - Per disegnare nel bitmap di memorizzazione nella cache (
extraBitmap) utilizzando l'API Canvas Drawing, crei un canvas di memorizzazione nella cache (extraCanvas) per il bitmap di memorizzazione nella cache. - Poi disegni sul canvas di memorizzazione nella cache (
extraCanvas), che disegna sulla bitmap di memorizzazione nella cache (extraBitmap). - Per visualizzare tutto ciò che è disegnato sullo schermo, devi indicare al canvas della visualizzazione (
canvas) di disegnare la bitmap della memorizzazione nella cache (extraBitmap).
Cosa devi già sapere
- Come creare un'app con un'attività, un layout di base ed eseguirla utilizzando Android Studio.
- Come associare i gestori di eventi alle visualizzazioni.
- Come creare una vista personalizzata.
Obiettivi didattici
- Come creare un
Canvase disegnare su di esso in risposta al tocco dell'utente.
In questo lab proverai a:
- Crea un'app che disegna linee sullo schermo in risposta al tocco dell'utente.
- Acquisire eventi di movimento e, in risposta, disegnare linee su un canvas visualizzato in una visualizzazione personalizzata a schermo intero sullo schermo.
L'app MiniPaint utilizza una visualizzazione personalizzata per mostrare una linea in risposta ai tocchi dell'utente, come mostrato nello screenshot di seguito.

Passaggio 1: Crea il progetto MiniPaint
- Crea un nuovo progetto Kotlin chiamato MiniPaint che utilizza il modello Empty Activity.
- Apri il file
app/res/values/colors.xmle aggiungi i due colori seguenti.
<color name="colorBackground">#FFFF5500</color>
<color name="colorPaint">#FFFFEB3B</color>- Apri
styles.xml - Nell'elemento principale dello stile
AppThemespecificato, sostituisciDarkActionBarconNoActionBar. In questo modo la barra delle azioni viene rimossa, in modo da poter disegnare a schermo intero.
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">Passaggio 2: Crea la classe MyCanvasView
In questo passaggio crei una vista personalizzata, MyCanvasView, per il disegno.
- Nel pacchetto
app/java/com.example.android.minipaint, crea un Nuovo > File/classe Kotlin denominatoMyCanvasView. - Fai in modo che la classe
MyCanvasViewestenda la classeViewe passi incontext: Context. Accetta le importazioni suggerite.
import android.content.Context
import android.view.View
class MyCanvasView(context: Context) : View(context) {
}Passaggio 3: Impostare MyCanvasView come visualizzazione dei contenuti
Per visualizzare ciò che disegnerai in MyCanvasView, devi impostarlo come visualizzazione dei contenuti di MainActivity.
- Apri
strings.xmle definisci una stringa da utilizzare per la descrizione dei contenuti della visualizzazione.
<string name="canvasContentDescription">Mini Paint is a simple line drawing app.
Drag your fingers to draw. Rotate the phone to clear.</string>- Apri
MainActivity.kt - In
onCreate(), eliminasetContentView(R.layout.activity_main). - Crea un'istanza di
MyCanvasView.
val myCanvasView = MyCanvasView(this)- Sotto, richiedi lo schermo intero per il layout di
myCanvasView. Per farlo, imposta il flagSYSTEM_UI_FLAG_FULLSCREENsumyCanvasView. In questo modo, la visualizzazione riempie completamente lo schermo.
myCanvasView.systemUiVisibility = SYSTEM_UI_FLAG_FULLSCREEN- Aggiungi una descrizione dei contenuti.
myCanvasView.contentDescription = getString(R.string.canvasContentDescription)- Sotto, imposta la visualizzazione dei contenuti su
myCanvasView.
setContentView(myCanvasView)- Esegui l'app. Vedrai una schermata completamente bianca, perché il canvas non ha dimensioni e non hai ancora disegnato nulla.
Passaggio 1: Override onSizeChanged()
Il metodo onSizeChanged() viene chiamato dal sistema Android ogni volta che una visualizzazione cambia dimensione. Poiché la visualizzazione inizia senza dimensioni, il metodo onSizeChanged() della visualizzazione viene chiamato anche dopo che l'attività la crea e la gonfia. Questo metodo onSizeChanged() è quindi il luogo ideale per creare e configurare il canvas della visualizzazione.
- In
MyCanvasView, a livello di classe, definisci le variabili per una tela e una bitmap. ChiamaliextraCanvaseextraBitmap. Questi sono la bitmap e il canvas per memorizzare nella cache ciò che è stato disegnato in precedenza.
private lateinit var extraCanvas: Canvas
private lateinit var extraBitmap: Bitmap- Definisci una variabile a livello di classe
backgroundColorper il colore di sfondo del canvas e inizializzala con il valorecolorBackgroundche hai definito in precedenza.
private val backgroundColor = ResourcesCompat.getColor(resources, R.color.colorBackground, null)- In
MyCanvasView, esegui l'override del metodoonSizeChanged(). Questo metodo di callback viene chiamato dal sistema Android con le dimensioni dello schermo modificate, ovvero con una nuova larghezza e altezza (a cui passare) e la larghezza e l'altezza precedenti (da cui passare).
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
super.onSizeChanged(width, height, oldWidth, oldHeight)
}- All'interno di
onSizeChanged(), crea un'istanza diBitmapcon la nuova larghezza e la nuova altezza, che sono le dimensioni dello schermo, e assegnala aextraBitmap. Il terzo argomento è la configurazione del colore bitmap.ARGB_8888memorizza ogni colore in 4 byte ed è consigliato.
extraBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)- Crea un'istanza
CanvasdaextraBitmape assegnala aextraCanvas.
extraCanvas = Canvas(extraBitmap)- Specifica il colore di sfondo in cui riempire
extraCanvas.
extraCanvas.drawColor(backgroundColor)- Se esaminiamo
onSizeChanged(), ogni volta che viene eseguita la funzione vengono creati una nuova bitmap e un nuovo canvas. Hai bisogno di una nuova bitmap perché le dimensioni sono cambiate. Tuttavia, si tratta di una perdita di memoria, che lascia in giro le vecchie bitmap. Per risolvere il problema, riciclaextraBitmapprima di creare il successivo aggiungendo questo codice subito dopo la chiamata asuper.
if (::extraBitmap.isInitialized) extraBitmap.recycle()Passaggio 2: Esegui l'override di onDraw()
Tutto il lavoro di disegno per MyCanvasView avviene in onDraw().
Per iniziare, visualizza il canvas, riempiendo lo schermo con il colore di sfondo impostato in onSizeChanged().
- Esegui l'override di
onDraw()e disegna i contenuti diextraBitmapmemorizzati nella cache sul canvas associato alla visualizzazione. Il metododrawBitmap()Canvasè disponibile in diverse versioni. In questo codice, fornisci la bitmap, le coordinate X e Y (in pixel) dell'angolo in alto a sinistra enullperPaint, che imposterai in un secondo momento.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawBitmap(extraBitmap, 0f, 0f, null)
}
Tieni presente che il canvas passato a onDraw() e utilizzato dal sistema per visualizzare la bitmap è diverso da quello creato nel metodo onSizeChanged() e utilizzato per disegnare sulla bitmap.
- Esegui l'app. Dovresti vedere l'intero schermo riempito con il colore di sfondo specificato.

Per disegnare, hai bisogno di un oggetto Paint che specifica lo stile degli elementi disegnati e di un oggetto Path che specifica cosa viene disegnato.
Passaggio 1: Inizializza un oggetto Paint
- In MyCanvasView.kt, a livello di file superiore, definisci una costante per lo spessore del tratto.
private const val STROKE_WIDTH = 12f // has to be float- A livello di classe di
MyCanvasView, definisci una variabiledrawColorper contenere il colore con cui disegnare e inizializzala con la risorsacolorPaintche hai definito in precedenza.
private val drawColor = ResourcesCompat.getColor(resources, R.color.colorPaint, null)- A livello di classe, aggiungi una variabile
paintper un oggettoPainte inizializzala come segue.
// Set up the paint with which to draw.
private val paint = Paint().apply {
color = drawColor
// Smooths out edges of what is drawn without affecting shape.
isAntiAlias = true
// Dithering affects how colors with higher-precision than the device are down-sampled.
isDither = true
style = Paint.Style.STROKE // default: FILL
strokeJoin = Paint.Join.ROUND // default: MITER
strokeCap = Paint.Cap.ROUND // default: BUTT
strokeWidth = STROKE_WIDTH // default: Hairline-width (really thin)
}- Il
colordipaintè ildrawColorche hai definito in precedenza. isAntiAliasdefinisce se applicare l'antialiasing. Se impostiisAntiAliassutrue, i bordi di ciò che viene disegnato vengono smussati senza influire sulla forma.isDither, quandotrue, influisce sul modo in cui i colori con una precisione superiore a quella del dispositivo vengono sottocampionati. Ad esempio, il dithering è il mezzo più comune per ridurre la gamma di colori delle immagini a 256 (o meno) colori.styleimposta il tipo di pittura da applicare a un tratto, che è essenzialmente una linea.Paint.Stylespecifica se la primitiva disegnata è riempita, tracciata o entrambe (nello stesso colore). L'impostazione predefinita è riempire l'oggetto a cui viene applicata la vernice. "Riempimento" colora l'interno della forma, mentre "Tratto" ne segue il contorno.strokeJoindiPaint.Joinspecifica come si uniscono le linee e i segmenti di curva su un tracciato con tratto. Il valore predefinito èMITER.strokeCapimposta la forma dell'estremità della linea come un cappuccio.Paint.Capspecifica l'inizio e la fine di linee e tracciati con tratto. Il valore predefinito èBUTT.strokeWidthspecifica la larghezza del tratto in pixel. Il valore predefinito è la larghezza di una linea sottile, quindi è impostato sulla costanteSTROKE_WIDTHche hai definito in precedenza.
Passaggio 2: Inizializzare un oggetto Path
Il Path è il percorso di ciò che l'utente sta disegnando.
- In
MyCanvasView, aggiungi una variabilepathe inizializzala con un oggettoPathper memorizzare il percorso che viene disegnato quando si segue il tocco dell'utente sullo schermo. Importaandroid.graphics.PathperPath.
private var path = Path()Passaggio 1: Rispondere al movimento sul display
Il metodo onTouchEvent() su una visualizzazione viene chiamato ogni volta che l'utente tocca il display.
- In
MyCanvasView, esegui l'override del metodoonTouchEvent()per memorizzare nella cache le coordinatexeydieventpassato. Quindi, utilizza un'espressionewhenper gestire gli eventi di movimento per il tocco sullo schermo, lo spostamento sullo schermo e il rilascio del tocco sullo schermo. Questi sono gli eventi di interesse per tracciare una linea sullo schermo. Per ogni tipo di evento, chiama un metodo di utilità, come mostrato nel codice riportato di seguito. Consulta la documentazione della classeMotionEventper un elenco completo degli eventi tocco.
override fun onTouchEvent(event: MotionEvent): Boolean {
motionTouchEventX = event.x
motionTouchEventY = event.y
when (event.action) {
MotionEvent.ACTION_DOWN -> touchStart()
MotionEvent.ACTION_MOVE -> touchMove()
MotionEvent.ACTION_UP -> touchUp()
}
return true
}- A livello di classe, aggiungi le variabili
motionTouchEventXemotionTouchEventYmancanti per memorizzare nella cache le coordinate x e y dell'evento tocco corrente (le coordinateMotionEvent). Inizializzali a0f.
private var motionTouchEventX = 0f
private var motionTouchEventY = 0f- Crea stub per le tre funzioni
touchStart(),touchMove()etouchUp().
private fun touchStart() {}
private fun touchMove() {}
private fun touchUp() {}- Il codice dovrebbe essere compilato ed eseguito, ma non vedrai ancora nulla di diverso dallo sfondo colorato.
Passaggio 2: Implementa touchStart()
Questo metodo viene chiamato quando l'utente tocca per la prima volta lo schermo.
- A livello di classe, aggiungi variabili per memorizzare nella cache i valori x e y più recenti. Dopo che l'utente si ferma e solleva il tocco, questi sono il punto di partenza del percorso successivo (il segmento successivo della linea da disegnare).
private var currentX = 0f
private var currentY = 0f- Implementa il metodo
touchStart()come segue. Reimpostapath, passa alle coordinate x-y dell'evento tocco (motionTouchEventXemotionTouchEventY) e assegnacurrentXecurrentYa questo valore.
private fun touchStart() {
path.reset()
path.moveTo(motionTouchEventX, motionTouchEventY)
currentX = motionTouchEventX
currentY = motionTouchEventY
}Passaggio 3: Implementa touchMove()
- A livello di classe, aggiungi una variabile
touchTolerancee impostala suViewConfiguration.get(context).scaledTouchSlop.
private val touchTolerance = ViewConfiguration.get(context).scaledTouchSlopSe utilizzi un percorso, non è necessario disegnare ogni pixel e richiedere ogni volta un aggiornamento del display. Al contrario, puoi (e devi) interpolare un percorso tra i punti per ottenere prestazioni molto migliori.
- Se il dito si è spostato di poco, non è necessario disegnare.
- Se il dito si è spostato di una distanza inferiore a
touchTolerance, non disegnare. scaledTouchSloprestituisce la distanza in pixel che un tocco può percorrere prima che il sistema ritenga che l'utente stia scorrendo.
- Definisci il metodo
touchMove(). Calcola la distanza percorsa (dx,dy), crea una curva tra i due punti e memorizzala inpath, aggiorna il conteggio dicurrentXecurrentYe disegnapath. Poi chiama il numeroinvalidate()per forzare il ridisegno dello schermo con ilpathaggiornato.
private fun touchMove() {
val dx = Math.abs(motionTouchEventX - currentX)
val dy = Math.abs(motionTouchEventY - currentY)
if (dx >= touchTolerance || dy >= touchTolerance) {
// QuadTo() adds a quadratic bezier from the last point,
// approaching control point (x1,y1), and ending at (x2,y2).
path.quadTo(currentX, currentY, (motionTouchEventX + currentX) / 2, (motionTouchEventY + currentY) / 2)
currentX = motionTouchEventX
currentY = motionTouchEventY
// Draw the path in the extra bitmap to cache it.
extraCanvas.drawPath(path, paint)
}
invalidate()
}Questo metodo in modo più dettagliato:
- Calcola la distanza percorsa (
dx, dy). - Se il movimento è maggiore della tolleranza al tocco, aggiungi un segmento al percorso.
- Imposta il punto di partenza del segmento successivo sull'endpoint di questo segmento.
- L'utilizzo di
quadTo()anziché dilineTo()crea una linea disegnata in modo uniforme senza angoli. Consulta la sezione Curve di Bézier. - Chiama
invalidate()per (alla fine chiamaonDraw()e) ridisegnare la visualizzazione.
Passaggio 4: implementa touchUp()
Quando l'utente solleva il tocco, è sufficiente reimpostare il percorso in modo che non venga disegnato di nuovo. Non viene disegnato nulla, quindi non è necessaria alcuna invalidazione.
- Implementa il metodo
touchUp().
private fun touchUp() {
// Reset the path so it doesn't get drawn again.
path.reset()
}- Esegui il codice e usa il dito per disegnare sullo schermo. Tieni presente che se ruoti il dispositivo, lo schermo viene cancellato perché lo stato del disegno non viene salvato. Per questa app di esempio, questa è una scelta progettuale, per offrire all'utente un modo semplice per cancellare lo schermo.

Passaggio 5: disegna un riquadro intorno allo schizzo
Mentre l'utente disegna sullo schermo, la tua app costruisce il percorso e lo salva nella bitmap extraBitmap. Il metodo onDraw() mostra la bitmap aggiuntiva nel canvas della visualizzazione. Puoi disegnare di più in onDraw(). Ad esempio, potresti disegnare forme dopo aver disegnato la bitmap.
In questo passaggio disegna una cornice attorno al bordo dell'immagine.
- In
MyCanvasView, aggiungi una variabile denominataframeche contenga un oggettoRect.
private lateinit var frame: Rect- Alla fine di
onSizeChanged()definisci un rientro e aggiungi il codice per creareRectche verrà utilizzato per il frame, utilizzando le nuove dimensioni e il rientro.
// Calculate a rectangular frame around the picture.
val inset = 40
frame = Rect(inset, inset, width - inset, height - inset)- In
onDraw(), dopo aver disegnato la bitmap, disegna un rettangolo.
// Draw a frame around the canvas.
canvas.drawRect(frame, paint)- Esegui l'app. Nota il frame.

(Facoltativo) Attività: archiviazione dei dati in un percorso
Nell'app attuale, le informazioni sul disegno vengono memorizzate in una bitmap. Sebbene questa sia una buona soluzione, non è l'unico modo possibile per archiviare le informazioni sui disegni. La modalità di memorizzazione della cronologia dei disegni dipende dall'app e dai vari requisiti. Ad esempio, se disegni forme, puoi salvare un elenco di forme con la loro posizione e le loro dimensioni. Per l'app MiniPaint, puoi salvare il percorso come Path. Di seguito è riportato lo schema generale su come procedere, se vuoi provare.
- In
MyCanvasView, rimuovi tutto il codice perextraCanvaseextraBitmap. - Aggiungi variabili per il percorso finora e per il percorso attualmente in fase di disegno.
// Path representing the drawing so far
private val drawing = Path()
// Path representing what's currently being drawn
private val curPath = Path()- In
onDraw(), anziché disegnare la bitmap, disegna i percorsi memorizzati e attuali.
// Draw the drawing so far
canvas.drawPath(drawing, paint)
// Draw any current squiggle
canvas.drawPath(curPath, paint)
// Draw a frame around the canvas
canvas.drawRect(frame, paint)- In
touchUp(), aggiungi il percorso attuale al percorso precedente e reimposta il percorso attuale.
// Add the current path to the drawing so far
drawing.addPath(curPath)
// Rewind the current path for the next touch
curPath.reset()- Esegui la tua app e, sì, non dovrebbe esserci alcuna differenza.
Scarica il codice per il codelab completato.
$ git clone https://github.com/googlecodelabs/android-kotlin-drawing-canvas
In alternativa, puoi scaricare il repository come file ZIP, decomprimerlo e aprirlo in Android Studio.
- Un
Canvasè una superficie di disegno 2D che fornisce metodi per disegnare. Canvaspuò essere associato a un'istanzaViewche lo visualizza.- L'oggetto
Paintcontiene le informazioni su stile e colore per disegnare geometrie (come linee, rettangoli, ovali e percorsi) e testo. - Un pattern comune per lavorare con un canvas è creare una visualizzazione personalizzata e sostituire i metodi
onDraw()eonSizeChanged(). - Esegui l'override del metodo
onTouchEvent()per acquisire i tocchi dell'utente e rispondere disegnando. - Puoi utilizzare una bitmap aggiuntiva per memorizzare nella cache le informazioni per i disegni che cambiano nel tempo. In alternativa, puoi memorizzare forme o un percorso.
Corso Udacity:
Documentazione per sviluppatori Android:
CanvasBitmapViewPaint- Configurazioni
Bitmap.config Path- Pagina Wikipedia sulle curve di Bézier
- Canvas e risorse disegnabili
- Serie di articoli sull'architettura grafica (avanzato)
- drawables
- onDraw()
- onSizeChanged()
MotionEventViewConfiguration.get(context).scaledTouchSlop
Questa sezione elenca i possibili compiti a casa per gli studenti che seguono questo codelab nell'ambito di un corso guidato da un insegnante. Spetta all'insegnante:
- Assegna i compiti, se richiesto.
- Comunica agli studenti come inviare i compiti.
- Valuta i compiti a casa.
Gli insegnanti possono utilizzare questi suggerimenti nella misura che ritengono opportuna e sono liberi di assegnare qualsiasi altro compito a casa che ritengono appropriato.
Se stai seguendo questo codelab in autonomia, sentiti libero di utilizzare questi compiti per casa per mettere alla prova le tue conoscenze.
Rispondi a queste domande
Domanda 1
Quali dei seguenti componenti sono necessari per lavorare con un Canvas? Seleziona tutte le opzioni pertinenti.
▢ Bitmap
▢ Paint
▢ Path
▢ View
Domanda 2
Che cosa fa una chiamata a invalidate() (in termini generali)?
▢ Invalida e riavvia l'app.
▢ Cancella il disegno dalla bitmap.
▢ Indica che il codice precedente non deve essere eseguito.
▢ Indica al sistema che deve ridisegnare lo schermo.
Domanda 3
Qual è la funzione degli oggetti Canvas, Bitmap e Paint?
▢ Superficie di disegno 2D, bitmap visualizzato sullo schermo, informazioni di stile per il disegno.
▢ Superficie di disegno 3D, bitmap per la memorizzazione nella cache del percorso, informazioni di stile per il disegno.
▢ Superficie di disegno 2D, bitmap visualizzata sullo schermo, stile della visualizzazione.
▢ Cache per informazioni sul disegno, bitmap su cui disegnare, informazioni di stile per il disegno.
Per i link ad altri codelab di questo corso, consulta la pagina di destinazione dei codelab Advanced Android in Kotlin.