image/svg+xml $ $ ing$ ing$ ces$ ces$ Res Res ea ea Res->ea ou ou Res->ou r r ea->r ch ch ea->ch r->ces$ r->ch ch->$ ch->ing$ T T T->ea ou->r

Disponibilité :

Principe de RecyclerView

Notification de modifications

Nécessité d'informer l'Adapter de la RecyclerView lorsque des items sont modifiés :

Sélection d'items sur RecyclerView

Sélection possible d'items (état d'activation) sur RecyclerView :

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());
    }
}

RecyclerView