Challenge : Créer une vue personnalisée

Temps : 3h.
Difficulté : * à ***

Ce challenge propose de créer une vue personnalisée Android avec le langage Kotlin, en 15 étapes :
> Afficher et déplacer un cercle noir : étape 1 à 4
> Déplacer un cercle coloré dans l'écran : étape 5 à 8
> Bonus functionnel : étape 9 à 11
> Gérer plusieurs cercles magiques : étape 11 à 15

Dans un premier temps, la vue personnalisée affiche un cercle magique. Plus spécifiquement, les exercices suivants proposent de dessiner puis d’animer un élément graphique, un cercle.
Dans un second temps, il s’agit d’afficher plusieurs cercles magiques comme dans l’écran Material Design de cette application.

> Afficher et déplacer un cercle noir

Dans cette partie, il est mis en pratique classe, propriété, et fonction du langage Kotlin.
Il s’agit de créer une vue personnalisée puis de l’ajouter dans la vue principale, d’un projet Android. La vue personnalisée affiche un cercle noir.

Étape 1 : Afficher un cercle noir

Cet exercice permet de mettre en pratique la déclaration de classe, les constructeurs et l’héritage.

  1. Créez un nouveau projet Android, nommez-le CustomView.
  2. Choisissez Empty Activity comme modèle de projet.
  3. Remarquez la convention de nommage MainActivity pour l’Activity principale et l’héritage de AppCompatActivity.
  4. Ajoutez une classe Kotlin, nommez-la CustomView.
  5. Faites hériter CustomView de l’objet View situé dans le package android.view.View :
  6. 
    class CustomView : View { }
    
    
  7. Implémentez le premier constructeur secondaire, de la classe CustomView :
  8. 
    constructor(context: Context) : super(context)
    
    
  9. Implémentez la fonction d’héritage onDraw() afin d’y dessiner un cercle :
  10. 
      override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        canvas?.drawCircle(50F, 50F, 40F, Paint())
      }
    
    

    Remarque : le symbôle ? est lié à la nullité (cf. Déclaration de variable nulle dans Variable, Opérateur, Condition).
    Note : F déclare un nombre de type Float.

  11. Ajoutez cet élément graphique dans la vue du MainActivity, dans le fichier activity_main.xml :
  12. 
    <com.chillcoding.customview.CustomView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
    
    
  13. Remarquez la déclaration de l’objet CustomView via le nom de package com.chillcoding.customview. Dans quel package votre objet CustomView est-il situé ?
  14. Exécutez, que se passe t-il ?
  15. Dans la classe CustomView, ajoutez le constructeur secondaire manquant :
  16. 
    constructor(context: Context?, attrs: AttributeSet?)
    : super(context, attrs)
    
    

À présent, un cercle noir s’affiche en haut, à gauche, de l’écran.

Étape 2 : Afficher un cercle magique

Il s’agit de créer une classe de donnée, data class, dans un nouveau fichier .kt, pour représenter le cercle à afficher.
Ce dernier possède des coordonnées (cx et cy), pour le point du centre, et un diamètre (radius).

  1. Créez une nouvelle classe Kotlin, appelez-là MagicCircle :
  2. 
    data class MagicCircle(val cx: Float = 50F,
    val cy: Float = 50F, val rad: Float = 40F)
    
    

    Remarque : dans ce constructeur primaire, il est déclaré trois propriétés immuables (cf. Propriété mutable ou immuable).

  3. Créez une instance de MagicCircle dans CustomView :
  4. 
    var mCircle = MagicCircle()
    
    
  5. Remplacez le code dans la fonction onDraw() afin d’utiliser mCircle :
  6. 
    canvas?.drawCircle(mCircle.cx, mCircle.cy, mCircle.rad, Paint())
    
    
  7. Exécutez votre projet.

À présent, un cercle magique s’affiche à l’écran.

Étape 3 : Déplacer le cercle magique

Il s’agit de créer une fonction move() afin de déplacer le cercle. De plus, l’intérêt de cet exercice est d’appréhender la propriété mutable.

  1. Ajoutez une fonction move() dans MagicCircle :
  2. 
    fun move() {
      cx++
      cy++
    }
    
    
  3. Quelle erreur est indiquée par Android Studio ?
  4. Modifiez les propriétés concernées, immuables en mutables :
  5. 
    data class MagicCircle(var cx: Float = 50F,
    var cy: Float = 50F, val rad: Float = 40F) {
      fun move() {
        cx++
        cy++
      }
    }
    
    
  6. Placez l’appel de la fonction move() dans la fonction onDraw() :
  7. 
      mCircle.move()
      canvas?.drawCircle(mCircle.cx, mCircle.cy, mCircle.rad, Paint())
      invalidate()
    
    
    Note : invalidate() est une fonction du cycle de vie de View, elle indique à la vue de se redessiner.
  8. Exécutez votre projet.

À présent, le cercle magique se déplace à l’écran et il en sort.

Étape 4 : Ajouter une vitesse de déplacement

Dans cette exercice, il s’agit de créer un objet compagnon, companion object, pour y stocker une constante DELTA, représentant la quantité de déplacement d’un élément graphique.

  1. Ajoutez le companion object, avec une constante, dans la classe MagicCircle, au même endroit que la déclaration des attributs de classe :
  2. 
    companion object {
      const val DELTA = 9
    }
    
    
  3. Modifiez la fonction move() afin d’utiliser cette constance :
  4. 
    cx += DELTA
    cy += DELTA
    
    
  5. Que se passe t-il à l’exécution de l’application ?

> Déplacer un cercle coloré dans l'écran

Dans cette partie, il est mis en pratique variable, opérateur, et condition du langage Kotlin.

Étape 5 : Colorer le cercle magique

Il s’agit de mettre en pratique l’initialisation tardive de variable afin de modifier la couleur du cercle.
Dans la mesure où la fonction onDraw() est appelée périodiquement, il n’est pas judicieux de créer ou initialiser des objets dans cette fonction. En particulier, il est préférable d’initialiser l’objet Paint depuis le constructeur de CustomView.

  1. Dans la classe CustomView, créez une variable à initialiser plus tard, de type Paint :
  2. 
      private lateinit var mPaint : Paint
    
    
  3. Initialisez la variable mPaint avec la couleur bleu, cela dans une fonction appelé init() :
  4. 
    private fun init()
      mPaint = Paint()
      mPaint.color = Color.BLUE
    }
    
    
  5. Appelez la fonction init() dans le deuxième constructeur et faites que le premier constructeur appelle le deuxième :
  6. 
    constructor(context: Context) : this(context, null)
    
    constructor(context: Context?, attrs: AttributeSet?)
    : super(context, attrs) {  init() }
    
    
  7. Remplacez le code dans la fonction onDraw() afin d’utiliser mPaint :
  8. 
    canvas?.drawCircle(mCircle.cx, mCircle.cy,
    mCircle.rad, mPaint)
    
    
  9. Exécutez votre application.

À présent, un cercle bleu s’affiche à l’écran.

Étape 6 : Récupérer les dimensions de l’écran

Il s’agit de mettre en pratique l’initialisation tardive de variable. En particulier, il s’agit de modifier l’initialisation de mCircle dans CustomView afin qu’elle prennent en compte les dimensions de l’écran. En effet, MagicCircle a besoin de connaître la largeur et la hauteur de la vue CustomView.
Hors, ces dernières sont connues depuis la fonction mère onLayout(), de View : mCircle ne peut pas être initialisé au départ avec les dimensions de l’écran, parce qu’on connait ces valeurs seulement lors de l’implementation de la fonction onLayout(), c’est-à-dire plus tard.

  1. Modifiez le constructeur primaire de MagicCircle de façon à ce qu’il prenne deux attributs de classe immuable, les dimensions de l’écran :
  2. 
    data class MagicCircle(val maxX: Int, val maxY: Int)
    
    
  3. Sortez les propriétés cx, cy, et rad du constructeur, déplacez les dans le corps de la classe :
  4. 
    data class MagicCircle(val maxX: Int, val maxY: Int) {
      var cx = 50F
      var cy = 50F
      val rad = 40F
      ...
    }
    
    
  5. Modifiez la déclaration de mCircle, dans CustomView :
  6. 
    lateinit var mCircle: MagicCircle
    
    

    Remarque : avec l’utilisation du mot clé lateinit la déclaration du type est obligatoire.

  7. Initialisez mCircle depuis onLayout() :
  8. 
    override fun onLayout(changed: Boolean,
    left: Int, top: Int, right: Int, bot: Int) {
      super.onLayout(changed, left, top, right, bot)
      mCircle = MagicCircle(width, height)
    }
    
    

Étape 7 : Déplacer le cercle dans l’écran

Il s’agit de mettre en pratique la condition when dans la fonction move() de la classe MagicCircle. Cela afin de prendre en compte les dimensions de l’écran lors du déplacement du cercle.
De plus, l’opérateur de rang .. est mis en pratique.

  1. Ajoutez les deux attributs de classe mutables, suivants dansMagicCircle :
  2. 
    var dx = DELTA
    var dy = DELTA
    
    

    Remarque : dx et dy intègrent le sens de déplacement du cercle en plus de la vitesse.

  3. Modifiez la fonction move() afin que les coordonnées du cercle restent dans le carcan :
  4. 
    fun move() {
      when {
        cx !in 0..maxX −> dx = −dx
        cy !in 0..maxY −> dy = −dy
      }
      cx += dx
      cy += dy
    }
    
    
  5. Quelle erreur est indiquée par Android Studio ?
  6. Convertissez cx et cy en Int dans les conditions du when.
  7. Que se passe t-il à l’exécution de l’application ?

À présent, le cercle bleu reste dans l’écran.

Étape 8 : Colorer le cercle magique avec magie

Il s’agit de mettre en pratique l’initialisation avec la classe déléguée, by lazy.
Dans un projet Android, le contexte ou les ressources de l’application sont disponibles après passage dans la fonction onCreate() (cf. cycle de vie d’une Activity). Dans cet exercice, les ressources sont nécessaires pour initialiser une liste de couleurs, définies dans les ressources de l’application.

  1. Ajoutez des couleurs dans le fichier res/values/colors.xml :
  2. 
    <color name="color_red">#ff5959</color>
    <color name="color_yellow">#facf5a</color>
    <color name="color_green">#49beb7</color>
    
    
  3. Ajoutez la classe App, héritant de Application avec la constante de la liste des couleurs:
  4. 
    class App : Application() {
        companion object {
            lateinit var instance: App
    	val sColors: List<Int> by lazy {
        listOf(
          ResourcesCompat.getColor(
            instance.resources, R.color.color_red, null),
          ResourcesCompat.getColor(
            instance.resources, R.color.color_yellow, null),
          ResourcesCompat.getColor(
            instance.resources, R.color.color_green, null)
           )
        }
      }
    
      override fun onCreate() {
        super.onCreate()
        instance = this
      }
    }
    
    
  5. Ajoutez son initialisation dans le fichier Manifest :
    
    <application
      android:name=".App"
    
    
    Note : La classe application est un singleton, elle est utilisée pour stocker des constantes globales.
  6. Ajoutez une constante de couleur dans la classe MagicCircle :
  7. 
      val color = App.sColors[0]
    
    
  8. Modifiez la couleur d’affichage de mCircle dans la fonction onDraw() :
  9. 
      mPaint.color = mCircle.color
    
    

> Bonus fonctionnel

Dans cette partie, il est mis en pratique quelques concepts de la programmation fonctionnelle.

Étape 9 : Disparition du cercle magique

Dans cette partie, il s’agit d’utiliser la fonction inline with. Cette dernière permet d’épurer le code de la fonction onDraw(), de la classe CustomView. De cette façon, il s’agit de faire disparaître le cercle magique dans le code.

  1. Utilisez with avec mCircle dans la fonction onDraw() :
  2. 
    with(mCircle){
      move()
      mPaint.color = color
      canvas?.drawCircle(cx, cy, rad, mPaint)
    }
    
    

    Remarque : Il n’y a plus besoin de répéter le nom de la variable mCircle à chaque fois que l’on souhaite accéder à ses attributs.

Étape 10 : Un Paint deux coups

Dans cette partie, il s’agit d’utiliser la fonction inline apply. Cette dernière permet d’épurer le code de la fonction init(), de la classe CustomView.

  1. Utilisez apply avec mPaint dans la fonction init() :
  2. 
    private fun init() {
        mPaint = Paint().apply {
          style = Paint.Style.STROKE
          strokeWidth = 10F
        }
    }
    
    

    Remarque : Il n’y a plus besoin de répéter le nom de la variable mPaint à chaque fois que l’on souhaite accéder à ses attributs.

Étape 11 : Colorier le cercle magique

Il s’agit de mettre en pratique une fonction d’extension. Dans cette partie, il s’agit d’initialiser la couleur du cercle magique de manière aléatoire. Pour cela, il est utilisée une fonction d’extension sur un nombre. En effet, l’idée est d’appliquer une fonction d’extension random() sur un nombre.

  1. Utilisez la fonction d’extension random() dans MagicCircle :
  2. 
    val mColor = App.sColors[App.sColors.indices.random()]
    
    
  3. Que se passe t-il à chaque exécution de l’application ?

À présent, le cercle change de couleur à chaque ouverture de l'application.

> Gérer plusieurs cercles magiques

Maintenant, commence le challenge, à vous de trouver le code pour réaliser un arc en ciel magique.

Étape 12 : Afficher plusieurs cercles magiques

Il s’agit de mettre en pratique les tableaux Kotlin, l'article de chillcoding Initialiser un tableau en Kotlin référence les possibilités de gestion de tableau.

  1. Créez un tableau de sept objets MagicCircle dans la classe CustomView.
  2. Initialisez les coordonnées d'un MagicCircle de manière aléatoire.
  3. Affichez tous les cercles à l’écran.

Étape 13 : Déplacer un cercle au doigt

Il s’agit de mettre en pratique la gestion du touché.

  1. Interceptez le touché de l’utilisateur dans la classe CustomView, via la fonction onTouchEvent().
  2. Déplacez un des cercles à l’endroit touché.

Étape 14 : Déplacer un cercle au poignet

Il s’agit de mettre en pratique les capteurs Android.

  1. Déplacez un des cercles en fonction des valeurs de l’accéléromètre.

Étape 15 : Afficher un arc en ciel

Il s’agit de mettre en pratique la liste et l'héritage Kotlin.

  1. Créer un objet RainBowCircle héritant de MagicCircle.
  2. RainBowCircle est initialisé avec une couleur spéciale aléatoire et avec des coordonnées spécifiées dans le constructeur.
  3. Ajouter un cercle à chaque touché.

</> Solution du challenge

Obtenez les codes sources dans les Ressources supplémentaires de ce thème sur Kotlin.

Référence :

developer.android.com: pathway on Classes and Objects