Disponibilité :
- RecyclerView introduit depuis l'API 21 (Lollipop)
- Bibliothèque de rétro-compatibilité proposée avec androidx
Principe de RecyclerView
- Permet l'affichage d'une collection d'items pouvant évoluer dynamiquement au cours de l'exécution
-
Visualisation de chaque item par une vue personnalisée
-
Fourniture d'une classe héritant de RecyclerView.Adapter avec méthodes pour créer un ViewHolder et le mettre à jour
- Utilisation d'une classe héritant de ViewHolder pour représenter la vue recyclable d'un item
- Méthode int getItemCount() : obtention du nombre d'items à afficher
- Méthode onCreateViewHolder(ViewGroup parent, int viewType) : création initiale du ViewHolder mutable (avec initialisation de la vue)
-
Méthode onBindViewHolder(ViewHolder holder, int position) : réutilisation d'un ViewHolder pour afficher un item (correspondant à la position donnée)
- Appel de setters sur les composants graphiques de la vue pour les mettre à jour
- Mise en place de listener pour réagir à certains événements (clic) sur la vue
-
Fourniture d'une classe héritant de RecyclerView.Adapter avec méthodes pour créer un ViewHolder et le mettre à jour
-
Fourniture d'un LayoutManager pour indiquer comment les items sont placés les uns par rapport aux autres
-
LinearLayoutManager(Context context, int orientation, boolean reverseLayout) : affichage vertical avec défilement des items les uns en dessous des autres (équivalent au déprécié ListView)
- orientation : GridLayoutManager.HORIZONTAL ou GridLayoutManager.VERTICAL
- reverseLayout : pour afficher les éléments dans l'ordre inverse (du dernier au premier)
-
GridLayoutManager(Context context, int spanCount, int orientation, boolean reverseLayout) : affichage des items sur une grille (équivalent au déprécié GridView)
- spanCount : nombre de lignes ou colonnes
- StaggeredGridLayoutManager(int spanCount, int orientation) : affichage sur une grille en quinquonce
- Possibilité de créer son LayoutManager personnalisé en héritant de la classe et en redéfinissant des méthodes
-
LinearLayoutManager(Context context, int orientation, boolean reverseLayout) : affichage vertical avec défilement des items les uns en dessous des autres (équivalent au déprécié ListView)
-
Fourniture d'un ItemAnimator pour animer les modifications réalisées (ajout, suppression, déplacement d'éléments...)
- Utilisation de DefaultItemAnimator par défaut
- Possibilité de créer son ItemAnimator par héritage et redéfinition de méthode
Notification de modifications
Nécessité d'informer l'Adapter de la RecyclerView lorsque des items sont modifiés :
- adapter.notifyItemChanged(int position) : changement de l'item à la position indiquée
- adapter.notifyItemInserted(int position) : item inséré à la position indiquée
- adapter.notifyItemMoved(int from, int to) : item déplacé d'une position à une autre
- adapter.notifyItemRemoved(int position) : item supprimé à une position
Sélection d'items sur RecyclerView
Sélection possible d'items (état d'activation) sur RecyclerView :
- Gestion assez complexe des éléments activés
- Marche à suivre décrite dans la documentation Android
Exemple : affichage de photos de desserts
Réalisation d'une activité avec un RecyclerView affichant des desserts (versions d'Android) avec leur nom.
La classe Dessert représente un dessert avec son nom et le chemin vers son image. Les desserts sont chargés depuis un répertoire d'assets de l'application.
// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License package fr.upem.coursand.desserts; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import androidx.annotation.NonNull; import android.util.Log; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** Android dessert picture */ public class Dessert implements Comparable<Dessert>, Serializable { private final String name; private final String assetLocation; /** Bitmap representation of the picture for the dessert */ private transient Bitmap cachedBitmap; public Dessert(String name, String assetLocation) { this.name = name; this.assetLocation = assetLocation; } public String getName() { return name; } /** Get the bitmap for the picture in the assets */ public Bitmap getBitmap(Context context) { if (cachedBitmap == null) { InputStream is = null; try { is = context.getAssets().open(assetLocation); cachedBitmap = BitmapFactory.decodeStream(is); } catch (IOException e) { try { is.close(); } catch (IOException ignored) { } } } return cachedBitmap; } @Override public int compareTo(@NonNull Dessert dessert) { return name.compareTo(dessert.name); } public static final String DESSERTS_ASSETS_LOCATION = "desserts"; /** Load all the desserts from the assets */ public static List<Dessert> loadAllDesserts(Context context, boolean shuffled) { List<Dessert> l = new ArrayList<>(); try { for (String filename : context.getAssets().list(DESSERTS_ASSETS_LOCATION)) { String name = filename.substring(0, filename.indexOf(".")); l.add(new Dessert(name, DESSERTS_ASSETS_LOCATION + "/" + filename)); } } catch (IOException e) { Log.e(Dessert.class.getName(), e.getMessage(), e); } if (shuffled) Collections.shuffle(l); return l; } }
Un layout est réalisé pour l'affichage de chaque item de dessert.
<?xml version="1.0" encoding="utf-8"?> <!-- Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center" android:gravity="center" android:orientation="vertical"> <ImageView android:id="@+id/dessertImageView" android:layout_width="match_parent" android:layout_height="0dp" android:gravity="center" android:scaleType="centerInside" android:layout_weight="1" /> <TextView android:id="@+id/dessertLabel" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="0" android:gravity="center" android:text="TextView" /> </LinearLayout>
Pour manipuler les items par le RecyclerView, nous écrivons un Adapter chargé d'initialiser la vue de l'item (chargée depuis le layout XML) ainsi que de configurer la vue (recyclable) avec les informations de l'item. Nous ajoutons une méthode pour modifier l'emplacement d'un item dans la liste.
// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License package fr.upem.coursand.desserts; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import java.util.List; import fr.upem.coursand.R; /** Adapter for RecyclerView displaying dessert pictures */ public class DessertAdapter extends RecyclerView.Adapter<DessertAdapter.ViewHolder> { private List<Dessert> desserts; public class ViewHolder extends RecyclerView.ViewHolder { private ImageView imageView; private TextView textView; public ViewHolder(@NonNull View itemView) { super(itemView); imageView = itemView.findViewById(R.id.dessertImageView); textView = itemView.findViewById(R.id.dessertLabel); } private void update(Dessert dessert) { imageView.setImageBitmap(dessert.getBitmap(imageView.getContext())); textView.setText(dessert.getName()); // adding an action when we click on the textview (display a toast) textView.setOnClickListener(view -> { Toast t = Toast.makeText(textView.getContext(), "click on " + dessert.getName(), Toast.LENGTH_SHORT); t.show(); }); } } public DessertAdapter(List<Dessert> desserts) { super(); this.desserts = desserts; } public List<Dessert> getDesserts() { return desserts; } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { return new ViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item_dessert, viewGroup, false)); } @Override public void onBindViewHolder(@NonNull ViewHolder viewHolder, int i) { viewHolder.update(desserts.get(i)); } @Override public int getItemCount() { return desserts.size(); } /** Exchange two items in the list */ public void moveItem(int a, int b) { if (b == a); else { Dessert tmp = desserts.remove(a); desserts.add(b, tmp); notifyItemMoved(a, b); } } }
Enfin, nous écrivons l'activité chargée d'afficher le RecyclerView. Cette activité comporte un formulaire permettant de changer des propriétés du LayoutManager à utiliser (à chaque changement un nouveau LayoutManager est créé et appliqué). On peut ainsi choisir le span count ainsi que l'orientation du RecyclerView.
// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License package fr.upem.coursand.desserts; import android.app.Activity; import android.os.Bundle; import android.os.Handler; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.StaggeredGridLayoutManager; import android.text.Editable; import android.text.TextWatcher; import android.util.Log; import android.view.View; import android.widget.CheckBox; import android.widget.EditText; import android.widget.RadioButton; import android.widget.RadioGroup; import fr.upem.coursand.R; /** An activity displaying Android dessert demonstrating the use of {@link RecyclerView} */ public class DessertActivity extends Activity { private RecyclerView recyclerView; private DessertAdapter dessertAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_dessert); recyclerView = findViewById(R.id.recyclerView); dessertAdapter = new DessertAdapter(Dessert.loadAllDesserts(this, true)); installListeners(); recyclerView.setAdapter(dessertAdapter); updateLayoutManager(null); } /** Install all the listeners to refresh when needed the LayoutManager */ private void installListeners() { // for spanEditText EditText spanEditText = findViewById(R.id.spanEditText); spanEditText.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { } @Override public void afterTextChanged(Editable editable) { } @Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { updateLayoutManager(spanEditText); } }); // for staggeredCheckBox CheckBox staggeredCheckBox = findViewById(R.id.staggeredCheckBox); staggeredCheckBox.setOnCheckedChangeListener((compoundButton, b) -> updateLayoutManager(staggeredCheckBox)); // for radio group RadioGroup orientationRadioGroup = findViewById(R.id.orientationRadioGroup); orientationRadioGroup.setOnCheckedChangeListener((radioGroup, i) -> updateLayoutManager(orientationRadioGroup)); } /** Create the layout manager used by the recyclerView according to the user options */ private RecyclerView.LayoutManager createLayoutManager() { RecyclerView.LayoutManager lm = null; int span = 1; // default valus is 1 try { span = Integer.parseInt(((EditText)findViewById(R.id.spanEditText)).getText().toString()); if (span <= 0) throw new RuntimeException("Invalid number: " + span); } catch (Exception e) { Log.e(getClass().getName(), "Text in the spanEditText is not a valid integer"); } boolean staggered = ((CheckBox)findViewById(R.id.staggeredCheckBox)).isChecked(); int orientation = 0; if (((RadioButton)findViewById(R.id.horizontalRadioButton)).isChecked()) orientation = LinearLayoutManager.HORIZONTAL; else if (((RadioButton)findViewById(R.id.verticalRadioButton)).isChecked()) orientation = LinearLayoutManager.VERTICAL; // Create the layout manager if (span == 1) // a classical linear layout is enough return new LinearLayoutManager(this, orientation, false); else if (! staggered) return new GridLayoutManager(this, span, orientation, false); else return new StaggeredGridLayoutManager(span, orientation); } private void updateLayoutManager(View v) { recyclerView.setLayoutManager(createLayoutManager()); } public void sort(View v) { new SelectionSort(new Handler(), dessertAdapter, 2000).start(); findViewById(R.id.sortButton).setEnabled(false); } }
Le bouton sort de l'activité permet de trier les desserts du RecyclerView par ordre de version d'Android. Pour cela nous avons réalisé une classe SelectionSort chargée de cette tâche qui (comme son nom l'indique) réalise un tri par sélection. Cela consiste à trouver le plus petit élément pour le déplacer en position 0, puis de continuer en trouvant le 2ème plus petit élément pour le placer en position 1... A chaque déplacement nous appelons la méthode moveItem de DessertAdapter qui elle-même appelle notifyItemMoved afin de prévenir le RecyclerView du changement : une animation est alors mise en œuvre pour représenter le déplacement.
// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License package fr.upem.coursand.desserts; import android.os.Handler; import java.util.List; /** * Simulate a selection sort using the adapter. */ public class SelectionSort { private DessertAdapter adapter; private Handler handler; private int period; private int step = 0; public SelectionSort(Handler handler, DessertAdapter adapter, int period) { this.adapter = adapter; this.handler = handler; this.period = period; } private Runnable getStepRunnable() { return stepRunnable; } private final Runnable stepRunnable = () -> { List<Dessert> dessertList = adapter.getDesserts(); if (step >= dessertList.size()-1) return; // the end // find min int min = step; for (int i = step+1; i < adapter.getDesserts().size(); i++) if (dessertList.get(i).compareTo(dessertList.get(min)) < 0) min = i; if (min != step) { adapter.moveItem(min, step); step++; handler.postDelayed(getStepRunnable(), period); } else { step++; handler.post(getStepRunnable()); } }; public void start() { handler.post(getStepRunnable()); } public void stop() { handler.removeCallbacks(getStepRunnable()); } }
