Dessiner sur des objets Canvas

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'objet Paint contient 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.

  1. 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).
  2. Comme toutes les vues, celle-ci est associée à son propre canevas (canvas).
  3. 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.
  4. 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.
  5. 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.
  6. Vous dessinez ensuite sur votre canevas de mise en cache (extraCanvas), qui dessine sur votre bitmap de mise en cache (extraBitmap).
  7. 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 Canvas et 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

  1. Créez un projet Kotlin intitulé MiniPaint qui utilise le modèle Empty Activity (Activité vide).
  2. Ouvrez le fichier app/res/values/colors.xml et ajoutez les deux couleurs suivantes.
<color name="colorBackground">#FFFF5500</color>
<color name="colorPaint">#FFFFEB3B</color>
  1. Ouvrir styles.xml
  2. Dans le parent du style AppTheme donné, remplacez DarkActionBar par NoActionBar. 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.

  1. Dans le package app/java/com.example.android.minipaint, créez un Nouveau > Fichier/Classe Kotlin appelé MyCanvasView.
  2. Faites en sorte que la classe MyCanvasView étende la classe View et transmette le context: 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.

  1. Ouvrez strings.xml et 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>
  1. Ouvrir MainActivity.kt
  2. Dans onCreate(), supprimez setContentView(R.layout.activity_main).
  3. Créez une instance de MyCanvasView.
val myCanvasView = MyCanvasView(this)
  1. En dessous, demandez le plein écran pour la mise en page de myCanvasView. Pour ce faire, définissez l'indicateur SYSTEM_UI_FLAG_FULLSCREEN sur myCanvasView. De cette façon, la vue remplit complètement l'écran.
myCanvasView.systemUiVisibility = SYSTEM_UI_FLAG_FULLSCREEN
  1. Ajoutez une description du contenu.
myCanvasView.contentDescription = getString(R.string.canvasContentDescription)
  1. En dessous, définissez la vue de contenu sur myCanvasView.
setContentView(myCanvasView)
  1. 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.

  1. Dans MyCanvasView, au niveau de la classe, définissez des variables pour un canevas et un bitmap. Appelez-les extraCanvas et extraBitmap. 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
  1. Définissez une variable de niveau classe backgroundColor pour la couleur d'arrière-plan du canevas et initialisez-la sur le colorBackground que vous avez défini précédemment.
private val backgroundColor = ResourcesCompat.getColor(resources, R.color.colorBackground, null)
  1. Dans MyCanvasView, remplacez la méthode onSizeChanged(). 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)
}
  1. Dans onSizeChanged(), créez une instance de Bitmap avec 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_8888 stocke chaque couleur sur 4 octets et est recommandé.
extraBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
  1. Créez une instance Canvas à partir de extraBitmap et attribuez-la à extraCanvas.
 extraCanvas = Canvas(extraBitmap)
  1. Spécifiez la couleur d'arrière-plan dans laquelle remplir extraCanvas.
extraCanvas.drawColor(backgroundColor)
  1. 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, recyclez extraBitmap avant de créer le suivant en ajoutant ce code juste après l'appel de super.
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().

  1. Remplacez onDraw() et dessinez le contenu de extraBitmap mis en cache sur le canevas associé à la vue. La méthode drawBitmap() Canvas est disponible en plusieurs versions. Dans ce code, vous fournissez le bitmap, les coordonnées x et y (en pixels) du coin supérieur gauche, et null pour Paint, 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.

  1. 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

  1. 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
  1. Au niveau de la classe MyCanvasView, définissez une variable drawColor pour contenir la couleur à utiliser et initialisez-la avec la ressource colorPaint que vous avez définie précédemment.
private val drawColor = ResourcesCompat.getColor(resources, R.color.colorPaint, null)
  1. Au niveau de la classe, ajoutez une variable paint pour un objet Paint et 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 color de paint correspond à drawColor que vous avez défini précédemment.
  • isAntiAlias définit si le lissage des bords doit être appliqué. Définir isAntiAlias sur true permet de lisser les bords de ce qui est dessiné sans affecter la forme.
  • isDither, lorsque true, 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).
  • style définit le type de peinture à appliquer à un trait, qui est essentiellement une ligne. Paint.Style indique 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.)
  • strokeJoin de Paint.Join spécifie la manière dont les lignes et les segments de courbe se rejoignent sur un tracé. La valeur par défaut est MITER.
  • strokeCap définit la forme de l'extrémité de la ligne sur une forme de capuchon. Paint.Cap spécifie le début et la fin des lignes et des chemins tracés. La valeur par défaut est BUTT.
  • strokeWidth spé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 constante STROKE_WIDTH que vous avez définie précédemment.

Étape 2 : Initialiser un objet Path

Path correspond au chemin de ce que l'utilisateur dessine.

  1. Dans MyCanvasView, ajoutez une variable path et initialisez-la avec un objet Path pour stocker le chemin qui est dessiné lorsque l'utilisateur touche l'écran. Importez android.graphics.Path pour Path.
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.

  1. Dans MyCanvasView, remplacez la méthode onTouchEvent() pour mettre en cache les coordonnées x et y du event transmis. Utilisez ensuite une expression when pour 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 classe MotionEvent.
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
}
  1. Au niveau de la classe, ajoutez les variables motionTouchEventX et motionTouchEventY manquantes pour mettre en cache les coordonnées x et y de l'événement tactile actuel (les coordonnées MotionEvent). Initialisez-les sur 0f.
private var motionTouchEventX = 0f
private var motionTouchEventY = 0f
  1. Créez des stubs pour les trois fonctions touchStart(), touchMove() et touchUp().
private fun touchStart() {}

private fun touchMove() {}

private fun touchUp() {}
  1. 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.

  1. 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
  1. Implémentez la méthode touchStart() comme suit. Réinitialisez path, passez aux coordonnées x-y de l'événement tactile (motionTouchEventX et motionTouchEventY), puis attribuez currentX et currentY à cette valeur.
private fun touchStart() {
   path.reset()
   path.moveTo(motionTouchEventX, motionTouchEventY)
   currentX = motionTouchEventX
   currentY = motionTouchEventY
}

Étape 3 : Implémenter touchMove()

  1. Au niveau de la classe, ajoutez une variable touchTolerance et définissez-la sur ViewConfiguration.get(context).scaledTouchSlop.
private val touchTolerance = ViewConfiguration.get(context).scaledTouchSlop

Avec 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.
  • scaledTouchSlop renvoie 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.
  1. Définissez la méthode touchMove(). Calcule la distance parcourue (dx, dy), crée une courbe entre les deux points et la stocke dans path, met à jour le décompte currentX et currentY en cours, puis dessine path. Appelez ensuite invalidate() pour forcer le redessin de l'écran avec le path mis à 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 :

  1. Calculez la distance parcourue (dx, dy).
  2. Si le mouvement dépasse la tolérance tactile, ajoutez un segment au chemin.
  3. Définissez le point de départ du segment suivant sur le point d'arrivée de ce segment.
  4. L'utilisation de quadTo() au lieu de lineTo() permet de créer une ligne lisse sans angles. Consultez Courbes de Bézier.
  5. Appelez invalidate() pour (éventuellement appeler onDraw() 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.

  1. Implémentez la méthode touchUp().
private fun touchUp() {
   // Reset the path so it doesn't get drawn again.
   path.reset()
}
  1. 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.

  1. Dans MyCanvasView, ajoutez une variable appelée frame qui contient un objet Rect.
private lateinit var frame: Rect
  1. À la fin de onSizeChanged(), définissez un encart et ajoutez du code pour créer le Rect qui 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)
  1. Dans onDraw(), après avoir dessiné le bitmap, dessinez un rectangle.
// Draw a frame around the canvas.
canvas.drawRect(frame, paint)
  1. 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.

  1. Dans MyCanvasView, supprimez tout le code pour extraCanvas et extraBitmap.
  2. 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()
  1. 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)
  1. 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()
  1. 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.

Télécharger le fichier ZIP

  • Un Canvas est une surface de dessin 2D qui fournit des méthodes de dessin.
  • Le Canvas peut être associé à une instance View qui l'affiche.
  • L'objet Paint contient 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() et onSizeChanged().
  • 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 :

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".