Cet atelier de programmation fait partie du cours Développement Android avancé en Kotlin. Vous tirerez pleinement parti de ce cours en suivant les ateliers de programmation dans l'ordre, mais ce n'est pas obligatoire. Tous les ateliers de programmation du cours sont listés sur la page de destination des ateliers de programmation Android avancé en Kotlin.
Introduction
Dans Android, plusieurs techniques sont disponibles pour implémenter des graphiques et des animations 2D personnalisés dans les vues.
En plus d'utiliser des drawables, vous pouvez créer des dessins 2D à l'aide des méthodes de dessin de la classe Canvas. Canvas est une surface de dessin 2D qui fournit des méthodes de dessin. Cela est utile lorsque votre application doit se redessiner régulièrement, car ce que l'utilisateur voit change au fil du temps. Dans cet atelier de programmation, vous allez apprendre à créer et à dessiner sur un canevas affiché dans un View.
Voici les différents types d'opérations que vous pouvez effectuer sur un canevas :
- Remplissez toute la zone de dessin avec une couleur.
- Dessinez des formes, telles que des rectangles, des arcs et des chemins stylisés comme défini dans un objet
Paint. L'objetPaintcontient des informations sur le style et la couleur pour dessiner des géométries (comme des lignes, des rectangles, des ovales et des chemins d'accès) ou, par exemple, la typographie du texte. - Appliquez des transformations, telles que la translation, la mise à l'échelle ou des transformations personnalisées.
- Découpez, c'est-à-dire appliquez une forme ou un chemin au canevas pour définir ses parties visibles.

Comment fonctionne le dessin sur Android (très simplifié)
Le dessin sur Android ou sur tout autre système moderne est un processus complexe qui inclut des couches d'abstraction et des optimisations jusqu'au matériel. La façon dont Android effectue le rendu est un sujet fascinant sur lequel de nombreux articles ont été écrits. Les détails dépassent le cadre de cet atelier de programmation.
Dans le contexte de cet atelier de programmation et de son application qui dessine sur un canevas pour l'affichage en mode plein écran, vous pouvez le considérer de la manière suivante.

- Vous avez besoin d'une vue pour afficher ce que vous dessinez. Il peut s'agir de l'une des vues fournies par le système Android. Dans cet atelier de programmation, vous allez créer une vue personnalisée qui servira de vue de contenu pour votre application (
MyCanvasView). - Comme toutes les vues, celle-ci est associée à son propre canevas (
canvas). - Pour dessiner sur le canevas d'une vue de la manière la plus élémentaire, vous remplacez sa méthode
onDraw()et dessinez sur son canevas. - Lorsque vous créez un dessin, vous devez mettre en cache ce que vous avez dessiné auparavant. Il existe plusieurs façons de mettre en cache vos données. L'une d'elles consiste à les stocker dans un bitmap (
extraBitmap). Une autre consiste à enregistrer un historique de ce que vous avez dessiné sous forme de coordonnées et d'instructions. - Pour dessiner sur votre bitmap de mise en cache (
extraBitmap) à l'aide de l'API de dessin du canevas, vous créez un canevas de mise en cache (extraCanvas) pour votre bitmap de mise en cache. - Vous dessinez ensuite sur votre canevas de mise en cache (
extraCanvas), qui dessine sur votre bitmap de mise en cache (extraBitmap). - Pour afficher tout ce qui est dessiné à l'écran, vous demandez au canevas de la vue (
canvas) de dessiner le bitmap de mise en cache (extraBitmap).
Ce que vous devez déjà savoir
- Comment créer une application avec une activité et une mise en page de base, et l'exécuter à l'aide d'Android Studio.
- Comment associer des gestionnaires d'événements à des vues.
- Comment créer une vue personnalisée
Points abordés
- Comment créer un
Canvaset dessiner dessus en réponse à l'interaction tactile de l'utilisateur.
Objectifs de l'atelier
- Créez une application qui dessine des lignes sur l'écran lorsque l'utilisateur le touche.
- Capturez les événements de mouvement et, en réponse, dessinez des lignes sur un canevas affiché dans une vue personnalisée en plein écran.
L'application MiniPaint utilise une vue personnalisée pour afficher une ligne en réponse aux actions tactiles de l'utilisateur, comme illustré dans la capture d'écran ci-dessous.

Étape 1 : Créer le projet MiniPaint
- Créez un projet Kotlin intitulé MiniPaint qui utilise le modèle Empty Activity (Activité vide).
- Ouvrez le fichier
app/res/values/colors.xmlet ajoutez les deux couleurs suivantes.
<color name="colorBackground">#FFFF5500</color>
<color name="colorPaint">#FFFFEB3B</color>- Ouvrir
styles.xml - Dans le parent du style
AppThemedonné, remplacezDarkActionBarparNoActionBar. Cela supprime la barre d'action pour que vous puissiez dessiner en plein écran.
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">Étape 2 : Créer la classe MyCanvasView
Dans cette étape, vous allez créer une vue personnalisée, MyCanvasView, pour le dessin.
- Dans le package
app/java/com.example.android.minipaint, créez un Nouveau > Fichier/Classe Kotlin appeléMyCanvasView. - Faites en sorte que la classe
MyCanvasViewétende la classeViewet transmette lecontext: Context. Acceptez les importations suggérées.
import android.content.Context
import android.view.View
class MyCanvasView(context: Context) : View(context) {
}Étape 3 : Définir MyCanvasView comme vue de contenu
Pour afficher ce que vous allez dessiner dans MyCanvasView, vous devez le définir comme vue de contenu de MainActivity.
- Ouvrez
strings.xmlet définissez une chaîne à utiliser pour la description du contenu de la vue.
<string name="canvasContentDescription">Mini Paint is a simple line drawing app.
Drag your fingers to draw. Rotate the phone to clear.</string>- Ouvrir
MainActivity.kt - Dans
onCreate(), supprimezsetContentView(R.layout.activity_main). - Créez une instance de
MyCanvasView.
val myCanvasView = MyCanvasView(this)- En dessous, demandez le plein écran pour la mise en page de
myCanvasView. Pour ce faire, définissez l'indicateurSYSTEM_UI_FLAG_FULLSCREENsurmyCanvasView. De cette façon, la vue remplit complètement l'écran.
myCanvasView.systemUiVisibility = SYSTEM_UI_FLAG_FULLSCREEN- Ajoutez une description du contenu.
myCanvasView.contentDescription = getString(R.string.canvasContentDescription)- En dessous, définissez la vue de contenu sur
myCanvasView.
setContentView(myCanvasView)- Exécutez votre application. Vous verrez un écran entièrement blanc, car le canevas n'a pas de taille et vous n'avez encore rien dessiné.
Étape 1 : Remplacer onSizeChanged()
La méthode onSizeChanged() est appelée par le système Android chaque fois qu'une vue change de taille. Étant donné que la vue commence sans taille, la méthode onSizeChanged() de la vue est également appelée après que l'activité l'a créée et gonflée pour la première fois. Cette méthode onSizeChanged() est donc l'endroit idéal pour créer et configurer le canevas de la vue.
- Dans
MyCanvasView, au niveau de la classe, définissez des variables pour un canevas et un bitmap. Appelez-lesextraCanvasetextraBitmap. Il s'agit de votre bitmap et de votre canevas pour mettre en cache ce qui a été dessiné auparavant.
private lateinit var extraCanvas: Canvas
private lateinit var extraBitmap: Bitmap- Définissez une variable de niveau classe
backgroundColorpour la couleur d'arrière-plan du canevas et initialisez-la sur lecolorBackgroundque vous avez défini précédemment.
private val backgroundColor = ResourcesCompat.getColor(resources, R.color.colorBackground, null)- Dans
MyCanvasView, remplacez la méthodeonSizeChanged(). Cette méthode de rappel est appelée par le système Android avec les nouvelles dimensions de l'écran, c'est-à-dire avec une nouvelle largeur et une nouvelle hauteur (vers lesquelles passer) et l'ancienne largeur et l'ancienne hauteur (à partir desquelles passer).
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
super.onSizeChanged(width, height, oldWidth, oldHeight)
}- Dans
onSizeChanged(), créez une instance deBitmapavec la nouvelle largeur et la nouvelle hauteur, qui correspondent à la taille de l'écran, et attribuez-la àextraBitmap. Le troisième argument est la configuration des couleurs du bitmap.ARGB_8888stocke chaque couleur sur 4 octets et est recommandé.
extraBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)- Créez une instance
Canvasà partir deextraBitmapet attribuez-la àextraCanvas.
extraCanvas = Canvas(extraBitmap)- Spécifiez la couleur d'arrière-plan dans laquelle remplir
extraCanvas.
extraCanvas.drawColor(backgroundColor)- En examinant
onSizeChanged(), vous pouvez voir qu'un bitmap et un canevas sont créés chaque fois que la fonction s'exécute. Vous avez besoin d'un nouveau bitmap, car la taille a changé. Toutefois, il s'agit d'une fuite de mémoire qui laisse les anciens bitmaps en place. Pour résoudre ce problème, recyclezextraBitmapavant de créer le suivant en ajoutant ce code juste après l'appel desuper.
if (::extraBitmap.isInitialized) extraBitmap.recycle()Étape 2 : Remplacer onDraw()
Tous les dessins pour MyCanvasView sont effectués dans onDraw().
Pour commencer, affichez le canevas en remplissant l'écran avec la couleur d'arrière-plan que vous avez définie dans onSizeChanged().
- Remplacez
onDraw()et dessinez le contenu deextraBitmapmis en cache sur le canevas associé à la vue. La méthodedrawBitmap()Canvasest disponible en plusieurs versions. Dans ce code, vous fournissez le bitmap, les coordonnées x et y (en pixels) du coin supérieur gauche, etnullpourPaint, car vous le définirez plus tard.
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawBitmap(extraBitmap, 0f, 0f, null)
}
Notez que le canevas transmis à onDraw() et utilisé par le système pour afficher le bitmap est différent de celui que vous avez créé dans la méthode onSizeChanged() et utilisé pour dessiner sur le bitmap.
- Exécutez votre application. L'écran entier devrait être rempli de la couleur d'arrière-plan spécifiée.

Pour dessiner, vous avez besoin d'un objet Paint qui spécifie le style des éléments dessinés et d'un objet Path qui spécifie ce qui est dessiné.
Étape 1 : Initialiser un objet Paint
- Dans MyCanvasView.kt, au niveau supérieur du fichier, définissez une constante pour la largeur du trait.
private const val STROKE_WIDTH = 12f // has to be float- Au niveau de la classe
MyCanvasView, définissez une variabledrawColorpour contenir la couleur à utiliser et initialisez-la avec la ressourcecolorPaintque vous avez définie précédemment.
private val drawColor = ResourcesCompat.getColor(resources, R.color.colorPaint, null)- Au niveau de la classe, ajoutez une variable
paintpour un objetPaintet initialisez-la comme suit.
// 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)
}- Le
colordepaintcorrespond àdrawColorque vous avez défini précédemment. isAntiAliasdéfinit si le lissage des bords doit être appliqué. DéfinirisAntiAliassurtruepermet de lisser les bords de ce qui est dessiné sans affecter la forme.isDither, lorsquetrue, affecte la façon dont les couleurs avec une précision supérieure à celle de l'appareil sont sous-échantillonnées. Par exemple, le tramage est le moyen le plus courant de réduire la gamme de couleurs des images à 256 couleurs (ou moins).styledéfinit le type de peinture à appliquer à un trait, qui est essentiellement une ligne.Paint.Styleindique si la primitive dessinée est remplie, tracée ou les deux (avec la même couleur). Par défaut, l'objet auquel la peinture est appliquée est rempli. (La couleur de remplissage colore l'intérieur de la forme, tandis que la couleur du trait suit son contour.)strokeJoindePaint.Joinspécifie la manière dont les lignes et les segments de courbe se rejoignent sur un tracé. La valeur par défaut estMITER.strokeCapdéfinit la forme de l'extrémité de la ligne sur une forme de capuchon.Paint.Capspécifie le début et la fin des lignes et des chemins tracés. La valeur par défaut estBUTT.strokeWidthspécifie la largeur du trait en pixels. La valeur par défaut est une largeur de trait très fine. Elle est donc définie sur la constanteSTROKE_WIDTHque vous avez définie précédemment.
Étape 2 : Initialiser un objet Path
Path correspond au chemin de ce que l'utilisateur dessine.
- Dans
MyCanvasView, ajoutez une variablepathet initialisez-la avec un objetPathpour stocker le chemin qui est dessiné lorsque l'utilisateur touche l'écran. Importezandroid.graphics.PathpourPath.
private var path = Path()Étape 1 : Répondre à un mouvement sur l'écran
La méthode onTouchEvent() sur une vue est appelée chaque fois que l'utilisateur appuie sur l'écran.
- Dans
MyCanvasView, remplacez la méthodeonTouchEvent()pour mettre en cache les coordonnéesxetydueventtransmis. Utilisez ensuite une expressionwhenpour gérer les événements de mouvement lorsque l'utilisateur pose le doigt sur l'écran, le déplace et le retire. Il s'agit des événements qui nous intéressent pour dessiner une ligne à l'écran. Pour chaque type d'événement, appelez une méthode utilitaire, comme indiqué dans le code ci-dessous. Pour obtenir la liste complète des événements tactiles, consultez la documentation de la classeMotionEvent.
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
}- Au niveau de la classe, ajoutez les variables
motionTouchEventXetmotionTouchEventYmanquantes pour mettre en cache les coordonnées x et y de l'événement tactile actuel (les coordonnéesMotionEvent). Initialisez-les sur0f.
private var motionTouchEventX = 0f
private var motionTouchEventY = 0f- Créez des stubs pour les trois fonctions
touchStart(),touchMove()ettouchUp().
private fun touchStart() {}
private fun touchMove() {}
private fun touchUp() {}- Votre code devrait se compiler et s'exécuter, mais vous ne verrez rien de différent du fond coloré pour le moment.
Étape 2 : Implémenter touchStart()
Cette méthode est appelée lorsque l'utilisateur touche l'écran pour la première fois.
- Au niveau de la classe, ajoutez des variables pour mettre en cache les dernières valeurs x et y. Une fois que l'utilisateur s'arrête de bouger et retire son doigt, ces points deviennent le point de départ du prochain chemin (le prochain segment de la ligne à tracer).
private var currentX = 0f
private var currentY = 0f- Implémentez la méthode
touchStart()comme suit. Réinitialisezpath, passez aux coordonnées x-y de l'événement tactile (motionTouchEventXetmotionTouchEventY), puis attribuezcurrentXetcurrentYà cette valeur.
private fun touchStart() {
path.reset()
path.moveTo(motionTouchEventX, motionTouchEventY)
currentX = motionTouchEventX
currentY = motionTouchEventY
}Étape 3 : Implémenter touchMove()
- Au niveau de la classe, ajoutez une variable
touchToleranceet définissez-la surViewConfiguration.get(context).scaledTouchSlop.
private val touchTolerance = ViewConfiguration.get(context).scaledTouchSlopAvec un chemin, il n'est pas nécessaire de dessiner chaque pixel ni de demander à chaque fois une actualisation de l'écran. Au lieu de cela, vous pouvez (et devez) interpoler un chemin entre les points pour obtenir de bien meilleures performances.
- Si le doigt a à peine bougé, il n'est pas nécessaire de dessiner.
- Si le doigt a parcouru une distance inférieure à
touchTolerance, ne dessinez pas. scaledTouchSloprenvoie la distance en pixels qu'un contact peut parcourir avant que le système ne considère que l'utilisateur fait défiler l'écran.
- Définissez la méthode
touchMove(). Calcule la distance parcourue (dx,dy), crée une courbe entre les deux points et la stocke danspath, met à jour le décomptecurrentXetcurrentYen cours, puis dessinepath. Appelez ensuiteinvalidate()pour forcer le redessin de l'écran avec lepathmis à jour.
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()
}Voici plus en détail cette méthode :
- Calculez la distance parcourue (
dx, dy). - Si le mouvement dépasse la tolérance tactile, ajoutez un segment au chemin.
- Définissez le point de départ du segment suivant sur le point d'arrivée de ce segment.
- L'utilisation de
quadTo()au lieu delineTo()permet de créer une ligne lisse sans angles. Consultez Courbes de Bézier. - Appelez
invalidate()pour (éventuellement appeleronDraw()et) redessiner la vue.
Étape 4 : Implémenter touchUp()
Lorsque l'utilisateur retire son doigt, il suffit de réinitialiser le chemin pour qu'il ne soit pas redessiné. Rien n'est dessiné, donc aucune invalidation n'est nécessaire.
- Implémentez la méthode
touchUp().
private fun touchUp() {
// Reset the path so it doesn't get drawn again.
path.reset()
}- Exécutez votre code et dessinez sur l'écran avec votre doigt. Notez que si vous faites pivoter l'appareil, l'écran est effacé, car l'état du dessin n'est pas enregistré. Pour cet exemple d'application, il s'agit d'un choix de conception, afin de permettre à l'utilisateur d'effacer l'écran facilement.

Étape 5 : Dessinez un cadre autour du croquis
Lorsque l'utilisateur dessine sur l'écran, votre application construit le chemin et l'enregistre dans le bitmap extraBitmap. La méthode onDraw() affiche le bitmap supplémentaire dans le canevas de la vue. Vous pouvez dessiner davantage dans onDraw(). Par exemple, vous pouvez dessiner des formes après avoir dessiné le bitmap.
Dans cette étape, vous dessinez un cadre autour du bord de l'image.
- Dans
MyCanvasView, ajoutez une variable appeléeframequi contient un objetRect.
private lateinit var frame: Rect- À la fin de
onSizeChanged(), définissez un encart et ajoutez du code pour créer leRectqui sera utilisé pour le cadre, en utilisant les nouvelles dimensions et l'encart.
// Calculate a rectangular frame around the picture.
val inset = 40
frame = Rect(inset, inset, width - inset, height - inset)- Dans
onDraw(), après avoir dessiné le bitmap, dessinez un rectangle.
// Draw a frame around the canvas.
canvas.drawRect(frame, paint)- Exécutez votre application et notez le cadre.

Tâche (facultative) : Stocker des données dans un chemin d'accès
Dans l'application actuelle, les informations de dessin sont stockées dans un bitmap. Bien que cette solution soit efficace, il existe d'autres moyens de stocker les informations de dessin. La façon dont vous stockez votre historique de dessins dépend de l'application et de vos différentes exigences. Par exemple, si vous dessinez des formes, vous pouvez enregistrer une liste de formes avec leur emplacement et leurs dimensions. Pour l'application MiniPaint, vous pouvez enregistrer le chemin en tant que Path. Vous trouverez ci-dessous un aperçu général de la procédure à suivre si vous souhaitez essayer.
- Dans
MyCanvasView, supprimez tout le code pourextraCanvasetextraBitmap. - Ajoutez des variables pour le chemin parcouru jusqu'à présent et le chemin en cours de tracé.
// Path representing the drawing so far
private val drawing = Path()
// Path representing what's currently being drawn
private val curPath = Path()- Dans
onDraw(), au lieu de dessiner le bitmap, dessinez les chemins stockés et actuels.
// 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)- Dans
touchUp(), ajoutez le chemin d'accès actuel au chemin d'accès précédent et réinitialisez le chemin d'accès actuel.
// Add the current path to the drawing so far
drawing.addPath(curPath)
// Rewind the current path for the next touch
curPath.reset()- Exécutez votre application. Vous ne devriez constater aucune différence.
Téléchargez le code de l'atelier de programmation terminé.
$ git clone https://github.com/googlecodelabs/android-kotlin-drawing-canvas
Vous pouvez également télécharger le dépôt sous forme de fichier ZIP, le décompresser et l'ouvrir dans Android Studio.
- Un
Canvasest une surface de dessin 2D qui fournit des méthodes de dessin. - Le
Canvaspeut être associé à une instanceViewqui l'affiche. - L'objet
Paintcontient des informations sur le style et la couleur pour dessiner des géométries (telles que des lignes, des rectangles, des ovales et des chemins) et du texte. - Un modèle courant pour travailler avec un canevas consiste à créer une vue personnalisée et à remplacer les méthodes
onDraw()etonSizeChanged(). - Remplacez la méthode
onTouchEvent()pour capturer les touches de l'utilisateur et y répondre en dessinant des éléments. - Vous pouvez utiliser un bitmap supplémentaire pour mettre en cache les informations des dessins qui changent au fil du temps. Vous pouvez également stocker des formes ou un chemin d'accès.
Cours Udacity :
Documentation pour les développeurs Android :
- Classe
Canvas - Classe
Bitmap - Classe
View - Classe
Paint - Configurations
Bitmap.config - Classe
Path - Page Wikipédia sur les courbes de Bézier
- Canevas et drawables
- Série d'articles sur l'architecture graphique (niveau avancé)
- drawables
- onDraw()
- onSizeChanged()
MotionEventViewConfiguration.get(context).scaledTouchSlop
Cette section répertorie les devoirs possibles pour les élèves qui suivent cet atelier de programmation dans le cadre d'un cours animé par un enseignant. Il revient à l'enseignant d'effectuer les opérations suivantes :
- Attribuer des devoirs si nécessaire
- Indiquer aux élèves comment rendre leurs devoirs
- Noter les devoirs
Les enseignants peuvent utiliser ces suggestions autant qu'ils le souhaitent, et ne doivent pas hésiter à attribuer d'autres devoirs aux élèves s'ils le jugent nécessaire.
Si vous suivez cet atelier de programmation par vous-même, n'hésitez pas à utiliser ces devoirs pour tester vos connaissances.
Répondre aux questions suivantes
Question 1
Parmi les composants suivants, lesquels sont nécessaires pour travailler avec un Canvas ? Plusieurs réponses possibles.
▢ Bitmap
▢ Paint
▢ Path
▢ View
Question 2
Que fait un appel à invalidate() (en termes généraux) ?
▢ Invalide et redémarre votre application.
▢ Efface le dessin du bitmap.
▢ Indique que le code précédent ne doit pas être exécuté.
▢ Indique au système qu'il doit redessiner l'écran.
Question 3
Quelle est la fonction des objets Canvas, Bitmap et Paint ?
▢ Surface de dessin 2D, bitmap affiché à l'écran, informations de style pour le dessin.
▢ Surface de dessin 3D, bitmap pour la mise en cache du chemin d'accès, informations de style pour le dessin.
▢ Surface de dessin 2D, bitmap affiché à l'écran, style de la vue.
▢ Cache pour les informations de dessin, bitmap sur lequel dessiner, informations de style pour le dessin.
Pour accéder aux autres ateliers de programmation de ce cours, consultez la page de destination des ateliers de programmation "Développement Android avancé en Kotlin".