Drag and drop items in RecyclerView with GridLayoutManager

53,093

Solution 1

There is actually a better way to achieve this. You can use some of the RecyclerView's "companion" classes:

ItemTouchHelper, which is

a utility class to add swipe to dismiss and drag & drop support to RecyclerView.

and its ItemTouchHelper.Callback, which is

the contract between ItemTouchHelper and your application

// Create an `ItemTouchHelper` and attach it to the `RecyclerView`
ItemTouchHelper ith = new ItemTouchHelper(_ithCallback);
ith.attachToRecyclerView(rv);

// Extend the Callback class
ItemTouchHelper.Callback _ithCallback = new ItemTouchHelper.Callback() {
    //and in your imlpementaion of
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
        // get the viewHolder's and target's positions in your adapter data, swap them
        Collections.swap(/*RecyclerView.Adapter's data collection*/, viewHolder.getAdapterPosition(), target.getAdapterPosition());
        // and notify the adapter that its dataset has changed
        _adapter.notifyItemMoved(viewHolder.getAdapterPosition(), target.getAdapterPosition());
        return true;
    }

    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
        //TODO    
    }

    //defines the enabled move directions in each state (idle, swiping, dragging). 
    @Override
    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        return makeFlag(ItemTouchHelper.ACTION_STATE_DRAG,
                ItemTouchHelper.DOWN | ItemTouchHelper.UP | ItemTouchHelper.START | ItemTouchHelper.END);
    }
};

For more details check their documentation.

Solution 2

This is my solution with database reordering:

    ItemTouchHelper.SimpleCallback simpleItemTouchCallback = new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
        @Override
        public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
            final int fromPosition = viewHolder.getAdapterPosition();
            final int toPosition = target.getAdapterPosition();
            if (fromPosition < toPosition) {
                for (int i = fromPosition; i < toPosition; i++) {
                    Collections.swap(mAdapter.getCapitolos(), i, i + 1);
                }
            } else {
                for (int i = fromPosition; i > toPosition; i--) {
                    Collections.swap(mAdapter.getCapitolos(), i, i - 1);
                }
            }
            mAdapter.notifyItemMoved(fromPosition, toPosition);
            return true;
        }

        @Override
        public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {
            MyViewHolder svH = (MyViewHolder ) viewHolder;
            int index = mAdapter.getCapitolos().indexOf(svH.currentItem);
            mAdapter.getCapitolos().remove(svH.currentItem);
            mAdapter.notifyItemRemoved(index);
            if (emptyView != null) {
                if (mAdapter.getCapitolos().size() > 0) {
                emptyView.setVisibility(TextView.GONE);
                } else {
                emptyView.setVisibility(TextView.VISIBLE);
                }
            }
        }

        @Override
        public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
            super.clearView(recyclerView, viewHolder);
            reorderData();
        }
    };

    ItemTouchHelper itemTouchHelper = new ItemTouchHelper(simpleItemTouchCallback);
    itemTouchHelper.attachToRecyclerView(recList);

There is a support functions tahat make use of AsyncTask:

private void reorderData() {
    AsyncTask<String, Void, Spanned> task = new AsyncTask<String, Void, Spanned>() {
        @Override
        protected Spanned doInBackground(String... strings) {
            dbService.deleteAllData();
            for (int i = mAdapter.getCapitolos().size() - 1; i >= 0; i--) {
                Segnalibro s = mAdapter.getCapitolos().get(i);
                dbService.saveData(s.getIdCapitolo(), s.getVersetto());
            }
            return null;
        }

        @Override
        protected void onPostExecute(Spanned spanned) {
        }
    };
    task.execute();
}

Solution 3

Here, I've made a full sample in Kotlin (here), and, if you wish, you can enable swipe-to-dismiss on it . Here's the entire code of it:

build.gradle

implementation 'androidx.appcompat:appcompat:1.1.0-rc01'
implementation 'androidx.core:core-ktx:1.2.0-alpha02'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta2'
implementation 'com.google.android.material:material:1.1.0-alpha08'
implementation 'androidx.recyclerview:recyclerview:1.1.0-beta01'

grid_item.xml

<TextView
    android:id="@+id/textView" xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="100dp" android:gravity="center"/>

activity_main.xml

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recyclerView" tools:listitem="@layout/grid_item"  xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent" android:layout_height="match_parent"
    android:orientation="vertical" app:spanCount="3" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"/>

manifest

<manifest package="com.sample.recyclerviewdraganddroptest" xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:tools="http://schemas.android.com/tools">

    <application
        android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true"
        android:theme="@style/AppTheme.NoActionBar" tools:ignore="AllowBackup,GoogleAppIndexingWarning">
        <activity
            android:name=".MainActivity" android:label="@string/app_name" android:theme="@style/AppTheme.NoActionBar">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>

</manifest>

MainActivity.kt

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val items = ArrayList<Int>(100)
        for (i in 0 until 100)
            items.add(i)
        recyclerView.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
            override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
                return object : RecyclerView.ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.grid_item, parent, false)) {}
            }

            override fun getItemCount() = items.size

            override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
                val data = items[position]
                holder.itemView.setBackgroundColor(if (data % 2 == 0) 0xffff0000.toInt() else 0xff00ff00.toInt())
                holder.itemView.textView.text = "item $data"
            }
        }
        val itemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() {
            override fun isLongPressDragEnabled() = true
            override fun isItemViewSwipeEnabled() = false

            override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
                val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
                val swipeFlags = if (isItemViewSwipeEnabled) ItemTouchHelper.START or ItemTouchHelper.END else 0
                return makeMovementFlags(dragFlags, swipeFlags)
            }

            override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
                if (viewHolder.itemViewType != target.itemViewType)
                    return false
                val fromPosition = viewHolder.adapterPosition
                val toPosition = target.adapterPosition
                val item = items.removeAt(fromPosition)
                items.add(toPosition, item)
                recyclerView.adapter!!.notifyItemMoved(fromPosition, toPosition)
                return true
            }

            override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
                val position = viewHolder.adapterPosition
                items.remove(position)
                recyclerView.adapter!!.notifyItemRemoved(position)
            }

        })
        itemTouchHelper.attachToRecyclerView(recyclerView)
    }

}
Share:
53,093

Related videos on Youtube

patrick.elmquist
Author by

patrick.elmquist

Software Engineer@IKEA

Updated on August 09, 2020

Comments

  • patrick.elmquist
    patrick.elmquist almost 4 years

    What I want to achieve: Have a RecyclerView with GridLayoutManager that supports drag'n'drop and that rearranges the items while dragging.

    Side note: First time developing anything with drag and drop.

    There are a lot of topics on how to achieve this feature using a ListView, for example: https://raw.githubusercontent.com/btownrippleman/FurthestProgress/master/FurthestProgress/src/com/anappforthat/android/languagelineup/DynamicListView.java

    However the examples are usually a lot of code with, creating bitmaps of the dragged view and it feels like it should be possible to achieve the same result using View.startDrag(...) and RecyclerView with notifyItemAdded(), notifyItemMoved() and notifyItemRemoved() since they provide rearrange animations.

    So I played around some and came up with this:

    final CardAdapter adapter = new CardAdapter(list);
    adapter.setHasStableIds(true);
    adapter.setListener(new CardAdapter.OnLongClickListener() {
        @Override
        public void onLongClick(View view) {
            ClipData data = ClipData.newPlainText("","");
            View.DragShadowBuilder builder = new View.DragShadowBuilder(view);
            final int pos = mRecyclerView.getChildAdapterPosition(view);
            final Goal item = list.remove(pos);
    
            mRecyclerView.setOnDragListener(new View.OnDragListener() {
                int prevPos = pos;
    
                @Override
                public boolean onDrag(View view, DragEvent dragEvent) {
                    final int action = dragEvent.getAction();
                    switch(action) {
                        case DragEvent.ACTION_DRAG_LOCATION:
                            View onTopOf = mRecyclerView.findChildViewUnder(dragEvent.getX(), dragEvent.getY());
                            int i = mRecyclerView.getChildAdapterPosition(onTopOf);
    
                            list.add(i, list.remove(prevPos));
                            adapter.notifyItemMoved(prevPos, i);
                            prevPos = i;
                            break;
    
                        case DragEvent.ACTION_DROP:
                            View underView = mRecyclerView.findChildViewUnder(dragEvent.getX(), dragEvent.getY());
                            int underPos = mRecyclerView.getChildAdapterPosition(underView);
    
                            list.add(underPos, item);
                            adapter.notifyItemInserted(underPos);
                            adapter.notifyDataSetChanged();
                            break;
                    }
    
                    return true;
                }
            });
    
            view.startDrag(data, builder, view, 0);
        }
    });
    mRecyclerView.setAdapter(adapter);
    

    This piece of code sort of work, I get the swapping, but very unstable/shaky and sometimes when it's refreshing the whole grid is rearranged back to original order or to something random. Anyway the code above is just my first quick attempt, what I'm really more interested in knowing is if there's some standard/best practice way of doing the drag and drop with ReyclerView's or if the correct way of solving it is still the same that's been used for ListViews for years?

  • patrick.elmquist
    patrick.elmquist about 9 years
    Interesting, will explore this solution on Monday but it seems promising.
  • stan0
    stan0 almost 9 years
    @PaulBurke: I updated the code but the original idea was to give an overview of the method and leave the details to the documentation
  • Paul Burke
    Paul Burke almost 9 years
    @stan0 Up-voted. Hopefully OP will accept the answer now (that it's complete).
  • stan0
    stan0 almost 9 years
    In addition: you can disable drag for specific item based on the viewHolder in getMovementFlag - simply call makeFlag wih '0' instead of the direction flags.
  • patrick.elmquist
    patrick.elmquist almost 9 years
    Marking this as the answer, didn't use the included code but the post lead me to a solution that works. Thanks!
  • Vishal Khakhkhar
    Vishal Khakhkhar almost 9 years
    Can I get Drop event with this ? I required to validate position on Drop event
  • Shine
    Shine over 8 years
    which solution @VishalKhakhkhar ? I added mandatory onSwiped() method
  • Denis Nek
    Denis Nek over 8 years
    For right order change: instead just Collection.swap() you should do: if (fromPosition < toPosition) { for (int i = fromPosition; i < toPosition; i++) { Collections.swap(gridItems, i, i + 1); } } else { for (int i = fromPosition; i > toPosition; i--) { Collections.swap(gridItems, i, i - 1); } }
  • abhishesh
    abhishesh about 8 years
    @PaulBurke having a problem with scroll, when user reaches the end and is also dragging, scroll and drag gets mixed up. Any suggestions
  • Shailesh
    Shailesh over 7 years
    If possible then please share your mAdapter.getCapitolos() method code in your adapter.
  • Flavio Barisi
    Flavio Barisi over 7 years
    it is just an ArrayList with the data that was read from the database in the beginning.
  • user25
    user25 over 7 years
    so f ing easy) no headache
  • Matan Dahan
    Matan Dahan about 7 years
    Your answer is awesome! the if (fromPosition < toPosition) is extremely important for this to work like it should..Thank You!
  • Murcielago
    Murcielago over 6 years
    Is there a way to initiate drag/move on short press instead of long press?
  • Rahul Khurana
    Rahul Khurana over 5 years
    Is there any way to halt drag and drop for specific position?
  • android developer
    android developer almost 5 years
    @DenisNek What you wrote isn't swapping between 2 items. Instead it's moving an item from one place to another. Also, there is no need for a loop. You could just call val item = items.removeAt(fromPosition) items.add(toPosition, item) recyclerView.adapter!!.notifyItemMoved(fromPosition, toPosition)
  • android developer
    android developer almost 5 years
    @RahulKhurana I don't think there is a way, at least not official one. Asked about this here: stackoverflow.com/q/57286355/878126
  • android developer
    android developer almost 5 years
  • Rahul Khurana
    Rahul Khurana almost 5 years
    @androiddeveloper Thanks I have resolved it here stackoverflow.com/a/54216384/4079010 using getMovementFlags method
  • Vishak A Kamath
    Vishak A Kamath over 4 years
    This worker for me Collections.swap(rvAdapter.items, startPos, target) rvAdapter.notifyItemMoved(startPos, target)
  • Muhammad Farhan
    Muhammad Farhan almost 4 years
    Hi, Can you tell me if it is possible to add Drag and Drop this implementation to RecyclerView which Uses ListAdapter with DiffUtils? Your help will be really appreciated.
  • android developer
    android developer almost 4 years
    @MuhammadFarhan I never used it. Thanks. Anyway, I think it might be possible. Just be careful about what you do with the operations. :)
  • Muhammad Farhan
    Muhammad Farhan almost 4 years
    Thanks for your Fast Reply really appreciated. It Works Like a charm with just little of changes like need to use adapter.submitList(newList). To be honest your Drag and Drop onMove Code Works only for me Perfectly. Thanks a lot, mate.
  • android developer
    android developer almost 4 years
    @MuhammadFarhan Nice. What do you mean by "Works only for me Perfectly" ? It worked for me too :)
  • Muhammad Farhan
    Muhammad Farhan almost 4 years
    hahaha It will work for everyone. I didn't found any solution with ListApadater only your's solution works for me to Remove and Add item in List. everyone Swap in List and calls NotifyItemChange which is not working in ListAdapter it show janky animation and set different item on a different position, I have also some Room Operations change Order Like. again Thanks a lot :)
  • android developer
    android developer almost 4 years
    @MuhammadFarhan I think that you should have ID for each item correctly for anything advanced to work properly. Maybe by having a new list, you've missed it on the way, creating new IDs ? I think using a new list is ok. I used it a lot (never used ListAdapter though).
  • SlowDeepCoder
    SlowDeepCoder about 3 years
    That code of the ItemTouchHelper is so beautiful, works like a charm!
  • android developer
    android developer about 3 years
    @SlowDeep I wish I had time to think how to add it to my own apps :)
  • marticztn
    marticztn over 2 years
    @androiddeveloper but if I set my RecyclerView Adapter with setHasStableIds(true), your solution won't work then right? Since the unique ids of the items are related to the item position, your solution does not modify the id of the item?
  • android developer
    android developer over 2 years
    @marticztn You actually have to set it as you wrote. Otherwise it won't work well. The changes to the list aren't enough, because the adapter needs to know about the changes. Each time you change the list in any way that the adapter needs to know, you need to tell it. In this case, it's notifyItemMoved .
  • marticztn
    marticztn over 2 years
    @androiddeveloper Thanks for your reply! I added the code that modifies the unique item ID inside my ItemTouchHelper callback, I also overrode getItemId() in my adapter class, but now when I drag and drop the item, it only drags to 1 position and it just stopped right there, not sure if it's related to the item id modification inside the callbacks function, I tried 4 hours looking for a solution but I got no luck so far, any idea how to solve this issue?
  • android developer
    android developer over 2 years
    @marticztn Create a new question on the website with a small code to demonstrate the issue. I'm sure people will help.