使用GridLayoutManager在RecyclerView中拖放项目

50
我想要实现的目标是:使用GridLayoutManager创建一个RecyclerView,支持拖放并在拖动时重新排列项。
附注:第一次开发拖放功能。
有很多关于如何使用ListView实现此功能的主题,例如:https://raw.githubusercontent.com/btownrippleman/FurthestProgress/master/FurthestProgress/src/com/anappforthat/android/languagelineup/DynamicListView.java
然而,这些示例通常需要大量代码,并且会创建拖动视图的位图。因为RecyclerView提供了重新排列动画,所以似乎应该能够使用View.startDrag(...)和RecyclerView来实现相同的结果,使用notifyItemAdded()notifyItemMoved()notifyItemRemoved()进行操作。
因此,我进行了一些尝试并得出了以下结论:
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);

这段代码有点起作用,我明白交换的部分,但非常不稳定/抖动,并且有时在刷新时整个网格会重新排列回原始顺序或某些随机顺序。无论如何,上面的代码只是我第一次快速尝试,我更感兴趣的是是否有一种标准/最佳实践方法来使用ReyclerView进行拖放,或者解决它的正确方式是否仍然与多年来用于ListViews的方式相同?

4个回答

124

实际上,有一种更好的方法可以实现这一点。您可以使用一些 RecyclerView 的“伴生”类:

ItemTouchHelper,它是

一个实用程序类,用于为 RecyclerView 添加滑动删除和拖放支持。

以及它的ItemTouchHelper.Callback,它是

ItemTouchHelper 和您的应用程序之间的合同

// 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);
    }
};

更多详细信息请查看他们的文档。


有趣,星期一会探索这个解决方案,看起来很有前途。 - patrick.elmquist
1
哪个解决方案,@VishalKhakhkhar?我添加了必需的onSwiped()方法。 - Shine
20
为了正确地进行调序更改,您需要执行以下操作,而不是仅仅使用 Collection.swap() 方法: 如果 fromPosition 小于 toPosition,则执行以下操作: 从 fromPosition 到 toPosition 的范围内,遍历 i,并将 gridItems 中索引为 i 和 i + 1 的元素进行交换。 否则,如果 fromPosition 大于 toPosition,则执行以下操作: 从 fromPosition 到 toPosition 的范围内,反向遍历 i,并将 gridItems 中索引为 i 和 i - 1 的元素进行交换。 - Denis Nek
3
可以通过短按触发拖动/移动吗?而不是长按触发? - Murcielago
8
你写的并不是两个项目之间的交换,而是将一个项目从一个位置移动到另一个位置。此外,不需要使用循环。你可以直接调用 val item = items.removeAt(fromPosition) items.add(toPosition, item) recyclerView.adapter!!.notifyItemMoved(fromPosition, toPosition) - android developer
显示剩余16条评论

20

这是我的数据库重新排序解决方案:

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

有一些支持函数利用了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();
}

如果可能的话,请在您的适配器中分享mAdapter.getCapitolos()方法的代码。 - Farmer
这只是一个ArrayList,其中包含最初从数据库中读取的数据。 - Flavio Barisi
你的答案太棒了!if (fromPosition < toPosition)非常重要,这样才能使它像应该一样工作。谢谢! - Matan Dahan

9

这里,我用Kotlin(在这里)制作了一个完整的示例,如果您愿意,可以启用滑动删除功能。以下是它的全部代码:

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 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)
    }

}

你好,能否告诉我是否可以将拖放功能添加到使用ListAdapter和DiffUtils的RecyclerView实现中?非常感谢您的帮助。 - Muhammad Farhan
1
@MuhammadFarhan我从未使用过它。谢谢。无论如何,我认为这可能是可能的。只是要小心你对操作所做的事情。 :) - android developer
1
感谢您的快速回复,真的非常感激。只需进行一些小的更改,例如需要使用 adapter.submitList(newList),它会像魔术般地运行。说实话,您的拖放 onMove 代码对我来说完美无缺。非常感谢,伙计。 - Muhammad Farhan
ItemTouchHelper的代码非常优美,功能也非常流畅! - SlowDeepCoder
@SlowDeep 我希望我有时间考虑如何将它添加到我的自己的应用程序中 :) - android developer
显示剩余3条评论

-4

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接