14 avril 2012

Tutorial Android – Les listes personnalisées

Nous avons vu  ici comment lancer notre première application : Hello, Android.
Puis, 
 ici, nous avons créé un programme interagissant avec l’utilisateur : récupération puis affichage de sa saisie dans un nouvel écran.

Passons aux choses sérieuses maintenant, avec un sujet non trivial : l'affichage d'une liste personnalisée d'items.

2 composants interviennent : la ListView et son Adaptateur de données. Une petite difficulté ici car nous créons un Adaptateur personnalisé.

  1. Au menu :

1. Affichage d’une liste de documents 
Chaque ligne est constituée du nom, de la taille, de l’icône selon type du document (image, vidéo, classeur, présentation, texte, …)
2. Gestion d’un événement
Sur clic d'un item de la liste, affichage d’un message (nom du document sélectionné ) dans un Toast (c’est comme ça que s’appelle le composant Android). 



  1. Les ingrédients :




    • Une liste de documents. Le modèle.
    • Des icônes pour représenter les différents types de document. Des ressources graphiques.
    • Une activité chargée d'afficher cette liste. La classe java principale.
    • Un layout principal contenant le composant ListView. Fichier XML.
    • Un layout spécifique pour décrire chaque ligne : l'icône, le nom et la taille du document. Fichier XML.
    • Un Adaptateur de données, composant Android associant chaque vue à sa donnée. Ici, nous écrirons un adaptateur personnalisé héritant d'ArrayAdapter spécialisé dans les listes et tableaux. Classe java.




    1. La préparation :


    1. Organisation des sources


    • nous créons le package fr.scherrda.android.tuto.list pour nos classes java.
    • rappel : les fichiers XML de layout sont à placer sous : res/layout
    • les ressources de type graphique (icônes) sont sous : res/drawable-hdpi


    1. Créer le layout principal : res/layout/list.xml


    Le layout contient un composant ListView.

    <?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">

       <ListView
           android:id="@+id/listdocs"
           android:layout_width="fill_parent"
           android:layout_height="fill_parent"
           android:padding="10dp"/>
    </LinearLayout>





    1. 3. Créer le layout d'une ligne : res/layout/row_list.xml


    Chaque ligne est constituée d'une icône à gauche, puis de 2 textes l'un au dessus de l'autre, le premier étant en caractère Gras:


    Icône


    texte 1
    texte 2







    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
      android:orientation="horizontal"
      android:layout_width="fill_parent"
       android:layout_height="fill_parent">

           <ImageView android:id="@+id/icon"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:padding="5dp"
            android:layout_gravity="center_vertical">
           </ImageView>

           <LinearLayout
            android:orientation="vertical"
            android:layout_height="fill_parent"
            android:layout_width="fill_parent"
            android:padding="10dp">
           
                   <TextView
                    android:id="@+id/name"
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content"
                    android:textStyle="bold"
                    android:textSize="14sp" />
                   <TextView
                    android:id="@+id/size"
                    android:layout_width="fill_parent"
                    android:layout_height="fill_parent"
                    android:textSize="10sp" />

           </LinearLayout>
    </LinearLayout>









    1. 4. Créer l’entité Document 

    package fr.scherrda.android.tuto.list.model;

    public class Document{
    private String name;
    private long size;
    private int icone ;
    public Document(String name, long size, int icone) {
    this.name = name;
    this.size = size;
    this.icone = icone;
    }
    }


    1. 5. Rajouter des icônes en tant que ressources graphiques dans votre projet

    Trouvez des icônes pour représenter les différents types de fichier (icn_video.png pour les video, icn_word.png pour les documents de type texte, icn_image.png pour les documents de type image, ...


    Les ressources Image sont à placer sous res/drawable-hdpi (résolution haute que nous utilisons par défaut)





    1. 6. Créer l'activité : ListDocumentsActivity

    Caractéristiques de cette activité :


    • Etend Activity
    (oui, nous aurions pu étendre ListActivity, un type fourni par Android pour les listes. La “simplification” apportée par cette classe ne me paraît pas flagrante. Je préfère vous expliquer les éléments manipulés, il n’y en a pas tant que ça.)

    • Possède un attribut : mListView,
    une instance du composant ListView défini  dans le fichier descriptif du layout principal.

    • Implémente OnItemClickListener :
    l'activité "écoute" les clics sur un item
    → elle redéfinit la méthode OnItemClick() ( affichage d’un Toast avec le nom de l’élément sélectionné)
    → nous l’enregistrons auprès de la liste en tant que Listener du clic d'un item de liste

    • ArrayDocumentAdapter, notre adaptateur personnalisé  pour gérer une liste de documents est créé et associé à la listView, lors de la phase d’initialisation de l’activité.
    Le constructeur de l’adapteur prend la liste de documents en paramètre (nous utiliserons pour la démonstration une liste fixe de documents).


    /**
    * initialisation de l’activité
    */

    public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.list);
    mListView = (ListView)findViewById(R.id.listdocs);
    //getDocuments : fabrique une liste de documents fixe
    List<Document> documents = getDocuments();

    //association avec l'adaptateur
    mListView.setAdapter(new ArrayDocumentAdapter(this, documents));

    //l'activité enregistrée comme listener sur le clic d'item mListView.setOnItemClickListener(this);
    }





    /**
    * L'activité implémente OnItemClickListener : redéfinition de onItemClick
    */
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position,
    long id) {
    //récupération de l’item sélectionné
    Document document =
                               (Document)mListView.getAdapter().getItem(position);

    String name = document.getName();

             //affichage d’un message Toast
    Toast.makeText(this, "Vous avez cliqué sur le document : " + name,
    Toast.LENGTH_LONG).show();
    }


    /**
    * retourne une liste de documents
    */
    private List<Document> getDocuments() {
    List<Document> liste = new ArrayList<Document>();
    Document doc1 = new Document(1, "doc1.doc", 10000, R.drawable.icn_word);
    liste.add(doc1);
    Document doc2 = new Document(2, "MonDocumentPres.ppt", 200000, R.drawable.icn_ppt);
    liste.add(doc2);
    Document doc3 = new Document(3, "unCalcul.xls", 300000, R.drawable.icn_xls);
    liste.add(doc3);
    Document doc4 = new Document(4, "doc4.doc", 150000, R.drawable.icn_word);
    liste.add(doc4);
    return liste;
    }

    A ce stade Eclipse déclare une erreur de compilation :

    → nous utilisons l’assistant d’Eclipse pour créer la classe ArrayDocumentAdapter automatiquement.





    7. Créer l'adaptateur : ArrayDocumentsAdapter
    Caractéristiques

    • étend ArrayAdapter
    • possède 2 attributs : mDocuments, la liste de documents, et mInflater de type LayoutInflater, initialisés par le constructeur.
    Le LayoutInflater est l’objet permettant de demander explicitement la création d’un objet View à partir de la ressource XML décrivant son Layout.
    Le processus de création des composants View à partir des ressources Layout est applelé le  “layout inflation”. Ce processus est géré par le système de manière sous-jacente lors de la phase de création d’une activité.

    • le plus important : la redéfinition de la méthode getView(int position, View view, ViewGroup parent)
    C’est ici que l’on personnalise la vue associée à un item identifié par sa position dans la liste.
    1. on créé l’objet View représentant la ligne courante à partir de la ressource layout
    2. on valorise les zones texte et l’icône de la vue
    3. La méthode retourne la vue ainsi mise à jour.

    /**
    * Constructeur
    */
    public ArrayDocumentAdapter(Context context, List<Document> documents) {
    super(context, R.layout.row_list, documents);
    this.mDocuments = documents;
    mInflater = LayoutInflater.from(context);
    }



    Une version non optimisée de getView

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
             //création de l’objet View à partir de la ressource Layout
    rowView = mInflater.inflate(R.layout.row_list, null);

             //récupération du document identifié par sa position dans la liste
    Document document = mDocuments.get(position);

             //Valorisation du Texte1 de la Vue avec le nom du document
    TextView nameView = (TextView) rowView.findViewById(R.id.name);
    nameView.setText(document.getName());

             //Valorisation du Texte2 avec la taille du document
    TextView sizeView = (TextView) rowView.findViewById(R.id.size);
    sizeView.setText(String.valueOf(document.getSize()) + "Ko");

             //Affichage de l’icône correspondante
    ImageView iconeView = (ImageView) rowView.findViewById(R.id.icon);
    iconeView.setImageResource(document.getIcone());

    return rowView;
    }


    Cette implémentation marche mais n’est pas optimisée. En conséquence, le thread gérant l’affichage de la liste risque de “ramer” et vous constaterez des ralentissements si votre liste à afficher contient beaucoup d’items.

    Les problèmes de performance :
    Chaque fois que l’utilisateur fait scroller sa liste, ou que l’orientation change, le système doit recalculer l’affichage de la liste. La méthode getView est appelée pour générer l’affichage de chaque ligne de l’écran.
    Vous pourrez constater des problèmes de ralentissements si votre liste contient beaucoup d’items.

    Or 2 opérations coûtes particulièrement chères et ne doivent être appelées que si nécessaires :
    - la création d’objets (ici via le layout inflation). Comme d’habitude en java, il ne faut créer les objets que si nécessaire.
    - la recherche d’un composant View dans l’arbre des ressources avec findViewById

    Pour optimiser la méthode, nous allons recycler, réutiliser ! Cela évitera de recréer un objet réutilisable ou de rechercher dans notre arborescence de composants, un composant déjà récupéré antérieurement.

    La version optimisée de getView
    Le recyclage
    - lorsque un item disparaît de l’écran, sa vue est conservée dans un espace pour le recyclage.
    - Réciproquement, lorsqu’un item apparaît à l’écran, s’il existe une vue disponible pour le recyclage, le système la passe à la méthode getView. Il s’agit du paramètre convertView
    → convertView est une vue utilisable pour notre ligne courante. Si convertView est nul, nous créerons l’objet View. Sinon, nous pourrons l’utiliser directement, économisant ainsi une création d’objet.
    La mise en cache
    - nameView, sizeView et iconeView sont des vues enfants de la vue associée à l’item courant.
    Au lieu de les récupérer à chaque fois à partir de l’arborescence avec un findViewById, nous allons les associer une fois pour toute à la vue parente en utilisant le pattern ViewHolder, une classe interne statique :

    //membre de classe (ne dépend pas de l'instance de la classe contenante)
          // utilisée pour stocker des références
    static class ViewHolder {
    public TextView nameView;
    public TextView sizeView;
    public ImageView iconeView;
    }


    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
             
              ViewHolder holder;
              
              View rowView = convertView;
    if (rowView == null) {
                //création de l’objet View à partir de la ressource Layout
      rowView = mInflater.inflate(R.layout.row_list, null);

                holder = new ViewHolder();
                    holder.nameView = (TextView) rowView.findViewById(R.id.name);
                    holder.sizeView = (TextView) rowView.findViewById(R.id.size);
                    holder.iconeView = (ImageView) rowView.findViewById(R.id.icon);
                    rowView.setTag(holder);
    }else {
                    holder = (ViewHolder) rowView.getTag();
             }

             //récupération du document identifié par sa position dans la liste
    Document document = mDocuments.get(position);

             //Valorisation du Texte1 de la Vue avec le nom du document
    holder.nameView.setText(document.getName());

             //Valorisation du Texte2 avec la taille du document
    holder.sizeView.setText(String.valueOf(document.getSize()) + "Ko");

             //Affichage de l’icône correspondante
    holder.iconeView.setImageResource(document.getIcone());

    return rowView;
    }


    1. La cuisson

    Laissons un peu reposer et profitons-en pour passer en revue les ingrédients de notre recette :

    • notre activité principale
      • gère une ListView ,  une vue qui affiche une liste défilante d’items
      • gère les actions clic qui se produisent sur les items.
    • La vue ListView demande à l’adaptateur qui lui est associé de lui fournir la vue correspondante pour chaque item visible à l’écran via la méthode getView().
    • pour définir un layout personnalisé pour chaque ligne, nous avons construit un adaptateur personnalisé, et redéfinit la méthode getView().
    • Il est important d’optimiser la méthode getView() :
      • utilisation du système de recyclage des vues fourni par Android.
      • mise en cache des vues enfants pour ne pas abuser des findViewById

    Et voilà, c’est prêt.
    1. La dégustation

    N’oubliez pas de déclarer la nouvelle activité dans le fichier AndroidManifest.xml !
    Juste encore une petite modification avant la dégustation :
    je vous propose de modifier le point d’entrée dans l’application : la classe Main, lancée lors de l’exécution sera une nouvelle activité qui se contentera de nous rediriger vers ListDocumentsActivity. Comme ça nous allons réviser le lancement d’une autre activité.

    Pour faire simple :
    1. Créez la classe MainActivity dans le package fr.scherrda.android.tuto;
    2. Lancez un Intent vers ListDocumentsActivity dans onCreate
    //Classe MainActivity
    package fr.scherrda.android.tuto;

    public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    // TODO Auto-generated method stub
    super.onCreate(savedInstanceState);

    Intent intent = new Intent(getApplicationContext(),                   ListDocumentsActivity.class);
    startActivity(intent);
    }
    }

    1. Mettez à jour le manifest : Déclarez nos 2 activités. Attention, MainActivity est la classe main, lancée à l’exécution
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="fr.scherrda.android.tuto" android:versionCode="1"
    android:versionName="1.0" android:installLocation="auto">
    <uses-sdk android:minSdkVersion="8" />

    <application android:icon="@drawable/icon" android:label="@string/app_name">
    <activity android:name="fr.scherrda.android.tuto.MainActivity">
    <intent-filter>
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    </activity>
    <activity android:name=".interactivity.LoginActivity"android:label="tuto inter-activité"></activity>
    <activity android:name="fr.scherrda.android.tuto.interactivity.ShowActivity"></activity>
    <activity android:name="fr.scherrda.android.tuto.list.ListDocumentsActivity"
    android:label="Liste personnalisée">
    </activity>

    </application>
    </manifest>


    SOURCES


    Régalez-vous bien.

    13 commentaires:

    1. j'ai essayé votre code mais ça n'a pas marché, la liste ne s'affiche pas. Je n'arrive pas à voir d'ou vient le problème puisque j'ai tout révisé pas mal de fois.

      RépondreSupprimer
      Réponses
      1. Merci pour ce tuto, très bien fait ! Adil pour ton soucis, redéfinit la méthode getCount() dans ArrayDocumentAdapter :

        @Override
        public int getCount(){
        return nbElementDeTaList; // ici = 4
        }

        Supprimer
      2. Bonjour,

        Vous pouvez récupérer les sources sur Github.

        @Adil : Sans info supplémentaires, difficile de dire. Avez-vous trouvé le problème ? N'avez-vous aucun message d'erreur dans la console ?

        Supprimer
    2. Bonjour, je m'excuse d'abord pour le retard de répondre, j'étais en voyage et je ne me connectais pas :), et merci pour vos réponses. Concernant le problème que j'ai rencontré, il était lié à la redéfinition de la méthode getCount(). Vous avez raison GuidedByYou, merci pour votre aide.

      Merci pour ce tuto Dahlia, ça m'a aidé à bien comprendre les listes personnalisées et la fonction des adaptateurs.

      Si vous voulez bien GuidedByYou, une petite explication serait la bienvenue.

      RépondreSupprimer
    3. Ce commentaire a été supprimé par l'auteur.

      RépondreSupprimer
    4. Bonjour,
      J'ai suivi votre tuto (et en passant, merci beaucoup). toutefois, j'ai uneanomalie que je ne parvient pas à expliquer :
      - le champs "size" ne s'affiche pas dans la liste, seulement le nom du fichier et l'icone.

      Merci pour votre aide.

      RépondreSupprimer
    5. Bonjour,
      Attention à l'erreur dans la classe Document, il faut rajouter le 1er champ "id"

      RépondreSupprimer
    6. bonjour
      merci pour ce tuto c'est le plus clair que j'ai trouvé sur le net
      et ca marche ...

      RépondreSupprimer
    7. Bonjour !
      C'est un super tuto. Merci beaucoup

      RépondreSupprimer
    8. C'est super bien fait je viens de le découvrir et je le trouve cool !!!!

      RépondreSupprimer
    9. Bonjour,
      Tuto parfait, propre et clair. Tout ce que j'aime quand je cherche une solution sur internet.
      Merci !

      RépondreSupprimer
    10. Bonjour,
      Merci pour ce tuto qui m'a bien aidé pour créer une listview.
      Par contre j'ai un petit soucis, lorsque je suis sur ma liste et que j'appuie sur le bouton retour du téléphone, j'accède à une page vierge. Je dois appuyer de nouveau sur le bouton retour du téléphone pour revenir à mon activité précédente.

      Comment cela se fait-il ?

      Merci

      RépondreSupprimer