I struggled with the same problem and tried to find a solution on the Internet. Most solutions use a two-tier approach (an element of one layer, other buttons of a layer), but I want to stick only to ItemTouchHelper. In the end, I came up with a proven 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();
Using:
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 utility class is for left wipes. You can change the direction of movement in the SwipeHelper constructor and make changes based on dX in the onChildDraw method.
If you want to display the image in a button, just use imageResId in the UnderlayButton and reimplement the onDraw method.
Known error, when you scroll an element diagonally from one element to another, the first affected element will blink. This can be fixed by decreasing the value of getSwipeVelocityThreshold , but this makes it difficult to scroll through the item. You can also customize the scroll feel by changing the other two values โโin getSwipeThreshold and getSwipeEscapeVelocity . Check out the source code for ItemTouchHelper, comments are very helpful.
I believe that there is a lot of room for optimization. This solution just gives an idea if you want to stick with ItemTouchHelper. Please let me know if you have problems using it. Below is a screenshot.

Confirmation : This solution is mainly inspired by AdamWei's answer in this article.