Android - RecyclerView Swipe to Edit/Delete

October 13, 2020 ยท 4 minute read

I recently had to add swipe mechanism to a RecyclerView. We were working with two options. One was to edit a row item and the other was to delete that item. This guide will cover both these options and the best thing is you won't have to use any external libraries.

The main class that we'd be dealing with today is the ItemTouchHelper class. This class is resposible for adding swipe and drag & drop support to RecyclerView.

SwipeHelper

We will create a class called SwipeHelper, which would extend the ItemTouchHelper class. The class would look something like this

  
public abstract class SwipeHelper extends ItemTouchHelper.SimpleCallback {

    int buttonWidth;
    private RecyclerView recyclerView;
    private List<MyButton> buttonList;
    private GestureDetector gestureDetector;
    private int swipePosition = -1;
    private float swipeThreshold = 0.5f;
    private Map<Integer, List<MyButton>> buttonBuffer;
    private Queue<Integer> removeQueue;

    private GestureDetector.SimpleOnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener() {
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            for(MyButton button: buttonList) {
                if(button.onClick(e.getX(), e.getY())) {
                    break;
                }
            }
            return true;
        }
    };

    private View.OnTouchListener onTouchListener = new View.OnTouchListener() {
        @Override
        public boolean onTouch(View view, MotionEvent motionEvent) {
            if(swipePosition < 0) {
                return false;
            }
            Point point = new Point((int) motionEvent.getRawX(), (int) motionEvent.getRawY());

            RecyclerView.ViewHolder swipeViewHolder = recyclerView.findViewHolderForAdapterPosition(swipePosition);

            if(swipeViewHolder != null) {
                View swipedItem = swipeViewHolder.itemView;
                Rect rect = new Rect();
                swipedItem.getGlobalVisibleRect(rect);

                if(motionEvent.getAction() ==  MotionEvent.ACTION_DOWN || motionEvent.getAction() == MotionEvent.ACTION_UP || motionEvent.getAction() == MotionEvent.ACTION_MOVE) {
                    if(rect.top < point.y && rect.bottom > point.y) {
                        gestureDetector.onTouchEvent(motionEvent);
                    }
                    else {
                        removeQueue.add(swipePosition);
                        swipePosition = -1;
                        recoverSwipedItem();
                    }
                }
                return false;
            }
            return false;
        }
    };

    public SwipeHelper(Context context, RecyclerView recyclerView, int buttonWidth) {
        super(0, ItemTouchHelper.LEFT);
        this.recyclerView = recyclerView;
        this.buttonList = new ArrayList<>();
        this.gestureDetector = new GestureDetector(context, gestureListener);
        this.recyclerView.setOnTouchListener(onTouchListener);
        this.buttonBuffer = new HashMap<>();
        this.buttonWidth = buttonWidth;

        removeQueue = new LinkedList() {

            @Override
            public boolean add(Integer integer) {
                if(contains(integer))
                    return false;
                else
                    return super.add(integer);
            }
        };
        
        attachSwipe();
    }

    private void attachSwipe() {
        ItemTouchHelper itemTouchHelper = new ItemTouchHelper(this);
        itemTouchHelper.attachToRecyclerView(recyclerView);
    }

    private synchronized void recoverSwipedItem() {
        while(!removeQueue.isEmpty()) {
            int pos = removeQueue.poll();
            if(pos > -1)
                recyclerView.getAdapter().notifyItemChanged(pos);
        }
    }

    public class MyButton {
        private int  imageResId, color, pos;
        private RectF clickRegion;
        private MyButtonClickListener listener;
        private Context context;
        private Resources resources;


        public MyButton(Context context, int imageResId, int color, MyButtonClickListener listener) {
            this.context = context;
            this.imageResId = imageResId;
            this.color = color;
            this.listener = listener;
            this.resources = context.getResources();
        }

        public boolean onClick(float x, float y)  {
            if(clickRegion != null && clickRegion.contains(x, y)) {
                listener.onClick(pos);
                return true;
            }
            
            return false;
        }

        public void onDraw(Canvas c, RectF  rectF, int pos) {
            Paint p = new Paint();
            p.setColor(color);
            c.drawRect(rectF, p);
            p.setColor(Color.WHITE);

            Rect r = new Rect();
            float cHeight  = rectF.height();
            float cWidth = rectF.width();
            p.setTextAlign(Paint.Align.LEFT);
           
            Drawable d = ContextCompat.getDrawable(context, imageResId);
            Bitmap bitmap = drawableToBitmap(d);
              
            float bw = bitmap.getWidth()/2;
            float bh = bitmap.getHeight()/2;
            c.drawBitmap(bitmap, ((rectF.left+rectF.right)/2)-bw, ((rectF.top+rectF.bottom)/2 - bh), p);
        
            clickRegion =  rectF;
            this.pos = pos;
        }
    }

    private Bitmap drawableToBitmap(Drawable d) {
        if(d instanceof BitmapDrawable) {
            return Bitmap.createScaledBitmap(((BitmapDrawable) d).getBitmap(), 160, 160, true);
        }
        Bitmap bitmap = Bitmap.createBitmap(d.getIntrinsicWidth(), d.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        d.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
        d.draw(canvas);

        return bitmap;
    }

    @Override
    public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
        return false;
    }

    @Override
    public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
        int pos = viewHolder.getAdapterPosition();
        if(swipePosition != pos)
            removeQueue.add(swipePosition);
        swipePosition = pos;
        if(buttonBuffer.containsKey(swipePosition))
            buttonList = buttonBuffer.get(swipePosition);
        else
            buttonList.clear();
        buttonBuffer.clear();
        swipeThreshold = 0.5f * buttonList.size() * buttonWidth;
        recoverSwipedItem();
    }

    public float getSwipeThreshold(RecyclerView.ViewHolder viewHolder) {
        return swipeThreshold;
    }

    @Override
    public float getSwipeEscapeVelocity(float defaultValue) {
        return 0.1f * defaultValue;
    }

    @Override
    public float getSwipeVelocityThreshold(float defaultValue) {
        return 5.0f * defaultValue;
    }

    @Override
    public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
        int pos = viewHolder.getAdapterPosition();
        float translationX = dX;
        View itemView = viewHolder.itemView;
        if(pos < 0) {
            swipePosition = pos;
            return;
        }
        if(actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
          
            if(dX < 0) {
                
                List buffer = new ArrayList<>();
                if(!buttonBuffer.containsKey(pos)) {
                    instantiateMyButton(viewHolder, buffer);
                    buttonBuffer.put(pos, buffer);
                }
                else {
                    buffer = buttonBuffer.get(pos);
                }
                translationX = dX * buffer.size() * buttonWidth / itemView.getWidth();
                drawButton(c, itemView, buffer, pos , translationX);
             }
        }
        super.onChildDraw(c, recyclerView, viewHolder, translationX, dY, actionState, isCurrentlyActive);
    }

    private void drawButton(Canvas c, View itemView, List buffer, int pos, float translationX) {
        float right = itemView.getRight();
        float dButtonWidth = -1 * translationX / buffer.size();
        for(MyButton button: buffer) {
            float left = right - dButtonWidth;
            button.onDraw(c, new RectF(left, itemView.getTop(), right, itemView.getBottom()), pos);
            right = left;
        }
    }

    public abstract void instantiateMyButton(RecyclerView.ViewHolder viewHolder, List buffer);
}
  

You will notice the MyButton class inside SwipeHelper. This class is responsible for creating the edit and delete buttons. I kept the scope to show buttons with images, you can also easily add text in the same class as well.

Using the Swipe Helper

Now we will look at how we can integrate the SwipeHelper class with our recyclerview.

  
  SwipeHelper swipeHelper = new SwipeHelper(getContext(), recyclerView, 250) {
      @Override
      public void instantiateMyButton(RecyclerView.ViewHolder viewHolder, List buffer) {
          
          buffer.add(new MyButton(getContext(), R.drawable.delete_white, Color.parseColor("#F24C05"),
              new MyButtonClickListener() {
                 @Override
                  public void onClick(int pos) {
                    adapter.callDeleteFunction(pos);
                  }
              }
          ));

          buffer.add(new MyButton(getContext(), R.drawable.edit_green_button_white, Color.parseColor("#B9D40B"),
              new MyButtonClickListener() {
                  @Override
                  public void onClick(int pos) {
                    adapter.callEditFunction(pos);
                  }
              }
          ));
      }
  };
  

We create the two buttons using preferred drawables and background colors, call the respective functions in our adapter class. If for example after editing a row item you'd like to close the swipe, you can do something like the following:

    
      recyclerView.getAdapter().notifyItemChanged(pos);
    
  

This was a very simple, getting started guide on how we can add swipe functionality to a RecyclerView, in the future we will look at more advanced solutions and possibly adding drap & drop as well.