Licence CC BY-NC-SA

Des widgets plus avancés et des boîtes de dialogue

Ce contenu est obsolète. Il peut contenir des informations intéressantes mais soyez prudent avec celles-ci.

On a vu dans un chapitre précédent les vues les plus courantes et les plus importantes. Mais le problème est que vous ne pourrez pas tout faire avec les éléments précédemment présentés. Je pense en particulier à une structure de données fondamentale pour représenter un ensemble de données… je parle bien entendu des listes.

On verra aussi les boîtes de dialogue, qui sont utilisées dans énormément d'applications. Enfin, je vous présenterai de manière un peu moins détaillée d'autres éléments, moins répandus mais qui pourraient éventuellement vous intéresser.

Les listes et les adaptateurs

N'oubliez pas que le Java est un langage orienté objet et que par conséquent il pourrait vous arriver d'avoir à afficher une liste d'un type d'objet particulier, des livres par exemple. Il existe plusieurs paramètres à prendre en compte dans ce cas-là. Tout d'abord, quelle est l'information à afficher pour chaque livre ? Le titre ? L'auteur ? Le genre littéraire ? Et que faire quand on clique sur un élément de la liste ? Et l'esthétique dans tout ça, c'est-à-dire comment sont représentés les livres ? Affiche-t-on leur couverture avec leur titre ? Ce sont autant d'éléments à prendre en compte quand on veut afficher une liste.

La gestion des listes se divise en deux parties distinctes. Tout d'abord les Adapter (que j’appellerai adaptateurs), qui sont les objets qui gèrent les données, mais pas leur affichage ou leur comportement en cas d’interaction avec l'utilisateur. On peut considérer un adaptateur comme un intermédiaire entre les données et la vue qui représente ces données. De l'autre côté, on trouve les AdapterView, qui, eux, vont gérer l'affichage et l'interaction avec l'utilisateur, mais sur lesquels on ne peut pas effectuer d'opération de modification des données.

Le comportement typique pour afficher une liste depuis un ensemble de données est celui-ci : on donne à l'adaptateur une liste d'éléments à traiter et la manière dont ils doivent l'être, puis on passe cet adaptateur à un AdapterView, comme schématisé à la figure suivante. Dans ce dernier, l'adaptateur va créer un widget pour chaque élément en fonction des informations fournies en amont.

Schéma du fonctionnement des « Adapter » et « AdapterView »

L'ovale rouge représente la liste des éléments. On la donne à l'adaptateur, qui se charge de créer une vue pour chaque élément, avec le layout à respecter. Puis, les vues sont fournies à un AdapterView (toutes au même instant, bien entendu), où elles seront affichées dans l'ordre fourni et avec le layout correspondant. L'AdapterView possède lui aussi un layout afin de le personnaliser.

Savez-vous ce qu'est une fonction callback (vous trouverez peut-être aussi l'expression « fonction de rappel ») ? Pour simplifier les choses, c'est une fonction qu'on n'appelle pas directement, c'est une autre fonction qui y fera appel. On a déjà vu une fonction de callback dans la section qui parlait de l'évènementiel chez les widgets : quand vous cliquez sur un bouton, la fonction onTouch est appelée, alors qu'on n'y fait pas appel nous-mêmes. Dans cette prochaine section figure aussi une fonction de callback, je tiens juste à être certain que vous connaissiez bien le terme.

Les adaptateurs

Adapter n'est en fait qu'une interface qui définit les comportements généraux des adaptateurs. Cependant, si vous voulez un jour construire un adaptateur, faites le dériver de BaseAdapter.

Si on veut construire un widget simple, on retiendra trois principaux adaptateurs :

  1. ArrayAdapter, qui permet d'afficher les informations simples ;
  2. SimpleAdapter est quant à lui utile dès qu'il s'agit d'écrire plusieurs informations pour chaque élément (s'il y a deux textes dans l'élément par exemple) ;
  3. CursorAdapter, pour adapter le contenu qui provient d'une base de données. On y reviendra dès qu'on abordera l'accès à une base de données.

Les listes simples : ArrayAdapter

La classe ArrayAdapter se trouve dans le package android.widget.ArrayAdapter.

On va considérer le constructeur suivant : public ArrayAdapter (Context contexte, int id, T[] objects) ou encore public ArrayAdapter (Context contexte, int id, List<T> objects). Pour vous aider, voici la signification de chaque paramètre :

  • Vous savez déjà ce qu'est le contexte, ce sont des informations sur l'activité, on passe donc l'activité.
  • Quant à id, il s'agira d'une référence à un layout. C'est donc elle qui déterminera la mise en page de l'élément. Vous pouvez bien entendu créer une ressource de layout par vous-mêmes, mais Android met à disposition certains layouts, qui dépendent beaucoup de la liste dans laquelle vont se trouver les widgets.
  • objects est la liste ou le tableau des éléments à afficher.

T[] signifie qu'il peut s'agir d'un tableau de n'importe quel type d'objet ; de manière similaire List<T> signifie que les objets de la liste peuvent être de n'importe quel type. Attention, j'ai dit « les objets », donc pas de primitives (comme int ou float par exemple) auquel cas vous devrez passer par des objets équivalents (comme Integer ou Float).

Des listes plus complexes : SimpleAdapter

On peut utiliser la classe SimpleAdapter à partir du package android.widget.SimpleAdapter.

Le SimpleAdapter est utile pour afficher simplement plusieurs informations par élément. En réalité, pour chaque information de l'élément on aura une vue dédiée qui affichera l'information voulue. Ainsi, on peut avoir du texte, une image… ou même une autre liste si l'envie vous en prend. Mieux qu'une longue explication, voici l'exemple d'un répertoire téléphonique :

 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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import android.app.Activity;
import android.os.Bundle;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.SimpleAdapter;

public class ListesActivity extends Activity {
  ListView vue;

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

    //On récupère une ListView de notre layout en XML, c'est la vue qui représente la liste
    vue = (ListView) findViewById(R.id.listView);

    /*
     * On entrepose nos données dans un tableau qui contient deux colonnes :
     *  - la première contiendra le nom de l'utilisateur
     *  - la seconde contiendra le numéro de téléphone de l'utilisateur
    */
    String[][] repertoire = new String[][]{
      {"Bill Gates", "06 06 06 06 06"},
      {"Niels Bohr", "05 05 05 05 05"},
      {"Alexandre III de Macédoine", "04 04 04 04 04"}};

    /*
     * On doit donner à notre adaptateur une liste du type « List<Map<String, ?> » :
     *  - la clé doit forcément être une chaîne de caractères
     *  - en revanche, la valeur peut être n'importe quoi, un objet ou un entier par exemple,
     *  si c'est un objet, on affichera son contenu avec la méthode « toString() »
     *
     * Dans notre cas, la valeur sera une chaîne de caractères, puisque le nom et le numéro de téléphone
     * sont entreposés dans des chaînes de caractères
    */
    List<HashMap<String, String>> liste = new ArrayList<HashMap<String, String>>();

    HashMap<String, String> element;
    //Pour chaque personne dans notre répertoire…
    for(int i = 0 ; i < repertoire.length ; i++) {
      //… on crée un élément pour la liste…
      element = new HashMap<String, String>();
      /*
       * … on déclare que la clé est « text1 » (j'ai choisi ce mot au hasard, sans sens technique particulier)  
       * pour le nom de la personne (première dimension du tableau de valeurs)…
      */
      element.put("text1", repertoire[i][0]);
      /*
       * … on déclare que la clé est « text2 »
       * pour le numéro de cette personne (seconde dimension du tableau de valeurs)
      */
      element.put("text2", repertoire[i][1]);
      liste.add(element);
    }

    ListAdapter adapter = new SimpleAdapter(this,  
      //Valeurs à insérer
      liste, 
      /*
       * Layout de chaque élément (là, il s'agit d'un layout par défaut
       * pour avoir deux textes l'un au-dessus de l'autre, c'est pourquoi on 
       * n'affiche que le nom et le numéro d'une personne)
      */
      android.R.layout.simple_list_item_2,
      /*
       * Les clés des informations à afficher pour chaque élément :
       *  - la valeur associée à la clé « text1 » sera la première information
       *  - la valeur associée à la clé « text2 » sera la seconde information
      */
      new String[] {"text1", "text2"}, 
      /*
       * Enfin, les layouts à appliquer à chaque widget de notre élément
       * (ce sont des layouts fournis par défaut) :
       *  - la première information appliquera le layout « android.R.id.text1 »
       *  - la seconde information appliquera le layout « android.R.id.text2 »
      */
      new int[] {android.R.id.text1, android.R.id.text2 });
    //Pour finir, on donne à la ListView le SimpleAdapter
    vue.setAdapter(adapter);
  }
}

Ce qui donne la figure suivante.

Le résultat en image

On a utilisé le constructeur public SimpleAdapter(Context context, List<? extends Map<String, ?>> data, int ressource, String[] from, int[] to).

Quelques méthodes communes à tous les adaptateurs

Tout d'abord, pour ajouter un objet à un adaptateur, on peut utiliser la méthode void add (T object) ou l'insérer à une position particulière avec void insert (T object, int position). Il est possible de récupérer un objet dont on connaît la position avec la méthode T getItem (int position), ou bien récupérer la position d'un objet précis avec la méthode int getPosition (T object).

On peut supprimer un objet avec la méthode void remove (T object) ou vider complètement l'adaptateur avec void clear().

Par défaut, un ArrayAdapter affichera pour chaque objet de la liste le résultat de la méthode String toString() associée et l'insérera dans une TextView.

Voici un exemple de la manière d'utiliser ces codes :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// On crée un adaptateur qui fonctionne avec des chaînes de caractères
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);
// On rajoute la chaîne de caractères "Pommes"
adapter.add("Pommes");
// On récupère la position de la chaîne dans l'adaptateur. Comme il n'y a pas d'autres chaînes dans l'adaptateur, position vaudra 0
int position = adapter.getPosition("Pommes");
// On affiche la valeur et la position de la chaîne de caractères
Toast.makeText(this, "Les " + adapter.getItem(position) + " se trouvent à la position " + position + ".", Toast.LENGTH_LONG).show();
// Puis on la supprime, n'en n'ayant plus besoin
adapter.remove("Pommes");

Les vues responsables de l'affichage des listes : les AdapterView

On trouve la classe AdapterView dans le package android.widget.AdapterView.

Alors que l'adaptateur se chargera de construire les sous-éléments, c'est l'AdapterView qui liera ces sous-éléments et qui fera en sorte de les afficher en une liste. De plus, c'est l'AdapterView qui gérera les interactions avec les utilisateurs : l'adaptateur s'occupe des éléments en tant que données, alors que l'AdapterView s'occupe de les afficher et veille aux interactions avec un utilisateur.

On observe trois principaux AdapterView :

  1. ListView, pour simplement afficher des éléments les uns après les autres ;
  2. GridView, afin d'organiser les éléments sous la forme d'une grille ;
  3. Spinner, qui est une liste défilante.

Pour associer un adaptateur à une AdapterView, on utilise la méthode void setAdapter (Adapter adapter), qui se chargera de peupler la vue, comme vous le verrez dans quelques instants.

Les listes standards : ListView

On les trouve dans le package android.widget.ListView. Elles affichent les éléments les uns après les autres, comme à la figure suivante. Le layout de base est android.R.layout.simple_list_item_1.

Une liste simple

L'exemple précédent est obtenu à l'aide de ce code :

 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
import java.util.ArrayList;

import android.app.Activity;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;

public class TutoListesActivity extends Activity {
  ListView liste = null;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    liste = (ListView) findViewById(R.id.listView);
    List<String> exemple = new ArrayList<String>();
    exemple.add("Item 1");
    exemple.add("Item 2");
    exemple.add("Item 3");

    ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, exemple);
    liste.setAdapter(adapter);
  }
}

Au niveau évènementiel, il est toujours possible de gérer plusieurs types de clic, comme par exemple :

  • void setOnItemClickListener (AdapterView.OnItemClickListener listener) pour un clic simple sur un élément de la liste. La fonction de callback associée est void onItemClick (AdapterView<?> adapter, View view, int position, long id), avec adapter l'AdapterView qui contient la vue sur laquelle le clic a été effectué, view qui est la vue en elle-même, position qui est la position de la vue dans la liste et enfin id qui est l'identifiant de la vue.
  • void setOnItemLongClickListener (AdapterView.OnItemLongClickListener listener) pour un clic prolongé sur un élément de la liste. La fonction de callback associée est boolean onItemLongClick (AdapterView<?> adapter, View view, int position, long id).

Ce qui donne :

1
2
3
4
5
6
7
8
9
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
  @Override
  public void onItemClick(AdapterView<?> adapterView, 
    View view, 
    int position,
    long id) {
      // Que faire quand on clique sur un élément de la liste ?
  }
});

En revanche il peut arriver qu'on ait besoin de sélectionner un ou plusieurs éléments. Tout d'abord, il faut indiquer à la liste quel mode de sélection elle accepte. On peut le préciser en XML à l'aide de l'attribut android:choiceMode qui peut prendre les valeurs singleChoice (sélectionner un seul élément) ou multipleChoice (sélectionner plusieurs éléments). En Java, il suffit d'utiliser la méthode void setChoiceMode(int mode) avec mode qui peut valoir ListView.CHOICE_MODE_SINGLE (sélectionner un seul élément) ou ListView.CHOICE_MODE_MULTIPLE (sélectionner plusieurs éléments).

À nouveau, il nous faut choisir un layout adapté. Pour les sélections uniques, on peut utiliser android.R.layout.simple_list_item_single_choice, ce qui donnera la figure suivante.

Une liste de sélection unique

Pour les sélections multiples, on peut utiliser android.R.layout.simple_list_item_multiple_choice, ce qui donnera la figure suivante.

Une liste de sélection multiple

Enfin, pour récupérer le rang de l'élément sélectionné dans le cas d'une sélection unique, on peut utiliser la méthode int getCheckedItemPosition() et dans le cas d'une sélection multiple, SparseBooleanArray getCheckedItemPositions().

Un SparseBooleanArray est un tableau associatif dans lequel on associe un entier à un booléen, c'est-à-dire que c'est un équivalent à la structure Java standard Hashmap<Integer, Boolean>, mais en plus optimisé. Vous vous rappelez ce que sont les hashmaps, les tableaux associatifs ? Ils permettent d'associer une clé (dans notre cas un Integer) à une valeur (dans ce cas-ci un Boolean) afin de retrouver facilement cette valeur. La clé n'est pas forcément un entier, on peut par exemple associer un nom à une liste de prénoms avec Hashmap<String, ArrayList<String>> afin de retrouver les prénoms des gens qui portent un nom en commun.

En ce qui concerne les SparseBooleanArray, il est possible de vérifier la valeur associée à une clé entière avec la méthode boolean get(int key). Par exemple dans notre cas de la sélection multiple, on peut savoir si le troisième élément de la liste est sélectionné en faisant liste.getCheckedItemPositions().get(3), et, si le résultat vaut true, alors l'élément est bien sélectionné dans la liste.

Application

Voici un petit exemple qui vous montre comment utiliser correctement tous ces attributs. Il s'agit d'une application qui réalise un sondage. L'utilisateur doit indiquer son sexe et les langages de programmation qu'il maîtrise. Notez que, comme l'application est destinée aux Zéros qui suivent ce tuto, par défaut on sélectionne le sexe masculin et on déclare que l'utilisateur connaît le Java !

Dès que l'utilisateur a fini d'entrer ses informations, il peut appuyer sur un bouton pour confirmer sa sélection. Ce faisant, on empêche l'utilisateur de changer ses informations en enlevant les boutons de sélection et en l'empêchant d'appuyer à nouveau sur le bouton, comme le montre la figure suivante.

À gauche, au démarrage de l'application ; à droite, après avoir appuyé sur le bouton « Envoyer »

Solution

Le layout :

 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
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:orientation="vertical" >

  <TextView
    android:id="@+id/textSexe"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="Quel est votre sexe :" />

    <!-- On choisit le mode de sélection avec android:choiceMode -->
    <ListView
      android:id="@+id/listSexe"
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"
      android:choiceMode="singleChoice" >
    </ListView>

    <TextView
      android:id="@+id/textProg"
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"
      android:text="Quel(s) langage(s) maîtrisez-vous :" />

    <ListView
      android:id="@+id/listProg"
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"
      android:choiceMode="multipleChoice" >
    </ListView>

    <Button
      android:id="@+id/send"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_gravity="center"
      android:text="Envoyer" />

</LinearLayout>

Et le code :

 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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package sdz.exemple.selectionMultiple;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.Toast;

public class SelectionMultipleActivity extends Activity {
  /** Affichage de la liste des sexes **/
  private ListView mListSexe = null;
  /** Affichage de la liste des langages connus **/
  private ListView mListProg = null;
  /** Bouton pour envoyer le sondage **/
  private Button mSend = null;

  /** Contient les deux sexes **/
  private String[] mSexes = {"Masculin", "Feminin"};
  /** Contient différents langages de programmation **/
  private String[] mLangages = null;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    //On récupère les trois vues définies dans notre layout
    mListSexe = (ListView) findViewById(R.id.listSexe);
    mListProg = (ListView) findViewById(R.id.listProg);
    mSend = (Button) findViewById(R.id.send);

    //Une autre manière de créer un tableau de chaînes de caractères
    mLangages = new String[]{"C", "Java", "COBOL", "Perl"};

    //On ajoute un adaptateur qui affiche des boutons radio (c'est l'affichage à considérer quand on ne peut
    //sélectionner qu'un élément d'une liste)
    mListSexe.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_single_choice, mSexes));
    //On déclare qu'on sélectionne de base le premier élément (Masculin)
    mListSexe.setItemChecked(0, true);

    //On ajoute un adaptateur qui affiche des cases à cocher (c'est l'affichage à considérer quand on peut sélectionner
    //autant d'éléments qu'on veut dans une liste)
    mListProg.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_multiple_choice, mLangages));
    //On déclare qu'on sélectionne de base le second élément (Féminin)
    mListProg.setItemChecked(1, true);

    //Que se passe-t-il dès qu'on clique sur le bouton ?
    mSend.setOnClickListener(new View.OnClickListener() {

      @Override
      public void onClick(View v) {
        Toast.makeText(SelectionMultipleActivity.this, "Merci ! Les données ont été envoyées !", Toast.LENGTH_LONG).show();

        //On déclare qu'on ne peut plus sélectionner d'élément
        mListSexe.setChoiceMode(ListView.CHOICE_MODE_NONE);
        //On affiche un layout qui ne permet pas de sélection
        mListSexe.setAdapter(new ArrayAdapter<String>(SelectionMultipleActivity.this, android.R.layout.simple_list_item_1, 
                      mSexes));

        //On déclare qu'on ne peut plus sélectionner d'élément
        mListProg.setChoiceMode(ListView.CHOICE_MODE_NONE);
        //On affiche un layout qui ne permet pas de sélection
        mListProg.setAdapter(new ArrayAdapter<String>(SelectionMultipleActivity.this, android.R.layout.simple_list_item_1, mLangages));

        //On désactive le bouton
        mSend.setEnabled(false);
      }
    });
  }
}

Dans un tableau : GridView

On peut utiliser la classe GridView à partir du package android.widget.GridView.

Ce type de liste fonctionne presque comme le précédent ; cependant, il met les éléments dans une grille dont il détermine automatiquement le nombre d'éléments par ligne, comme le montre la figure suivante.

Les éléments sont placés sur une grille

Il est cependant possible d'imposer ce nombre d'éléments par ligne à l'aide de android:numColumns en XML et void setNumColumns (int column) en Java.

Les listes défilantes : Spinner

La classe Spinner se trouve dans le package android.widget.Spinner.

Encore une fois, cet AdapterView ne réinvente pas l'eau chaude. Cependant, on utilisera deux vues. Une pour l'élément sélectionné qui est affiché, et une pour la liste d'éléments sélectionnables. La figure suivante montre ce qui arrive si on ne définit pas de mise en page pour la liste d'éléments.

Aucune mise en page pour la liste d'éléments n'a été définie

La première vue affiche uniquement « Element 2 », l'élément actuellement sélectionné. La seconde vue affiche la liste de tous les éléments qu'il est possible de sélectionner.

Heureusement, on peut personnaliser l'affichage de la seconde vue, celle qui affiche une liste, avec la fonction void setDropDownViewResource (int id). D'ailleurs, il existe déjà un layout par défaut pour cela. Voici un exemple :

 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
import java.util.ArrayList;

import android.app.Activity;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.Spinner;

public class TutoListesActivity extends Activity {
  private Spinner liste = null;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    liste = (Spinner) findViewById(R.id.spinner1);
    List<String> exemple = new ArrayList<String>();
    exemple.add("Element 1");
    exemple.add("Element 2");
    exemple.add("Element 3");
    exemple.add("Element 4");
    exemple.add("Element 5");
    exemple.add("Element 6");

    ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, exemple);
    //Le layout par défaut est android.R.layout.simple_spinner_dropdown_item
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    liste.setAdapter(adapter);
  }
}

Ce code donnera la figure suivante.

Un style a été défini

Plus complexe : les adaptateurs personnalisés

Imaginez que vous vouliez faire un répertoire téléphonique. Il consisterait donc en une liste, et chaque élément de la liste aurait une photo de l'utilisateur, son nom et prénom ainsi que son numéro de téléphone. Ainsi, on peut déduire que les items de notre liste auront un layout qui utilisera deux TextView et une ImageView. Je vous vois vous trémousser sur votre chaise en vous disant qu'on va utiliser un SimpleAdapter pour faire l'intermédiaire entre les données (complexes) et les vues, mais comme nous sommes des Zéros d'exception, nous allons plutôt créer notre propre adaptateur. :D

Je vous ai dit qu'un adaptateur implémentait l'interface Adapter, ce qui est vrai ; cependant, quand on crée notre propre adaptateur, il est plus sage de partir de BaseAdapter afin de nous simplifier l’existence.

Un adaptateur est le conteneur des informations d'une liste, au contraire de l'AdapterView, qui affiche les informations et régit ses interactions avec l'utilisateur. C'est donc dans l'adaptateur que se trouve la structure de données qui détermine comment sont rangées les données. Ainsi, dans notre adaptateur se trouvera une liste de contacts sous forme de ArrayList.

Dès qu'une classe hérite de BaseAdapter, il faut implémenter obligatoirement trois méthodes :

 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
import android.widget.BaseAdapter;

public class RepertoireAdapter extends BaseAdapter {
  /**
   * Récupérer un item de la liste en fonction de sa position
   * @param position - Position de l'item à récupérer
   * @return l'item récupéré
  */
  public Object getItem(int position) {
    // …
  }

  /**
   * Récupérer l'identifiant d'un item de la liste en fonction de sa position (plutôt utilisé dans le cas d'une
   * base de données, mais on va l'utiliser aussi)
   * @param position - Position de l'item à récupérer
   * @return l'identifiant de l'item
  */
  public long getItemId(int position) {
    // …
  }

  /**
   * Explication juste en dessous.
  */
  public View getView(int position, View convertView, ViewGroup parent) {
    //…
  }
}

La méthode View getView(int position, View convertView, ViewGroup parent) est la plus délicate à utiliser. En fait, cette méthode est appelée à chaque fois qu'un item est affiché à l'écran, comme à la figure suivante.

Dans cet exemple, la méthode « getView » a été appelée sur les sept lignes visibles, mais pas sur les autres lignes de la liste

En ce qui concerne les trois paramètres :

  • position est la position de l'item dans la liste (et donc dans l'adaptateur).
  • parent est le layout auquel rattacher la vue.
  • Et convertView vaut null… ou pas, mais une meilleure explication s'impose.

convertView vaut null uniquement les premières fois qu'on affiche la liste. Dans notre exemple, convertView vaudra null aux sept premiers appels de getView (donc les sept premières créations de vues), c'est-à-dire pour tous les éléments affichés à l'écran au démarrage. Toutefois, dès qu'on fait défiler la liste jusqu'à afficher un élément qui n'était pas à l'écran à l'instant d'avant, convertView ne vaut plus null, mais plutôt la valeur de la vue qui vient de disparaître de l'écran. Ce qui se passe en interne, c'est que la vue qu'on n'affiche plus est recyclée, puisqu'on a plus besoin de la voir.

Il nous faut alors un moyen d'inflater une vue, mais sans l'associer à notre activité. Il existe au moins trois méthodes pour cela :

  • LayoutInflater getSystemService (LAYOUT_INFLATER_SERVICE) sur une activité.
  • LayoutInflater getLayoutInflater () sur une activité.
  • LayoutInflater LayoutInflater.from(Context contexte), sachant que Activity dérive de Context.

Puis vous pouvez inflater une vue à partir de ce LayoutInflater à l'aide de la méthode View inflate (int id, ViewGroup root), avec root la racine à laquelle attacher la hiérarchie désérialisée. Si vous indiquez null, c'est la racine actuelle de la hiérarchie qui sera renvoyée, sinon la hiérarchie s'attachera à la racine indiquée.

Pourquoi ce mécanisme me demanderez-vous ? C'est encore une histoire d'optimisation. En effet, si vous avez un layout personnalisé pour votre liste, à chaque appel de getView vous allez peupler votre rangée avec le layout à inflater depuis son fichier XML :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
LayoutInflater mInflater;
String[] mListe;

public View getView(int position, View convertView, ViewGroup parent) {
  TextView vue = (TextView) mInflater.inflate(R.layout.ligne, null);

  vue.setText(mListe[position]);

  return vue;
}

Cependant, je vous l'ai déjà dit plein de fois, la désérialisation est un processus lent ! C'est pourquoi il faut utiliser convertView pour vérifier si cette vue n'est pas déjà peuplée et ainsi ne pas désérialiser à chaque construction d'une vue :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
LayoutInflater mInflater;
String[] mListe;

public View getView(int position, View convertView, ViewGroup parent) {
  TextView vue = null;
  // Si la vue est recyclée, elle contient déjà le bon layout
  if(convertView != null)
    // On n'a plus qu'à la récupérer
    vue  = (TextView) convertView;
  else
    // Sinon, il faut en effet utiliser le LayoutInflater
    vue = mInflater.inflate(R.layout.ligne, null);

  vue.setText(mListe[position]);

  return vue;
}

En faisant cela, votre liste devient au moins deux fois plus fluide.

Quand vous utilisez votre propre adaptateur et que vous souhaitez pouvoir sélectionner des éléments dans votre liste, je vous conseille d'ignorer les solutions de sélection présentées dans le chapitre sur les listes (vous savez, void setChoiceMode (int mode)) et de développer votre propre méthode, vous aurez moins de soucis. Ici, j'ai ajouté un booléen dans chaque contact pour savoir s'il est sélectionné ou pas.

Amélioration : le pattern ViewHolder

Dans notre adaptateur, on remarque qu'on a optimisé le layout de chaque contact en ne l'inflatant que quand c'est nécessaire… mais on inflate quand même les trois vues qui ont le même layout ! C'est moins grave, parce que les vues inflatées par findViewById le sont plus rapidement, mais quand même. Il existe une alternative pour améliorer encore le rendu. Il faut utiliser une classe interne statique, qu'on appelle ViewHolder d'habitude. Cette classe devra contenir toutes les vues de notre layout :

1
2
3
4
5
static class ViewHolder {
  public TextView mNom;
  public TextView mNumero;
  public ImageView mPhoto;
}

Ensuite, la première fois qu'on inflate le layout, on récupère chaque vue pour les mettre dans le ViewHolder, puis on insère le ViewHolder dans le layout à l'aide de la méthode void setTag (Object tag), qui peut être utilisée sur n'importe quel View. Cette technique permet d'insérer dans notre vue des objets afin de les récupérer plus tard avec la méthode Object getTag (). On récupérera le ViewHolder si le convertView n'est pas null, comme ça on n'aura inflaté les vues qu'une fois chacune.

 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 View getView(int r, View convertView, ViewGroup parent) {
  ViewHolder holder = null;
  // Si la vue n'est pas recyclée
  if(convertView == null) {
    // On récupère le layout
    convertView  = mInflater.inflate(R.layout.item, null);

    holder = new ViewHolder();
    // On place les widgets de notre layout dans le holder
    holder.mNom = (TextView) convertView.findViewById(R.id.nom);
    holder.mNumero = (TextView) convertView.findViewById(R.id.numero);
    holder.mPhoto = (ImageView) convertView.findViewById(R.id.photo);

    // puis on insère le holder en tant que tag dans le layout
    convertView.setTag(holder);
  } else {
    // Si on recycle la vue, on récupère son holder en tag
    holder = (ViewHolder)convertView.getTag();
  }

  // Dans tous les cas, on récupère le contact téléphonique concerné
  Contact c = (Contact)getItem(r);
  // Si cet élément existe vraiment…
  if(c != null) {
    // On place dans le holder les informations sur le contact
    holder.mNom.setText(c.getNom());
    holder.mNumero.setText(c.getNumero());
  }
  return convertView;
}

Les boîtes de dialogue

Une boîte de dialogue est une petite fenêtre qui passe au premier plan pour informer l'utilisateur ou lui demander ce qu'il souhaite faire. Par exemple, si je compte quitter mon navigateur internet alors que j'ai plusieurs onglets ouverts, une boîte de dialogue s'ouvrira pour me demander confirmation, comme le montre la figure suivante.

Firefox demande confirmation avant de se fermer si plusieurs onglets sont ouverts

On les utilise souvent pour annoncer des erreurs, donner une information ou indiquer un état d'avancement d'une tâche à l'aide d'une barre de progression par exemple.

Généralités

Les boîtes de dialogue d'Android sont dites modales, c'est-à-dire qu'elles bloquent l’interaction avec l'activité sous-jacente. Dès qu'elles apparaissent, elles passent au premier plan en surbrillance devant notre activité et, comme on l'a vu dans le chapitre introduisant les activités, une activité qu'on ne voit plus que partiellement est suspendue.

Les boîtes de dialogue héritent de la classe Dialog et on les trouve dans le package android.app.Dialog.

On verra ici les boîtes de dialogue les plus communes, celles que vous utiliserez certainement un jour ou l'autre. Il en existe d'autres, et il vous est même possible de faire votre propre boîte de dialogue. Mais chaque chose en son temps. ;)

Dans un souci d'optimisation, les développeurs d'Android ont envisagé un système très astucieux. En effet, on fera en sorte de ne pas avoir à créer de nouvelle boîte de dialogue à chaque occasion, mais plutôt de recycler les anciennes.

La classe Activity possède la méthode de callback Dialog onCreateDialog (int id), qui sera appelée quand on instancie pour la première fois une boîte de dialogue. Elle prend en argument un entier qui sera l'identifiant de la boîte. Mais un exemple vaut mieux qu'un long discours :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
private final static int IDENTIFIANT_BOITE_UN  = 0;
private final static int IDENTIFIANT_BOITE_DEUX  = 1;

@Override
public Dialog onCreateDialog(int identifiant) {
  Dialog box = null;
  //En fonction de l'identifiant de la boîte qu'on veut créer
  switch(identifiant) {
    case IDENTIFIANT_BOITE_UN :
      // On construit la première boîte de dialogue, que l'on insère dans « box »
      break;

    case IDENTIFIANT_BOITE_DEUX :
      // On construit la seconde boîte de dialogue, que l'on insère dans « box »
      break;
  }
  return box;
}

Bien sûr, comme il s'agit d'une méthode de callback, on ne fait pas appel directement à onCreateDialog. Pour appeler une boîte de dialogue, on utilise la méthode void showDialog (int id), qui se chargera d'appeler onCreateDialog(id) en lui passant le même identifiant.

Quand on utilise la méthode showDialog pour un certain identifiant la première fois, elle se charge d'appeler onCreateDialog comme nous l'avons vu, mais aussi la méthode void onPrepareDialog (int id, Dialog dialog), avec le paramètre id qui est encore une fois l'identifiant de la boîte de dialogue, alors que le paramètre dialog est tout simplement la boîte de dialogue en elle-même. La seconde fois qu'on utilise showDialog avec un identifiant, onCreateDialog ne sera pas appelée (puisqu'on ne crée pas une boîte de dialogue deux fois), mais onPrepareDialog sera en revanche appelée.

Autrement dit, onPrepareDialog est appelée à chaque fois qu'on veut montrer la boîte de dialogue. Cette méthode est donc à redéfinir uniquement si on veut afficher un contenu différent pour la boîte de dialogue à chaque appel, mais, si le contenu est toujours le même à chaque appel, il suffit de définir le contenu dans onCreateDialog, qui n'est appelée qu'à la création. Et cela tombe bien, c'est le sujet du prochain exercice !

Application

Énoncé

L'activité consistera en un gros bouton. Cliquer sur ce bouton lancera une boîte de dialogue dont le texte indiquera le nombre de fois que la boîte a été lancée. Cependant une autre boîte de dialogue devient jalouse au bout de 5 appels et souhaite être sollicitée plus souvent, comme à la figure suivante.

Après le cinquième clic

Instructions

Pour créer une boîte de dialogue, on va passer par le constructeur Dialog (Context context). On pourra ensuite lui donner un texte à afficher à l'aide de la méthode void setTitle (CharSequence text).

Ma solution

 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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import android.app.Activity;
import android.app.Dialog;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;

public class StringExampleActivity extends Activity {
  private Button bouton;
  //Variable globale, au-dessus de cette valeur c'est l'autre boîte de dialogue qui s'exprime
  private final static int ENERVEMENT = 4;
  private int compteur = 0;

  private final static int ID_NORMAL_DIALOG = 0;
  private final static int ID_ENERVEE_DIALOG = 1;

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

    bouton = (Button) findViewById(R.id.bouton);
    bouton.setOnClickListener(boutonClik);
  }

  private OnClickListener boutonClik = new OnClickListener() {
    @Override
    public void onClick(View v) {
      // Tant qu'on n'a pas invoqué la première boîte de dialogue 5 fois
      if(compteur < ENERVEMENT) {
        //on appelle la boîte normale
        compteur ++;
        showDialog(ID_NORMAL_DIALOG);
      } else
        showDialog(ID_ENERVEE_DIALOG);
    }
  };

  /*
   * Appelée qu'à la première création d'une boîte de dialogue
   * Les fois suivantes, on se contente de récupérer la boîte de dialogue déjà créée…
   * Sauf si la méthode « onPrepareDialog » modifie la boîte de dialogue.
  */
  @Override
  public Dialog onCreateDialog (int id) {
    Dialog box = null;
    switch(id) {
    // Quand on appelle avec l'identifiant de la boîte normale
    case ID_NORMAL_DIALOG:
      box = new Dialog(this);
      box.setTitle("Je viens tout juste de naître.");
      break;

    // Quand on appelle avec l'identifiant de la boîte qui s'énerve
    case ID_ENERVEE_DIALOG:
      box = new Dialog(this);
      box.setTitle("ET MOI ALORS ???");
    }
    return box;
  }

  @Override
  public void onPrepareDialog (int id, Dialog box) {
    if(id == ID_NORMAL_DIALOG && compteur > 1)
      box.setTitle("On est au " + compteur + "ème lancement !");
     //On ne s'intéresse pas au cas où l'identifiant vaut 1, puisque cette boîte affiche le même texte à chaque lancement
  }
}

On va maintenant discuter des types de boîte de dialogue les plus courantes.

La boîte de dialogue de base

On sait déjà qu'une boîte de dialogue provient de la classe Dialog. Cependant, vous avez bien vu qu'on ne pouvait mettre qu'un titre de manière programmatique. Alors, de la même façon qu'on fait une interface graphique pour une activité, on peut créer un fichier XML pour définir la mise en page de notre boîte de dialogue.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent">
  <LinearLayout 
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content"
    android:orientation="horizontal">
    <ImageView 
      android:layout_width="wrap_content"
      android:layout_height="wrap_content" 
      android:src="@drawable/ic_launcher"/>
    <TextView 
      android:layout_width="fill_parent"
      android:layout_height="fill_parent"
      android:text="Je suis une jolie boîte de dialogue !"/>
  </LinearLayout>
</LinearLayout>

On peut associer ce fichier XML à une boîte de dialogue comme on le fait pour une activité :

1
2
3
Dialog box = new Dialog(this);
box.setContentView(R.layout.dialog);
box.setTitle("Belle ! On dirait un mot inventé pour moiiii !");

Sur le résultat, visible à la figure suivante, on voit bien à gauche l'icône de notre application et à droite le texte qu'on avait inséré. On voit aussi une des contraintes des boîtes de dialogue : le titre ne doit pas dépasser une certaine taille limite.

Résultat en image

Cependant il est assez rare d'utiliser ce type de boîte de dialogue. Il y a des classes bien plus pratiques.

AlertDialog

On les utilise à partir du package android.app.AlertDialog. Il s'agit de la boîte de dialogue la plus polyvalente. Typiquement, elle peut afficher un titre, un texte et/ou une liste d'éléments.

La force d'une AlertDialog est qu'elle peut contenir jusqu'à trois boutons pour demander à l'utilisateur ce qu'il souhaite faire. Bien entendu, elle peut aussi n'en contenir aucun.

Pour construire une AlertDialog, on peut passer par le constructeur de la classe AlertDialog bien entendu, mais on préférera utiliser la classe AlertDialog.Builder, qui permet de simplifier énormément la construction. Ce constructeur prend en argument un Context.

Un objet de type AlertDialog.Builder connaît les méthodes suivantes :

  • AlertDialog.Builder setCancelable (boolean cancelable) : si le paramètre cancelable vaut true, alors on pourra sortir de la boîte grâce au bouton retour de notre appareil.
  • AlertDialog.Builder setIcon (int ressource) ou AlertDialog.Builder setIcon (Drawable icon): le paramètre icon doit référencer une ressource de type drawable ou directement un objet de type Drawable. Permet d'ajouter une icône à la boîte de dialogue.
  • AlertDialog.Builder setMessage (int ressource) ou AlertDialog.Builder setMessage (String message) : le paramètre message doit être une ressource de type String ou une String.
  • AlertDialog.Builder setTitle (int ressource) ou AlertDialog.Builder setTitle (String title) : le paramètre title doit être une ressource de type String ou une String.
  • AlertDialog.Builder setView (View view) ou AlertDialog.Builder setView (int ressource): le paramètre view doit être une vue. Il s'agit de l'équivalent de setContentView pour un objet de type Context. Ne perdez pas de vue qu'il ne s'agit que d'une boîte de dialogue, elle est censée être de dimension réduite : il ne faut donc pas ajouter trop d'éléments à afficher.

On peut ensuite ajouter des boutons avec les méthodes suivantes :

  • AlertDialog.Builder setPositiveButton (text, DialogInterface.OnClickListener listener), avec text qui doit être une ressource de type String ou une String, et listener qui définira que faire en cas de clic. Ce bouton se trouvera tout à gauche.
  • AlertDialog.Builder setNeutralButton (text, DialogInterface.OnClickListener listener). Ce bouton se trouvera entre les deux autres boutons.
  • AlertDialog.Builder setNegativeButton (text, DialogInterface.OnClickListener listener). Ce bouton se trouvera tout à droite.

Enfin, il est possible de mettre une liste d'éléments et de déterminer combien d'éléments on souhaite pouvoir choisir :

Méthode

Éléments sélectionnables

Usage

AlertDialog.Builder setItems (CharSequence[] items, DialogInterface.OnClickListener listener)

Aucun

Le paramètre items correspond au tableau contenant les éléments à mettre dans la liste, alors que le paramètre listener décrit l'action à effectuer quand on clique sur un élément.

AlertDialog.Builder setSingleChoiceItems (CharSequence[] items, int checkedItem, DialogInterface.OnClickListener listener)

Un seul à la fois

Le paramètre checkedItem indique l'élément qui est sélectionné par défaut. Comme d'habitude, on commence par le rang 0 pour le premier élément. Pour ne sélectionner aucun élément, il suffit de mettre -1. Les éléments seront associés à un bouton radio afin que l'on ne puisse en sélectionner qu'un seul.

AlertDialog.Builder setMultipleChoiceItems (CharSequence[] items, boolean[] checkedItems, DialogInterface.OnClickListener listener)

Plusieurs

Le tableau checkedItems permet de déterminer les éléments qui sont sélectionnés par défaut. Les éléments seront associés à une case à cocher afin que l'on puisse en sélectionner plusieurs.

Les autres widgets

Date et heure

Il arrive assez fréquemment qu'on ait à demander à un utilisateur de préciser une date ou une heure, par exemple pour ajouter un rendez-vous dans un calendrier.

On va d'abord réviser comment on utilise les dates en Java. C'est simple, il suffit de récupérer un objet de type Calendar à l'aide de la méthode de classe Calendar.getInstance(). Cette méthode retourne un Calendar qui contiendra les informations sur la date et l'heure, au moment de la création de l'objet.

Si le Calendar a été créé le 23 janvier 2012 à 23h58, il vaudra toujours « 23 janvier 2012 à 23h58 », même dix jours plus tard. Il faut demander une nouvelle instance de Calendar à chaque fois que c'est nécessaire.

Il est ensuite possible de récupérer des informations à partir de la méthode int get(int champ) avec champ qui prend une valeur telle que :

  • Calendar.YEAR pour l'année ;
  • Calendar.MONTH pour le mois. Attention, le premier mois est de rang 0, alors que le premier jour du mois est bien de rang 1 !
  • Calendar.DAY_OF_MONTH pour le jour dans le mois ;
  • Calendar.HOUR_OF_DAY pour l'heure ;
  • Calendar.MINUTE pour les minutes ;
  • Calendar.SECOND pour les secondes.
1
2
3
4
// Contient la date et l'heure au moment de sa création
Calendar calendrier = Calendar.getInstance();
// On peut ainsi lui récupérer des attributs
int mois = calendrier.get(Calendar.MONTH);

Insertion de dates

Pour insérer une date, on utilise le widget DatePicker. Ce widget possède en particulier deux attributs XML intéressants. Tout d'abord android:minDate pour indiquer quelle est la date la plus ancienne à laquelle peut remonter le calendrier, et son opposé android:maxDate.

En Java, on peut tout d'abord initialiser le widget à l'aide de la méthode void init(int annee, int mois, int jour_dans_le_mois, DatePicker.OnDateChangedListener listener_en_cas_de_changement_de_date). Tous les attributs semblent assez évidents de prime abord à l'exception du dernier, peut-être. Il s'agit d'un Listener qui s'enclenche dès que la date du widget est modifiée, on l'utilise comme n'importe quel autre Listener. Remarquez cependant que ce paramètre peut très bien rester null.

Enfin vous pouvez à tout moment récupérer l'année avec int getYear(), le mois avec int getMonth() et le jour dans le mois avec int getDayOfMonth().

Par exemple, j'ai créé un DatePicker en XML, qui commence en 2012 et se termine en 2032 :

1
2
3
4
5
6
7
8
<DatePicker
  android:id="@+id/datePicker"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:layout_centerHorizontal="true"
  android:layout_centerVertical="true"
  android:startYear="2012"
  android:endYear="2032" />

Puis je l'ai récupéré en Java afin de changer la date de départ (par défaut, un DatePicker s'initialise à la date du jour) :

1
2
mDatePicker = (DatePicker) findViewById(R.id.datePicker);
mDatePicker.updateDate(mDatePicker.getYear(), 0, 1);

Ce qui donne le résultat visible à la figure suivante.

Notre DatePicker

Insertion d'horaires

Pour choisir un horaire, on utilise TimePicker, classe pas très contraignante puisqu'elle fonctionne comme DatePicker ! Alors qu'il n'est pas possible de définir un horaire maximal et un horaire minimal cette fois, il est possible de définir l'heure avec void setCurrentHour(Integer hour), de la récupérer avec Integer getCurrentHour(), et de définir les minutes avec void setCurrentMinute(Integer minute), puis de les récupérer avec Integer getCurrentMinute().

Comme nous utilisons en grande majorité le format 24 heures (rappelons que pour nos amis américains il n'existe pas de 13e heure, mais une deuxième 1re heure), notez qu'il est possible de l'activer à l'aide de la méthode void setIs24HourView(Boolean mettre_en_format_24h).

Le Listener pour le changement d'horaire est cette fois géré par void setOnTimeChangedListener(TimePicker.OnTimeChangedListener onTimeChangedListener).

Cette fois encore, je définis le TimePicker en XML :

1
2
3
4
5
6
<TimePicker
  android:id="@+id/timePicker"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:layout_centerHorizontal="true"
  android:layout_centerVertical="true" />

Puis je le récupère en Java pour rajouter un Listener qui se déclenche à chaque fois que l'utilisateur change l'heure :

1
2
3
4
5
6
7
8
mTimePicker = (TimePicker) findViewById(R.id.timePicker);
mTimePicker.setIs24HourView(true);
mTimePicker.setOnTimeChangedListener(new TimePicker.OnTimeChangedListener() {
  @Override
  public void onTimeChanged(TimePicker view, int hourOfDay, int minute) {
    Toast.makeText(MainActivity.this, "C'est vous qui voyez, il est donc " + String.valueOf(hourOfDay) + ":" + String.valueOf(minute), Toast.LENGTH_SHORT).show();
  }
});

Ce qui donne la figure suivante.

Changement de l'heure

Sachez enfin que vous pouvez utiliser de manière équivalente des boîtes de dialogue qui contiennent ces widgets. Ces boîtes s'appellent DatePickerDialog et TimePickerDialog.

Affichage de dates

Il n'existe malheureusement pas de widgets permettant d'afficher la date pour l'API 7, mais il existe deux façons d'écrire l'heure actuelle, soit avec une horloge analogique (comme sur une montre avec des aiguilles) qui s'appelle AnalogClock, soit avec une horloge numérique (comme sur une montre sans aiguilles) qui s'appelle DigitalClock, les deux visibles à la figure suivante.

À gauche une « AnalogClock » et à droite une « DigitalClock »

Afficher des images

Le widget de base pour afficher une image est ImageView. On peut lui fournir une image en XML à l'aide de l'attribut android:src dont la valeur est une ressource de type drawable. L'attribut android:scaleType permet de préciser comment vous souhaitez que l'image réagisse si elle doit être agrandie à un moment (si vous mettez android:layout_width="fill_parent" par exemple).

Le ratio d'une image est le rapport entre la hauteur et la largeur. Si le ratio d'une image est constant, alors en augmentant la hauteur, la largeur augmente dans une proportion identique et vice versa. Ainsi, avec un ratio constant, un carré reste toujours un carré, puisque quand on augmente la hauteur de x la longueur augmente aussi de x. Si le ratio n'est pas constant, en augmentant une des dimensions l'autre ne bouge pas. Ainsi, un carré devient un rectangle, car, si on étire la hauteur par exemple, la largeur n'augmente pas.

Les différentes valeurs qu'on peut attribuer sont visibles à la figure suivante.

L'image peut prendre différentes valeurs

  • fitXY : la première image est redimensionnée avec un ratio variable et elle prendra le plus de place possible. Cependant, elle restera dans le cadre de l'ImageView.
  • fitStart : la deuxième image est redimensionnée avec un ratio constant et elle prendra le plus de place possible. Cependant, elle restera dans le cadre de l'ImageView, puis ira se placer sur le côté en haut à gauche de l'ImageView.
  • fitCenter : la troisième image est redimensionnée avec un ratio constant et elle prendra le plus de place possible. Cependant, elle restera dans le cadre de l'ImageView, puis ira se placer au centre de l'ImageView.
  • fitEnd : la quatrième image est redimensionnée avec un ratio constant et elle prendra le plus de place possible. Cependant, elle restera dans le cadre de l'ImageView, puis ira se placer sur le côté bas à droite de l'ImageView.
  • center : la cinquième image n'est pas redimensionnée. Elle ira se placer au centre de l'ImageView.
  • centerCrop : la sixième image est redimensionnée avec un ratio constant et elle prendra le plus de place possible. Cependant, elle pourra dépasser le cadre de l'ImageView.
  • centerInside : la dernière image est redimensionnée avec un ratio constant et elle prendra le plus de place possible. Cependant, elle restera dans le cadre de l'ImageView, puis ira se placer au centre de l'ImageView.

En Java, la méthode à employer dépend du typage de l'image. Par exemple, si l'image est décrite dans une ressource, on va passer par void setImageResource(int id). On peut aussi insérer un objet Drawable avec la méthode void setImageDrawable(Drawable image) ou un fichier Bitmap avec void setImageBitmap(Bitmap bm).

Enfin, il est possible de récupérer l'image avec la méthode Drawable getDrawable().

C'est quoi la différence entre un Drawable et un Bitmap ?

Un Bitmap est une image de manière générale, pour être précis une image matricielle comme je les avais déjà décrites précédemment, c'est-à-dire une matrice (un tableau à deux dimensions) pour laquelle chaque case correspond à une couleur ; toutes les cases mises les unes à côté des autres forment une image. Un Drawable est un objet qui représente tout ce qui peut être dessiné. C'est-à-dire autant une image qu'un ensemble d'images pour former une animation, qu'une forme (on peut définir un rectangle rouge dans un drawable), etc.

Notez enfin qu'il existe une classe appelée ImageButton, qui est un bouton normal, mais avec une image. ImageButton dérive de ImageView.

Pour des raisons d'accessibilité, il est conseillé de préciser le contenu d'un widget qui contient une image à l'aide de l'attribut XML android:contentDescription, afin que les malvoyants puissent avoir un aperçu sonore de ce que contient le widget. Cet attribut est disponible pour toutes les vues.

Autocomplétion

Quand on tape un mot, on risque toujours de faire une faute de frappe, ce qui est agaçant ! C'est pourquoi il existe une classe qui hérite de EditText et qui permet, en passant par un adaptateur, de suggérer à l'utilisateur le mot qu'il souhaite insérer.

Cette classe s'appelle AutoCompleteTextView et on va voir son utilisation dans un exemple dans lequel on va demander à l'utilisateur quelle est sa couleur préférée et l'aider à l'écrire plus facilement.

On peut modifier le nombre de lettres nécessaires avant de lancer l'autocomplétion à l'aide de l'attribut android:completionThreshold en XML et avec la méthode void setThreshold(int threshold) en Java.

Voici le fichier main.xml :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:orientation="vertical" >

  <AutoCompleteTextView
    android:id="@+id/complete"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content" />

</LinearLayout>

Ensuite, je déclare l'activité AutoCompletionActivity suivante :

 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
import android.app.Activity;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;

public class AutoCompletionActivity extends Activity {
  private AutoCompleteTextView complete = null;

  // Notre liste de mots que connaîtra l'AutoCompleteTextView 
  private static final String[] COULEUR = new String[] {
    "Bleu", "Vert", "Jaune", "Jaune canari", "Rouge", "Orange"
  };

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    // On récupère l'AutoCompleteTextView déclaré dans notre layout
    complete = (AutoCompleteTextView) findViewById(R.id.complete);
    complete.setThreshold(2);        

    // On associe un adaptateur à notre liste de couleurs…
    ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_dropdown_item_1line, COULEUR);
    // … puis on indique que notre AutoCompleteTextView utilise cet adaptateur
    complete.setAdapter(adapter);
  }
}

Et voilà, dès que notre utilisateur a tapé deux lettres du nom d'une couleur, une liste défilante nous permet de sélectionner celle qui correspond à notre choix, comme le montre la figure suivante.

L'autocomplétion en marche

Vous remarquerez que cette autocomplétion se fait sur la ligne entière, c'est-à-dire que si vous tapez « Jaune rouge », l'application pensera que vous cherchez une couleur qui s'appelle « Jaune rouge », alors que bien entendu vous vouliez le mot « jaune » puis le mot « rouge ». Pour faire en sorte qu'une autocomplétion soit répartie entre plusieurs constituants d'une même chaîne de caractères, il faut utiliser la classe MultiAutoCompleteTextView. Toutefois, il faut préciser quel caractère sera utilisé pour séparer deux éléments avec la méthode void setTokenizer(MultiAutoCompleteTextView.Tokenizer t). Par défaut, on peut par exemple utiliser un MultiAutoCompleteTextView.CommaTokenizer, qui différencie les éléments par des virgules (ce qui signifie qu'à chaque fois que vous écrirez une virgule, le MultiAutoCompleteTextView vous proposera une nouvelle suggestion).


  • L'affichage d'une liste s'organise de la manière suivante : on donne une liste de données à un adaptateur (Adapter) qui sera attaché à une liste (AdapterView). L'adaptateur se chargera alors de construire les différentes vues à afficher ainsi que de gérer leurs cycles de vie.
  • Les adaptateurs peuvent être attachés à plusieurs types d'AdapterView :
    • ListView pour lister les vues les unes en dessous des autres.
    • GridView pour afficher les vues dans une grille.
    • Spinner pour afficher une liste de vues défilante.
  • Lorsque vous désirez afficher des vues plus complexes, vous devez créer votre propre adaptateur qui étend la super classe BaseAdapter et redéfinir les méthodes en fonction de votre liste de données.
  • Les boîtes de dialogue peuvent être confectionnées de 2 manières différentes : par la classe Dialog ou par un builder pour construire une AlertDialog, ce qui est plus puissant.
  • Pour insérer un widget capable de gérer l'autocomplétion, on utilisera le widget AutoCompleteTextView.