Android - Show Button After Swiping Recyclerview Item

RecyclerView ItemTouchHelper Buttons on Swipe

I struggled with the same issue, and tried to find a solution online. Most of the solutions use a two-layer approach (one layer view item, another layer buttons), but I want to stick with ItemTouchHelper only. At the end, I came up with a worked solution. Please check below.

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;

public abstract class SwipeHelper extends ItemTouchHelper.SimpleCallback {

public static final int BUTTON_WIDTH = YOUR_WIDTH_IN_PIXEL_PER_BUTTON
private RecyclerView recyclerView;
private List<UnderlayButton> buttons;
private GestureDetector gestureDetector;
private int swipedPos = -1;
private float swipeThreshold = 0.5f;
private Map<Integer, List<UnderlayButton>> buttonsBuffer;
private Queue<Integer> recoverQueue;

private GestureDetector.SimpleOnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener(){
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
for (UnderlayButton button : buttons){
if(button.onClick(e.getX(), e.getY()))
break;
}

return true;
}
};

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

RecyclerView.ViewHolder swipedViewHolder = recyclerView.findViewHolderForAdapterPosition(swipedPos);
View swipedItem = swipedViewHolder.itemView;
Rect rect = new Rect();
swipedItem.getGlobalVisibleRect(rect);

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

public SwipeHelper(Context context, RecyclerView recyclerView) {
super(0, ItemTouchHelper.LEFT);
this.recyclerView = recyclerView;
this.buttons = new ArrayList<>();
this.gestureDetector = new GestureDetector(context, gestureListener);
this.recyclerView.setOnTouchListener(onTouchListener);
buttonsBuffer = new HashMap<>();
recoverQueue = new LinkedList<Integer>(){
@Override
public boolean add(Integer o) {
if (contains(o))
return false;
else
return super.add(o);
}
};

attachSwipe();
}


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

@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
int pos = viewHolder.getAdapterPosition();

if (swipedPos != pos)
recoverQueue.add(swipedPos);

swipedPos = pos;

if (buttonsBuffer.containsKey(swipedPos))
buttons = buttonsBuffer.get(swipedPos);
else
buttons.clear();

buttonsBuffer.clear();
swipeThreshold = 0.5f * buttons.size() * BUTTON_WIDTH;
recoverSwipedItem();
}

@Override
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(Canvas c, RecyclerView recyclerView, 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){
swipedPos = pos;
return;
}

if(actionState == ItemTouchHelper.ACTION_STATE_SWIPE){
if(dX < 0) {
List<UnderlayButton> buffer = new ArrayList<>();

if (!buttonsBuffer.containsKey(pos)){
instantiateUnderlayButton(viewHolder, buffer);
buttonsBuffer.put(pos, buffer);
}
else {
buffer = buttonsBuffer.get(pos);
}

translationX = dX * buffer.size() * BUTTON_WIDTH / itemView.getWidth();
drawButtons(c, itemView, buffer, pos, translationX);
}
}

super.onChildDraw(c, recyclerView, viewHolder, translationX, dY, actionState, isCurrentlyActive);
}

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

private void drawButtons(Canvas c, View itemView, List<UnderlayButton> buffer, int pos, float dX){
float right = itemView.getRight();
float dButtonWidth = (-1) * dX / buffer.size();

for (UnderlayButton button : buffer) {
float left = right - dButtonWidth;
button.onDraw(
c,
new RectF(
left,
itemView.getTop(),
right,
itemView.getBottom()
),
pos
);

right = left;
}
}

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

public abstract void instantiateUnderlayButton(RecyclerView.ViewHolder viewHolder, List<UnderlayButton> underlayButtons);

public static class UnderlayButton {
private String text;
private int imageResId;
private int color;
private int pos;
private RectF clickRegion;
private UnderlayButtonClickListener clickListener;

public UnderlayButton(String text, int imageResId, int color, UnderlayButtonClickListener clickListener) {
this.text = text;
this.imageResId = imageResId;
this.color = color;
this.clickListener = clickListener;
}

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

return false;
}

public void onDraw(Canvas c, RectF rect, int pos){
Paint p = new Paint();

// Draw background
p.setColor(color);
c.drawRect(rect, p);

// Draw Text
p.setColor(Color.WHITE);
p.setTextSize(LayoutHelper.getPx(MyApplication.getAppContext(), 12));

Rect r = new Rect();
float cHeight = rect.height();
float cWidth = rect.width();
p.setTextAlign(Paint.Align.LEFT);
p.getTextBounds(text, 0, text.length(), r);
float x = cWidth / 2f - r.width() / 2f - r.left;
float y = cHeight / 2f + r.height() / 2f - r.bottom;
c.drawText(text, rect.left + x, rect.top + y, p);

clickRegion = rect;
this.pos = pos;
}
}

public interface UnderlayButtonClickListener {
void onClick(int pos);
}
}

Usage:

SwipeHelper swipeHelper = new SwipeHelper(this, recyclerView) {
@Override
public void instantiateUnderlayButton(RecyclerView.ViewHolder viewHolder, List<UnderlayButton> underlayButtons) {
underlayButtons.add(new SwipeHelper.UnderlayButton(
"Delete",
0,
Color.parseColor("#FF3C30"),
new SwipeHelper.UnderlayButtonClickListener() {
@Override
public void onClick(int pos) {
// TODO: onDelete
}
}
));

underlayButtons.add(new SwipeHelper.UnderlayButton(
"Transfer",
0,
Color.parseColor("#FF9502"),
new SwipeHelper.UnderlayButtonClickListener() {
@Override
public void onClick(int pos) {
// TODO: OnTransfer
}
}
));
underlayButtons.add(new SwipeHelper.UnderlayButton(
"Unshare",
0,
Color.parseColor("#C7C7CB"),
new SwipeHelper.UnderlayButtonClickListener() {
@Override
public void onClick(int pos) {
// TODO: OnUnshare
}
}
));
}
};

Note: This helper class is designed for left swipe. You can change swipe direction in SwipeHelper's constructor, and making changes based on dX in onChildDraw method accordingly.

If you want to show image in the button, just make the use of imageResId in UnderlayButton, and re-implement the onDraw method.

There is a known bug, when you swipe an item diagonally from one item to another, the first touched item will flash a little. This could be addressed by decreasing the value of getSwipeVelocityThreshold, but this makes harder for user to swipe the item. You can also adjust the swiping feeling by changing two other values in getSwipeThreshold and getSwipeEscapeVelocity. Check into the ItemTouchHelper source code, the comments are very helpful.

I believe there is a lot place for optimization. This solution just gives an idea if you want to stick with ItemTouchHelper. Please let me know if you have problem using it. Below is a screenshot.

Sample Image

Acknowledgment: this solution is mostly inspired from AdamWei's answer in this post

Android - show button after swiping recyclerview item

If you consider Swipeable items in lists, the logic is a bit different for Android and iOS. In Android you don't need to confirm deletion with a click. The fact that user swiped the item is enough of a confirmation.

That's why ItemTouchHelper won't give you a way to attach an OnClickListener.

You have two choices:

  1. You can write your own custom swipe management system (painful).
  2. Agree on Android way of doing that and ask user for confirmation after the swipe.

How to restore swiped item from RecyclerView after dialog show?

You need to do this in your Fragment class

  madapter.notifyItemChanged(viewholder.getAbsoluteAdapterPostion());


Related Topics



Leave a reply



Submit