Créer la surface View

Contexte

Nous allons démarrer la programmation de notre première animation graphique : le petit billard. Elle est très simple.

Néanmoins, tout ce que nous allons découvrir par la suite nous sera d'une très grande utilité pour la deuxième animation, plus réaliste et ludique à la fois. Nous allons avant tout créer une surfaceView, faire le lien avec le fichier activity_main.xml et approfondir quelque peu la notion de View en programmation Android.

Comme autre exemple de View, nous allons présenter et utiliser les ImageView.

Objectifs de la partie

Mettre en place les bases graphiques qui vont permettre au billard et au jeu canon d'apparaître à l'écran.

Les premières manipulations que nous allons vous demander d'effectuer risquent de vous apparaître à nouveau des plus ésotériques. L'idée est de créer et d'installer dans votre application une vue spéciale, une DrawingView qui sera la surface de jeu, sur laquelle vous représenterez tout ce qui constituera l'aspect graphique du billard. C'est donc un composant essentiel de nos 2 projets et il est normal que nous y consacrions à nouveau un peu de temps sans rentrer dans le vif d'Android et de Kotlin.

Ces manipulations font appel aux mécanismes clés de l'orienté objet, que nous expliquerons plus loin avec les balles et les parois du billard. Prenez encore votre mal en patience.

Comme première manipulation glissez dans la fenêtre de code un élément SurfaceView, que vous trouverez dans le répertoire widget de la palette des composants graphiques. C'est en effet un des composants graphiques (widgets) qu'Android Studio met à votre disposition. Collez-le à même l'écran en dessous d'un bouton comme l'illustration ci-dessous vous l'indique.

À nouveau, Android Studio prend quelques initiatives, dont celle de produire le XML correspondant. N'hésitez pas à recourir à l'infer constraint pour faciliter les choses et apportez des modifications à même le fichier XML, de manière à obtenir le code suivant :

1
<?xml version="1.0" encoding="utf-8"?>
2
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
3
    xmlns:app="http://schemas.android.com/apk/res-auto"
4
    xmlns:tools="http://schemas.android.com/tools"
5
    android:layout_width="match_parent"
6
    android:layout_height="match_parent"
7
    tools:context=".MainActivity">
8
9
10
    <Button
11
        android:id="@+id/button"
12
        android:layout_width="wrap_content"
13
        android:layout_height="wrap_content"
14
        android:layout_marginTop="45dp"
15
        android:text="Mon Premier Bouton"
16
        app:layout_constraintEnd_toEndOf="parent"
17
        app:layout_constraintStart_toStartOf="parent"
18
        app:layout_constraintTop_toTopOf="parent" />
19
20
    <com.example.smallbillard.DrawingView
21
        android:layout_width="wrap_content"
22
        android:layout_height="wrap_content"
23
        android:id="@+id/vMain"
24
        app:layout_constraintTop_toTopOf="parent"
25
        app:layout_constraintStart_toStartOf="parent"
26
        android:layout_marginTop="104dp"/>
27
28
</androidx.constraintlayout.widget.ConstraintLayout>

Les valeurs numériques présentes dans ce XML sont souvent des distances en pixels prises à partir de l'extrémité droite, gauche, ou supérieure de votre écran. Les plus passionnés d'entre vous pourront se ruer sur Internet pour comprendre leur signification précise et s'amuser à en modifier la valeur. Elles vous sont données ici à titre indicatif, mais rien ne vous empêche évidemment d'ajuster le positionnement relatif des composants apparaissant sur votre écran.

La présence des match_parent et autres wrap_content sert précisément à cela : ajuster vos composants par rapport à ceux sur lesquels vous les déposez délicatement (les parents).

Prenez garde aussi à ce surfaceView que vous avez transformé comme par enchantement en un com.example.smallbillard.DrawingView. Remplacez évidemment « smallbillard » par le nom que vous avez donné à votre projet. Cette modification est effectuée afin de faire le lien avec le code Kotlin de votre application principale, que nous allons découvrir tout de suite. Le chemin qui mène au DrawingView est exactement celui qui contient votre fichier MainActivity.

Notez finalement son id qui permettra d'y avoir accès dans ce même code source. Reproduisez donc au pixel près ce code XML.

Voici la nouvelle version de votre fichier MainActivity.kt :

1
package com.example.smallbillard
2
3
import android.app.Activity
4
import androidx.appcompat.app.AppCompatActivity
5
import android.os.Bundle
6
import android.view.View
7
import android.widget.Toast
8
import kotlinx.android.synthetic.main.activity_main.*
9
10
class MainActivity: Activity() {
11
12
    lateinit var drawingView: DrawingView
13
14
    override fun onCreate(savedInstanceState: Bundle?) {
15
        super.onCreate(savedInstanceState)
16
        setContentView(R.layout.activity_main)
17
        drawingView = findViewById(R.id.vMain)
18
    }
19
20
    fun onClick(v: View) {
21
        val x = 10
22
        if (x >= 10)
23
            Toast.makeText(this, "Je presse mon premier bouton",
24
                Toast.LENGTH_LONG).show()
25
    }
26
}

Android Studio vous propose souvent de taper « alt-enter » de manière à ce qu'il prenne l'initiative pour importer les packages qui font défaut. Obéissez-lui car il a souvent raison. Il lit dans vos pensées.

Là encore, remarquez bien les modifications qu'il convient d'apporter à l'existant. Votre AppCompActivity s'est transformée en une simple Activity, dont hérite votre classe MainActivity (nous ne tarderons pas à vous expliquer le concept d'héritage dans la suite – n'oubliez pas les alt-enter).

Vous voyez également apparaître dans le code une variable appelée drawingView de type DrawingView. Elle est déclarée comme var et lateinit. Cette dernière précision nous informe que son initialisation ne se fait pas au moment de sa déclaration (comme est en droit de l'attendre Kotlin afin d'en inférer le type), mais plus bas dans le code. Et, de fait, nous initialisons bien cette variable dans la fonction onCreate en allant la chercher dans le fichier XML.

C'est une particularité remarquable d'Android Studio que de permettre à tout moment cette synchronisation entre les parties graphiques de votre application (codées dans le fichier activity_main.xml) et les instructions Kotlin qui en font usage. Ici l'instruction findViewById(R.id.vMain) va, en effet, rechercher dans le répertoire ressource (toujours ce fameux R) l'objet drawingView dans le fichier XML qu'elle récupère grâce à son id.

Malheureusement, les ajouts sont loin d'être terminés. En effet, à ce stade, le type DrawingView est souligné de rouge car non reconnu dans votre code MainActivity. Il nous reste donc à définir ce type, l'élément évidemment central de notre projet, car responsable de toute l'activité graphique de l'application, non pas ce à quoi les composants ressemblent mais ce qu'ils sont censés faire.

Pour cela, nous allons créer une nouvelle classe appelée DrawingView et l'installer dans un nouveau fichier qui, tradition objet oblige, portera le même nom. La manipulation consiste donc, dans Android Studio, à se rendre dans le même répertoire que celui contenant le fichier MainActivity.kt et à y créer un nouveau fichier/classe Kotlin appelé DrawingView, dont le contenu devra être le suivant :

1
2
package com.example.smallbillard
3
4
import android.content.Context
5
import android.util.AttributeSet
6
import android.view.SurfaceView
7
8
class DrawingView @JvmOverloads constructor (context: Context, attributes: AttributeSet? = null, defStyleAttr: Int = 0): SurfaceView(context, attributes,defStyleAttr) {
9
10
}

Cette écriture est assez sophistiquée et ésotérique ; certains aspects en seront démystifiés par la suite. Bien qu'il nous reste encore à nous y plonger plus à fond, la démarche orientée objet consiste en la séparation d'un projet en ces acteurs essentiels que nous installons dans des classes.

À ce stade, nous nous contentons de 2 classes, la MainActivity et la DrawingView mais, bien vite, les balles et les parois requerront l'ajout des 2 classes correspondantes, ce qui fait que, à la toute fin de notre petit projet, 4 classes y seront présentes.

Ici, la classe MainActivity s'avère pivot dans notre projet car elle est responsable du démarrage de l'application et de sa synchronisation avec les informations graphiques contenues dans le fichier XML. La classe DrawingView, quant à elle, est responsable de l'animation graphique à proprement parler.

Autre aspect non négligeable, elle hérite de la classe SurfaceView (présence des :SurfaceView), déjà connue dans l'environnement Android Studio, et dont notre DrawingView va récupérer un ensemble de fonctionnalités sans avoir à les reprogrammer.

Et l'on découvre ici un autre aspect bien précieux de l'orienté objet, l'héritage, qui permet (et nous y reviendrons) à une classe fille de bénéficier d'un ensemble de fonctionnalités déjà définies dans la classe mère. En orienté objet, les classes, soit dépendent les unes des autres, comme MainActivity qui utilise les fonctionnalités de DrawingView (la première réfère bien la deuxième parmi ses attributs), soit héritent les unes des autres, comme DrawingView et SurfaceView. On dira aussi plus simplement que MainActivity dépend de DrawingView et que cette dernière est un cas particulier de SurfaceView.

Par exemple, petit avant-goût de ce qui va suivre, la classe Voiture est une classe fille de la classe Moyen de transport et dépend des classes Moteur et Roues. Vous assistez à une sous-classe de la classe Cours-En-Ligne-De-Programmation (un objet hérité de la classe Cours-En-Ligne) contenant des objets de la classe Ecran dont l'intérêt ne se dément pas au fil de la lecture.

Exécutez votre projet. Rien de très existant ne devrait se produire sur votre écran si ce n'est ce que nous connaissons déjà : le fameux bouton. Mais au moins cela permet de vous rassurer que tout fonctionne comme prévu et qu'aucune erreur de compilation ou de synchronisation entre le xml et le Kotlin n'est à déplorer.