Conception d’interfaces graphiques

La plupart des applications Android impliquent une interaction avec l’utilisateur. Nous avons introduit au chapitre précédent la notion d’activité qui correspond à un écran. Dans ce chapitre nous allons voir plus en détail comment créer des activités riches et responsives.

View et Layout

Une activité est composée de views. Il s’agit des composants graphiques qui peuvent être affichés à l’écran (on parle plus généralement en programmation de widgets graphiques). Par exemple, une zone de texte ou un bouton sont des views représentés en Java respectivement par les classes TextView et Button. Certaines vues permettent de grouper d’autres vues ensembles (ViewGroup) afin de créer une hiérarchie structurée des composants graphiques à afficher.

../_images/vue_hierarchique_composants_graphiques.png

Exemple de hiérarchie des composants graphiques d’une activité

La difficulté d’une bonne conception d’interfaces graphiques avec Android et de concevoir des activités responsives, c’est-à-dire capables de s’adapter à des configurations diverses : la résolution de l’écran qui varie suivant les modèles d’appareil, la ratio hauteur / largeur de l’écran qui dépend de l’orientation de l’appareil (mode paysage ou mode portrait), la langue utilisée, la configuration de l’utilisateur… De plus une application Android peut parfois être installée sur des appareils très différents : smartphones, tablettes, Smart TV…

Pour aider à la conception d’interfaces graphiques, l’API Android fournit des layouts. Un layout agit comme un ViewGroup : il peut contenir plusieurs vues. Un layout n’a pas de représentation graphique particulière. Cependant, il va positionner à l’écran les vues qu’il contient suivant un modèle et en s’adaptant aux conditions particulières d’affichage. Par exemple, le layout LinearLayoutCompat permet d’organiser toutes le vues qu’il contient horizontalement ou verticalement.

Note

Un layout étant un ViewGroup, cela signifie qu’il peut être placé dans un autre layout. En effet, le rendu d’une activité peut rarement être réalisé grâce à l’utilisation d’un seul layout. Plus généralement, les layouts sont imbriqués les uns dans les autres pour obtenir le rendu voulu.

Layout XML

Nous avons vu au chapitre précédent qu’il est possible de créer un fichier XML pour décrire le layout d’une activité (le terme layout est ici à prendre au sens général d’un arrangement de composants graphiques sur l’écran). Au démarrage de l’activité, on associe ce layout grâce à la méthode setContentView :

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}

Note

Il est tout à fait possible de réaliser la même opération par programmation en Java. La plupart du temps, le recours à un fichier XML est plus simple et la programmation Java d’un layout est laissée pour des implémentations avancées.

Avec Android Studio, vous avez la possibilité d’utiliser l’outil de conception graphique ou d’éditer et de modifier directement le fichier XML.

Layout linéaire et positionnement

Pour illustrer les bases du layout, nous allons utiliser le fichier suivant :

Layout linéaire vertical
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Bonjour" />

</androidx.appcompat.widget.LinearLayoutCompat>

Ce fichier commence par utiliser un LinearLayoutCompat. Ce dernier permet d’organiser les vues qu’il contient horizontalement ou verticalement. L’attribut android:orientation indique pour cet exemple que le placement sera vertical.

../_images/linear_layout_vertical.png

Rendu du layout linéaire vertical

Note

On peut également spécifier un orientation horizontale :

Layout linéaire horizontal
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Bonjour" />

</androidx.appcompat.widget.LinearLayoutCompat>
../_images/linear_layout_horizontal.png

Rendu du layout linéaire horizontal

Le layout contient deux TextView avec les libellés : « Hello » et « Bonjour ». Chacun des éléments dans ce fichier possède les attributs android:layout_width et android:layout_height. Ces attributs permettent de définir comment une vue doit décider de ses dimensions lors de l’affichage à l’écran. Il est possible de donner les valeurs suivantes :

match_parent

Le composant doit, autant que possible, adapter sa taille à celle du composant dans lequel il se trouve sans tenir compte de son contenu.

wrap_content

Le composant doit s’assurer que sa taille d’affichage est suffisante pour afficher correctement son contenu (par exemple le texte pour un TextView).

fill_parent

Le composant doit simplement recouvrir son composant parent.

Si nous modifions notre fichier layout :

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:text="Hello"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Bonjour" />

</androidx.appcompat.widget.LinearLayoutCompat>

Nous indiquons maintenant que le premier TextView a une hauteur match_parent.

../_images/linear_layout_match_parent.png

Rendu du layout modifié

Nous voyons sur le rendu que le second TextView semble avoir disparu. En fait, le premier TextView occupe la totalité de la hauteur du LinearLayoutCompat, qui, lui-même, occupe la totalité de l’activité (c’est-à-dire de l’écran). Le second TextView n’a donc pas disparu, il est placé hors écran, sous le premier.

Note

Vous pouvez également définir une valeur chiffrée pour définir une hauteur ou une largeur d’une vue dans un layout. La valeur peut être exprimée en dp (Density-independent Pixels). Il s’agit d’une unité de mesure basée sur la taille d’un pixel physique pour un écran de 160 dpi (Dots Per Inch). L’avantage de cette mesure est que le système applique un ratio de correction en fonction des capacités réelles de l’écran pour assurer un affichage correct. Même s’il est possible de donner également des mesures en pixels, en centimètres, en millimètres ou en pouce, le dp est la mesure recommandée lorsque vous devez donner une taille chiffrée pour un composant.

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="100dp"
        android:text="Hello"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Bonjour" />

</androidx.appcompat.widget.LinearLayoutCompat>
../_images/linear_layout_match_exemple_dp.png

Rendu du layout modifié

Techniques avancées de positionnement dans un layout

Pour ajuster le rendu d’un layout vous pouvez agir sur chaque vue individuellement avec plusieurs attributs.

layout_margin et padding

Vous pouvez définir une marge grâce à l’attribut android:layout_margin (et toutes ses variantes : android:layout_marginBottom, android:layout_marginTop android:layout_marginHorizontal, android:layout_marginLeft, android:layout_marginRight, android:layout_marginStart, android:layout_marginEnd, android:layout_marginVertical) qui permet définir une marge autour de la vue.

De même, vous pouvez définir un padding grâce à l’attribut android:padding (et toutes ses variantes : android:paddingBottom, android:paddingTop android:paddingHorizontal, android:paddingLeft, android:paddingRight, android:paddingStart, android:paddingEnd, android:paddingVertical).

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="50dp"
        android:text="Hello"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="50dp"
        android:text="Bonjour" />

</androidx.appcompat.widget.LinearLayoutCompat>
../_images/linear_layout_layout_margin_padding.png

Utilisation du layout_margin et du padding

Même si, dans les cas les plus simples, il n’y a pas de différence de rendu entre l’utilisation de android:layout_margin et de android:padding, il faut garder à l’esprit que :

  • android:layout_margin définit une marge autour de la vue. Cette marge impacte le comportement du layout qui doit positionner les autres vue en accord.

  • android:padding définit un écart à l’intérieur de la vue elle-même. Si la vue déclare une largeur et/ou une hauteur à partir de wrap_content, la vue aura simplement sa largeur et/ou sa hauteur modifiées pour tenir compte du padding. Le padding n’a dont pas d’impact direct sur le layout.

layout_gravity et gravity

Les attributs android:layout_gravity et android:gravity permettent de définir comment le contenu d’une vue doit être attiré vers tel ou tel bord ou au contraire être centré. android:gravity n’agit que sur le contenu de la vue indépendamment du reste du layout tandis que android:layout_gravity agit sur le positionnement de la vue en fonction du layout.

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="right"
        android:text="Hello"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="Bonjour" />

</androidx.appcompat.widget.LinearLayoutCompat>
../_images/linear_layout_layout_gravity.png

Utilisation du layout_gravity

layout_weight

Vous pouvez définir un poids pour chaque vue dans un layout grâce à l’attribut android:layout_weight. S’il s’agit d’un layout vertical, alors la hauteur de chaque vue sera proportionnelle à son poids et, respectivement, s’il s’agit d’un layout horizontal, alors la largeur de chaque vue sera proportionnelle à son poids. Si la valeur de l’attribut android:layout_weight est inférieur à 1, alors elle est interprétée comme un pourcentage.

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_orange_dark"
        android:layout_weight=".2"
        android:text="Hello"/>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_blue_bright"
        android:layout_weight=".3"
        android:text="Bonjour" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_purple"
        android:layout_weight=".5"
        android:text="à tous" />
</androidx.appcompat.widget.LinearLayoutCompat>
../_images/linear_layout_layout_weight.png

Utilisation du layout_weight

Dans l’exemple ci-dessus, nous déclarons trois TextView avec des poids respectifs de 20%, 30% et 50%. Pour que le résultat soit plus facile à comprendre, nous utilisons également l’attribut android:background pour attribuer une couleur prédéfinie comme fond à chaque TextView.

Layout par contraintes

Même s’il est possible d’utiliser une combinaison de LinearLayoutCompat pour aligner horizontalement et verticalement les vues, cela finit par créer une arborescence complexe de layouts. Comme alternative, vous pouvez également utiliser un ConstraintLayout. Ce dernier permet de créer plus librement des agencements complexes avec un unique layout au prix, néanmoins, d’une plus grande difficulté de prise en main.

Note

La vue Designer de Android Studio dispose d’outils graphiques pour aider à la conception d’un layout contraint.

Le principe du ConstraintLayout est de positionner les vues les unes par rapport aux autres. Une vue doit au minimum posséder une contrainte pour sa position verticale et une pour sa position horizontale. Par exemple, vous pouvez contraindre un bouton à se positionner à droite d’un autre bouton et à la même hauteur.

Ci-dessous un exemple de layout contraint avec le rendu :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?xml version="1.0" encoding="utf-8"?>

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <TextView
        android:id="@+id/textViewHello"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="5dp"
        android:layout_marginTop="50dp"
        android:text="Hello"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="5dp"
        android:text="world"
        app:layout_constraintLeft_toRightOf="@+id/textViewHello"
        app:layout_constraintBaseline_toBaselineOf="@+id/textViewHello" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="50dp"
        android:text="Bas de page"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
../_images/contraint_layout.png

Le rendu du layout contraint

Pour le layout précédent, le premier TextView doit avoir le haut au même niveau que le composant parent (c’est-à-dire le layout lui-même) et la bordure gauche doit également être au même niveau que le parent. On précise également une marge en haut et à gauche pour décoller le TextView.

Le second TextView a sa bordure gauche placée à droite du premier TextView. Notez que le premier TextView est référencé par un id (@+id/textViewHello) et la ligne de texte est au même niveau (à la même hauteur) que la ligne de texte du premier TextView. On applique à ce second TextView une marge à gauche. Donc le layout doit positionner le second TextView exactement à droite du premier avec un espacement de 5dp.

Enfin le troisième TextView doit avoir sa bordure du bas au même niveau que le bas du composant parent (c’est-à-dire le layout lui-même). De même, le début et la fin du TextView sont identiques au composant parent (c’est-à-dire le layout lui-même). Comme cela n’est pas possible car le taille du TextView est limité par son contenu, cela va avoir pour conséquence de centrer ce TextView. Enfin une marge du bas de 50dp est appliquée pour remonter légèrement vers le haut le composant.

../_images/contraint_layout_avec_cadre.png

Le rendu du layout contraint

Note

Pour explorer les possibilités du ConstraintLayout, vous pouvez vous reporter au tutoriel sur le site officiel :

Ajouter une image

La vue ImageView permet d’insérer une image dans une activité. Grâce à l’attribut android:src, vous spécifiez la source de l’image (par exemple un fichier PNG). La source correspond à un identifiant de ressource de la forme @drawable/ suivi du nom du fichier image sans l’extension.

<ImageView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/mon_image"/>

Note

Vous pouvez également référencer une ressource drawable avec l’attribut android:background pour positionner une image en arrière-plan d’une vue.

Une image PNG ou JPG est une image rastérisée (ou image matricielle). L’image est définie par une grille ou chaque point à une couleur. L’inconvénient de ce type d’images est qu’il s’adapte mal à des résolutions d’écran différentes.

Android supporte la possibilité de fournir plusieurs fois la même ressource en fonction du contexte d’exécution de l’application (par exemple de la résolution de l’écran). Pour une image, une application Android accepte plusieurs résolutions :

  • ldpi (low dots per inch) correspond à une image en 120 dpi.

  • mdpi (medium dots per inch) correspond à une image en 160 dpi.

  • hdpi (high dots per inch) correspond à une image en 240 dpi.

  • xhdpi (extra high dots per inch) correspond à une image en 320 dpi.

  • xxhdpi (extra extra high dots per inch) correspond à une image en 480 dpi.

  • xxxhdpi (extra extra extra high dots per inch) correspond à une image en 640 dpi.

Pour mémoire, vous pouvez utiliser un facteur multiplicateur de zoom pour connaître les dimensions nécessaires d’une image en prenant comme référence le mdpi. Chacune des images doit être placée dans un répertoire ressource comme indiqué ci-dessous. Chaque fichier image doit porter le même nom pour indiquer qu’il s’agit de la même ressource avec une résolution différente.

densité

facteur de zoom

répertoire

ldpi

3/4

res/drawable-ldpi

mdpi

1

res/drawable-mdpi

hdpi

1,5

res/drawable-hdpi

xhdpi

2

res/drawable-xhdpi

xxhdpi

3

res/drawable-xxhdpi

xxxhdpi

4

res/drawable-xxxhdpi

Vous devez créer ces répertoires dans votre projet et placer chaque image dans le bon répertoire.

Note

Avec ce système, vous pouvez définir quel fichier image utiliser en fonction de la densité de l’écran. Cette utilisation conditionnelle d’une ressource peut être étendue de manière générale avec la notion de qualifier. Un qualifier est une condition ajoutée dans le nom du répertoire contenant la ressource (comme par exemple res/drawable-ldpi-ldpi est un qualifier).

L’utilisation de qualifier permet de fournir des ressources différentes en fonction de la langue, de l’orientation de l’écran, de la densité de l’écran, du type d’appareil… Pour en savoir plus, reportez-vous à la documentation officielle :

Exemple de formulaire

Une activité assez courante consiste à présenter un formulaire à l’utilisateur pour qu’il puisse saisir un ensemble d’informations. Ci-dessous, un exemple de layout avec son rendu :

Un layout pour un formulaire
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <androidx.appcompat.widget.LinearLayoutCompat
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <EditText
            android:id="@+id/prenom"
            android:layout_margin="5sp"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:inputType="textPersonName"
            android:hint="Prénom"/>

        <EditText
            android:id="@+id/nom"
            android:layout_margin="5sp"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:inputType="textPersonName"
            android:hint="Nom" />

        <EditText
            android:id="@+id/email"
            android:layout_margin="5sp"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:inputType="textEmailAddress"
            android:hint="Email"/>

        <EditText
            android:id="@+id/naissance"
            android:layout_margin="5sp"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:inputType="date"
            android:hint="Date de naissance JJ/MM/AAAA"/>

        <EditText
            android:id="@+id/description"
            android:layout_margin="5sp"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:inputType="textMultiLine"
            android:hint="Description"
            android:gravity="top"
            android:height="200dp"/>

        <EditText
            android:id="@+id/commentaire"
            android:layout_margin="5sp"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:inputType="textMultiLine"
            android:hint="Commentaire"
            android:gravity="top"
            android:height="200dp"/>

        <CheckBox
            android:id="@+id/acceptation_conditions"
            android:layout_margin="5sp"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:text="Accepter les conditions d'utilisation" />

        <androidx.appcompat.widget.LinearLayoutCompat
            android:layout_margin="5sp"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:gravity="right">

            <Button
                android:id="@+id/bouton_annuler"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:onClick="handleAnnuler"
                android:text="Annuler"/>

            <Button
                android:id="@+id/bouton_ok"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:onClick="handleOk"
                android:text="Ok" />
        </androidx.appcompat.widget.LinearLayoutCompat>

    </androidx.appcompat.widget.LinearLayoutCompat>
</ScrollView>
../_images/layout_user_form.png

Le rendu du formulaire

Notez l’utilisation de la ScrollView à la racine du layout pour permettre à l’utilisateur de faire défiler l’écran pour parcourir le formulaire. Ce layout utilise ensuite un LinearLayoutCompat vertical pour afficher chaque élément du formulaire. En dernier, il utilise un autre LinearLayoutCompat horizontal pour afficher les boutons côte-à-côte.

Le formulaire utilise des vues EditText en précisant, avec l’attribut android:inputType, le type du champ pour permettre au système de proposer un type de saisie adapté au champ. L’utilisation de l’attribut android:hint permet de préciser le rôle du champ sans avoir à ajouter de libellé de manière à ne pas surcharger le formulaire.

Chaque champ de saisie, ainsi que chaque bouton, déclare un ID afin de pouvoir être identifié dans le code Java.

Pour traiter les données saisies par l’utilisateur, nous pouvons modifier l’implémentation de l’activité.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package dev.gayerie.premiereapplication;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.CheckBox;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    private TextView nom;
    private TextView prenom;
    private TextView email;
    private TextView naissance;
    private TextView description;
    private TextView commentaire;
    private CheckBox acceptationConditions;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        nom = findViewById(R.id.nom);
        prenom = findViewById(R.id.prenom);
        email = findViewById(R.id.email);
        naissance = findViewById(R.id.naissance);
        description = findViewById(R.id.description);
        commentaire = findViewById(R.id.commentaire);
        acceptationConditions = findViewById(R.id.acceptation_conditions);
    }

    public void handleOk(View v) {
        // TODO faire quelque chose de plus utile
        Log.i(TAG, "Ok");
        Log.i(TAG, String.format("Prénom : %s", prenom.getText()));
        Log.i(TAG, String.format("Nom : %s", nom.getText()));
        Log.i(TAG, String.format("Email : %s", email.getText()));
        Log.i(TAG, String.format("Naissance : %s", naissance.getText()));
        Log.i(TAG, String.format("Description : %s", description.getText()));
        Log.i(TAG, String.format("Commentaire : %s", commentaire.getText()));
        Log.i(TAG, String.format("Conditions acceptées : %b", acceptationConditions.isChecked()));
    }

    public void handleAnnuler(View v) {
        // TODO faire quelque chose de plus utile
        Log.i(TAG, "Annulé");
    }

}

Dans la méthode onCreate, à partir de la ligne 28, nous utilisons la méthode findViewById pour récupérer chaque vue et la conserver dans un attribut.

Pour associer une action à chaque bouton, nous utilisons l’attribut android:onClick dans le layout XML pour donner le nom d’une méthode de l’activité qui doit être appelée. Cela évite de déclarer manuellement un listener dans le code de l’activité (comme nous l’avons fait au chapitre précédent). La méthode de l’activité doit être publique, ne retourner aucun résultat (void) et prendre en paramètre un objet de type View correspondant à l’objet cliqué. Nous déclarons une méthode handleOk pour l’associer au clic sur le bouton Ok et une méthode handleAnnuler pour l’associer au clic sur le bouton Annuler.

Note

Dans cet exemple, en réaction au clic sur un bouton, nous nous contentons d’afficher des messages d’information dans les logs du système grâce à la classe Log.

À l’exécution de l’application, les logs sont accessibles depuis Android Studio dans l’onglet Logcat.

La barre d’action

La barre d’action est un composant habituel de la conception d’une interface mobile. Elle s’affiche en haut de l’écran et propose des actions contextuelles sous la forme d’icônes ou d’un menu.

../_images/action_bar.png

Un exemple de barre d’action

Créer un menu dans la barre d’action

Par défaut, une projet créé avec Android Studio utilise un thème qui affiche une barre d’action avec le libellé de l’application. Pour ajouter des boutons dans la barre d’action, vous devez commencer par créer un menu. Un menu est une ressource dans le répertoire res/menu. Vous pouvez le créer directement dans Android Studio en faisant un clic droit dans le navigateur de projet et en choisissant New > Android Resource File. Vous pouvez ensuite saisir le nom du menu et préciser le type Menu.

../_images/dialog_new_resource_file.png

La fenêtre de création d’une ressource pour un menu

En cliquant sur Ok, un fichier XML nommé app_menu.xml est ajouté dans le répertoire res/menu qui est créé à cette occasion. Un menu est décrit par un document XML.

Exemple de menu
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="utf-8"?>
<menu
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/menu_edition"
        android:title="Edition"
        android:icon="@android:drawable/ic_menu_edit"
        app:showAsAction="ifRoom"/>

    <item
        android:id="@+id/menu_preferences"
        android:title="Préférences"
        android:icon="@android:drawable/ic_menu_preferences"
        app:showAsAction="collapseActionView"/>

    <item
        android:id="@+id/menu_aide"
        android:title="Aide"
        android:icon="@android:drawable/ic_menu_help"
        app:showAsAction="collapseActionView"/>

</menu>

Chaque item du menu possède un identifiant unique grâce à l’attribut android:id ainsi qu’un titre et une icône. Vous pouvez fournir votre propre icône sous la forme d’une ressource de type drawable. Notez qu’il existe des ressources fournies directement par Android, et notamment des icônes de menu auxquelles vous pouvez accéder avec les identifiants commençant par @android:drawable/ic_menu_.

L’attribut app:showAsAction indique comment un item de menu doit être pris en compte par la barre d’action. Vous pouvez donner une des valeurs suivantes :

always

L’item doit être toujours affiché dans la barre d’action.

never

L’item ne doit pas être affiché ni être présent dans le menu.

collapseActionView

L’item ne sera présent que dans le menu et jamais directement dans la barre d’action

withText

L’item doit afficher son titre et pas uniquement son icône. Cela entraîne la plupart du temps que l’item ne peut pas s’afficher correctement dans la barre d’action et sera donc présent dans le menu.

ifRoom

L’item peut s’afficher dans la barre d’action que si cela est possible. Sinon, il sera présent dans le menu.

Note

Si votre application n’affiche pas de barre d’action, tous les items du menu seront présents dans le menu contextuel de l’activité.

Associer un menu à une activité

Pour associer un menu à une activité, il faut surcharger la méthode onCreateOptionsMenu dans la classe de l’activité :

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.app_menu, menu);
    return true;
}

L’objet retourné par la méthode getMenuInflater permet d’associer l’identifiant du layout de menu au menu de l’activité.

Implémenter les actions associées aux items de menu

Pour déclencher une action lorsque l’utilisateur clique sur un bouton du menu, il faut surcharger la méthode onOptionsItemSelected. Cette dernière prend en paramètre l’item choisi dont il est possible de récupérer l’ID pour décider de l’action à réaliser.

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        case R.id.menu_aide:
            Log.i(TAG, "Clic sur le bouton d'aide");
            return true;
        case R.id.menu_edition:
            Log.i(TAG, "Clic sur le bouton d'édition");
            return true;
        case R.id.menu_preferences:
            Log.i(TAG, "Clic sur le bouton des préférences");
            return true;
        default:
            return super.onOptionsItemSelected(item);
    }
}

Les boites de dialogue

Pour créer une boite de dialogue, vous pouvez utiliser la classe AlertDialog ainsi que la classe AlertDialog.Builder pour vous aider à créer votre boite de dialogue.

Boite de dialogue avec boutons

Dans une application Android, les boites de dialogue ont généralement un, deux ou trois boutons désignés sous le terme de positif (par exemple ok), de neutre (par exemple « plus tard ») et de négatif (par exemple « annuler »). Vous pouvez utiliser la classe AlertDialog.Builder pour définir le titre, le message et les boutons de la boite de dialogue et appeler ensuite la méthode show pour présenter la boite de dialogue. L’exemple ci-dessous présente du code qui peut être ajouté dans une activité :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public void afficherBoiteDeDialogue() {
    DialogInterface.OnClickListener dialogListener = new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            onClickBoiteDeDialogue(dialog, which);
        }
    };

    new AlertDialog.Builder(this)
            .setTitle("Ma boite")
            .setMessage("Comment allez-vous ?")
            .setPositiveButton("Bien", dialogListener)
            .setNeutralButton("Ne sais pas", dialogListener)
            .setNegativeButton("Pas super", dialogListener)
            .show();
}

private void onClickBoiteDeDialogue(DialogInterface dialog, int which) {
    switch (which) {
        case AlertDialog.BUTTON_POSITIVE:
            new AlertDialog.Builder(this).setMessage("Vous positivez !").show();
            break;
        case AlertDialog.BUTTON_NEUTRAL:
            new AlertDialog.Builder(this).setMessage("Vous avez le droit de ne pas savoir.").show();
            break;
        case AlertDialog.BUTTON_NEGATIVE:
            new AlertDialog.Builder(this).setMessage("Ça ne va pas fort !").show();
            break;
    }
}
../_images/dialogue_avec_boutons.png

Exemple d’une boite de dialogue avec des boutons

../_images/dialogue_sans_bouton.png

Exemple d’une boite de dialogue sans bouton

À la ligne 2, la méthode afficherBoiteDeDialogue commence par déclarer un listener qui implémente l’interface DialogInterface.OnClickListener. Cette interface ne contient qu’une seule méthode qui est appelée lorsque l’utilisateur interagit avec la boite de dialogue. Cette méthode reçoit en paramètre la boite de dialogue concernée ainsi qu’un paramètre which indiquant quel bouton a été cliqué. Dans cet exemple, on se contente d’appeler une autre méthode onClickBoiteDeDialogue qui affiche elle-même une autre boite de dialogue sans bouton.

À la ligne 9, on affiche une boite de dialogue en précisant les différents éléments (titre, message, boutons avec leur listener) pour finalement appeler la méthode show(). Notez que pour créer une instance de AlertDialog.Builder, il faut passer en paramètre un contexte d’exécution Android. Une activité est un contexte d’exécution. Si ces méthodes sont ajoutées au code d’une activité, il suffit de passer this en paramètre du constructeur.

Boite de dialogue avec choix

Un autre type de boite de dialogue assez courant consiste à permettre à l’utilisateur de faire un choix dans une liste. Pour cela, il est possible d’utiliser la méthode setItems pour préciser la liste des choix (ou l’ID de la ressource correspondant à un array). Comme pour les boutons, on associe un listener à la liste des items. Le paramètre which reçu par la méthode onClick correspond à l’index du choix de l’utilisateur.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public void afficherBoiteDeDialogue() {
    DialogInterface.OnClickListener dialogListener = new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            onClickBoiteDeDialogue(dialog, which);
        }
    };

    String[] listeEtats = {"Bien", "Pas super", "Ne sais pas"};

    new AlertDialog.Builder(this)
            .setTitle("Mon autre boite")
            .setItems(listeEtats, dialogListener)
            .show();
}

private void onClickBoiteDeDialogue(DialogInterface dialog, int which) {
    switch (which) {
        case 0:
            new AlertDialog.Builder(this).setMessage("Vous positivez !").show();
            break;
        case 1:
            new AlertDialog.Builder(this).setMessage("Ça ne va pas fort !").show();
            break;
        case 2:
            new AlertDialog.Builder(this).setMessage("Vous avez le droit de ne pas savoir.").show();
            break;
    }
}
../_images/dialogue_avec_items.png

Exemple d’une boite de dialogue avec une liste de choix

Note

Vous pouvez même concevoir une boite de dialogue avec n’importe quel layout en utilisant la méthode setView pour fournir un ID de ressource de layout.

Message à l’utilisateur

Il est possible de faire apparaître brièvement un message à l’utilisateur pour confirmer un choix ou signaler un problème. Pour cela, vous pouvez utiliser soit la classe Toast soit la classe Snackbar (la seconde est recommandée pour un nouvelle application).

Toast

Toast.makeText(this, "Hello world", Toast.LENGTH_SHORT).show();

La méthode statique makeText permet de créer un message que l’on affiche avec la méthode show(). Comme premier paramètre, il faut passer un contexte d’exécution Android. Si cette ligne est ajoutée au code d’une activité, il suffit de passer this en paramètre. Le second paramètre est le texte à afficher et le troisième paramètre est une constante pour indiquer la durée d’affichage du message (courte ou longue).

../_images/toast.png

Affichage d’un toast

Snackbar

View content = findViewById(android.R.id.content);
Snackbar.make(content, "Hello world", Snackbar.LENGTH_LONG).show();

Le Snackbar fonctionne de manière très similaire au Toast. La méthode statique make permet de créer le composant pour l’afficher avec la méthode show(). Le premier paramètre n’est pas un contexte d’exécution mais une vue de l’activité. Dans l’exemple ci-dessus, on utilise le contenu de l’activité, c’est-à-dire le layout pour créer le Snackbar. Le troisième paramètre est une constante pour indiquer la durée d’affichage du message (courte ou longue).

../_images/snackbar.png

Affichage d’un Snackbar

Note

Pour des cas plus complexes, notamment si l’utilisateur doit pouvoir réagir au message affiché, vous pouvez utiliser des notifications :