Android : ajouter des swipes actions sur un RecyclerView vertical
Dans cet article, nous verrons comment ajouter des swipes actions (actions de balayage en français) sur un RecyclerView affichant une liste d'éléments.
Voici un exemple en image de ce que l'on souhaite faire dans une application de gestion de séries :
Pour rappel, cet article fait partie d'une série sur le composant RecyclerView d'Android :
- Android : Optimiser l'affichage de vos listes en utilisant un RecyclerView
- Ajouter des sticky headers sur vos RecyclerView
- Ajouter des en-têtes repliables (collapsible headers) sur vos RecyclerView
- Créer des RecyclerView horizontaux, les fameux "horizontal RecyclerView"
- Comment créer un RecyclerView infini / endless ?
- Afficher une liste d'éléments en mode grille avec un RecyclerView
- Cet article.
- Scroller via du code dans un RecyclerView vertical
- Suspens.
Mise en place théorique
Pour mettre cela en place, nous allons utiliser la classe ItemTouchHelper
du SDK que l'on peut attacher à un RecyclerView. Cette dernière prend en paramètre de son constructeur une instance d'une classe dérivant de ItemTouchHelper.Callback
. Cette classe sera alors appelée par le SDK lorsque différentes gestuelles sont réalisées par l'utilisateur sur un ViewHolder : drag et swipe. Sous le capot, cette classe dérive d'ItemDecoration
que l'on a déjà utilisé pour dessiner des sticky headers.
Le plus gros du travail sera donc de créer notre propre ItemTouchHelper.Callback
qui va indiquer à quelles gestuelles il souhaite réagir et dessiner par dessus le ViewHolder ciblé sur un Canvas mis à notre disposition. Cela sera donc à nous de faire le rendu du "bloc vert" qui se déplace depuis la droite mais pas besoin de faire la translation du ViewHolder
.
Création de l'infrastructure de base
Avant d'aller plus loin, on va commencer par créer quelques interfaces qui nous aideront plus tard :
ISwipableItemsAdapter
: une interface implémentée par notre adapter qui définira une méthodeOnItemSwippedFromRightAsync
indiquant qu'un Swipe a été fait sur un de ses éléments.ISwipableItem
: une interface implémentée par nos ViewHolder pour indiquer s'ils peuvent être swipés (CanBeActivated
), ainsi que le texte à afficher sur le "bloc vert".
public interface ISwipableItemsAdapter { Task OnItemSwippedFromRightAsync(int position); } public interface ISwipableItem { string Text { get; } bool CanBeActivated { get; } }
Configuration des gestuelles suivies
Nous allons maintenant ajouter à notre projet une classe SwipeToDeleteCallBack
dérivant d'ItemTouchHelper.Callback
.
L'initialisation via son constructeur est une étape importante : il faut créer une fois pour toutes les différentes couleurs et peintures utilisées pour les dessins ultérieurs. On demande aussi le RecyclerView en paramètre du constructeur pour pouvoir en extraire plus tard son adapter.
public class SwipeToDeleteCallback : ItemTouchHelper.Callback { private readonly RecyclerView _recyclerView; private readonly ColorDrawable _background = new ColorDrawable(); private readonly Color _backgroundColor = new Color(ContextCompat.GetColor(Application.Context, ThemeHelper.SelectedColor)); private readonly Color _textColor = new Color(ContextCompat.GetColor(Application.Context, ThemeHelper.ForegroundColor)); private readonly Paint _clearPaint = new Paint(); private readonly Paint _textPaint = new Paint(); public SwipeToDeleteCallback(RecyclerView recyclerView) { // couleur transparente pour effacer le rendu var mode =new PorterDuffXfermode( PorterDuff.Mode.Clear); _clearPaint.SetXfermode(mode); // couleur du texte _textPaint.Color = _textColor; // Drawable utilisé pour la couleur de fond _background.Color = _backgroundColor; // définition de la taille du texte à 16dp var scale = Application.Context.Resources.DisplayMetrics.Density; _textPaint.TextSize = (int)(16 * scale + 0.5f); _recyclerView = recyclerView; } }
Une fois ceci fait, on va pouvoir ajouter les surcharges de configuration :
- La propriété
IsLongPressDragEnabled
indique si l'on s'intéresse au déplacement suite à un appui long : cela ne sera pas notre cas. - La méthode
GetMovementFlags
retourne un flag indiquant les directions des gestuelles à "suivre". On retourne 0 si l'on souhaite qu'aucune gestuelle ne soit possible pour l'élément en question.
public override bool IsLongPressDragEnabled => false; public override int GetMovementFlags( RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { if (!(viewHolder is ISwipableItem realHolder) || !realHolder.CanBeActivated) { return 0; } int swipeFlags = ItemTouchHelper.Start; return MakeMovementFlags(0, swipeFlags); }
Réaction à un Swipe
La méthode OnSwiped
est appelée lorsqu'un swipe est detecté, il ne nous reste donc plus qu'à récupérer l'adapter, le caster en ISwipableItemsAdapter
pour lui indiquer qu'un swipe a été fait.
public override void OnSwiped( RecyclerView.ViewHolder viewHolder, int direction) { var itemTouchHelperAdapter = _recyclerView.GetAdapter() as ISwipableItemsAdapter; var position = viewHolder.AdapterPosition; itemTouchHelperAdapter?.OnItemSwippedFromRightAsync(position); }
Le code de l'adapter est assez simple. Il faut bien penser à indiquer à l'adapter que l'élément a changé une fois que le Swipe doit être "masqué".
public async Task OnItemSwippedFromRightAsync(int position) { var item = GetItemForPosition(position); // action sur swipe // masquage du "swipe" this.NotifyItemChanged(position); }
Affichage du Swipe
Il reste le plus intéressant à faire : afficher visuellement la barre de Swipe. Pour cela, il faut surcharger la méthode OnChildDraw
et dessiner sur le Canvas passé en paramètre. Nous avons aussi accès au ViewHolder pour récupérer le texte à afficher, ainsi qu'aux différentes informations de contexte (delta de translation en cours, est-ce que la gestuelle est actuellement active, etc.). Si l'élément n'est pas actif ou qu'il ne peut pas être swipé, on se contente de "vider" le Canvas.
public override void OnChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, bool isCurrentlyActive) { var itemView = viewHolder.ItemView; var isCanceled = Math.Abs(dX) < 0.003 && !isCurrentlyActive; // vidage du Canvas if (isCanceled || !(viewHolder is ISwipableItem realHolder) || !realHolder.CanBeActivated) { c?.DrawRect( itemView.Right + dX, itemView.Top, itemView.Right, itemView.Bottom, _clearPaint); base.OnChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); return; } // On configure le Drawable pour correspondre à la partie droite _background.SetBounds( (int)(itemView.Right + dX), itemView.Top, itemView.Right, itemView.Bottom); _background.Draw(c); // dessin du texte var itemHeight = itemView.Bottom - itemView.Top; c.DrawText(realHolder.Text, (int)(itemView.Right + dX + _textPaint.TextSize), itemView.Top + (itemHeight + _textPaint.TextSize) / 2, _textPaint); base.OnChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); }
Branchement sur le RecyclerView
Le branchement final est alors un jeu d'enfant :
var callback = new SwipeToDeleteCallback(_recyclerView); var itemTouchHelper = new ItemTouchHelper(callback); itemTouchHelper.AttachToRecyclerView(_recyclerView);
Et voilà, happy coding :)
Commentaires