IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Le RecyclerView

Ce tutoriel va s'intéresser à un nouveau composant qui est le RecyclerView permettant d'afficher une liste d'articles dans un ensemble de sous vues.

Article lu   fois.

L'auteur

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Histoire

Pour mémoire , l'affichage de liste d'article sous Android se faisait via le composant ListView, GridView, etc. Ces composants avaient quelques ralentissements lorsque nous affichions leurs articles (items).  Un nouveau composant est apparu il y a peu, sur la bibliothèque de support v7. Ce composant qui s'appelle RecyclerView, nous permet de gérer ces listes d'articles et d'apporter quelques modifications par rapport à ces derniers composants.

II. RecyclerView

Cette classe garde en gros la même philosophie que les autres composants. C'est à dire, un adapter pour gérer la liste d'articles (d'items) ainsi que l'affichage de ses données, puis une liste d'événements pour interagir avec cette liste. L'adapter étant toujours personnalisable.

Quelques nouveautés sont apparues, comme l'écouteur nous permettant d'accéder aux vues nettoyées ( RecyclerListener), ainsi que de pouvoir récupérer une vue depuis une position donnée ( findChildViewUnder ),  l'utilisation d'un LayoutManager ( pour gérer les colonnes et lignes), l'utilisation de Décoration , etc..

II-A. Vue d'ensemble

Le RecyclerView est composé de sous composant permettant de personnaliser celui-ci.

Image non disponible

Nous allons présenter brièvement ses sous-composants.

  • Adapter:

L'adapter permet de pouvoir contenir l'ensemble des données à afficher dans le RecyclerView en gérant également ses mises à jours. Nous retrouvons le même principe que les adapters des ListView, GridView , etc...

  • LayoutManager :

Les LayoutManager permettent de structurer l'ensemble des sous vues contenues dans le RecyclerView.

  • ItemAnimator :

Les ItemAnimator nous permettent de pouvoir personnaliser les animations en fonction de l'état de l'item. Par exemple nous pouvons exécuter une animation lors de la suppression d'un article, lors de son ajout, etc ..

  • ItemDecoration :

Les ItemDecoration nous permettent de pouvoir personnaliser les sous vues et les séparateurs. Par exemple nous pouvons dessiner avant et après l'affichage de vue tout en ayant accès à la donnée de celle-ci.  

 

III. Exemple simple, une liste

Voici un exemple concret sur l'utilisation d'un RecyclerView, dans un premier temps il vous faudra insérer la bibliothèque v7 du RecyclerView dans votre nouveau projet. (com.android.support:recyclerview-v7). Depuis le 8 décembre 2014, il est préconisé d'utiliser AndroidStudio, il vous faut ainsi rajouter dans votre fichier build.gradle la ligne suivante:

 
Sélectionnez
dependencies {
   compile 'com.android.support:recyclerview-v7'
}

Et si vous êtes toujours sous Eclipse, sachez qu'il vous suffit de faire comme avant; insérer le fichier jar du projet RecyclerView que vous trouverez dans la sdk (extras/android/support/v7/recyclerView) dans le dossier libs de votre projet.

III-A. Le layout

Tout d'abord créons un layout pour notre vue principale qui contiendra notre RecyclerView. Rajoutons également un bouton pour pouvoir ajouter un nouvel article:

 
Sélectionnez
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:paddingBottom="@dimen/activity_vertical_margin"
   android:paddingLeft="@dimen/activity_horizontal_margin"
   android:paddingRight="@dimen/activity_horizontal_margin"
   android:paddingTop="@dimen/activity_vertical_margin"
   android:orientation="vertical"
   tools:context="com.android2ee.recyclerview.SimpleActivity" >
   
   <Button android:id="@+id/myButtonSimpleAdd"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:text="Add" />
      
<android.support.v7.widget.RecyclerView
   android:id="@+id/myListSimple"
   android:scrollbars="vertical"
   android:layout_width="match_parent"
   android:layout_height="match_parent"/>
  
</LinearLayout>

III-B. Le fichier MainActivity

Instancions le RecyclerView dans la méthode onCreate de l' Activity, ou la méthode onActivityCreated des Fragments, (exactement comme avant avec les ListView).

 
Sélectionnez
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.myListSimple);

Puis créer les articles que nous afficherons dans la liste.

 
Sélectionnez
// fill the list items
List<String> items = new ArrayList<String>();
for (int i = 0; i < 6; i++) {
        // new item
           items.add("test " + i);
}

Nous assignons  après l'adapter à ce RecyclerView, prenons comme layout celui fourni par le SDK android.R.layout.simple_list_item_1.

 
Sélectionnez
adapter = new RecyclerViewAdapter(items, android.R.layout.simple_list_item_1);
recyclerView.setAdapter(adapter);

Définir ensuite notre LayoutManager, ici nous resterons sur un cas classique d'affichage de ligne

 
Sélectionnez
recyclerView.setLayoutManager(new LinearLayoutManager(this));

III-C. Le fichier RecyclerViewAdapter

Nous créons ensuite notre adapter avec les deux nouvelles méthodes (onCreateViewHolder et onBindViewHolder), ainsi que votre nouvelle classe ViewHolder qui, comme toujours, établit un lien entre la vue et ses éléments que vous allez mettre à jour en fonction de l'article qu'elle affiche.

 
Sélectionnez
/**
* 
* @author florian
* RecyclerSimpleViewAdapter provide a simple RecyclerViewAdapter
*
*/
public class RecyclerSimpleViewAdapter extends RecyclerView.Adapter<RecyclerSimpleViewAdapter.ViewHolder> {

/**
* List items
*/
   private List<String> items;
   /**
    * the resource id of item Layout
    */
   private int itemLayout;

   /**
    * Constructor RecyclerSimpleViewAdapter
    * @param items : the list items
    * @param itemLayout : the resource id of itemView
    */
   public RecyclerSimpleViewAdapter(List<String> items, int itemLayout) {
       this.items = items;
       this.itemLayout = itemLayout;
   }

   /**
* Create View Holder by Type
* @param parent, the view parent
* @param viewType : the type of View
*/
   @Override 
   public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    // get inflater and get view by resource id itemLayout
       View v = LayoutInflater.from(parent.getContext()).inflate(itemLayout, parent, false);
       // return ViewHolder with View
       return new ViewHolder(v);
   }

   /**
* Get the size of items in adapter
* @return the size of items in adapter
*/
   @Override 
   public int getItemCount() {
       return items.size();
   }
   
   /**
* Bind View Holder with Items 
* @param holder: the view holder
* @param position : the current position
*/
   @Override
public void onBindViewHolder(RecyclerSimpleViewAdapter.ViewHolder holder, int position) {
    // find item by position
String item = items.get(position);
// save information in holder, we have one type in this adapter
       holder.primaryText.setText(item);
       holder.itemView.setTag(item);
       if ((position % 2) == 0) {
        holder.itemView.setBackgroundResource(R.color.color1);
       } else {
        holder.itemView.setBackgroundResource(R.color.color2);
       }

}
   
   /**
* 
* @author florian
* Class viewHolder
* Hold an textView
*/
   public static class ViewHolder extends RecyclerView.ViewHolder {
    // TextViex
       public TextView primaryText;
       
      /**
       * Constructor ViewHolder
       * @param itemView: the itemView
       */
       public ViewHolder(View itemView) {
           super(itemView);
           // link primaryText
           primaryText = (TextView) itemView.findViewById(android.R.id.text1);
       }
   }
}

III-D. Mise à jour

Pour mettre à jour l'affichage, nous gardons le même principe qu'auparavant par l'appel de la méthode asynchrone notifyDataSetChanged. De nouvelles méthodes sont apparues pour optimiser ce rafraîchissement en fonction du type de mise à jour (notifyItemChanged, notifyItemInserted, notifyItemRemoved, etc..).

Par exemple depuis le code (le fichier Main), nous aurons:

 
Sélectionnez
public void add(ViewModel item, int position) {
   items.add(position, item); // on insère le nouvel objet dans notre       liste d'article lié à l'adapter
   adapter.notifyItemInserted(position); // on notifie à l'adapter d'un rafraîchissement
}

III-E. Résultat

Voici ce nous obtenons, dans cet exemple nous avons rajouter un bouton ajouter en haut de la liste :

 
Image non disponible

IV. Les sous composants

Prenons un peu plus de temps pour comprendre ces nouveaux sous composants.

IV-A. L' Adapter

Cet adapter n'est plus basé sur la classe BaseAdapter mais sur le nouvel adapter RecyclerViewAdapter. Ce nouvel adapter amène une nouvelle gestion des ViewHolder en le forçant à utiliser ceux-ci. Celui -ci possède une liste d'articles (comme avec l' ArrayAdapter) et deux principales méthodes qui remplacent la méthode getView (du BaseAdapter). La principale différence est que les BaseAdapter géraient des vues là où les RecyclerViewAdapter gèrent les ViewHolder associés à ces vues. Cette différence est purement syntaxique pour vous obliger à utiliser le pattern du ViewHolder, en effet, le ViewHolder et sa vue sont liés par une liaison 1-1, ce qui signifie que l'on peut les considérer comme étant un seul et même objet. Ainsi, le RecyclerView possède deux méthodes à surcharger :

onCreateViewHolder(ViewGroup parent, int viewType) : Cette méthode nous permet de pouvoir créer la vue à afficher, et de retourner associé à ce composant, un objet ViewHolder.

onBindViewHolder(VIewHolder holder, int position) : Cette méthode , nous permet de pouvoir afficher les données de l'article (l'item) dans la sous vue courante.

Le principe du RecyclerViewAdapter est identique au BaseAdapter dans sa gestion des convertView. Ce sont les vues qui ont été recyclées et vous ont été renvoyées pour que vous les mettiez à jour. Cela évite de créer une vue par item et de dévaster les performances à cause du passage intempestif du GarabageCollector.

Image non disponible

Voici un schéma présentant l'enchaînement de ces fonctions :

 

Image non disponible

 

IV-B. Le LayoutManager

Voici ce que nous pouvons avoir de base comme LayoutManager.

LinearLayoutManager : Ce manager permet de créer les sous vues de manières linéaires ( horizontalement ou verticalement) . Cela revient à la classe ListVIew.

Image non disponible

Ce LayoutManager reste très simple et nous propose les deux orientations possibles (VERTICAL et HORIZONTAL), ainsi que la possibilité d'inverser l'ordre de l'affichage.

L'utilisation de ce layout se fera comme cela :

 
Sélectionnez
RecyclerView recyclerView = …;
LinearLayoutManager manager = new LinearLayoutManager(monContext, LinearLayoutManager.VERTICAL, false);
recyclerView.setLayoutManager(manager);

Par défaut nous avons un constructeur LinearLayoutManager(monContext) qui sera vertical et non inversé.


GridLayoutManager : Ce manager permet de créer les sous vues de manières à afficher une grille comme préalablement ce que faisait la classe GridView.

Image non disponible

Le GridLayoutManager est un layout basé sur le LinearLayoutManager, ce manager qui est plus complexe, nous permet de définir en plus le nombre de ligne ou colonne en fonction de l'orientation choisie que nous voulons. Il est possible de modifier ce nombre par la suite via l'appel à la méthode setSpanCount(spanCount) ;

Par défaut un article ne prend qu'un espace, mais vous avez la possibilité de changer ce nombre en fonction de la position de l'article via la classe SpanSizeLookup.


Voici un exemple pour utiliser un GridLayout :

 
Sélectionnez
RecyclerView recyclerView = …;
GridLayoutManager manager = new GridLayoutManager (monContext, 2, GridLayoutManager .VERTICAL, false);
manager .setSpanSizeLookup(new SpanSizeLookup() {
   
   @Override
   public int getSpanSize(int arg0) {
   return (arg0 % 3) == 0 ? 2 : 1;
   }
   });     
recyclerView.setLayoutManager(manager);

StaggeredGridLayoutManager : Ce manager permet de créer de sous vues dans une grille avec un décalage entre les articles.

Image non disponible

Le StaggeredLayoutManager est un layout simple qui affiche les éléments en fonction d'un nombre de colonne ou ligne établie. Celui-ci n'a pas besoin de connaître le contexte.

 
Sélectionnez
RecyclerView recyclerView = …;
StaggeredLayoutManager manager = new StaggeredLayoutManager (2, GridLayoutManager .VERTICAL);
recyclerView.setGapStrategy(StaggeredLayoutManager .GAP_HANDLING_NONE);
recyclerView.setLayoutManager(manager);

Custom: Vous pouvez également personnaliser vos LayoutManager.  Il existe des bibliothèques sur github comme celle-ci  qui vous propose différentes manières d'afficher vos sous vues.

https://github.com/lucasr/twoway-view

 
Image non disponible

IV-C. L' ItemAnimator

Pour gérer les animations des sous vues,  nous devons utiliser la classe ItemAnimator ou une classe dérivée. Par défaut la classe DefaultItemAnimator est utilisée par le RecyclerView.  Cette classe nous fournit un ensemble de méthodes nous permettant d'exécuter les animations. Ces méthodes nous permettent de pouvoir créer ou interagir avec les animations que nous aurons ajoutées.

animateAdd(RecyclerView.ViewHolder holder) :  Cette méthode est appelée lors d'un ajout d'un article.

animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromX, int fromY, int toX, int toY) : Cette méthode est appelée lors d'une modification d'un article.

animateMove(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) :  Cette méthode est appelée lors d'un déplacement d'un article.

animateRemove(RecyclerView.ViewHolder holder) : Cette méthode est appelée lors d'une suppression d'un article.

animateEnd(RecyclerView.ViewHolder holder): Cette méthode est appelé lorsqu'une animation se termine d'un article.

endAnimations() :  Cette méthode est appelé lorsque toutes les animations du RecyclerView sont terminées.

isRunning() : Cette méthode  nous permet de retourner si une animation est en cours.

runPendingAnimations() :  Cette méthode est appelé lorsque les animations sont en attente pour être exécutées.

Si vous avez crée une classe personnalisée, c'est à dire une classe qui surchargera les classes de bases (ItemAnimation ou DefaultItemAnimator), il vous suffira alors d'appeler cette fonction pour quelle soit prise en compte par le RecyclerView

 
Sélectionnez
recyclerView.setItemAnimator(MaPropreClasse);

IV-D. L' ItemDecoration

L' ItemDecoration nous permet de pouvoir “spécialiser” les sous vues, en ayant la possibilité de pouvoir interagir avec le canvas de ces sous vues avant et après leurs affichages. Pour résumer les séparateurs , etc.. se feront à partir de ce  sous composant. Nous retrouvons un ensemble de méthode.

getItemOffsets (Rect outRect, View view, RecyclerView parent, RecyclerView.State state): Cette méthode nous permet de créer un offset  (un nouvel espace) pour les articles, ce qui peut être intéressant par exemple pour les séparateurs.

onDraw(Canvas canvas, RecyclerView parent) : Cette méthode est appelé avant l'affichage des sous vues.

onDrawOver(Canvas canvas, RecyclerView parent) : Cette méthode est appelé après l'affichage des sous vues.

Si vous avez crée une classe personnalisée, il vous suffira alors d'appeler cette fonction pour quelle soit prise en compte par le RecyclerView.

 
Sélectionnez
recyclerView.addItemAnimator(MaPropreClasse);

Vous pouvez rajouter x ItemDecoration à votre RecyclerView.

V. Personnalisation

Prenons un exemple un peu plus complexe. Supposons que nous voulons personnaliser l'ensemble de notre composant. Comme exemple partons sur l'affichage d'un Header et un Footer depuis une grille avec comme article un affichage d'une image et son texte correspondant. Nous rajouterons également une animation sur l'ajout et suppression d'un article, ainsi qu'un séparateur entre les articles.

L'ajout des articles se fera via un bouton, la suppression via un clique sur l'article.

V-A. Le Layout

Le layout affichera seulement la liste ainsi que un bouton permettant d'ajouter un nouvel article :

 
Sélectionnez
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:paddingBottom="@dimen/activity_vertical_margin"
   android:paddingLeft="@dimen/activity_horizontal_margin"
   android:paddingRight="@dimen/activity_horizontal_margin"
   android:paddingTop="@dimen/activity_vertical_margin"
   android:orientation="vertical"
   tools:context="com.android2ee.recyclerview.ComplexActivity" >
   
   <Button android:id="@+id/myButtonComplexAdd"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:text="Add" />
      
<android.support.v7.widget.RecyclerView
   android:id="@+id/myListComplex"
   android:scrollbars="vertical"
   android:layout_width="match_parent"
   android:layout_height="match_parent"/>
  
</LinearLayout>

Nous aurons également besoin de créer un layout pour nos articles dû à leurs personnalisations, nous aurons alors une image et un texte dans ce layout, appelons le item.xml :

 
Sélectionnez
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:layout_margin="16dp"
   android:background="#FFFFFF"
   android:orientation="vertical" >

   <ImageView
       android:id="@+id/image"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:layout_centerInParent="true"
       android:scaleType="fitXY" />

   <TextView 
       android:id="@+id/title"
       android:layout_width="match_parent"
       android:layout_height="40dp"
       android:layout_alignParentBottom="true"
       android:background="#99918a8a"
       android:textColor="#FFFFFF"
       android:textSize="12dp"
       android:gravity="center"
       
       />

</RelativeLayout>

V-B. Le modèle des articles

Créons maintenant une classe nous permettant de contenir l'ensemble des informations nécessaires pour un article, appelons là ImageModel :

 
Sélectionnez
package com.android2ee.recyclerview.adapter.model;

/**
* 
* @author florian
* Class ImageModel
* POJO for ComplexRecyclerView
*/
public class ImageModel {

   /**
   * Title Text
   */
   String mTitle;
   /**
   * Resource Id of image
   */
   int mResId;

   /**
   * Get Res Id
   * @return mResId
   */
   public int getResId() {
      return mResId;
   }

   /**
   * Set Res Id
   * @param resId : the new resource Id
   */
   public void setResId(int resId) {
      mResId = resId;
   }

   /**
   * Get Title
   * @return mTitle
   */
   public String getTitle() {
      return mTitle;
   }

   /**
   * Set Title
   * @param title ; the new title
   */
   public void setTitle(String title) {
      mTitle = title;
   }


}

V-C. Le RecyclerViewAdapter

Maintenant attaquons nous à notre adapter, cet adapter devra donc prendre en compte notre classe ImageModel pour traiter les articles ainsi que le layout des articles. Insérons également le Header et Footer depuis l'adapter. Cela aurait pu se faire également depuis les ItemDecoration par le canvas.

Pour gérer les Headers et Footers depuis l'adapter nous utiliserons les types de vues, de ce fait nous saurons depuis la méthode onCreateViewHolder et onBindViewHolder de quel type de vue nous devons traiter. Pour gérer les types de vue utilisons la méthode  getItemViewType, qui nous permet de retourner le type de vue en fonction de la position de l'article, soit :

 
Sélectionnez
/**
* Get type of view by position (HEADER, FOOTER, STANDARD)
* @param position : the position of item
*/
@Override
   public int getItemViewType(int position) {
// test if header
       if (position == 0 && useHeader()) {
           return TYPE_HEADER;
       }
       // get the size of item in adapter
       int max = useHeader() ? mItems.size() + 1 : mItems.size();
       // test footer
       if (position == max && useFooter()) {
           return TYPE_FOOTER;
       }
       return TYPE_ADAPTERVIEW;
   }

La classe complète :

 
Sélectionnez
/**
* 
* @author florian
* RecyclerComplexViewAdapter provides and adpater custom of RecyclerView.Adapter
* which can display a footer and header on start and end of list
* to add and remove item you must be use function addItem and removeItem provide by this class
* 
*/
public class RecyclerComplexViewAdapter extends RecyclerView.Adapter<RecyclerComplexViewAdapter.ViewHolder> {

// type Header View
private static final int TYPE_HEADER = 0;
// type Footer View
private static final int TYPE_FOOTER = 1;
// Type standard
private static final int TYPE_ADAPTERVIEW = 2;

// list items
public List<ImageModel> mItems;
// Context
Context mContext;

// Resource Id of HeaderView
Integer mViewHeader;
// Resource Id of FooterView
Integer mViewFooter;

/**
* Constructor RecyclerComplexViewAdapter
* @param context : the context
* @param objects : the list of items
*/
public RecyclerComplexViewAdapter(Context context,List<ImageModel> objects) {
mContext = context;
mItems = objects;

// set null header and footer by default
mViewHeader = null;
mViewFooter = null;

}

/**
* Constructor RecyclerComplexViewAdapter
* @param context : the context
* @param objects : the list items
* @param viewHeaderId : the header resource id
* @param viewFooterId : the footer resource id
*/
public RecyclerComplexViewAdapter(Context context,List<ImageModel> objects, Integer viewHeaderId, Integer viewFooterId) {
mContext = context;
mItems = objects;

mViewHeader = viewHeaderId;
mViewFooter = viewFooterId;

}


/**
* 
* @author florian
* Class viewHolder
* Hold an imageView and textView
*/
static class ViewHolder extends RecyclerView.ViewHolder{
// The imageView
public  ImageView mImageView;
// the TextView
public  TextView mTextView;
// the rootView
public View rootView;

/**
* Constructor
* @param itemView
*/
public ViewHolder(View itemView) {
super(itemView);
// set RootView
rootView = itemView;
// link imageView
mImageView =(ImageView)itemView.findViewById(R.id.image);
// link TextView
mTextView =(TextView)itemView.findViewById(R.id.title);
}
}

/**
* Test if a Header resource is present
* @return if a resource is present
*/
private boolean useHeader() {
return mViewHeader != null;
}

/**
* Test if a Footer resource is present
* @return if a resource is present
*/
private boolean useFooter() {
return mViewFooter != null;
}

/**
* Get the size of items in adapter
* @return the size of items in adapter
*/
@Override
public int getItemCount() {
// get the size of items
int count = mItems.size();
// if header uses add 1
if (useHeader()) {
count ++;
}
// if footer uses add 1
if (useFooter()) {
count++;
}
return count;
}

/**
* Bind View Holder with Items 
* @param holder: the view holder
* @param position : the current position
*/
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
// test type of holder
if (holder.getItemViewType() == TYPE_HEADER) {
// nothing
Log.i("TAG", "POSITION TYPE_HEADER " + position);
} else if (holder.getItemViewType() == TYPE_FOOTER) {
// nothing
Log.i("TAG", "POSITION TYPE_FOOTER " + position);
       } else {
        // save information in holder of item
        Log.i("TAG", "POSITION" + position);
        // get item  if header user shift the position by 1
    ImageModel item = mItems.get(useHeader() ? position - 1 : position); 
    holder.mImageView.setBackgroundResource(item.getResId());
           holder.mTextView.setText("Position " + position);
       }
   }


/**
* Create View Holder by Type
* @param parent, the view parent
* @param viewType : the type of View
*/
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// get inflater
LayoutInflater inflater =    
(LayoutInflater) mContext.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
View convertView;
// in function of type return the right viewHolder
if (viewType == TYPE_HEADER  && useHeader()) {
// Header
convertView = inflater.inflate(mViewHeader, parent, false);
} else if (viewType == TYPE_FOOTER && useFooter()) {
// Footer
convertView = inflater.inflate(mViewFooter, parent, false);
       } else {
        // Standard
        convertView = inflater.inflate(R.layout.item, parent, false);
       }
       return new ViewHolder(convertView);

}


/**
* Get type of view by position (HEADER, FOOTER, STANDARD)
* @param position : the position of item
*/
@Override
   public int getItemViewType(int position) {
// test if header
       if (position == 0 && useHeader()) {
           return TYPE_HEADER;
       }
       // get the size of item in adapter
       int max = useHeader() ? mItems.size() + 1 : mItems.size();
       // test footer
       if (position == max && useFooter()) {
           return TYPE_FOOTER;
       }
       return TYPE_ADAPTERVIEW;
   }

/**
* Remove item in list and notify adapter
* @param position : the position of item to remove
*/
public void removeItem(int position) {
// if header or footer don't move it
int max = useHeader() ? mItems.size() + 1 : mItems.size();
if (useHeader() && position == 0) {
return;
}
// if header or footer don't move it
if (useFooter() && position == max) {
return;
}
// notify the adapter
notifyItemRemoved(position);
// remove item, if header used shift position by one
mItems.remove(useHeader() ? position -1 : position);
}

/**
* Add item in adapter
* @param item : the new item to add
* @param position : the position to add item
*/
public void addItem(ImageModel item, int position) {
// notify the adapter, shift position by one if header used
notifyItemInserted(useHeader() ? position + 1: position);
// add the item
   mItems.add(position, item);
}



}

V-D. Le LayoutManager

Maintenant que nous avons crée l'adapter, intéressons nous au LayoutManager. Il nous faut traiter le problème sur l'espace que le Header et le Footer doivent prendre, puisque nous sommes dans une grille, ces deux espaces doivent prendre l'ensemble de la ligne pour s'afficher. Pour se faire créons une nouvelle classe qui héritera de GridLayoutManager, et depuis la méthode getSpanSize de la classe SpanSizeLookUp, retournons l'espace nécessaire, soit la ligne pour le Header et le Footer, et l'espace de un pour les articles standards :

 
Sélectionnez
public int getSpanSize(int position) {
int max = mUseHeader ? mObjects.size() + 1 : mObjects.size();
// if header
if (position == 0 && mUseHeader) {
// return all spanCount
return mSpanCount;
// if footer
} else if (position == max && mUseFooter) {
// return all spanCount
return mSpanCount;
}
// standard return 1
return 1;
}

Le classe complète :

 
Sélectionnez
/**
* 
* @author florian
* Class MyGridLayoutManager
* Provides a way to display Header and Footer in a GridLayoutManager, and take all row or column
*
*/
public class MyGridLayoutManager extends GridLayoutManager {

// useHeader
private boolean mUseHeader;
// useFooter
private boolean mUseFooter;
// spanCount of the Grid
private int mSpanCount;
// list items
private List<ImageModel> mObjects;

/**
* New SpanSizeLookup, to treat Header and Footer size
*/
public SpanSizeLookup mySpanSizeLookup = new SpanSizeLookup() {

/**
* Get Span Size of the position
* @return the size of item
*/
@Override
public int getSpanSize(int position) {
int max = mUseHeader ? mObjects.size() + 1 : mObjects.size();
// if header
if (position == 0 && mUseHeader) {
// return all spanCount
return mSpanCount;
// if footer
} else if (position == max && mUseFooter) {
// return all spanCount
return mSpanCount;
}
// standard return 1
return 1;
}
};

/**
* Constructor MyGridLayoutManager
* @param context : the context
* @param spanCount : the spanCount of Grid
* @param objects : the list Items
*/
public MyGridLayoutManager(Context context, int spanCount, List<ImageModel> objects) {
super(context, spanCount);
mUseHeader = false;
mUseFooter = false;
mSpanCount = spanCount;
mObjects = objects;
// set mySpanSizeLookup to treat header and footer
setSpanSizeLookup(mySpanSizeLookup);
}

/**
* Display Header, if an header has displayed in adapter
* @param value, true of false
*/
public void displayHeader(boolean value) {
mUseHeader = value;
}

/**
* Display Footer, if an footer has displayed in adapter
* @param value, true of false
*/
public void displayFooter(boolean value) {
mUseFooter = value;
}

}

V-E. L' ItemDecoration

Pour les ItemDecorations nous allons insérer un séparateur entre les articles. Pour ce faire créons notre propre classe MyDividerItemDecoration, nous récupérerons le divider fourni dans les styles de l'application. Pour créer l'espace de ce divider, cela se fera depuis la méthode getItemOffsets en retournant l'objet outRect , soit :

 
Sélectionnez
/**
    * Return the dimension outRect for itemPosition and parent (Offset)
    * @param outRect : the new Rect
    * @param itemPosition: the position of item
    * @param parent : the view parent
    * 
    */
   @Override
   public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
    // depends orientation
       if (mOrientation == VERTICAL_LIST) {
        // return rect with height of divider
           outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
       } else {
        // return rect with width of divider
           outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
       }
   }

La classe complète :

 
Sélectionnez
/**
* 
* @author florian
* Class MyDividerItemDecoration
* Provides an separator between row or column in RecyclerView
*
*/
public class MyDividerItemDecoration extends RecyclerView.ItemDecoration {

/**
* Attributes of separator  android.R.attr.listDivider
*/
   private static final int[] ATTRS = new int[]{
           android.R.attr.listDivider
   };

   // static HORIZONTAL_LIST
   public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
   // static VERTICAL_LIST
   public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;

   /**
    * The drawable of separator to display
    */
   private Drawable mDivider;

   /**
    * The orientation of recyclerView HORIZONTAL_LIST, VERTICAL_LIST
    */
   private int mOrientation;

   /**
    * Constructor MyDividerItemDecoration
    * @param context : the context
    * @param orientation : the orientation HORIZONTAL_LIST, VERTICAL_LIST
    */
   public MyDividerItemDecoration(Context context, int orientation) {
    // get the attributes style
       final TypedArray a = context.obtainStyledAttributes(ATTRS);
       // get drawable in style
       mDivider = a.getDrawable(0);
       a.recycle();
       // set orientation
       setOrientation(orientation);
   }

   /**
    * Set Orientation
    * @param orientation : the new orientation HORIZONTAL_LIST or VERTICAL_LIST
    */
   public void setOrientation(int orientation) {
    // if not HORIZONTAL_LIST or VERTICAL_LIST , throw an exception
       if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
           throw new IllegalArgumentException("invalid orientation");
       }
       mOrientation = orientation;
   }

   @Override
   public void onDraw(Canvas c, RecyclerView parent) {
    // call the right draw depends the orientation
       if (mOrientation == VERTICAL_LIST) {
        // vertical
           drawVertical(c, parent);
       } else {
        // horizontal
           drawHorizontal(c, parent);
       }
   }

   /**
    * Draw Vertical, so horizontal separator
    * @param c : the canvas
    * @param parent : the view parent
    */
   public void drawVertical(Canvas c, RecyclerView parent) {
    // get padding of parents (Left and right
       final int left = parent.getPaddingLeft();
       final int right = parent.getWidth() - parent.getPaddingRight();
       
       // get the count of child
       final int childCount = parent.getChildCount();
       for (int i = 0; i < childCount; i++) {
           final View child = parent.getChildAt(i);
           // create layoutParams
           final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                   .getLayoutParams();
           // calculate new padding child
           final int top = child.getBottom() + params.bottomMargin;
           final int bottom = top + mDivider.getIntrinsicHeight();
           // set bounds
           mDivider.setBounds(left, top, right, bottom);
           // draw in canvas
           mDivider.draw(c);
       }
   }

   /**
    * Draw Horizontal, so vertical separator
    * @param c : the canvas
    * @param parent : the view parent
    */
   public void drawHorizontal(Canvas c, RecyclerView parent) {
    // get padding of parents (Top and Bottom
       final int top = parent.getPaddingTop();
       final int bottom = parent.getHeight() - parent.getPaddingBottom();

       // get the count of child
       final int childCount = parent.getChildCount();
       for (int i = 0; i < childCount; i++) {
           final View child = parent.getChildAt(i);
           // create layoutParams
           final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                   .getLayoutParams();
           // calculate new padding child
           final int left = child.getRight() + params.rightMargin;
           final int right = left + mDivider.getIntrinsicHeight();
        // set bounds
           mDivider.setBounds(left, top, right, bottom);
           // draw in canvas
           mDivider.draw(c);
       }
   }

   /**
    * Return the dimension outRect for itemPosition and parent (Offset)
    * @param outRect : the new Rect
    * @param itemPosition: the position of item
    * @param parent : the view parent
    * 
    */
   @Override
   public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
    // depends orientation
       if (mOrientation == VERTICAL_LIST) {
        // return rect with height of divider
           outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
       } else {
        // return rect with width of divider
           outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
       }
   }
}

Pour personnaliser le divider il vous suffit depuis le style de votre application de rajouter cette ligne :

 
Sélectionnez
<item name="android:listDivider">@drawable/divider</item>

V-F. L' ItemAnimator

Pour les ItemAnimator nous allons rajouter une animation de Flip sur l'ajout ou suppression des articles. Nous nous baserons sur la classe BaseItemAnimator de ce projet qui nous permettra de simplifier les pré et post traitements des animations :

https://github.com/wasabeef/recyclerview-animators/blob/master/animators/src/main/java/jp/wasabeef/recyclerview/animators/BaseItemAnimator.java.

Maintenant créons la classe FlipInAnimator qui elle contiendra les animations de flip que nous voulons, sur l'ajout et la suppression des articles. Soit depuis les méthodes animateRemoveImpl et animateAddImpl. La méthode preAnimateAdd nous permettant de pré-positionner l'animation avant de l'exécuter.

 
Sélectionnez
/**
* 
* @author florian
* Class FlipInAnimator 
* Provides an animation with flip in top 
* for a RecyclerView
*
*/
public class FlipInAnimator extends BaseItemAnimator {

   @Override
   protected void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
    // start animate rotationX 0 --> 90
       ViewCompat.animate(holder.itemView)
               .rotationX(90)
               .setDuration(getRemoveDuration())
               .setListener(new DefaultRemoveVpaListener(holder))
               .start();
       // Add holder in mRemoveAnimations
       mRemoveAnimations.add(holder);
   }

   @Override
   protected void preAnimateAdd(RecyclerView.ViewHolder holder) {
    // prepare animate 
       ViewCompat.setRotationX(holder.itemView, 90);
   }

   @Override
   protected void animateAddImpl(final RecyclerView.ViewHolder holder) {
    // start animate rotationX 90 -> 0
       ViewCompat.animate(holder.itemView)
               .rotationX(0)
               .setDuration(getAddDuration())
               .setListener(new DefaultAddVpaListener(holder)).start();
       // Add holder in mAddAnimations
       mAddAnimations.add(holder);
   }
}

V-G. Le fichier principal

Il ne nous reste plus qu'à rassembler tous ces sous composants crées depuis notre RecyclerView, votre Activity ou votre Fragment . Ne pas oublier de créer notre liste d'articles.

 
Sélectionnez
// create the recyclerView
RecyclerView recyclerView =  (RecyclerView) findViewById(R.id.myListComplex);
recyclerView.setHasFixedSize(false);
// add new Decoration, provide the separator between View (row in this case)
recyclerView.addItemDecoration(new MyDividerItemDecoration(this, MyDividerItemDecoration.VERTICAL_LIST));

// fill the list
items = new ArrayList<ImageModel>();
for (int i = 1; i < 29 ; i++) {
// create a new ImageModel
ImageModel img = new ImageModel();
img.setTitle("Image No " + i);
int drawableResourceId = this.getResources().getIdentifier("image"+String.valueOf(i), "drawable", this.getPackageName());
img.setResId(drawableResourceId);
// add in list
items.add(img);

}

// create the adapter with header and footer
// header represent by R.layout.header and footer by  R.layout.footer
adapter = new RecyclerComplexViewAdapter(ComplexActivity.this, items, R.layout.header,  R.layout.footer);    
// set the adapter
recyclerView.setAdapter(adapter);
// create my LayoutManager Custom
MyGridLayoutManager layoutManager = new MyGridLayoutManager(this, 2, items);
// set parameter of this layoutManager
layoutManager.displayHeader(true);
layoutManager.displayFooter(true);
// set this LayoutManager in RecyclerView
recyclerView.setLayoutManager(layoutManager);
// create and set FlipInAnimator in RecyclerView
recyclerView.setItemAnimator(new FlipInAnimator());

Nous nous sommes basé sur une Activité, voici la classe complète :

 
Sélectionnez
/**
* 
* @author florian
* Class ComplexActivity
* Present a complex RecyclerView 
* Grid with a header and footer
* with an animation
* with a custom adapter
* 
*/
public class ComplexActivity extends ActionBarActivity implements OnItemClickListener {

/**
* List of items
*/
List<ImageModel> items ;
/**
* The RecyclerView Adapter
*/
RecyclerComplexViewAdapter adapter;

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

// create the recyclerView
RecyclerView recyclerView =  (RecyclerView) findViewById(R.id.myListComplex);
recyclerView.setHasFixedSize(false);
// add new Decoration, provide the separator between View (row in this case)
recyclerView.addItemDecoration(new MyDividerItemDecoration(this, MyDividerItemDecoration.VERTICAL_LIST));
//  Add the RecyclerItemClickListener, to intercept click on his child
recyclerView.addOnItemTouchListener(new RecyclerItemClickListener(this, this));

// fill the list
items = new ArrayList<ImageModel>();
for (int i = 1; i < 29 ; i++) {
// create a new ImageModel
ImageModel img = new ImageModel();
img.setTitle("Image No " + i);
int drawableResourceId = this.getResources().getIdentifier("image"+String.valueOf(i), "drawable", this.getPackageName());
img.setResId(drawableResourceId);
// add in list
items.add(img);

}

// create the adapter with header and footer
// header represent by R.layout.header and footer by  R.layout.footer
adapter = new RecyclerComplexViewAdapter(ComplexActivity.this, items, R.layout.header,  R.layout.footer);    
// set the adapter
recyclerView.setAdapter(adapter);
// create my LayoutManager Custom
MyGridLayoutManager layoutManager = new MyGridLayoutManager(this, 2, items);
// set parameter of this layoutManager
layoutManager.displayHeader(true);
layoutManager.displayFooter(true);
// set this LayoutManager in RecyclerView
recyclerView.setLayoutManager(layoutManager);
// create and set FlipInAnimator in RecyclerView
recyclerView.setItemAnimator(new FlipInAnimator());

// Link Button Add 
Button buttonAdd = (Button) findViewById(R.id.myButtonComplexAdd);
buttonAdd.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View v) {

// max and min random possible for item (1-28 images)
int min = 1;
int max = 28;

// create a new item random
Random r = new Random();
int i = r.nextInt(max - min + 1) + min;
// create new item ImageModel
ImageModel img = new ImageModel();
img.setTitle("Image No " + i);
// get drawable by name image
int drawableResourceId = getResources().getIdentifier("image"+String.valueOf(i), "drawable", getPackageName());
img.setResId(drawableResourceId);
Log.i(getClass().getCanonicalName(), "Add Image Position" + "image"+String.valueOf(i) + adapter.getItemCount());
// add in list
add(img, adapter.mItems.size());
}
});

}

/**
* Add new item in list
* @param item : the new item
* @param position : the position of new item (insert)
*/
public void add(ImageModel item, int position) {
adapter.addItem(item, position);
}

/**
* Remove item in list
* @param item : the item to remove
*/
public void remove(ImageModel item) {
// find position of item in list
   int position = adapter.mItems.indexOf(item);
   // remove item
   adapter.removeItem(position);
}

/**
* Remove item by position in list
* @param position : the position of item to remove
*/
public void remove(int position) {
// remove item
adapter.removeItem(position);
}

/**
* Intercept Click on Item
* @view : the view has clicked
* @position : the position of view in list
*/
@Override
public void onItemClick(View view, int position) {
Log.i(getClass().getCanonicalName(), "Id found : " + adapter.getItemId(position));
// remove item
remove(position);
}

}

V-H. Résultat

Voici ce que nous obtenons :

Image non disponible
 

VI. Astuces pour les événements

Il manque la gestion majeure des événements sur les sous vues, pour l'instant seule l'interface onItemTouchListener n'est accessible. Pour ne pas perdre du temps à chaque utilisation de ce composant, créer une nouvelle interface qui nous permettra d'accéder directement aux autres événements (click, longclick, etc ..). Cette interface sera déduite du traitement de l'événement récupéré depuis les méthodes onInterceptTouhEvent et onTouchEvent de onItemTouchListener.

Un exemple pour le ClickListener :

 
Sélectionnez
public class RecyclerItemClickListener implements RecyclerView.OnItemTouchListener {
 private OnItemClickListener mListener;

 public interface OnItemClickListener {
   public void onItemClick(View view, int position);
 }

 GestureDetector mGestureDetector;

 public RecyclerItemClickListener(Context context, OnItemClickListener listener) {
   mListener = listener;
   mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
     @Override public boolean onSingleTapUp(MotionEvent e) {
       return true;
     }
   });
 }

 @Override public boolean onInterceptTouchEvent(RecyclerView view, MotionEvent e) {
   View childView = view.findChildViewUnder(e.getX(), e.getY());
   if (childView != null && mListener != null && mGestureDetector.onTouchEvent(e)) {
     mListener.onItemClick(childView, view.getChildPosition(childView));
   }
   return false;
 }

 @Override public void onTouchEvent(RecyclerView view, MotionEvent motionEvent) { }
 
}

On pourrait remonter d'autres événements à partir de cette classe ou en créer d'autres !

Pour utiliser cette classe il vous suffit de rajouter cette classe dans votre RecyclerView comme ceci :

 
Sélectionnez
//  Add the RecyclerItemClickListener, to intercept click on his child
recyclerView.addOnItemTouchListener(new RecyclerItemClickListener(MyContext, MyOnItemClickListener));

VII. Conclusion

Cette nouvelle classe nous apporte des méthodes communes pour gérer toutes sortes de listes ou grilles, avec une architecture propre acquise tout le long de l'utilisation des ListView et autres composants durant ces dernières années. (Utilisation des ViewHolder, etc ..).

Par contre il faut bien voir cette classe comme étant commune donc épurée par rapport aux anciens composants qui étaient utilisés spécifiquement. Par exemple toutes les gestions des propriétés que nous avons pour la ListView ne sont pas implémenté de base (divider, fade, etc..) . Ou tout simplement la multi sélection qui n'est pas implémentée de base :).

Cela demande alors de personnaliser cette classe en diverses sous classes nous permettant de récupérer notre ancien confort sur l'affichage des articles. Dans tous les cas ; nous retrouvons la philosophie de l'utilisation des adapters, ce qui ne devraient pas poser problème à ceux ayant déjà utiliser les anciens composants. Le passage à cette classe devrait se faire en douceur !


Le projet sous github : https://github.com/ffournier/RecyclerView.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2015 Florian. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.