Loupe

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 :

  1. Android : Optimiser l'affichage de vos listes en utilisant un RecyclerView
  2. Ajouter des sticky headers sur vos RecyclerView
  3. Ajouter des en-têtes repliables (collapsible headers) sur vos RecyclerView
  4. Créer des RecyclerView horizontaux, les fameux "horizontal RecyclerView"
  5. Comment créer un RecyclerView infini / endless ?
  6. Afficher une liste d'éléments en mode grille avec un RecyclerView
  7. Cet article.
  8. Scroller via du code dans un RecyclerView vertical
  9. 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

Screenshot_2019-03-26-10-05-57-210_com.jonathanantoine.TVST.png

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éthode OnItemSwippedFromRightAsync 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 :)

Photo de profil

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus