Dessiner sur des objets en toile

Cet atelier de programmation fait partie du cours "Advanced Android" en langage Kotlin. Vous tirerez pleinement parti de ce cours si vous suivez les ateliers en séquence, mais ce n'est pas obligatoire. Tous les ateliers de programmation du cours sont répertoriés sur la page de destination des ateliers de programmation Android avancés sur Kotlin.

Introduction

Il existe plusieurs techniques pour créer des animations et des graphismes 2D personnalisés dans des vues.

L'outil drawable vous permet de créer des dessins en 2D à l'aide des méthodes de dessin de la classe Canvas. L'Canvas est une surface de dessin en 2D qui fournit des méthodes de dessin. Cette fonctionnalité est utile lorsque votre application doit régulièrement se redéfinir, car ce que change l'utilisateur au fil du temps. Dans cet atelier de programmation, vous allez apprendre à créer et à dessiner sur une toile affichée dans un View.

Voici les types d'opérations que vous pouvez effectuer sur un canevas:

  • Remplissez la toile avec des couleurs.
  • Dessinez des formes (rectangles, arcs, tracés, etc.) comme défini dans un objet Paint. L'objet Paint contient les informations de style et de couleur permettant de dessiner des géométries (ligne, rectangle, ovale et tracés, par exemple) ou la police de caractères.
  • Appliquez des transformations, telles que des traductions, des scalings ou des transformations personnalisées.
  • Utilisez le clip pour appliquer une forme ou un tracé à la toile afin de définir ses parties visibles.

Comment utiliser le dessin Android (super-simplifié !)

Dessiner sur Android ou sur tout autre système moderne est un processus complexe qui inclut des couches d'abstractions et des optimisations jusqu'au matériel. La manière dont Android dessine est un sujet fascinant qui a été écrit, et ses détails dépassent le cadre de cet atelier de programmation.

Dans le contexte de cet atelier de programmation et de son application qui s'affiche sur un canevas pour une affichage en 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. Ou, 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, cette vue intègre sa propre toile (canvas).
  3. Pour dessiner les dessins sur la toile de manière la plus simple, vous devez remplacer la méthode onDraw() par rapport à la toile.
  4. Lorsque vous créez un dessin, vous devez le mettre en cache. Il existe plusieurs manières de mettre vos données en cache, l'une en bitmap (extraBitmap) ou l'autre d'enregistrer l'historique de ce que vous avez dessiné en tant que coordonnées et instructions.
  5. Pour dessiner sur votre bitmap de mise en cache (extraBitmap) à l'aide de l'API de dessin de canevas, créez un canevas de mise en cache (extraCanvas) pour votre bitmap de mise en cache.
  6. Vous dessinerez ensuite sur le 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, indiquez à la toile (canvas) de dessiner le bitmap de mise en cache (extraBitmap).

Ce que vous devez déjà savoir

  • Créer une application avec une activité, 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

  • Création d'un Canvas et dessinez-le en réponse à l'appui d'un utilisateur.

Objectifs de l'atelier

  • Créer une application qui affiche des lignes à l'écran en réponse à un utilisateur qui touche l'écran
  • Capturez des événements de mouvement et, en réponse, tracez des lignes sur une toile qui est affichée en mode plein écran personnalisé à l'écran.

L'application MiniPaint utilise une vue personnalisée pour afficher une ligne en réponse aux interactions 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 appelé 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. La barre d'action est ainsi supprimée, et vous pouvez 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 fichier New > Kotlin File/Class (Nouveau fichier/classe Kotlin) appelé MyCanvasView.
  2. Faites en sorte que la classe MyCanvasView étend la classe View et transmette la classe 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 en tant que 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 mode plein écran pour la mise en page de myCanvasView. Pour ce faire, définissez l'option SYSTEM_UI_FLAG_FULLSCREEN sur myCanvasView. De cette façon, la vue remplit tout 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 du contenu sur myCanvasView.
setContentView(myCanvasView)
  1. Exécutez votre application. L'écran est entièrement blanc, car la toile n'a pas de taille et vous n'avez encore rien dessiné.

Étape 1 : Remplacement de onSizeChanged()

La méthode onSizeChanged() est appelée par le système Android chaque fois qu'une vue change de taille. Comme la vue démarre sans taille, la méthode onSizeChanged() de la vue est également appelée après que l'activité a été créée et gonflée. La méthode onSizeChanged() est donc l'endroit idéal pour créer et configurer la toile 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. Voici votre bitmap et votre canevas pour la mise en cache des éléments dessinés précédemment.
private lateinit var extraCanvas: Canvas
private lateinit var extraBitmap: Bitmap
  1. Définissez une variable de niveau de classe backgroundColor pour la couleur d'arrière-plan du canevas, puis initialisez-la dans le colorBackground que vous avez défini précédemment.
private val backgroundColor = ResourcesCompat.getColor(resources, R.color.colorBackground, null)
  1. Dans MyCanvasView, ignorez la méthode onSizeChanged(). Cette méthode de rappel est appelée par le système Android avec les dimensions d'écran modifiées, c'est-à-dire avec une nouvelle largeur et une nouvelle hauteur (pour passer à) et l'ancienne largeur et hauteur (pour passer à une plus grande hauteur).
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, puis attribuez-la à extraBitmap. Le troisième argument est la configuration des couleurs du bitmap. ARGB_8888 stocke chaque couleur en 4 octets (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. Indiquez la couleur d'arrière-plan de la collection extraCanvas.
extraCanvas.drawColor(backgroundColor)
  1. En examinant onSizeChanged(), un nouveau bitmap et un canevas sont créés à chaque exécution de la fonction. Vous avez besoin d'un nouveau bitmap, car la taille a changé. Cependant, il s'agit d'une fuite de mémoire et laissant d'anciens bitmaps à proximité. Pour résoudre ce problème, recyclez extraBitmap avant de créer le suivant en ajoutant ce code juste après l'appel à super.
if (::extraBitmap.isInitialized) extraBitmap.recycle()

Étape 2. Remplacement de onDraw()

Tout le travail de dessin pour MyCanvasView est exécuté dans onDraw().

Pour commencer, affichez la toile en utilisant la couleur d'arrière-plan que vous avez définie dans onSizeChanged().

  1. Remplacez onDraw() et dessinez le contenu du extraBitmap mis en cache sur la toile associée à la vue. La méthode Canvas de drawBitmap() est disponible dans plusieurs versions. Dans ce code, vous devez fournir le bitmap, les coordonnées X et Y (en pixels) de l'angle supérieur gauche, ainsi que 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 que vous utilisez pour dessiner sur le bitmap.

  1. Exécutez votre application. L'intégralité de l'écran doit être remplie de la couleur d'arrière-plan spécifiée.

Pour dessiner, vous devez disposer d'un objet Paint qui indique la manière dont les éléments sont stylisés lors du dessin et d'un Path qui indique ce qui est dessiné.

Étape 1 : Initialiser un objet Paint

  1. Dans MyCanvasView.kt, définissez une constante pour l'épaisseur du trait au niveau du fichier.
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 à dessiner, puis 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, ci-dessous, 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 est le drawColor que vous avez défini précédemment.
  • isAntiAlias définit si le lissage des bords doit être appliqué. La définition de isAntiAlias sur true lisse les bords de ce qui est dessiné sans modifier la forme.
  • Lorsqu'elle est définie sur true, isDither affecte la façon dont les couleurs dont la précision est supérieure à celle de l'appareil sont sous-échantillonnées. Par exemple, il s'agit du moyen le plus courant de réduire la plage de couleurs des images à 256 couleurs maximum.
  • style définit le type de peinture à effectuer par un trait, c'est-à-dire une ligne. Paint.Style indique si le début du dessin est rempli, tracé et/ou combiné (dans la même couleur). Par défaut, le remplissage de l'objet auquel la peinture est appliquée est appliqué. (le "remplissage" colore l'intérieur de la forme, tandis que le "trait" suit le contour).
  • strokeJoin sur Paint.Join indique la façon dont les lignes et les segments de courbe se rejoignent sur un tracé tracé. La valeur par défaut est MITER.
  • strokeCap définit la forme de la fin de la ligne comme une limite. Paint.Cap spécifie le début et la fin des lignes et des tracés tracés. La valeur par défaut est BUTT.
  • strokeWidth spécifie l'épaisseur du trait en pixels. La largeur par défaut des lignes de cheveux est vraiment 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 est le chemin que prend le dessin.

  1. Dans MyCanvasView, ajoutez une variable path et initialisez-la avec un objet Path pour stocker le chemin tracé lorsque l'utilisateur appuie sur 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() d'une vue est appelée chaque fois que l'utilisateur touche l'écran.

  1. Dans MyCanvasView, ignorez la méthode onTouchEvent() pour mettre en cache les coordonnées x et y des event transmis. Utilisez ensuite une expression when pour gérer les événements de mouvement lorsque vous balayez l'écran vers le bas, que vous le déplacez et que vous relâchez le doigt sur l'écran. Voici les événements permettant de dessiner une ligne à l'écran. Pour chaque type d'événement, appelez une méthode utilitaire, comme indiqué dans le code ci-dessous. Consultez la documentation du cours MotionEvent pour obtenir la liste complète des événements tactiles.
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 en cours (les coordonnées MotionEvent). Initialisez-les sur 0f.
private var motionTouchEventX = 0f
private var motionTouchEventY = 0f
  1. Créez des bouchons pour les trois fonctions touchStart(), touchMove() et touchUp().
private fun touchStart() {}

private fun touchMove() {}

private fun touchUp() {}
  1. Votre code devrait s'exécuter et s'exécuter, mais vous ne verrez rien d'autre que l'arrière-plan coloré.

Étape 2. Mettre en œuvre tactileStart()

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. Lorsque l'utilisateur arrête de se déplacer et soulève le doigt, il s'agit du point de départ du prochain tracé (le segment suivant de la ligne qu'il doit tracer).
private var currentX = 0f
private var currentY = 0f
  1. Mettez en œuvre la méthode touchStart() comme suit. Réinitialisez l'élément path, déplacez les 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. Mettre en œuvre 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 trajet, il n'est pas nécessaire de dessiner chaque pixel, et chaque fois l'affichage doit être actualisé. Pour améliorer vos performances, vous pouvez (et pouvez) intervertir le chemin entre ces points.

  • Si vous avez à peine déplacé le doigt, il n'est pas nécessaire de dessiner.
  • Si le doigt a descendu moins de touchTolerance fois, ne dessinez pas.
  • scaledTouchSlop affiche la distance en pixels que l'utilisateur peut parcourir avant que le système pense que l'utilisateur fait défiler la page.
  1. Définissez la méthode touchMove(). Calculez la distance parcourue (dx, dy), créez une courbe entre les deux points et stockez-la dans path, mettez à jour le total de currentX et currentY, puis tracez path. Appelez ensuite invalidate() pour forcer l'affichage 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()
}

Cette méthode est plus détaillée:

  1. Calculez la distance déplacée (dx, dy).
  2. Si le mouvement dépasse la tolérance tactile, ajoutez un segment au tracé.
  3. Indiquez le point de terminaison de ce segment comme point de départ du prochain segment.
  4. Au lieu de lineTo(), la ligne quadTo() est tracée de façon fluide, sans angles. Voir Bezier Courves.
  5. Appelez invalidate() pour (éventuellement appeler onDraw()) et redessiner la vue.

Étape 4: Implémentez la fonctionnalité touchUp()

Lorsque l'utilisateur soulève le doigt, il suffit de réinitialiser le trajet pour qu'il ne soit plus dessiné à nouveau. Rien n'étant dessiné, 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 utilisez votre doigt pour dessiner sur l'écran. Notez que si vous faites pivoter l'appareil, l'écran est effacé, car l'état du dessin n'est pas enregistré. Cette application est conçue pour permettre à l'utilisateur de vider facilement son écran.

Étape 5: Dessinez un cadre autour du sketch

À mesure que l'utilisateur dessine à l'écran, votre application crée le chemin et l'enregistre dans le bitmap extraBitmap. La méthode onDraw() affiche le bitmap supplémentaire sur la toile de la vue. Vous pouvez effectuer d'autres dessins dans onDraw(). Par exemple, vous pouvez dessiner des formes après avoir dessiné le bitmap.

Dans cette étape, vous allez dessiner un cadre autour du bord de la photo.

  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 ensemble et ajoutez du code pour créer le Rect qui sera utilisé pour l'image à l'aide des nouvelles dimensions et de l'encart.
// Calculate a rectangular frame around the picture.
val inset = 40
frame = Rect(inset, inset, width - inset, height - inset)
  1. Dans onDraw(), dessinez un rectangle après avoir dessiné le bitmap.
// Draw a frame around the canvas.
canvas.drawRect(frame, paint)
  1. Exécutez votre application. Remarquez le cadre.

Tâche (facultatif): Stocker des données dans un chemin

Dans l'application actuelle, les informations de dessin sont stockées dans un bitmap. Bien qu'il s'agisse d'une bonne solution, ce n'est pas la seule méthode possible pour stocker des informations sur les dessins. La façon dont vous stockez votre historique 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. Dans le cas de l'application MiniPaint, vous pouvez enregistrer le chemin sous la forme d'un Path. Vous trouverez ci-dessous une explication générale de la procédure à suivre.

  1. Dans MyCanvasView, supprimez tout le code de extraCanvas et extraBitmap.
  2. Ajoutez des variables permettant de parcourir le chemin jusqu'à présent, et le chemin actuellement 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 d'accès 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 actuel au chemin précédent et réinitialisez-le.
// 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, et il ne devrait y avoir aucune différence.

Téléchargez le code pour 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

  • Une Canvas est une surface de dessin en 2D qui fournit des méthodes de dessin.
  • Le Canvas peut être associé à une instance View qui l'affiche.
  • L'objet Paint contient les informations de style et de couleur permettant de dessiner des géométries (ligne, rectangle, ovale et tracés, par exemple) et du texte.
  • Pour utiliser un canevas, il est courant de créer une vue personnalisée et d'ignorer les méthodes onDraw() et onSizeChanged().
  • Ignorez la méthode onTouchEvent() pour capturer les touches utilisateur et y répondre en dessinant des éléments.
  • Vous pouvez utiliser un bitmap supplémentaire pour mettre en cache des informations sur les dessins qui changent au fil du temps. Vous pouvez également stocker des formes ou un trajet.

Cours Udacity:

Documentation pour les développeurs Android:

Cette section répertorie les devoirs possibles pour les élèves qui effectuent cet atelier de programmation dans le cadre d'un cours animé par un enseignant. C'est à l'enseignant de procéder comme suit:

  • Si nécessaire, rendez-les.
  • Communiquez aux élèves comment rendre leurs devoirs.
  • Notez les devoirs.

Les enseignants peuvent utiliser ces suggestions autant qu'ils le souhaitent, et ils n'ont pas besoin d'attribuer les devoirs de leur choix.

Si vous suivez vous-même cet atelier de programmation, n'hésitez pas à utiliser ces devoirs pour tester vos connaissances.

Répondez à ces questions.

Question 1

Quels composants sont nécessaires pour utiliser une Canvas ? Sélectionnez toutes les réponses applicables.

Bitmap

Paint

Path

View

Question 2

Que fait un appel à invalidate() (en général) ?

▢ Invalide et redémarre votre appli.

▢ Effacer 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 du dessin 2D, bitmap affiché à l'écran, informations de style pour le dessin

▢ : surface du dessin 3D, bitmap pour la mise en cache du chemin et informations de style pour le dessin

▢ La surface de dessin 2D, le bitmap affiché à l'écran et le style de la vue

▢ Cache pour informations de dessin, bitmap sur lequel dessiner, informations sur le style pour dessin

Pour obtenir des liens vers d'autres ateliers de programmation dans ce cours, consultez la page de destination "Avancé Android" dans les ateliers de programmation Kotlin.