Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to multi-select using drag gesture in RecyclerView ?

This is a very short question: Recently Google has updated its material design guidelines, showing that multi selection of items should be like on the Google-Photos app (here), as such:

enter image description here

I've noticed that even if you are already in multi-selection mode, you can still use this gesture anywhere you wish.

What I did so far is handling clicking of items for multi-selecting them, but how do I do what Google has shown?

like image 810
android developer Avatar asked Dec 17 '16 14:12

android developer


2 Answers

Seems it's more complicated than I thought, but there is a library for this:

https://android-arsenal.com/details/1/5152

https://github.com/MFlisar/DragSelectRecyclerView

which is based on :

https://github.com/weidongjian/AndroidDragSelect-SimulateGooglePhoto

like image 158
android developer Avatar answered Oct 14 '22 20:10

android developer


Although it's complicated and there are some libraries to use, it will be better if you can develop one for your purpose (let's imagine we want this function for one type of game like Boogle, Bookworm or Bejeweled...).

So, if you like to make your own, here are some good tips to start with:

1) Understand completely touch event (when action down, move, up, ... will be sent; how to distinguish multi and single touch).

2) Differentiate from onTouch and onIntercepTouch well.

3) Do some researches on RecyclerView.OnItemTouchListener, since we're gonna utilise it.

4) Put some logs to know the flow event while researching, do not debug when you learn touch event.

And here you go, the small example for you to start off:

abstract public class OnItemTouchMultiDragListener implements RecyclerView.OnItemTouchListener {
        private static final int MAX_CLICK_DURATION = 200;

        private boolean isIn;   // Is touching still at the same item.
        private String tagTouchableView;    // View catches touch event in each item of recycler view.

        private int countIn;    // How many times touch event in item.
        private long timeDown;  // Time touch down on touchable view.
        protected int startPos; // Position of item to start touching.
        protected int lastPos;  // Position of item last touching in.
        protected int endPos;   // Position of item to end touching (touch up).

        private boolean isTouchDownAtTouchableView; // If touch down event is in a touchable view.

        public OnItemTouchMultiDragListener(String tagTouchableView){
            this.tagTouchableView = tagTouchableView;
        }

        @Override
        public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) {

            if(motionEvent.getPointerCount() > 1){

                // Touch with many fingers, don't handle.
                return false;
            }

            int action = motionEvent.getAction();
            if(action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_CANCEL){

                View v = recyclerView.findChildViewUnder(motionEvent.getX(), motionEvent.getY());
                RecyclerView.ViewHolder holder = null;
                endPos = -1;

                if(v != null) {

                    View vTouchable = v.findViewWithTag(tagTouchableView);

                    // If up in that item too, since we only in one item.
                    if (vTouchable != null && isInView(motionEvent, vTouchable)) {

                        holder = recyclerView.getChildViewHolder(v);
                        endPos = holder.getAdapterPosition();
                    }
                }

                // If touch down/ move only in one item.
                if(countIn == 1 && isTouchDownAtTouchableView){
                    if(holder != null && endPos != -1) {
                        if (isPossiblyClick()) {
                            onClickUp(endPos, holder);
                        } else {
                            onLongClickUp(endPos, holder);
                        }
                    }
                }else if (countIn > 1){
                    onDragMultiUp(lastPos, holder);
                }else {
                    onUp();
                }

                // Reset touch status.
                isIn = false;
                isTouchDownAtTouchableView = false;
                countIn = 0;

                return false;
            }

            View v = recyclerView.findChildViewUnder(motionEvent.getX(), motionEvent.getY());

            if(v != null) {

                View vTouchable = v.findViewWithTag(tagTouchableView);

                if(vTouchable != null && isInView(motionEvent, vTouchable)) {

                    RecyclerView.ViewHolder holder = recyclerView.getChildViewHolder(v);
                    int pos = holder.getAdapterPosition();

                    if(isIn && pos == lastPos || pos == -1){
                        // Still in the same pos or can not determine what the pos is.
                        return false;
                    }

                    timeDown = Calendar.getInstance().getTimeInMillis();

                    isIn = true;

                    RecyclerView.ViewHolder holder = recyclerView.getChildViewHolder(v);
                    int pos = holder.getAdapterPosition();

                    if(action == MotionEvent.ACTION_DOWN){
                        onDownTouchableView(pos);
                        isTouchDownAtTouchableView = true;
                    }else if(isTouchDownAtTouchableView && action == MotionEvent.ACTION_MOVE){
                        onMoveTouchableView(pos);
                    }

                    onInItemAndInTouchableView(motionEvent, pos);

                    if(countIn == 0){
                        startPos = pos;
                    }

                    lastPos = pos;
                    countIn ++;
                }else {
                    isIn = false;
                    onInItemButNotInTouchable();
                }
            }else {
                isIn = false;
                onOutItem();
            }

            return false;
        }

        @Override
        public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) { }

        @Override
        public void onRequestDisallowInterceptTouchEvent(boolean b) { }

        private boolean isPossiblyClick() {
            long clickDuration = Calendar.getInstance().getTimeInMillis() - timeDown;
            return clickDuration < MAX_CLICK_DURATION;
        }

        private boolean isInView(MotionEvent ev, View... views) {
            Rect rect = new Rect();
            for (View v : views) {
                v.getGlobalVisibleRect(rect);
                Log.d("","");

                if (rect.contains((int) ev.getRawX(), (int) ev.getRawY()))
                    return true;
            }
            return false;
        }

        public void onInItemAndInTouchableView(MotionEvent motionEvent, int pos){}
        abstract public void onDownTouchableView(int pos);
        abstract public void onMoveTouchableView(int pos);
        public void onUp(){}
        public void onClickUp(int pos, RecyclerView.ViewHolder holder){}
        public void onLongClickUp(int pos, RecyclerView.ViewHolder holder){}
        public void onDragMultiUp(int endPos, RecyclerView.ViewHolder holder){}
        public void onInItemButNotInTouchable(){}
        public void onOutItem(){}

        public int getStartPos() {
            return startPos;
        }

        public int getEndPos() {
            return endPos;
        }
    }

To use it do it like the following:

private OnItemTouchMultiDragListener onItemTouchMultiDragListener = new OnItemTouchMultiDragListener("touchable") {

        @Override
        public void onDownTouchableView(int pos) {
            // Write log here.
            // This is an abstract method, you must implement.
        }

        @Override
        public void onMoveTouchableView(int pos) {
            // Write log here.
            // This is an abstract method, you must implement.
        }
    };

And the other methods you can override are: onInItemAndInTouchableView, onUp, onClickUp, onLongClickUp, onDragMultiUp, onInItemButNotInTouchable, onOutItem. Btw, write log in these methods while testing to know when it is called.

Of course, after having instantiated an instance of OnItemTouchListener, we have to add it into a recycler view like:

recyclerView.addOnItemTouchListener(onItemTouchMultiDragListener);

And one more thing you should note is the layout of the item in recycler view, here it's mine which is a square item in a grid layout with padding around to apart items to each other (so only a part of item view accepts touch event, not a padding area):

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="90dp"
    android:layout_height="90dp"
    xmlns:app="http://schemas.android.com/apk/res-auto">

   <!-- Please note on the tag "touchable" which we must pass as param in constructor to let our listener know which part of the item will be listening to touch event -->
    <View
        android:tag="touchable"
        android:id="@+id/v_background"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="@drawable/bkg_corner_gray"
        app:layout_constraintWidth_percent="0.78846153846"
        app:layout_constraintDimensionRatio="1"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

    <TextView
        android:id="@+id/tv_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:ellipsize="end"
        android:maxLines="1"
        android:textSize="35sp"
        android:textColor="@color/black"
        app:layout_constraintStart_toStartOf="@id/v_background"
        app:layout_constraintEnd_toEndOf="@id/v_background"
        app:layout_constraintTop_toTopOf="@id/v_background"
        app:layout_constraintBottom_toBottomOf="@id/v_background"/>

</android.support.constraint.ConstraintLayout>

UPDATE (2019 Nov 25)

Here is a quick example: https://github.com/mttdat/example-multi-drag-recycleview

like image 34
Nguyen Tan Dat Avatar answered Oct 14 '22 21:10

Nguyen Tan Dat