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.
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.
Cet exercice permet de mettre en pratique la déclaration de classe, les constructeurs et l’héritage.
MainActivity
pour
l’Activity
principale et l’héritage de AppCompatActivity
.CustomView
.CustomView
de l’objet View
situé dans le package
android.view.View
:
class CustomView : View { }
CustomView
:
constructor(context: Context) : super(context)
onDraw()
afin d’y dessiner un cercle :
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
.
MainActivity
,
dans le fichier activity_main.xml
:
<com.chillcoding.customview.CustomView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" />
CustomView
via le nom de package com.chillcoding.customview
. Dans quel package
votre objet CustomView
est-il situé ?CustomView
, ajoutez le constructeur secondaire manquant :
constructor(context: Context?, attrs: AttributeSet?)
: super(context, attrs)
À présent, un cercle noir s’affiche en haut, à gauche, de l’écran.
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
).
MagicCircle
:
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).
MagicCircle
dans CustomView
:
var mCircle = MagicCircle()
onDraw()
afin d’utiliser mCircle
:
canvas?.drawCircle(mCircle.cx, mCircle.cy, mCircle.rad, Paint())
À présent, un cercle magique s’affiche à l’écran.
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.
move()
dans MagicCircle
:
fun move() {
cx++
cy++
}
data class MagicCircle(var cx: Float = 50F,
var cy: Float = 50F, val rad: Float = 40F) {
fun move() {
cx++
cy++
}
}
move()
dans la fonction
onDraw()
:
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.
À présent, le cercle magique se déplace à l’écran et il en sort.
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.
companion object
, avec une constante,
dans la classe MagicCircle
, au même endroit que la déclaration des attributs de classe :
companion object {
const val DELTA = 9
}
move()
afin d’utiliser cette constance :
cx += DELTA
cy += DELTA
Dans cette partie, il est mis en pratique variable, opérateur, et condition du langage Kotlin.
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
.
CustomView
, créez une variable à initialiser
plus tard, de type Paint
:
private lateinit var mPaint : Paint
mPaint
avec la couleur bleu, cela
dans une fonction appelé init()
:
private fun init()
mPaint = Paint()
mPaint.color = Color.BLUE
}
init()
dans le deuxième constructeur
et faites que le premier constructeur appelle le deuxième :
constructor(context: Context) : this(context, null)
constructor(context: Context?, attrs: AttributeSet?)
: super(context, attrs) { init() }
onDraw()
afin d’utiliser mPaint
:
canvas?.drawCircle(mCircle.cx, mCircle.cy,
mCircle.rad, mPaint)
À présent, un cercle bleu s’affiche à 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.
MagicCircle
de façon à ce qu’il
prenne deux attributs de classe immuable, les dimensions de l’écran :
data class MagicCircle(val maxX: Int, val maxY: Int)
cx
, cy
, et rad
du constructeur, déplacez les dans le corps de la classe :
data class MagicCircle(val maxX: Int, val maxY: Int) {
var cx = 50F
var cy = 50F
val rad = 40F
...
}
mCircle
, dans CustomView
:
lateinit var mCircle: MagicCircle
Remarque : avec l’utilisation du mot clé lateinit la déclaration du type est obligatoire.
mCircle
depuis onLayout()
:
override fun onLayout(changed: Boolean,
left: Int, top: Int, right: Int, bot: Int) {
super.onLayout(changed, left, top, right, bot)
mCircle = MagicCircle(width, height)
}
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.
MagicCircle
:
var dx = DELTA
var dy = DELTA
Remarque : dx
et dy
intègrent
le sens de déplacement du cercle en plus de la vitesse.
move()
afin que les coordonnées
du cercle restent dans le carcan :
fun move() {
when {
cx !in 0..maxX −> dx = −dx
cy !in 0..maxY −> dy = −dy
}
cx += dx
cy += dy
}
cx
et cy
en Int
dans les conditions du when
.À présent, le cercle bleu reste dans l’écran.
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.
res/values/colors.xml
:
<color name="color_red">#ff5959</color>
<color name="color_yellow">#facf5a</color>
<color name="color_green">#49beb7</color>
App
, héritant de Application
avec la constante de la liste des couleurs:
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
}
}
Manifest
:
<application
android:name=".App"
Note : La classe application est un singleton, elle est utilisée pour stocker des
constantes globales.
MagicCircle
:
val color = App.sColors[0]
mCircle
dans la fonction onDraw()
:
mPaint.color = mCircle.color
Dans cette partie, il est mis en pratique quelques concepts de la programmation fonctionnelle.
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.
with
avec mCircle
dans la fonction onDraw()
:
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.
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
.
apply
avec mPaint
dans la fonction init()
:
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.
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.
random()
dans MagicCircle
:
val mColor = App.sColors[App.sColors.indices.random()]
À présent, le cercle change de couleur à chaque ouverture de l'application.
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.
MagicCircle
dans la classe CustomView
.MagicCircle
de manière aléatoire.Il s’agit de mettre en pratique la gestion du touché.
CustomView
, via
la fonction onTouchEvent()
.Il s’agit de mettre en pratique les capteurs Android.
Il s’agit de mettre en pratique la liste et l'héritage Kotlin.
RainBowCircle
héritant de MagicCircle
.RainBowCircle
est initialisé avec une couleur spéciale
aléatoire et avec des coordonnées spécifiées dans le constructeur.Obtenez les codes sources dans les Ressources supplémentaires de ce thème sur Kotlin.