mardi 20 septembre 2011

Utilisation du ArrayAdapter avec les ListView

L'un des composants les plus utilisés dans les applications Android est le ListView. A priori facilement utilisable, il permet d'afficher une liste d'éléments en quelques lignes de codes.


Cependant, si on veut afficher autre chose qu'une simple liste dont chaque élément est une chaine de caractères, il faut travailler un peu plus. Et, d'après ce que j'en ai vu sur le web, c'est là que les choses se compliquent pour certains.  
Je vais reprendre l'implémentation typique, et on va un peu l'améliorer, pour la rendre plus générique. Ensuite, on jouera un peu avec le composant...


Commençons par définir une classe qui représentera les éléments à afficher...



public class Moto
{
      public string Name;
      public string Pays;


      public Moto(string name, string pays)
      {
            Name = name;
            Pays = pays;
      }
}


Le main activity a classiquement cette implémentation  :



namespace GenericAdapter
{
  [Activity (Label = "BasicAdapterUsage", MainLauncher = true)]
  public class BasicAdapterUsage : Activity
  {
     List<Moto> items;


     protected override void OnCreate (Bundle bundle)
     {
        base.OnCreate (bundle);
     // Set our view from the "main" layout resource
        SetContentView (Resource.Layout.BasicView);
     // Settings data
        items = new List<Moto>();
        Moto yamaha = new Moto("Yamaha","Japon");
        Moto ducati = new Moto("Ducati","Italie");
        Moto bmw = new Moto("BMW","Allemagne");
        items.Add(yamaha);items.Add(ducati);items.Add(bmw);
        // Settings ListView
        ListView listOfMotorCycle = FindViewById<ListView>(Resource.Id.listMotorCycle);
        MotoAdapter myAdapter = new MotoAdapter(this,Resource.Layout.Moto,items);
        listOfMotorCycle.Adapter = myAdapter;
        Button btnAdd = FindViewById<Button>(Resource.Id.btnAdd);
        btnAdd.Click+=delegate{
           Moto triumph = new Moto("Triumph","Angleterre");
           myAdapter.Add(triumph);
           myAdapter.NotifyDataSetChanged();
        };
     }
  }
}


Le layout associé sera basiquement un bouton pour ajouter un élément et une listview verticale : 
>> fichier BasicView.axml dans le répertoire resources.layout


<?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"
    >
<Button
android:id="@+id/btnAdd"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/Add"  
/>
<ListView 
android:id="@+id/listMotorCycle"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
/>
</LinearLayout>


Maintenant, passons aux choses plus intéressantes, avec la surcharge du ArrayAdapter utilisé (MotoAdapter, dans le code du "main")


public class MotoAdapter : ArrayAdapter<Moto>
{
   public  MotoAdapter(Context context, int textViewResourceId, IList<Moto> data) :base(context, textViewResourceId, data)
   {
   }
   public override View GetView (int position, View convertView, ViewGroup parent) 
   {
      View v = convertView;
      if (v == null) 
      {
         LayoutInflater vi = (LayoutInflater)Context.GetSystemService(Context.LayoutInflaterService);
         v = vi.Inflate(Resource.Layout.Moto, null);
      }
      if(position<this.Count) 
      {
         Moto aMoto = this.GetItem(position);
         if (aMoto != null) 
         {
            
            TextView name = (TextView)v.FindViewById(Resource.Id.txtName);
            if (name != null) 
            {
               name.Text = aMoto.Name;                           
            }
            TextView pays = (TextView)v.FindViewById(Resource.Id.txtPays);
            if (pays != null) 
            {
               pays.Text = aMoto.Pays;                           
            }


         }
      }
          return v;
   }
}




Pour finir, Le layout utilisé pour chaque élément de la liste (Moto, dans le code du main)


>> fichier Moto.xml dans le répertoire resources.layout


<?xml version="1.0" encoding="utf-8"?> 
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
>
 <TextView 
      android:id="@+id/txtName"   
       android:layout_width="wrap_content"  
       android:layout_height="wrap_content"  
       android:padding="10dp"  
       android:textSize="16sp"
/>
<TextView 
      android:id="@+id/txtPays"   
      android:layout_width="wrap_content"  
      android:layout_height="wrap_content"  
      android:padding="10dp"  
      android:textSize="12sp"
      android:layout_toRightOf="@id/txtName"
/>
</RelativeLayout> 


Voilà, tout est défini. Les lignes intéressantes sont d'une part, l'instanciation de l'Adapter de la listView, et d'autre part, la surcharge de l'ArrayAdapter et l'ajout d'éléments à la liste.


L'adapter fourni à la listView est instancié de la façon suivante : 

MotoAdapter myAdapter = new MotoAdapter(this,Resource.Layout.Moto,items);
listOfMotorCycle.Adapter = myAdapter;

On indique à l'adapteur quel View utiliser pour l'affichage de chaque item, et on donne la source des données.


Dans le constructeur, on ne fait qu'un appel au constructeur de la classe mère.


public  MotoAdapter(Context context, int textViewResourceId, IList<Moto> data) :base(context, textViewResourceId, data)
{
}



Et, la raison qui fait que l'on est obligé de surcharger l'ArrayAdapter, l'override du GetView ;
La méthode est appelée pour l'affichage de chaque élément de la liste.



   public override View GetView (int position, View convertView, ViewGroup parent) 
   {
      View v = convertView;
      if (v == null) 
      {
         LayoutInflater vi = (LayoutInflater)Context.GetSystemService(Context.LayoutInflaterService);
         v = vi.Inflate(Resource.Layout.Moto, null);
      }
      if(position<this.Count) 
      {
         Moto aMoto = this.GetItem(position);
         if (aMoto != null) 
         {
            
            TextView name = (TextView)v.FindViewById(Resource.Id.txtName);
            if (name != null) 
            {
               name.Text = aMoto.Name;                           
            }
            TextView pays = (TextView)v.FindViewById(Resource.Id.txtPays);
            if (pays != null) 
            {
               pays.Text = aMoto.Pays;                           
            }


         }
      }
          return v;
   }

Il faut noter un petit point stupide dans le code ci-dessus : la ligne pour récupérer la View pour l'item en cours :
 v = vi.Inflate(Resource.Layout.Moto, null);
Apparemment, il n'y a aucun moyen prévu pour récupérer nativement l'id de la vue (alors qu'il est passé en paramètre dans le constructeur). Soit c'est une erreur de jeunesse du framework, soit j'ai loupé un truc... On corrigera ça plus tard, quand on rendra l'adapter générique.

Le dernier point à noter dans cet exemple concerne l'ajout d'éléments dans la liste : il faut faire l'ajout via l'adapter, et non directement dans la source de données :

btnAdd.Click+=delegate{
           Moto triumph = new Moto("Triumph","Angleterre");
           myAdapter.Add(triumph);
           myAdapter.NotifyDataSetChanged();
        };
Après l'ajout, il ne faut pas oublier de notifier au composant ListView que les données ont changés :

     myAdapter.NotifyDataSetChanged();

Execution, hop, ça donne quelque chose comme ça :


avant de cliquer :


après un click :


Bon, maintenant que l'on a notre canevas de ListView qui fonctionne, on peut rendre notre adapter un peu plus générique.
La seule chose à gérer, finalement, est la méthode GetView. Pas trop compliqué.

Déjà, passons en type générique :


public class BasicAdapter<T> : ArrayAdapter<T> 
{
   
private int itemViewResourceId;
   public  BasicAdapter(Context context, int textViewResourceId, List<T> data) :base(context, textViewResourceId, data)
{
itemViewResourceId = textViewResourceId;

}

   public override View GetView (int position, View convertView, ViewGroup parent) 
   {
      View v = convertView;
      if (v == null) 
      {
         LayoutInflater vi = (LayoutInflater)Context.GetSystemService(Context.LayoutInflaterService);
         v = vi.Inflate(itemViewResourceId,null);
      }
      if(position<this.Count) 
      {
         T o = this.GetItem(position);
         if (o != null) 
           doSomething();// .... remplir la view
      }
    return v;
   }
}

Pratiquement rien à faire : on déclare un int pour stocker l'id de la resource qui sera utilisée pour l'affichage dans le GetView.
Et pour remplir la vue, une interface toute simple fera l'affaire...

public interface iViewItem { void FillView(View v); }

au final, notre adapteur MotoAdapter devient un BasicAdapter<T>:



public class BasicAdapter<T> : ArrayAdapter<T> where T : iViewItem
{
         
   private int itemViewResourceId;
         
   public  BasicAdapter(Context context, int textViewResourceId, List<T> data) :base(context, textViewResourceId, data)
   {
      itemViewResourceId = textViewResourceId;
   }
          
   public override View GetView (int position, View convertView, ViewGroup parent) 
   {
      View v = convertView;
      if (v == null) 
      {
         LayoutInflater vi = (LayoutInflater)Context.GetSystemService(Context.LayoutInflaterService);
         v = vi.Inflate(itemViewResourceId,null);
      }
      if(position<this.Count) 
      {
         T o = this.GetItem(position);
         if (o != null) 
            o.FillView(v);
      }
      return v;
   }
}





et la classe Moto :



public class Moto : iViewItem
{
   public string Name;
   public string Pays;
   
   
   public Moto(string name, string pays)
   {
      Name = name;
      Pays = pays;
   }
   
   public void FillView(View v) 
   {
      TextView txtName = v.FindViewById<TextView>(Resource.Id.txtName);
      TextView txtPays = v.FindViewById<TextView>(Resource.Id.txtPays);
      if(txtName==null || txtPays==null)
         throw new Exception("Can't find resources for element of class Moto");
      txtName.Text = this.Name;
      txtPays.Text = this.Pays;
   }
}



Le code du main change un petit peu :
au lieu de :
MotoAdapter myAdapter = new MotoAdapter(this,Resource.Layout.Moto,items);
on a :
BasicAdapter<Moto> myAdapter = new BasicAdapter<Moto>(this,Resource.Layout.Moto,items);


Le code complet (toutes les classes sont dans le même fichier)...



namespace GenericAdapter
{
   [Activity (Label = "BasicAdapterUsage", MainLauncher = true)]         
   public class BasicAdapterUsage : Activity
   {
      
      public interface iViewItem
      {
         void FillView(View v);
      }
      
      public class Moto : iViewItem
      {
         public string Name;
         public string Pays;
         
         
         public Moto(string name, string pays)
         {
            Name = name;
            Pays = pays;
         }
         
         public void FillView(View v) 
         {
            TextView txtName = v.FindViewById<TextView>(Resource.Id.txtName);
            TextView txtPays = v.FindViewById<TextView>(Resource.Id.txtPays);
            if(txtName==null || txtPays==null)
               throw new Exception("Can't find resources for element of class Moto");
            txtName.Text = this.Name;
            txtPays.Text = this.Pays;
         }
      }
      
      
      List<Moto> items;
      protected override void OnCreate (Bundle bundle)
      {
         base.OnCreate (bundle);
         
         // Set our view from the "main" layout resource
         SetContentView (Resource.Layout.BasicView);
         
         // Settings data
         items = new List<Moto>();
         Moto yamaha = new Moto("Yamaha","Japon");
         Moto ducati = new Moto("Ducati","Italie");
         Moto bmw = new Moto("BMW","Allemagne");
         items.Add(yamaha);items.Add(ducati);items.Add(bmw);


         // Settings ListView
         ListView listOfMotorCycle = FindViewById<ListView>(Resource.Id.listMotorCycle);
         BasicAdapter<Moto> myAdapter = new BasicAdapter<Moto>(this,Resource.Layout.Moto,items);
         listOfMotorCycle.Adapter = myAdapter;
         Button btnAdd = FindViewById<Button>(Resource.Id.btnAdd);
         btnAdd.Click+=delegate{
            Moto triumph = new Moto("Triumph","Angleterre");
            myAdapter.Add(triumph);
            myAdapter.NotifyDataSetChanged();
         };
      }
      
      public class BasicAdapter<T> : ArrayAdapter<T> where T : iViewItem
      {
         
         private int itemViewResourceId;
         
         public  BasicAdapter(Context context, int textViewResourceId, List<T> data) :base(context, textViewResourceId, data)
         {
            itemViewResourceId = textViewResourceId;
         }
          public override View GetView (int position, View convertView, ViewGroup parent) 
         {
               View v = convertView;
               if (v == null) 
            {
                   LayoutInflater vi = (LayoutInflater)Context.GetSystemService(Context.LayoutInflaterService);
               v = vi.Inflate(itemViewResourceId,null);
               }
            if(position<this.Count) 
            {
                   T o = this.GetItem(position);
                   if (o != null) 
                  o.FillView(v);
            }
               return v;
          }
      }


   }
}

voilà, c'est fait. Ca fonctionne, on n'a pas besoin d'écrire trop de code à chaque nouvelle liste, et c'est assez simple.

La suite dans un autre post...



Aucun commentaire:

Enregistrer un commentaire