45

I am trying to port some iOS functionality to Android.

I intent to create a table where on swipe to the left shows 2 button: Edit and Delete.

enter image description here

I have been playing with it and I know I am very close. The secret really lies on the method OnChildDraw.

I would like to Draw a Rect that fits the text Delete then draw the Edit text besides it with their respective background color. The remaining white space when clicked should restore the row to its initial position.

I have managed to paint the background while the user is swiping to the sides but I don't know how to add the listeners and once it is swiped to the side, the dragging function begins to misbehave.

I am working on Xamarin but pure java solutions also are accepted as I can easily port them to c#.

    public class SavedPlacesItemTouchHelper : ItemTouchHelper.SimpleCallback
    {
        private SavedPlacesRecyclerviewAdapter adapter;
        private Paint paint = new Paint();
        private Context context;

        public SavedPlacesItemTouchHelper(Context context, SavedPlacesRecyclerviewAdapter adapter) : base(ItemTouchHelper.ActionStateIdle, ItemTouchHelper.Left)
        {
            this.context = context;
            this.adapter = adapter;
        }

        public override bool OnMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target)
        {
            return false;
        }

        public override void OnSwiped(RecyclerView.ViewHolder viewHolder, int direction)
        {
        }

        public override void OnChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, bool isCurrentlyActive)
        {
            float translationX = dX;
            View itemView = viewHolder.ItemView;
            float height = (float)itemView.Bottom - (float)itemView.Top;

            if (actionState == ItemTouchHelper.ActionStateSwipe && dX <= 0) // Swiping Left
            {
                 translationX = -Math.Min(-dX, height * 2);
                paint.Color = Color.Red;
                RectF background = new RectF((float)itemView.Right + translationX, (float)itemView.Top, (float)itemView.Right, (float)itemView.Bottom);
                c.DrawRect(background, paint);

                //viewHolder.ItemView.TranslationX = translationX;
            }
            else if (actionState == ItemTouchHelper.ActionStateSwipe && dX > 0) // Swiping Right
            {
                translationX = Math.Min(dX, height * 2);
                paint.Color = Color.Red;
                RectF background = new RectF((float)itemView.Right + translationX, (float)itemView.Top, (float)itemView.Right, (float)itemView.Bottom);
                c.DrawRect(background, paint);
            }

            base.OnChildDraw(c, recyclerView, viewHolder, translationX, dY, actionState, isCurrentlyActive);
        }
    }
}

This is what I currently have.

If you know how to add listeners or any suggestions please leave a comment!

UPDATE:

I just realized that on double tap on the white remaining space of the row already restore the row to its initial state. Not a single tap though :(

Gustavo Baiocchi Costa
  • 1,164
  • 3
  • 15
  • 30

11 Answers11

109

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.

enter image description here

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

Wenxi Zeng
  • 1,293
  • 1
  • 6
  • 12
  • 1
    I have to give up on this solution. Ended up doing delete on left swipe and edit on right swipe. But I would like to use the buttons. Your solution seems legit, I will try them next week, if I have problems I will let you know, if it works, I will mark it as the accepted answer :D – Gustavo Baiocchi Costa Jul 13 '17 at 08:27
  • @GustavoBaiocchiCosta Hey, any luck with this solution? If it worked, please mark it as accepted. :D – Wenxi Zeng Aug 22 '17 at 16:00
  • I've manage to make it appear with itemTouchHelper, but couldnt click on it... So I changed the implementation for swiping left or right. Sorry, but my question asks a itemTouchHelper solution :D – Gustavo Baiocchi Costa Aug 23 '17 at 07:48
  • @GustavoBaiocchiCosta Hmm, I think this is using itemTouchHelper. Check attachSwipe() methond. In order to make it clickable, you will have to have the help of OnTouchListener, which is implemented in my solution as well. Probably I got your question wrong. Anyway, glad to hear you solved it – Wenxi Zeng Aug 23 '17 at 15:44
  • I didn't solve it yet as I changed the problem. Sorry, I am really busy with others projects. I will get back to it later and if that works I will mark yours as accepted or let you know. Liked the GIF you posted. I think your solution is probably correct :D – Gustavo Baiocchi Costa Aug 24 '17 at 09:56
  • @WenxiZeng i). i tried above code ,its working fine.but one problem i faced.In position 1 is open state ,at position 3 (row) try to swipe on right side ,it close the position 1.ii) how to implement the different view for different row like above gif image. – rafeek Sep 10 '17 at 16:59
  • @rafeek great to hear it's working for you. to answer your questions, i) this helper class is designed for swiping one item at a time. if you want to keep the previous items opened when a new item swiped, remove the else statement recoverQueue.add(swipedPos); swipedPos = -1; recoverSwipedItem(); in the onTouchListener.OnTouch() method. but I bet that will cause some weird behavior when the buttons clicked. you probably have to write your own logic to handle this situation. ii) to have different view for different row, all you have to do is to add a switch/if when you add the underlayButtons. – Wenxi Zeng Sep 11 '17 at 15:54
  • @rafeek in instantiateUnderlayButton, make the use of viewHolder.getAdapterPosition() to get the pos. and use the pos to get data from you adapter. then use switch/if statement based on your data to instantiate different buttons. – Wenxi Zeng Sep 11 '17 at 16:52
  • @WenxiZeng thanks for reply.I understood answer for second question.But i think u didnt get my first question.when first row in open state(Half swipe),while swipe opposite direction in second row, first row is closed. – rafeek Sep 12 '17 at 07:21
  • @rafeek ah, sorry. I misunderstood it. yeah. that behavior is caused by the onTouchListener. in fact, any touch or move events outside the swiped item, will make the swiped item closed. in order to prevent that happen, you will need to use the MotionEvent argument to determine if it's a left swipe. if so, don't recover the swiped item. – Wenxi Zeng Sep 12 '17 at 16:32
  • If you want to have the text in the buttons on multiple lines, see answer below – Castaldi Oct 19 '17 at 09:57
  • Working In xamarin.Forms, I have ported all yout code to C# and then used in a Listview renderer, everything works OK with a few changes! – CarLoOSX Jun 08 '18 at 12:26
  • @CarLoOSX Great to hear it's also applicable in xamarin! :D – Wenxi Zeng Jun 08 '18 at 14:41
  • @WenxiZeng thanks for this!. one thing I am running into is that when I swipe the same item back to the original position after left swipe, the buttons still give me a callback. I think this is because of the onTouchListener not knowing if the same item was brought back. Is there a way to swipe the if() inside onTouchListener so that it hits the else? Also if i wanted to have a long swipe so that i can directly delete the item what would be the approach ? – iamsujan Jun 26 '18 at 21:23
  • @coderindigo glad it helped. for your first question, do you mean the underlay button callback? it shouldn't give you a callback when swipe back. I checked the logic and tested with my app, seems work properly. for second question, my idea would be adding a threshold in the if (dx<0) in onChildDraw method. if dx>threshold, set translationX to itemView width, and call drawButtons. in drawButtons, check if dx>threshold, if so only draw delete button with dx, and then call item delete method. – Wenxi Zeng Jun 27 '18 at 15:29
  • @WenxiZeng for first one, yea it is triggering on single tap when I manually swipe the same item back. but if I touch the other part of the screen then it brings the swiped item back and clicks on the previous item does not give a callback. steps to repro: swipe an item, swipe same item back, click on the position where there were buttons, gives me a ontap callback. for the second one I will try the above approach thanks! – iamsujan Jun 27 '18 at 21:00
  • 1
    @coderindigo sorry. I still can't reproduce it. but I can see that happening if the swipedPos isn't reset correctly. your idea is doable. adding an extra condition in the if (rect.top < point.y && rect.bottom > point.y) statement in onTouch method would fix it. I would add (&& point.x is in the total click regions of underlay buttons). – Wenxi Zeng Jun 27 '18 at 22:02
  • 2
    @WenxiZengI thanks! fixed the issue I was having by maintaining a variable swipedBack and setting it in onChildDraw when dx==0. Also, one more thing I need to ask is can I have both right and left swipe at the same time? I am thinking I need to have a new left button buffer? – iamsujan Jun 28 '18 at 18:31
  • @coderindigo that's a good fix! yes, it's possible. I actually did implement dual-direction swipe in my app. to keep your code clean, you would want to create another swipe helper that only handles right swipe logic, and attached both helpers to rv. I'm afraid you probably have to re-implement the draw button methods, since the solution I posted here is just for left swipe. – Wenxi Zeng Jun 28 '18 at 21:12
  • @WenxiZengI got it within the same swiper but yea had to change the drawBottom to draw to left. one of the things I am noticing is that if I have the itemView in swiped position and swipe all the way from the button to the itemview in the same direction the screen does some weird stuff. its just expands the button if i just swipe on itemview. any idea on this so that i can start looking ? – iamsujan Jun 28 '18 at 21:59
  • @coderindigo that's probably something wrong with the onChildDraw method after merged the right swipe logic. not sure about that. as I have sugguested, worst case scenario, you can always split it to two swipers. though seems bring more work, it makes the logic clean and easy to maintain. – Wenxi Zeng Jun 28 '18 at 22:39
  • could you five me link to repository with example? I don't understand how to use SwipeHelper. – Georgiy Chebotarev Jul 27 '18 at 12:10
  • 1
    @GeorgiyChebotarev sorry I don't have a repository for it. for the usage, in your activity or fragment or wherever you have an instance of recycler view, copy/paste the usage and pass the context and recycler view. then you should be good to go – Wenxi Zeng Jul 27 '18 at 14:12
  • Is there a github repository that shows what happens with this code, i am unable to infer what is going on here, expecially with the lack of comments – Ebi Igweze Aug 22 '18 at 01:31
  • @Wenxi Zeng I converted your code to C# in xamarin and was sucessfully able to implement it. But only one problem I am facing is in onTouch method. If I keep on deleting item then for second last item my swipedViewHolder variable in onTouch method gives null value. Kindly suggest. – Nadeem Shaikh Sep 26 '18 at 12:55
  • I got workaround for my problem and now it is working well. Thanks. – Nadeem Shaikh Sep 27 '18 at 06:06
  • @WenxiZeng It is working fine with the recycleview, but if I've got the same menu within nested recyclerview that is for child recyclerview buttons are not throwing call back for clicks, Could you help me why it happens? – Moorthy Nov 13 '18 at 08:50
  • @NadeemShaikh Can you please Share the Xamarin Converted Code? – ManiKandan Selvanathan Feb 19 '19 at 02:24
  • 1
    How to add an icon to the button instead of text from R.drawable?? – Kolhoznik Apr 18 '19 at 10:51
  • I am having a problem with the first swipe, it registers the event but always stays at -1, so I have to swipe a different item and then the original items jumps open and then closes. After this it works fine. Is there something that needs to be done to allow the first swipe to register correctly? – imposterSyndrome May 20 '19 at 15:13
  • how to close the swiped recycleview item when swipe item click ?? – param May 31 '19 at 10:21
  • Please Can you explain to me which code, make the swipe back to the initialise state – Ghizlane Lotfi Jun 14 '19 at 15:45
  • @GhizlaneLotfi onSwiped -> recoverSwipedItem -> recyclerView.getAdapter().notifyItemChanged(pos). it's been a while. I can remember exactly. I think notifyItemChanged causes the item to be redrawn – Wenxi Zeng Jun 14 '19 at 18:58
  • LayoutHelper inside of onDraw is not found – RRGT19 Jun 14 '19 at 20:55
  • @RRGT19 LayoutHelper is just a helper class covert dp to px. you can search it online – Wenxi Zeng Jun 14 '19 at 21:45
  • @WenxiZeng How can I get the context from the activity, adapter or SwipeHelper? I'm trying to fix `MyApplication.getAppContext()`. – RRGT19 Jun 14 '19 at 22:44
  • @WenxiZeng thanks for ur response, but I don't know why notifyItemChanged(pos) doesn't work and i have always the button displayed. – Ghizlane Lotfi Jun 24 '19 at 14:48
  • 3
    You might want to use `Paint.setAntiAlias(true)` and `Paint.setSubpixelText(true)` inside `UnderlayButton`'s `onDraw()` to make the button text smoother (not pixelated) – koceeng Sep 10 '19 at 04:31
  • How can I add another listener for swipe-deleting the item while it has already been swiped once? – George Apr 03 '20 at 12:44
  • 1
    @George in our product we were able to use two listeners, but for different direction, one for left swipe, one for right. we didn't try two listeners on the same direction. I think it's doable, but implementation-wise could be messy. I would suggest go for a two/three layer approach for that specific need. – Wenxi Zeng Apr 03 '20 at 19:54
  • 1
    Can this be used with icons as well, instead of buttons? – Narendra Singh May 02 '20 at 07:26
  • @NarendraSingh yes, it's possible. please refer to George's implementation in this page below. basically, you need to make the use of imageResId – Wenxi Zeng May 02 '20 at 19:49
  • @Wenxi Zeng, I am having a problem. I have two actions on recyclerItem. On click of item navigate to anothe screen to see details and come back here. ANother if delete option of swipe. The problem is when I m comking back after seeing details, delete call back is not happening. Please help. – Tara Jun 10 '20 at 10:48
  • @Tara your problem is too specific. probably you are delegating the events in a wrong place.but I can assure you switching to another view won't impact the functionality of this helper. – Wenxi Zeng Jun 10 '20 at 15:30
  • @Wenxi Zeng for recyclerItem Click i have listener in adapter, which is working perfectly fine. recycler swipe is also working fine, i am able to swipe but when clicks on delete icon, it is not happening. – Tara Jun 10 '20 at 15:35
  • How to use this for both sides at the same time? – Makalele Dec 07 '20 at 22:09
  • Finally found a solution by using this library: https://github.com/xabaras/RecyclerViewSwipeDecorator – Makalele Dec 08 '20 at 20:00
  • i want to make some space between buttons, is this possible? – Vivek Thummar Mar 04 '21 at 11:02
  • in onTouch() method, swipedViewHolder.itemView was null after deleting last item from adapter and list, any solution? – Vivek Thummar Mar 18 '21 at 06:44
22

Here is the Kotlin version based on the accepted answer approach. With some minor changes I managed to render the buttons width based on the intrinsic size of the text instead of using a fixed width.

Demo project: https://github.com/ntnhon/RecyclerViewRowOptionsDemo

enter image description here

Implementation of SwipeHelper:

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import android.view.MotionEvent
import android.view.View
import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import java.util.*
import kotlin.math.abs
import kotlin.math.max

abstract class SwipeHelper(
    private val recyclerView: RecyclerView
) : ItemTouchHelper.SimpleCallback(
    ItemTouchHelper.ACTION_STATE_IDLE,
    ItemTouchHelper.LEFT
) {
    private var swipedPosition = -1
    private val buttonsBuffer: MutableMap<Int, List<UnderlayButton>> = mutableMapOf()
    private val recoverQueue = object : LinkedList<Int>() {
        override fun add(element: Int): Boolean {
            if (contains(element)) return false
            return super.add(element)
        }
    }

    @SuppressLint("ClickableViewAccessibility")
    private val touchListener = View.OnTouchListener { _, event ->
        if (swipedPosition < 0) return@OnTouchListener false
        buttonsBuffer[swipedPosition]?.forEach { it.handle(event) }
        recoverQueue.add(swipedPosition)
        swipedPosition = -1
        recoverSwipedItem()
        true
    }

    init {
        recyclerView.setOnTouchListener(touchListener)
    }

    private fun recoverSwipedItem() {
        while (!recoverQueue.isEmpty()) {
            val position = recoverQueue.poll() ?: return
            recyclerView.adapter?.notifyItemChanged(position)
        }
    }

    private fun drawButtons(
        canvas: Canvas,
        buttons: List<UnderlayButton>,
        itemView: View,
        dX: Float
    ) {
        var right = itemView.right
        buttons.forEach { button ->
            val width = button.intrinsicWidth / buttons.intrinsicWidth() * abs(dX)
            val left = right - width
            button.draw(
                canvas,
                RectF(left, itemView.top.toFloat(), right.toFloat(), itemView.bottom.toFloat())
            )

            right = left.toInt()
        }
    }

    override fun onChildDraw(
        c: Canvas,
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder,
        dX: Float,
        dY: Float,
        actionState: Int,
        isCurrentlyActive: Boolean
    ) {
        val position = viewHolder.adapterPosition
        var maxDX = dX
        val itemView = viewHolder.itemView

        if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
            if (dX < 0) {
                if (!buttonsBuffer.containsKey(position)) {
                    buttonsBuffer[position] = instantiateUnderlayButton(position)
                }

                val buttons = buttonsBuffer[position] ?: return
                if (buttons.isEmpty()) return
                maxDX = max(-buttons.intrinsicWidth(), dX)
                drawButtons(c, buttons, itemView, maxDX)
            }
        }

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

    override fun onMove(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder,
        target: RecyclerView.ViewHolder
    ): Boolean {
        return false
    }

    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
        val position = viewHolder.adapterPosition
        if (swipedPosition != position) recoverQueue.add(swipedPosition)
        swipedPosition = position
        recoverSwipedItem()
    }

    abstract fun instantiateUnderlayButton(position: Int): List<UnderlayButton>

    //region UnderlayButton
    interface UnderlayButtonClickListener {
        fun onClick()
    }

    class UnderlayButton(
        private val context: Context,
        private val title: String,
        textSize: Float,
        @ColorRes private val colorRes: Int,
        private val clickListener: UnderlayButtonClickListener
    ) {
        private var clickableRegion: RectF? = null
        private val textSizeInPixel: Float = textSize * context.resources.displayMetrics.density // dp to px
        private val horizontalPadding = 50.0f
        val intrinsicWidth: Float

        init {
            val paint = Paint()
            paint.textSize = textSizeInPixel
            paint.typeface = Typeface.DEFAULT_BOLD
            paint.textAlign = Paint.Align.LEFT
            val titleBounds = Rect()
            paint.getTextBounds(title, 0, title.length, titleBounds)
            intrinsicWidth = titleBounds.width() + 2 * horizontalPadding
        }

        fun draw(canvas: Canvas, rect: RectF) {
            val paint = Paint()

            // Draw background
            paint.color = ContextCompat.getColor(context, colorRes)
            canvas.drawRect(rect, paint)

            // Draw title
            paint.color = ContextCompat.getColor(context, android.R.color.white)
            paint.textSize = textSizeInPixel
            paint.typeface = Typeface.DEFAULT_BOLD
            paint.textAlign = Paint.Align.LEFT

            val titleBounds = Rect()
            paint.getTextBounds(title, 0, title.length, titleBounds)

            val y = rect.height() / 2 + titleBounds.height() / 2 - titleBounds.bottom
            canvas.drawText(title, rect.left + horizontalPadding, rect.top + y, paint)

            clickableRegion = rect
        }

        fun handle(event: MotionEvent) {
            clickableRegion?.let {
                if (it.contains(event.x, event.y)) {
                    clickListener.onClick()
                }
            }
        }
    }
    //endregion
}

private fun List<SwipeHelper.UnderlayButton>.intrinsicWidth(): Float {
    if (isEmpty()) return 0.0f
    return map { it.intrinsicWidth }.reduce { acc, fl -> acc + fl }
}

Usage:

private fun setUpRecyclerView() {
        binding.recyclerView.adapter = Adapter(listOf(
            "Item 0: No action",
            "Item 1: Delete",
            "Item 2: Delete & Mark as unread",
            "Item 3: Delete, Mark as unread & Archive"
        ))
        binding.recyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
        binding.recyclerView.layoutManager = LinearLayoutManager(this)

        val itemTouchHelper = ItemTouchHelper(object : SwipeHelper(binding.recyclerView) {
            override fun instantiateUnderlayButton(position: Int): List<UnderlayButton> {
                var buttons = listOf<UnderlayButton>()
                val deleteButton = deleteButton(position)
                val markAsUnreadButton = markAsUnreadButton(position)
                val archiveButton = archiveButton(position)
                when (position) {
                    1 -> buttons = listOf(deleteButton)
                    2 -> buttons = listOf(deleteButton, markAsUnreadButton)
                    3 -> buttons = listOf(deleteButton, markAsUnreadButton, archiveButton)
                    else -> Unit
                }
                return buttons
            }
        })

        itemTouchHelper.attachToRecyclerView(binding.recyclerView)
    }

    private fun toast(text: String) {
        toast?.cancel()
        toast = Toast.makeText(this, text, Toast.LENGTH_SHORT)
        toast?.show()
    }

    private fun deleteButton(position: Int) : SwipeHelper.UnderlayButton {
        return SwipeHelper.UnderlayButton(
            this,
            "Delete",
            14.0f,
            android.R.color.holo_red_light,
            object : SwipeHelper.UnderlayButtonClickListener {
                override fun onClick() {
                    toast("Deleted item $position")
                }
            })
    }

    private fun markAsUnreadButton(position: Int) : SwipeHelper.UnderlayButton {
        return SwipeHelper.UnderlayButton(
            this,
            "Mark as unread",
            14.0f,
            android.R.color.holo_green_light,
            object : SwipeHelper.UnderlayButtonClickListener {
                override fun onClick() {
                    toast("Marked as unread item $position")
                }
            })
    }

    private fun archiveButton(position: Int) : SwipeHelper.UnderlayButton {
        return SwipeHelper.UnderlayButton(
            this,
            "Archive",
            14.0f,
            android.R.color.holo_blue_light,
            object : SwipeHelper.UnderlayButtonClickListener {
                override fun onClick() {
                    toast("Archived item $position")
                }
            })
    }
Thanh-Nhon Nguyen
  • 3,026
  • 3
  • 25
  • 37
4

For all those who wanna use a library for this, check this out:

https://github.com/chthai64/SwipeRevealLayout

And, for a stripped down version of this lib, checkout:

https://android.jlelse.eu/android-recyclerview-swipeable-items-46a3c763498d

P.S. You can create any custom layout (even with Image Buttons) as your hidden layout using these.

h8pathak
  • 1,122
  • 2
  • 13
  • 27
2

Following Wenxi Zeng's answer here, if you want to have the text in the buttons on multiple lines, replace UnderlayButton's onDraw method with this:

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

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

        // Draw Text
        TextPaint textPaint = new TextPaint();
        textPaint.setTextSize(UtilitiesOperations.convertDpToPx(getContext(), 15));
        textPaint.setColor(Color.WHITE);
        StaticLayout sl = new StaticLayout(text, textPaint, (int)rect.width(),
                Layout.Alignment.ALIGN_CENTER, 1, 1, false);

        canvas.save();
        Rect r = new Rect();
        float y = (rect.height() / 2f) + (r.height() / 2f) - r.bottom - (sl.getHeight() /2);
        canvas.translate(rect.left, rect.top + y);
        sl.draw(canvas);
        canvas.restore();

        clickRegion = rect;
        this.pos = pos;
    }
Castaldi
  • 631
  • 7
  • 20
2

If you want button(s) on left side as well when swipe in the other direction, just try to add this simple lines in the existing answer:

  1. In the drawButtons method:

    private void drawButtons(Canvas c, View itemView, List<UnderlayButton> buffer, int pos, float dX) {
        float right = itemView.getRight();
        float left = itemView.getLeft();
        float dButtonWidth = (-1) * dX / buffer.size();
    
        for (UnderlayButton button : buffer) {
            if (dX < 0) {
                left = right - dButtonWidth;
                button.onDraw(
                        c,
                        new RectF(
                                left,
                                itemView.getTop(),
                                right,
                                itemView.getBottom()
                        ),
                        pos, dX //(to draw button on right)
                );
    
                right = left;
            } else if (dX > 0) {
                right = left - dButtonWidth;
                button.onDraw(c,
                        new RectF(
                                right,
                                itemView.getTop(),
                                left,
                                itemView.getBottom()
                        ), pos, dX //(to draw button on left)
                );
    
    
            }
        }
    }
    
  2. In the onDraw method check the value of dX and set the text and the colour of the buttons:

    public void onDraw(Canvas c, RectF rect, int pos, float dX) {
            Paint p = new Paint();
    
            // Draw background
    
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    
    
                if (dX > 0)
                    p.setColor(Color.parseColor("#23d2c5"));
                else if (dX < 0)
                    p.setColor(Color.parseColor("#23d2c5"));
    
                c.drawRect(rect, p);
    
    
                // Draw Text
                p.setColor(Color.WHITE);
                p.setTextSize(36);
                //  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;
                if (dX > 0) {
                    p.setColor(Color.parseColor("#23d2c5"));
                    c.drawText("Reject", rect.left + x, rect.top + y, p);
                } else if (dX < 0) {
    
                    c.drawText(text, rect.left + x, rect.top + y, p);
                }
                clickRegion = rect;
                this.pos = pos;
            }
    
        }
    
Spikatrix
  • 19,378
  • 7
  • 34
  • 77
Ashutosh Shukla
  • 318
  • 3
  • 13
2

I am late to the party but if anyone looks for an UIKit UITableView delete button behaviour then you can use something like this with a RecyclerView in Xamarin.Android:

public class SwipeDeleteHelper : ItemTouchHelper.Callback
{
    private int _startingWidth = 0;
    private bool? _rightAlignedText = null;
    private bool _alreadyClicked = false;

    private static float _previousDx = float.NegativeInfinity;
    private static float _viewWidth = float.NegativeInfinity;
    private static float _permanentlyDeleteThreshold = float.NegativeInfinity;

    private static RecyclerView.ViewHolder _currentViewHolder;
    private RecyclerView.ViewHolder CurrentViewHolder
    {
        get => _currentViewHolder;

        set
        {
            _startingWidth = 0;
            _rightAlignedText = null;
            _alreadyClicked = false;

            _previousDx = float.NegativeInfinity;

            _currentViewHolder = value;
        }
    }
    /*
    You can create a method in a utility class for the buttonwidth conversion like this:
    public static float GetPxFromDp(float dp)
    {
        return dp * Application.Context.ApplicationContext.Resources.DisplayMetrics.Density;
    }
    Also you can use text width measurement to determine the optimal width of the button for your delete text.
    */
    public static int buttonWidth = 60 * Application.Context.ApplicationContext.Resources.DisplayMetrics.Density;

    public override int GetMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder)
    {
        if (viewHolder is EntryCell)
        {
            return MakeMovementFlags(ItemTouchHelper.ActionStateIdle, ItemTouchHelper.Left | ItemTouchHelper.Start | ItemTouchHelper.Right | ItemTouchHelper.End);
        }

        return MakeMovementFlags(ItemTouchHelper.ActionStateIdle, ItemTouchHelper.ActionStateIdle);
    }

    public override void OnSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState)
    {
        if (float.IsNegativeInfinity(_permanentlyDeleteThreshold))
        {
            _viewWidth = viewHolder.ItemView.Width;
            _permanentlyDeleteThreshold = (viewHolder.ItemView.Width * 3f / 4f);
        }

        if (viewHolder != CurrentViewHolder)
        {
            if (viewHolder != null) // This is a new selection and the button of the previous viewHolder should get hidden.
            {
                (CurrentViewHolder as EntryCell)?.ResetView(CurrentViewHolder);

                CurrentViewHolder = viewHolder;
            }
            else if (CurrentViewHolder != null) // This is the end of the previous selection
            {
                var hidden = CurrentViewHolder.ItemView.FindViewById<Button>(Resource.Id.fileListDeleteButton);

                _previousDx = float.NegativeInfinity;

                if (hidden.LayoutParameters.Width > _permanentlyDeleteThreshold && !_alreadyClicked) // released in permanent delete area
                {
                    _alreadyClicked = true;

                    hidden.LayoutParameters.Width = CurrentViewHolder.ItemView.Width;

                    hidden.CallOnClick();

                    CurrentViewHolder = null;
                }
                else
                {
                    _startingWidth = hidden.LayoutParameters.Width >= buttonWidth ? buttonWidth : 0;

                    hidden.LayoutParameters.Width = _startingWidth;
                }

                AlignDeleteButtonText(hidden);

                hidden.RequestLayout();
            }
        }

        base.OnSelectedChanged(viewHolder, actionState);
    }

    public override void OnChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, bool isCurrentlyActive)
    {
        if (actionState == ItemTouchHelper.ActionStateSwipe && !_alreadyClicked)
        {
            var hidden = viewHolder.ItemView.FindViewById<Button>(Resource.Id.fileListDeleteButton);

            if (isCurrentlyActive) // swiping
            {
                if (float.IsNegativeInfinity(_previousDx)) // This is a new swipe
                {
                    _previousDx = dX;
                }

                if (Math.Abs(dX - _previousDx) > 0.1f && Math.Abs(dX - (-_viewWidth)) > 0.1f)
                {
                    hidden.LayoutParameters.Width = Math.Max(0, (int)Math.Round(hidden.LayoutParameters.Width - (dX >= _previousDx ? 1 : -1) * (Math.Abs(dX - _previousDx))));

                    _previousDx = dX;

                    AlignDeleteButtonText(hidden);

                    hidden.RequestLayout();
                }
            }
        }
    }

    private void AlignDeleteButtonText(Button hidden)
    {
        if (_rightAlignedText != false && hidden.LayoutParameters.Width >= _permanentlyDeleteThreshold) // pulled into permanent delete area
        {
            hidden.Gravity = GravityFlags.AxisSpecified | GravityFlags.AxisPullBefore | GravityFlags.CenterVertical;
            _rightAlignedText = false;
        }
        else if (_rightAlignedText != null && hidden.LayoutParameters.Width <= buttonWidth)
        {
            hidden.Gravity = GravityFlags.Center;
            _rightAlignedText = null;
        }
        else if (_rightAlignedText != true && hidden.LayoutParameters.Width > buttonWidth && hidden.LayoutParameters.Width < _permanentlyDeleteThreshold) // pulled back from permanent delete area
        {
            hidden.Gravity = GravityFlags.AxisSpecified | GravityFlags.AxisPullAfter | GravityFlags.CenterVertical;
            _rightAlignedText = true;
        }
    }

    public override bool OnMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { return false; }

    public override void OnSwiped(RecyclerView.ViewHolder viewHolder, int direction) { }
}

The EntryCell is a descendant of MvxRecyclerViewHolder and it should contain something like this:

public class EntryCell : MvxRecyclerViewHolder
{
    public EntryCell(View itemView, IMvxAndroidBindingContext context) : base(itemView, context)
    {
        Button _delButton = itemView.FindViewById<Button>(Resource.Id.fileListDeleteButton);

        _delButton.Text = "Delete";
    }

    public void ResetView(RecyclerView.ViewHolder currentViewHolder)
    {
        var hidden = currentViewHolder.ItemView.FindViewById<Button>(Resource.Id.fileListDeleteButton);

        hidden.LayoutParameters.Width = 0;

        hidden.RequestLayout();
    }
}

Your view should have a button (Referenced in EntryCell as Resource.Id.fileListDeleteButton so the ID of the button is fileListDeleteButton) in it. I use an XML as a view and it looks like this:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="40dp"
    android:orientation="vertical">

<!-- The rest of your code... -->

<Button
    android:id="@+id/fileListDeleteButton"
    android:layout_width="0dp"
    android:layout_height="match_parent"
    android:layout_alignParentRight="true"
    android:paddingHorizontal="@dimen/abc_button_padding_horizontal_material"
    android:background="#f00"
    android:textColor="@android:color/white"
    android:textAllCaps="false"
    android:singleLine="true"
    android:ellipsize="none"
    android:text="dummy" />
</RelativeLayout>

In your code, where the RecyclerView is, use it like this:

ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new SwipeDeleteHelper());
itemTouchHelper.AttachToRecyclerView(yourRecyclerView);

I hope this helps someone.

alldevboii
  • 51
  • 2
2

I did the following to be able to draw a drawable instead of text:

  1. In SwipeHelper, I changed

    UnderlayButton(String text, int imageResId, int color, UnderlayButtonClickListener clickListener)
    

    to

    UnderlayButton(String text, Bitmap bitmap, int color, UnderlayButtonClickListener clickListener)
    

    Of course I removed imageResId and instead created a Bitmap bitmap and passed the constructor variable to it using this.bitmap = bitmap; as the rest of the variables.

  2. In SwipeHelper.onDaw() you may then call drawBitmap() to apply your bitmap to the canvas. For example:

    c.drawBitmap(bitmap, rect.left, rect.top, p);
    

    Where c and p and your Canvas and Paint variables respectively.

  3. In the activity where I call UnderlayButton, I convert my drawable (in my case it is a VectorDrawable) to a bitmap using this method:

    int idDrawable = R.drawable.ic_delete_white;
    Bitmap bitmap = getBitmapFromVectorDrawable(getContext(), idDrawable);
    

What remains to be done is the centring of the icon.

Full onDraw method with text and bitmap both centered:

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(24);


            float spaceHeight = 10; // change to whatever you deem looks better
            float textWidth = p.measureText(text);
            Rect bounds = new Rect();
            p.getTextBounds(text, 0, text.length(), bounds);
            float combinedHeight = bitmap.getHeight() + spaceHeight + bounds.height();
            c.drawBitmap(bitmap, rect.centerX() - (bitmap.getWidth() / 2), rect.centerY() - (combinedHeight / 2), null);
            //If you want text as well with bitmap
            c.drawText(text, rect.centerX() - (textWidth / 2), rect.centerY() + (combinedHeight / 2), p);

            clickRegion = rect;
            this.pos = pos;
        }
dan_13th
  • 33
  • 4
George
  • 309
  • 2
  • 14
1

Since I haven't seen anywhere how to implement this and I did manage to get this to work, I will post a solution to this problem that is working however it is in c# Xamarin Android.

If you need it native android you will have to convert it android native which shouldn't be really hard. I might do this at a later date if very requested.

This is my ItemHelper base class:

 internal abstract class ItemTouchHelperBase : ItemTouchHelper.Callback
    {
        protected RecyclerViewAdapterBase adapter;
        public int currentPosition = -1;
        public Rect ItemRect = new Rect();

        private Paint backgroundPaint = new Paint();
        private Rect backgroundBounds = new Rect();
        private TextPaint textPaint = new TextPaint();
        private string deleteText;
        private readonly float textWidth;
        private readonly float textHeight;

        public ItemTouchHelperBase()
        {
            backgroundPaint.Color = new Color(ContextCompat.GetColor(Application.Context, Resource.Color.delete_red));
            textPaint.Color = Color.White;
            textPaint.AntiAlias = true;
            textPaint.TextSize = FontHelper.GetFontSize(Application.Context, Resource.Dimension.font_size_button);
            deleteText = "  " + StringResource.delete + "  ";

            Rect textBounds = new Rect();
            textPaint.GetTextBounds(deleteText, 0, deleteText.Length, textBounds);

            textHeight = textBounds.Height();
            textWidth = textPaint.MeasureText(deleteText);
        }

        public override bool OnMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target)
        {
            return false;
        }

        public override void ClearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder)
        {
            if (adapter != null)
            {
                ItemRect = new Rect();
            }
            base.ClearView(recyclerView, viewHolder);
        }

        public override void OnSwiped(RecyclerView.ViewHolder viewHolder, int direction)
        {
        }

        public override void OnChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, bool isCurrentlyActive)
        {
            // Note: Don't create variables inside OnDraw due to performance issues
            try
            {
                if (actionState == ItemTouchHelper.ActionStateSwipe)
                {
                    if (dX <= 0) // Left swipe
                    {
                        // Swipe up to text width accordingly to ratio
                        dX /= viewHolder.ItemView.Right / textWidth;

                        //Draw background
                        backgroundBounds = new Rect(
                            viewHolder.ItemView.Right + (int) dX,
                            viewHolder.ItemView.Top,
                            viewHolder.ItemView.Right,
                            viewHolder.ItemView.Bottom);
                        c.DrawRect(backgroundBounds, backgroundPaint);

                        if (adapter != null)
                        {
                            ItemRect = backgroundBounds;
                        }

                        //Draw text
                        c.DrawText(
                            deleteText,
                            (float) viewHolder.ItemView.Right - textWidth, viewHolder.ItemView.Top + (viewHolder.ItemView.Height / 2) + (textHeight / 2),
                            textPaint);
                    }

                    base.OnChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
                }
            }
            catch (Exception)
            {
            }
        }

        internal void AttachToRecyclerview(RecyclerView recycleview)
        {
            new ItemTouchHelper(this).AttachToRecyclerView(recycleview);
        }

        public void ClickOutsideDeleteButton()
        {
            try
            {
                if (currentPosition != -1)
                {
                    PutRowBackToDefault();
                }
            }
            catch (Exception)
            {
            }
        }

        protected void PutRowBackToDefault()
        {
            adapter.NotifyItemChanged(currentPosition);
            currentPosition = -1;
        }
    }

Then on your item helper class you have:

 internal class MyItemsTouchHelperCallback : ItemTouchHelperBase
    {
        public MyItemsTouchHelperCallback (MyAdapter adapter)
        {
            this.adapter = adapter;
        }

        public override int GetMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder)
        {
            try
            {
                if (currentPosition != -1 && currentPosition != viewHolder.AdapterPosition)
                {
                    PutRowBackToDefault();
                }
                currentPosition = viewHolder.AdapterPosition;
            }
            catch (Exception)
            {
            }

            int swipeFlags = viewHolder is MyViewHolder ? ItemTouchHelper.Start : ItemTouchHelper.ActionStateIdle;
            return MakeMovementFlags(ItemTouchHelper.ActionStateIdle, swipeFlags);
        }
    }

Then on your activity you have:

Put this OnCreate

        recycleViewLayoutManager = new LinearLayoutManager(this);
        recycler_view_main.SetLayoutManager(recycleViewLayoutManager);
        recyclerAdapter = new MyAdapter(this, this);
        recycler_view_main.SetAdapter(recyclerAdapter);
        myItemsTouchHelperCallback = new MyItemsTouchHelperCallback (recyclerAdapter);
        myItemsTouchHelperCallback .AttachToRecyclerview(recycler_view_main);

Then on activity you override this method:

 public override bool DispatchTouchEvent(MotionEvent e)
            {
                int[] recyclerviewLocationOnScreen = new int[2];
                recycler_view_main.GetLocationOnScreen(recyclerviewLocationOnScreen);

                TouchEventsHelper.TouchUpEvent(
                    e.Action,
                    e.GetX() - recyclerviewLocationOnScreen[0],
                    e.GetY() - recyclerviewLocationOnScreen[1],
                    myItemsTouchHelperCallback .ItemRect,
                    delegate
                    {
                        // Delete your row
                    },
                    delegate
                    { myItemsTouchHelperCallback .ClickOutsideDeleteButton(); });


                return base.DispatchTouchEvent(e);
            }

This is the helper method i created to be used by the dispatch event:

internal static void TouchUpEvent(MotionEventActions eventActions, float x, float y, Rect rectangle, Action ActionDeleteClick, Action NormalClick)
        {
            try
            {
                if (rectangle.Contains((int) x, (int) y))
                {
                    //inside delete button
                    if (eventActions == MotionEventActions.Down)
                    {
                        isClick = true;
                    }
                    else if (eventActions == MotionEventActions.Up || eventActions == MotionEventActions.Cancel)
                    {
                        if (isClick)
                        {
                            ActionDeleteClick.Invoke();
                        }
                    }
                }
                else if (eventActions == MotionEventActions.Up ||
                         eventActions == MotionEventActions.Cancel ||
                         eventActions == MotionEventActions.Down)
                {
                    //click anywhere outside delete button
                    isClick = false;
                    if (eventActions == MotionEventActions.Down)
                    {
                        NormalClick.Invoke();
                    }
                }
            }
            catch (Exception)
            {
            }
        }

It is a bit complex but it works well. I have tested this in many ways. Let me know if you have any trouble implementing this

Gustavo Baiocchi Costa
  • 1,164
  • 3
  • 15
  • 30
0

If you use a RecyclerView, try to use OnScrollListener. Do something like this.

 private class YourOnScrollListener extends RecyclerView.OnScrollListener {

    private boolean directionLeft;

    @Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
        if (newState == RecyclerView.SCROLL_STATE_IDLE) {
            if (directionLeft) drawButtons();
            //Draw buttons here if you want them to be drawn after scroll is finished
            //here you can play with states, to draw buttons or erase it whenever you want
        }
    }

    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        super.onScrolled(recyclerView, dx, dy);
        if (dx < 0) directionLeft = true;
        else directionLeft = false;
    }

}
Dmitry Smolyaninov
  • 1,829
  • 11
  • 30
0

I wanted to use this touch gesture in my app too, after working too much with Itemtouchhelper I decided to write my own touch handler:

    private class TouchHelper : Java.Lang.Object, View.IOnTouchListener
    {
        ViewHolder vh;
        public TouchHelper(ViewHolder vh)
        { this.vh = vh;  }

        float DownX, DownY; bool isSliding;
        TimeSpan tsDown;
        public bool OnTouch(View v, MotionEvent e)
        {
            switch (e.Action)
            {
                case MotionEventActions.Down:
                    DownX = e.GetX(); DownY = e.GetY();
                    tsDown = TimeSpan.Now;
                    break;
                case MotionEventActions.Move:
                    float deltaX = e.GetX() - DownX, deltaY = e.GetX() - DownY;
                    if (Math.Abs(deltaX) >= Values.ScreenWidth / 20 || Math.Abs(deltaY) >= Values.ScreenWidth / 20)
                        isSliding = Math.Abs(deltaX) > Math.Abs(deltaY);

                    //TextsPlace is the layout that moves with touch
                    if(isSliding)
                        vh.TextsPlace.TranslationX = deltaX / 2;

                    break;
                case MotionEventActions.Cancel:
                case MotionEventActions.Up:
                    //handle if touch was for clicking
                    if (Math.Abs(deltaX) <= 50 && (TimeSpan.Now - tsDown).TotalMilliseconds <= 400)
                        vh.OnTextsPlaceClick(vh.TextsPlace, null);
                    break;
            }
            return true;
        }
    }

Note: Set this as ontouchlistener of your viewholder content when creating the viewholder. You can add your animations to return the item to its first place.

You can also write your custom layoutmanager to block vertical scroll while item is sliding.

momt99
  • 1,060
  • 16
  • 25
0

I have a much simpler solution:

  1. Add a button to your row XML, width 0, floating on the right:
> <Button
>     android:id="@+id/hidden"
>     android:layout_width="0dp"
>     android:layout_height="match_parent"
>     android:layout_alignParentRight = "true">
  1. in onChildDraw(), just increase its width by the dX value.

     int position = viewHolder.getAdapterPosition();
     View v = recyclerView.getLayoutManager().findViewByPosition(position);
     Button hidden = v.findViewById(R.id.hidden);
     hidden.setLayoutParams(new LinearLayout.LayoutParams((int)-dX, -1));
    

Make sure not to call the default super.onChildDraw()

john ktejik
  • 4,724
  • 3
  • 38
  • 48