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.