RecyclerView ItemTouchHelper Buttons on Swipe

后端 未结 11 1243
春和景丽
春和景丽 2020-11-27 11:05

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.

I have

相关标签:
11条回答
  • 2020-11-27 11:16

    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;
            }
    
    0 讨论(0)
  • 2020-11-27 11:20

    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

    0 讨论(0)
  • 2020-11-27 11:20

    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;
        }
    
    }
    
    0 讨论(0)
  • 2020-11-27 11:21

    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.

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

    0 讨论(0)
  • 2020-11-27 11:32

    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.

    0 讨论(0)
提交回复
热议问题