Tutoriel sur la présentation du composant graphique « RecyclerView » sous Android

Auteur

Florian

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.

- Titre : Tutoriel sur la présentation du composant graphique « RecyclerView » sous Android

- Auteur : Florian

- Parution : 7 février 2015

- Licence : 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 et 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.

Sommaire

- I. Histoire

- II. RecyclerView

- II-A. Vue d'ensemble

- III. Exemple simple, une liste

- III-A. Le layout

- III-B. Le fichier MainActivity

- III-C. Le fichier RecyclerViewAdapter

- III-D. Mise à jour

- III-E. Résultat

- IV. Les sous composants

- IV-A. L' Adapter

- IV-B. Le LayoutManager

- IV-C. L' ItemAnimator

- IV-D. L' ItemDecoration

- V. Personnalisation

- V-A. Le Layout

- V-B. Le modèle des articles

- V-C. Le RecyclerViewAdapter

- V-D. Le LayoutManager

- V-E. L' ItemDecoration

- V-F. L' ItemAnimator

- V-G. Le fichier principal

- V-H. Résultat

- VI. Astuces pour les événements

- VII. Conclusion

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.

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:

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:

1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

2. xmlns:tools="http://schemas.android.com/tools"

3. xmlns:app="http://schemas.android.com/apk/res-auto"

4. android:layout_width="match_parent"

5. android:layout_height="match_parent"

6. android:paddingBottom="@dimen/activity_vertical_margin"

7. android:paddingLeft="@dimen/activity_horizontal_margin"

8. android:paddingRight="@dimen/activity_horizontal_margin"

9. android:paddingTop="@dimen/activity_vertical_margin"

10. android:orientation="vertical"

11. tools:context="com.android2ee.recyclerview.SimpleActivity" >

13. <Button android:id="@+id/myButtonSimpleAdd"

14. android:layout_width="match_parent"

15. android:layout_height="wrap_content"

16. android:text="Add" />

18. <android.support.v7.widget.RecyclerView

19. android:id="@+id/myListSimple"

20. android:scrollbars="vertical"

21. android:layout_width="match_parent"

22. android:layout_height="match_parent"/>

24. </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).

RecyclerView recyclerView = (RecyclerView) findViewById(R.id.myListSimple);

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

// 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.

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

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.

1. /**

2. *

3. * @author florian

4. * RecyclerSimpleViewAdapter provide a simple RecyclerViewAdapter

5. *

6. */

7. public class RecyclerSimpleViewAdapter extends RecyclerView.Adapter<RecyclerSimpleViewAdapter.ViewHolder> {

9. /**

10. * List items

11. */

12. private List<String> items;

13. /**

14. * the resource id of item Layout

15. */

16. private int itemLayout;

18. /**

19. * Constructor RecyclerSimpleViewAdapter

20. * @param items : the list items

21. * @param itemLayout : the resource id of itemView

22. */

23. public RecyclerSimpleViewAdapter(List<String> items, int itemLayout) {

24. this.items = items;

25. this.itemLayout = itemLayout;

26. }

28. /**

29. * Create View Holder by Type

30. * @param parent, the view parent

31. * @param viewType : the type of View

32. */

33. @Override

34. public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

35. // get inflater and get view by resource id itemLayout

36. View v = LayoutInflater.from(parent.getContext()).inflate(itemLayout, parent, false);

37. // return ViewHolder with View

38. return new ViewHolder(v);

39. }

41. /**

42. * Get the size of items in adapter

43. * @return the size of items in adapter

44. */

45. @Override

46. public int getItemCount() {

47. return items.size();

48. }

50. /**

51. * Bind View Holder with Items

52. * @param holder: the view holder

53. * @param position : the current position

54. */

55. @Override

56. public void onBindViewHolder(RecyclerSimpleViewAdapter.ViewHolder holder, int position) {

57. // find item by position

58. String item = items.get(position);

59. // save information in holder, we have one type in this adapter

60. holder.primaryText.setText(item);

61. holder.itemView.setTag(item);

62. if ((position % 2) == 0) {

63. holder.itemView.setBackgroundResource(R.color.color1);

64. } else {

65. holder.itemView.setBackgroundResource(R.color.color2);

66. }

68. }

70. /**

71. *

72. * @author florian

73. * Class viewHolder

74. * Hold an textView

75. */

76. public static class ViewHolder extends RecyclerView.ViewHolder {

77. // TextViex

78. public TextView primaryText;

80. /**

81. * Constructor ViewHolder

82. * @param itemView: the itemView

83. */

84. public ViewHolder(View itemView) {

85. super(itemView);

86. // link primaryText

87. primaryText = (TextView) itemView.findViewById(android.R.id.text1);

88. }

89. }

90. }

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:

1. public void add(ViewModel item, int position) {

2. items.add(position, item); // on insère le nouvel objet dans notre       liste d'article lié à l'adapter

3. adapter.notifyItemInserted(position); // on notifie à l'adapter d'un rafraîchissement

4. }

III-E - Résultat

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

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.

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

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.

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 :

1. RecyclerView recyclerView = …;

2. LinearLayoutManager manager = new LinearLayoutManager(monContext, LinearLayoutManager.VERTICAL, false);

3. 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.

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 :

1. RecyclerView recyclerView = …;

2. GridLayoutManager manager = new GridLayoutManager (monContext, 2, GridLayoutManager .VERTICAL, false);

3. manager .setSpanSizeLookup(new SpanSizeLookup() {

5. @Override

6. public int getSpanSize(int arg0) {

7. return (arg0 % 3) == 0 ? 2 : 1;

8. }

9. });

10. recyclerView.setLayoutManager(manager);

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

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.

1. RecyclerView recyclerView = …;

2. StaggeredLayoutManager manager = new StaggeredLayoutManager (2, GridLayoutManager .VERTICAL);

3. recyclerView.setGapStrategy(StaggeredLayoutManager .GAP_HANDLING_NONE);

4. 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

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

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.

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 :

1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

2. xmlns:tools="http://schemas.android.com/tools"

3. xmlns:app="http://schemas.android.com/apk/res-auto"

4. android:layout_width="match_parent"

5. android:layout_height="match_parent"

6. android:paddingBottom="@dimen/activity_vertical_margin"

7. android:paddingLeft="@dimen/activity_horizontal_margin"

8. android:paddingRight="@dimen/activity_horizontal_margin"

9. android:paddingTop="@dimen/activity_vertical_margin"

10. android:orientation="vertical"

11. tools:context="com.android2ee.recyclerview.ComplexActivity" >

13. <Button android:id="@+id/myButtonComplexAdd"

14. android:layout_width="match_parent"

15. android:layout_height="wrap_content"

16. android:text="Add" />

18. <android.support.v7.widget.RecyclerView

19. android:id="@+id/myListComplex"

20. android:scrollbars="vertical"

21. android:layout_width="match_parent"

22. android:layout_height="match_parent"/>

24. </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 :

1. <?xml version="1.0" encoding="utf-8"?>

2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

3. android:layout_width="match_parent"

4. android:layout_height="wrap_content"

5. android:layout_margin="16dp"

6. android:background="#FFFFFF"

7. android:orientation="vertical" >

9. <ImageView

10. android:id="@+id/image"

11. android:layout_width="match_parent"

12. android:layout_height="match_parent"

13. android:layout_centerInParent="true"

14. android:scaleType="fitXY" />

16. <TextView

17. android:id="@+id/title"

18. android:layout_width="match_parent"

19. android:layout_height="40dp"

20. android:layout_alignParentBottom="true"

21. android:background="#99918a8a"

22. android:textColor="#FFFFFF"

23. android:textSize="12dp"

24. android:gravity="center"

26. />

28. </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 :

1. package com.android2ee.recyclerview.adapter.model;

3. /**

4. *

5. * @author florian

6. * Class ImageModel

7. * POJO for ComplexRecyclerView

8. */

9. public class ImageModel {

11. /**

12. * Title Text

13. */

14. String mTitle;

15. /**

16. * Resource Id of image

17. */

18. int mResId;

20. /**

21. * Get Res Id

22. * @return mResId

23. */

24. public int getResId() {

25. return mResId;

26. }

28. /**

29. * Set Res Id

30. * @param resId : the new resource Id

31. */

32. public void setResId(int resId) {

33. mResId = resId;

34. }

36. /**

37. * Get Title

38. * @return mTitle

39. */

40. public String getTitle() {

41. return mTitle;

42. }

44. /**

45. * Set Title

46. * @param title ; the new title

47. */

48. public void setTitle(String title) {

49. mTitle = title;

50. }

53. }

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 :

1. /**

2. * Get type of view by position (HEADER, FOOTER, STANDARD)

3. * @param position : the position of item

4. */

5. @Override

6. public int getItemViewType(int position) {

7. // test if header

8. if (position == 0 && useHeader()) {

9. return TYPE_HEADER;

10. }

11. // get the size of item in adapter

12. int max = useHeader() ? mItems.size() + 1 : mItems.size();

13. // test footer

14. if (position == max && useFooter()) {

15. return TYPE_FOOTER;

16. }

17. return TYPE_ADAPTERVIEW;

18. }

La classe complète :

1. /**

2. *

3. * @author florian

4. * RecyclerComplexViewAdapter provides and adpater custom of RecyclerView.Adapter

5. * which can display a footer and header on start and end of list

6. * to add and remove item you must be use function addItem and removeItem provide by this class

7. *

8. */

9. public class RecyclerComplexViewAdapter extends RecyclerView.Adapter<RecyclerComplexViewAdapter.ViewHolder> {

11. // type Header View

12. private static final int TYPE_HEADER = 0;

13. // type Footer View

14. private static final int TYPE_FOOTER = 1;

15. // Type standard

16. private static final int TYPE_ADAPTERVIEW = 2;

18. // list items

19. public List<ImageModel> mItems;

20. // Context

21. Context mContext;

23. // Resource Id of HeaderView

24. Integer mViewHeader;

25. // Resource Id of FooterView

26. Integer mViewFooter;

28. /**

29. * Constructor RecyclerComplexViewAdapter

30. * @param context : the context

31. * @param objects : the list of items

32. */

33. public RecyclerComplexViewAdapter(Context context,List<ImageModel> objects) {

34. mContext = context;

35. mItems = objects;

37. // set null header and footer by default

38. mViewHeader = null;

39. mViewFooter = null;

41. }

43. /**

44. * Constructor RecyclerComplexViewAdapter

45. * @param context : the context

46. * @param objects : the list items

47. * @param viewHeaderId : the header resource id

48. * @param viewFooterId : the footer resource id

49. */

50. public RecyclerComplexViewAdapter(Context context,List<ImageModel> objects, Integer viewHeaderId, Integer viewFooterId) {

51. mContext = context;

52. mItems = objects;

54. mViewHeader = viewHeaderId;

55. mViewFooter = viewFooterId;

57. }

60. /**

61. *

62. * @author florian

63. * Class viewHolder

64. * Hold an imageView and textView

65. */

66. static class ViewHolder extends RecyclerView.ViewHolder{

67. // The imageView

68. public  ImageView mImageView;

69. // the TextView

70. public  TextView mTextView;

71. // the rootView

72. public View rootView;

74. /**

75. * Constructor

76. * @param itemView

77. */

78. public ViewHolder(View itemView) {

79. super(itemView);

80. // set RootView

81. rootView = itemView;

82. // link imageView

83. mImageView =(ImageView)itemView.findViewById(R.id.image);

84. // link TextView

85. mTextView =(TextView)itemView.findViewById(R.id.title);

86. }

87. }

89. /**

90. * Test if a Header resource is present

91. * @return if a resource is present

92. */

93. private boolean useHeader() {

94. return mViewHeader != null;

95. }

97. /**

98. * Test if a Footer resource is present

99. * @return if a resource is present

100. */

101. private boolean useFooter() {

102. return mViewFooter != null;

103. }

105. /**

106. * Get the size of items in adapter

107. * @return the size of items in adapter

108. */

109. @Override

110. public int getItemCount() {

111. // get the size of items

112. int count = mItems.size();

113. // if header uses add 1

114. if (useHeader()) {

115. count ++;

116. }

117. // if footer uses add 1

118. if (useFooter()) {

119. count++;

120. }

121. return count;

122. }

124. /**

125. * Bind View Holder with Items

126. * @param holder: the view holder

127. * @param position : the current position

128. */

129. @Override

130. public void onBindViewHolder(ViewHolder holder, int position) {

131. // test type of holder

132. if (holder.getItemViewType() == TYPE_HEADER) {

133. // nothing

134. Log.i("TAG", "POSITION TYPE_HEADER " + position);

135. } else if (holder.getItemViewType() == TYPE_FOOTER) {

136. // nothing

137. Log.i("TAG", "POSITION TYPE_FOOTER " + position);

138. } else {

139. // save information in holder of item

140. Log.i("TAG", "POSITION" + position);

141. // get item  if header user shift the position by 1

142. ImageModel item = mItems.get(useHeader() ? position - 1 : position);

143. holder.mImageView.setBackgroundResource(item.getResId());

144. holder.mTextView.setText("Position " + position);

145. }

146. }

149. /**

150. * Create View Holder by Type

151. * @param parent, the view parent

152. * @param viewType : the type of View

153. */

154. @Override

155. public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

156. // get inflater

157. LayoutInflater inflater =

158. (LayoutInflater) mContext.getSystemService(Activity.LAYOUT_INFLATER_SERVICE);

159. View convertView;

160. // in function of type return the right viewHolder

161. if (viewType == TYPE_HEADER  && useHeader()) {

162. // Header

163. convertView = inflater.inflate(mViewHeader, parent, false);

164. } else if (viewType == TYPE_FOOTER && useFooter()) {

165. // Footer

166. convertView = inflater.inflate(mViewFooter, parent, false);

167. } else {

168. // Standard

169. convertView = inflater.inflate(R.layout.item, parent, false);

170. }

171. return new ViewHolder(convertView);

173. }

176. /**

177. * Get type of view by position (HEADER, FOOTER, STANDARD)

178. * @param position : the position of item

179. */

180. @Override

181. public int getItemViewType(int position) {

182. // test if header

183. if (position == 0 && useHeader()) {

184. return TYPE_HEADER;

185. }

186. // get the size of item in adapter

187. int max = useHeader() ? mItems.size() + 1 : mItems.size();

188. // test footer

189. if (position == max && useFooter()) {

190. return TYPE_FOOTER;

191. }

192. return TYPE_ADAPTERVIEW;

193. }

195. /**

196. * Remove item in list and notify adapter

197. * @param position : the position of item to remove

198. */

199. public void removeItem(int position) {

200. // if header or footer don't move it

201. int max = useHeader() ? mItems.size() + 1 : mItems.size();

202. if (useHeader() && position == 0) {

203. return;

204. }

205. // if header or footer don't move it

206. if (useFooter() && position == max) {

207. return;

208. }

209. // notify the adapter

210. notifyItemRemoved(position);

211. // remove item, if header used shift position by one

212. mItems.remove(useHeader() ? position -1 : position);

213. }

215. /**

216. * Add item in adapter

217. * @param item : the new item to add

218. * @param position : the position to add item

219. */

220. public void addItem(ImageModel item, int position) {

221. // notify the adapter, shift position by one if header used

222. notifyItemInserted(useHeader() ? position + 1: position);

223. // add the item

224. mItems.add(position, item);

225. }

229. }

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 :

1. public int getSpanSize(int position) {

2. int max = mUseHeader ? mObjects.size() + 1 : mObjects.size();

3. // if header

4. if (position == 0 && mUseHeader) {

5. // return all spanCount

6. return mSpanCount;

7. // if footer

8. } else if (position == max && mUseFooter) {

9. // return all spanCount

10. return mSpanCount;

11. }

12. // standard return 1

13. return 1;

14. }

Le classe complète :

1. /**

2. *

3. * @author florian

4. * Class MyGridLayoutManager

5. * Provides a way to display Header and Footer in a GridLayoutManager, and take all row or column

6. *

7. */

8. public class MyGridLayoutManager extends GridLayoutManager {

10. // useHeader

11. private boolean mUseHeader;

12. // useFooter

13. private boolean mUseFooter;

14. // spanCount of the Grid

15. private int mSpanCount;

16. // list items

17. private List<ImageModel> mObjects;

19. /**

20. * New SpanSizeLookup, to treat Header and Footer size

21. */

22. public SpanSizeLookup mySpanSizeLookup = new SpanSizeLookup() {

24. /**

25. * Get Span Size of the position

26. * @return the size of item

27. */

28. @Override

29. public int getSpanSize(int position) {

30. int max = mUseHeader ? mObjects.size() + 1 : mObjects.size();

31. // if header

32. if (position == 0 && mUseHeader) {

33. // return all spanCount

34. return mSpanCount;

35. // if footer

36. } else if (position == max && mUseFooter) {

37. // return all spanCount

38. return mSpanCount;

39. }

40. // standard return 1

41. return 1;

42. }

43. };

45. /**

46. * Constructor MyGridLayoutManager

47. * @param context : the context

48. * @param spanCount : the spanCount of Grid

49. * @param objects : the list Items

50. */

51. public MyGridLayoutManager(Context context, int spanCount, List<ImageModel> objects) {

52. super(context, spanCount);

53. mUseHeader = false;

54. mUseFooter = false;

55. mSpanCount = spanCount;

56. mObjects = objects;

57. // set mySpanSizeLookup to treat header and footer

58. setSpanSizeLookup(mySpanSizeLookup);

59. }

61. /**

62. * Display Header, if an header has displayed in adapter

63. * @param value, true of false

64. */

65. public void displayHeader(boolean value) {

66. mUseHeader = value;

67. }

69. /**

70. * Display Footer, if an footer has displayed in adapter

71. * @param value, true of false

72. */

73. public void displayFooter(boolean value) {

74. mUseFooter = value;

75. }

77. }

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 :

1. /**

2. * Return the dimension outRect for itemPosition and parent (Offset)

3. * @param outRect : the new Rect

4. * @param itemPosition: the position of item

5. * @param parent : the view parent

6. *

7. */

8. @Override

9. public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {

10. // depends orientation

11. if (mOrientation == VERTICAL_LIST) {

12. // return rect with height of divider

13. outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());

14. } else {

15. // return rect with width of divider

16. outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);

17. }

18. }

La classe complète :

1. /**

2. *

3. * @author florian

4. * Class MyDividerItemDecoration

5. * Provides an separator between row or column in RecyclerView

6. *

7. */

8. public class MyDividerItemDecoration extends RecyclerView.ItemDecoration {

10. /**

11. * Attributes of separator  android.R.attr.listDivider

12. */

13. private static final int[] ATTRS = new int[]{

14. android.R.attr.listDivider

15. };

17. // static HORIZONTAL_LIST

18. public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;

19. // static VERTICAL_LIST

20. public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;

22. /**

23. * The drawable of separator to display

24. */

25. private Drawable mDivider;

27. /**

28. * The orientation of recyclerView HORIZONTAL_LIST, VERTICAL_LIST

29. */

30. private int mOrientation;

32. /**

33. * Constructor MyDividerItemDecoration

34. * @param context : the context

35. * @param orientation : the orientation HORIZONTAL_LIST, VERTICAL_LIST

36. */

37. public MyDividerItemDecoration(Context context, int orientation) {

38. // get the attributes style

39. final TypedArray a = context.obtainStyledAttributes(ATTRS);

40. // get drawable in style

41. mDivider = a.getDrawable(0);

42. a.recycle();

43. // set orientation

44. setOrientation(orientation);

45. }

47. /**

48. * Set Orientation

49. * @param orientation : the new orientation HORIZONTAL_LIST or VERTICAL_LIST

50. */

51. public void setOrientation(int orientation) {

52. // if not HORIZONTAL_LIST or VERTICAL_LIST , throw an exception

53. if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {

54. throw new IllegalArgumentException("invalid orientation");

55. }

56. mOrientation = orientation;

57. }

59. @Override

60. public void onDraw(Canvas c, RecyclerView parent) {

61. // call the right draw depends the orientation

62. if (mOrientation == VERTICAL_LIST) {

63. // vertical

64. drawVertical(c, parent);

65. } else {

66. // horizontal

67. drawHorizontal(c, parent);

68. }

69. }

71. /**

72. * Draw Vertical, so horizontal separator

73. * @param c : the canvas

74. * @param parent : the view parent

75. */

76. public void drawVertical(Canvas c, RecyclerView parent) {

77. // get padding of parents (Left and right

78. final int left = parent.getPaddingLeft();

79. final int right = parent.getWidth() - parent.getPaddingRight();

81. // get the count of child

82. final int childCount = parent.getChildCount();

83. for (int i = 0; i < childCount; i++) {

84. final View child = parent.getChildAt(i);

85. // create layoutParams

86. final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child

87. .getLayoutParams();

88. // calculate new padding child

89. final int top = child.getBottom() + params.bottomMargin;

90. final int bottom = top + mDivider.getIntrinsicHeight();

91. // set bounds

92. mDivider.setBounds(left, top, right, bottom);

93. // draw in canvas

94. mDivider.draw(c);

95. }

96. }

98. /**

99. * Draw Horizontal, so vertical separator

100. * @param c : the canvas

101. * @param parent : the view parent

102. */

103. public void drawHorizontal(Canvas c, RecyclerView parent) {

104. // get padding of parents (Top and Bottom

105. final int top = parent.getPaddingTop();

106. final int bottom = parent.getHeight() - parent.getPaddingBottom();

108. // get the count of child

109. final int childCount = parent.getChildCount();

110. for (int i = 0; i < childCount; i++) {

111. final View child = parent.getChildAt(i);

112. // create layoutParams

113. final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child

114. .getLayoutParams();

115. // calculate new padding child

116. final int left = child.getRight() + params.rightMargin;

117. final int right = left + mDivider.getIntrinsicHeight();

118. // set bounds

119. mDivider.setBounds(left, top, right, bottom);

120. // draw in canvas

121. mDivider.draw(c);

122. }

123. }

125. /**

126. * Return the dimension outRect for itemPosition and parent (Offset)

127. * @param outRect : the new Rect

128. * @param itemPosition: the position of item

129. * @param parent : the view parent

130. *

131. */

132. @Override

133. public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {

134. // depends orientation

135. if (mOrientation == VERTICAL_LIST) {

136. // return rect with height of divider

137. outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());

138. } else {

139. // return rect with width of divider

140. outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);

141. }

142. }

143. }

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

<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.

1. /**

2. *

3. * @author florian

4. * Class FlipInAnimator

5. * Provides an animation with flip in top

6. * for a RecyclerView

7. *

8. */

9. public class FlipInAnimator extends BaseItemAnimator {

11. @Override

12. protected void animateRemoveImpl(final RecyclerView.ViewHolder holder) {

13. // start animate rotationX 0 --> 90

14. ViewCompat.animate(holder.itemView)

15. .rotationX(90)

16. .setDuration(getRemoveDuration())

17. .setListener(new DefaultRemoveVpaListener(holder))

18. .start();

19. // Add holder in mRemoveAnimations

20. mRemoveAnimations.add(holder);

21. }

23. @Override

24. protected void preAnimateAdd(RecyclerView.ViewHolder holder) {

25. // prepare animate

26. ViewCompat.setRotationX(holder.itemView, 90);

27. }

29. @Override

30. protected void animateAddImpl(final RecyclerView.ViewHolder holder) {

31. // start animate rotationX 90 -> 0

32. ViewCompat.animate(holder.itemView)

33. .rotationX(0)

34. .setDuration(getAddDuration())

35. .setListener(new DefaultAddVpaListener(holder)).start();

36. // Add holder in mAddAnimations

37. mAddAnimations.add(holder);

38. }

39. }

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.

1. // create the recyclerView

2. RecyclerView recyclerView =  (RecyclerView) findViewById(R.id.myListComplex);

3. recyclerView.setHasFixedSize(false);

4. // add new Decoration, provide the separator between View (row in this case)

5. recyclerView.addItemDecoration(new MyDividerItemDecoration(this, MyDividerItemDecoration.VERTICAL_LIST));

7. // fill the list

8. items = new ArrayList<ImageModel>();

9. for (int i = 1; i < 29 ; i++) {

10. // create a new ImageModel

11. ImageModel img = new ImageModel();

12. img.setTitle("Image No " + i);

13. int drawableResourceId = this.getResources().getIdentifier("image"+String.valueOf(i), "drawable", this.getPackageName());

14. img.setResId(drawableResourceId);

15. // add in list

16. items.add(img);

18. }

20. // create the adapter with header and footer

21. // header represent by R.layout.header and footer by  R.layout.footer

22. adapter = new RecyclerComplexViewAdapter(ComplexActivity.this, items, R.layout.header,  R.layout.footer);

23. // set the adapter

24. recyclerView.setAdapter(adapter);

25. // create my LayoutManager Custom

26. MyGridLayoutManager layoutManager = new MyGridLayoutManager(this, 2, items);

27. // set parameter of this layoutManager

28. layoutManager.displayHeader(true);

29. layoutManager.displayFooter(true);

30. // set this LayoutManager in RecyclerView

31. recyclerView.setLayoutManager(layoutManager);

32. // create and set FlipInAnimator in RecyclerView

33. recyclerView.setItemAnimator(new FlipInAnimator());

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

1. /**

2. *

3. * @author florian

4. * Class ComplexActivity

5. * Present a complex RecyclerView

6. * Grid with a header and footer

7. * with an animation

8. * with a custom adapter

9. *

10. */

11. public class ComplexActivity extends ActionBarActivity implements OnItemClickListener {

13. /**

14. * List of items

15. */

16. List<ImageModel> items ;

17. /**

18. * The RecyclerView Adapter

19. */

20. RecyclerComplexViewAdapter adapter;

22. @Override

23. protected void onCreate(Bundle savedInstanceState) {

24. super.onCreate(savedInstanceState);

25. setContentView(R.layout.activity_complex);

27. // create the recyclerView

28. RecyclerView recyclerView =  (RecyclerView) findViewById(R.id.myListComplex);

29. recyclerView.setHasFixedSize(false);

30. // add new Decoration, provide the separator between View (row in this case)

31. recyclerView.addItemDecoration(new MyDividerItemDecoration(this, MyDividerItemDecoration.VERTICAL_LIST));

32. //  Add the RecyclerItemClickListener, to intercept click on his child

33. recyclerView.addOnItemTouchListener(new RecyclerItemClickListener(this, this));

35. // fill the list

36. items = new ArrayList<ImageModel>();

37. for (int i = 1; i < 29 ; i++) {

38. // create a new ImageModel

39. ImageModel img = new ImageModel();

40. img.setTitle("Image No " + i);

41. int drawableResourceId = this.getResources().getIdentifier("image"+String.valueOf(i), "drawable", this.getPackageName());

42. img.setResId(drawableResourceId);

43. // add in list

44. items.add(img);

46. }

48. // create the adapter with header and footer

49. // header represent by R.layout.header and footer by  R.layout.footer

50. adapter = new RecyclerComplexViewAdapter(ComplexActivity.this, items, R.layout.header,  R.layout.footer);

51. // set the adapter

52. recyclerView.setAdapter(adapter);

53. // create my LayoutManager Custom

54. MyGridLayoutManager layoutManager = new MyGridLayoutManager(this, 2, items);

55. // set parameter of this layoutManager

56. layoutManager.displayHeader(true);

57. layoutManager.displayFooter(true);

58. // set this LayoutManager in RecyclerView

59. recyclerView.setLayoutManager(layoutManager);

60. // create and set FlipInAnimator in RecyclerView

61. recyclerView.setItemAnimator(new FlipInAnimator());

63. // Link Button Add

64. Button buttonAdd = (Button) findViewById(R.id.myButtonComplexAdd);

65. buttonAdd.setOnClickListener(new OnClickListener() {

67. @Override

68. public void onClick(View v) {

70. // max and min random possible for item (1-28 images)

71. int min = 1;

72. int max = 28;

74. // create a new item random

75. Random r = new Random();

76. int i = r.nextInt(max - min + 1) + min;

77. // create new item ImageModel

78. ImageModel img = new ImageModel();

79. img.setTitle("Image No " + i);

80. // get drawable by name image

81. int drawableResourceId = getResources().getIdentifier("image"+String.valueOf(i), "drawable", getPackageName());

82. img.setResId(drawableResourceId);

83. Log.i(getClass().getCanonicalName(), "Add Image Position" + "image"+String.valueOf(i) + adapter.getItemCount());

84. // add in list

85. add(img, adapter.mItems.size());

86. }

87. });

89. }

91. /**

92. * Add new item in list

93. * @param item : the new item

94. * @param position : the position of new item (insert)

95. */

96. public void add(ImageModel item, int position) {

97. adapter.addItem(item, position);

98. }

100. /**

101. * Remove item in list

102. * @param item : the item to remove

103. */

104. public void remove(ImageModel item) {

105. // find position of item in list

106. int position = adapter.mItems.indexOf(item);

107. // remove item

108. adapter.removeItem(position);

109. }

111. /**

112. * Remove item by position in list

113. * @param position : the position of item to remove

114. */

115. public void remove(int position) {

116. // remove item

117. adapter.removeItem(position);

118. }

120. /**

121. * Intercept Click on Item

122. * @view : the view has clicked

123. * @position : the position of view in list

124. */

125. @Override

126. public void onItemClick(View view, int position) {

127. Log.i(getClass().getCanonicalName(), "Id found : " + adapter.getItemId(position));

128. // remove item

129. remove(position);

130. }

132. }

V-H - Résultat

Voici ce que nous obtenons :

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 :

1. public class RecyclerItemClickListener implements RecyclerView.OnItemTouchListener {

2. private OnItemClickListener mListener;

4. public interface OnItemClickListener {

5. public void onItemClick(View view, int position);

6. }

8. GestureDetector mGestureDetector;

10. public RecyclerItemClickListener(Context context, OnItemClickListener listener) {

11. mListener = listener;

12. mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {

13. @Override public boolean onSingleTapUp(MotionEvent e) {

14. return true;

15. }

16. });

17. }

19. @Override public boolean onInterceptTouchEvent(RecyclerView view, MotionEvent e) {

20. View childView = view.findChildViewUnder(e.getX(), e.getY());

21. if (childView != null && mListener != null && mGestureDetector.onTouchEvent(e)) {

22. mListener.onItemClick(childView, view.getChildPosition(childView));

23. }

24. return false;

25. }

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

29. }

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 :

//  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.