【问题标题】:Drag and drop items in RecyclerView with GridLayoutManager使用 GridLayoutManager 在 RecyclerView 中拖放项目
【发布时间】:2015-07-06 05:15:32
【问题描述】:

我想要达到的目标: 有一个带有 GridLayoutManager 的 RecyclerView,它支持拖放并在拖动时重新排列项目。

旁注:第一次使用拖放开发任何东西。

关于如何使用 ListView 实现这个功能的话题很多,例如:https://raw.githubusercontent.com/btownrippleman/FurthestProgress/master/FurthestProgress/src/com/anappforthat/android/languagelineup/DynamicListView.java

然而,这些示例通常包含大量代码,用于创建拖动视图的位图,感觉应该可以使用 View.startDrag(...) 和 RecyclerView 以及 notifyItemAdded()notifyItemMoved() 和 @987654325 来实现相同的结果@ 因为它们提供重新排列动画。

所以我玩了一些并想出了这个:

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?

【问题讨论】:

    标签: android drag-and-drop android-recyclerview gridlayoutmanager


    【解决方案1】:

    实际上有更好的方法来实现这一点。您可以使用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);
        }
    };
    

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

    【讨论】:

    • 很有趣,将在周一探索这个解决方案,但看起来很有希望。
    • 哪个解决方案@VishalKhakhkhar ?我添加了强制 onSwiped() 方法
    • 对于正确的订单更改:而不是 Collection.swap() 你应该这样做: if (fromPosition toPosition; i--) { Collections.swap(gridItems, i, i - 1); } }
    • 有没有办法在短按而不是长按时启动拖动/移动?
    • @DenisNek 你写的不是在两个项目之间交换。相反,它是将一个项目从一个地方移动到另一个地方。此外,不需要循环。你可以打电话给val item = items.removeAt(fromPosition) items.add(toPosition, item) recyclerView.adapter!!.notifyItemMoved(fromPosition, toPosition)
    【解决方案2】:

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

        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() 方法代码。
    • 它只是一个ArrayList,其中包含一开始从数据库中读取的数据。
    • 你的回答太棒了! if (fromPosition
    【解决方案3】:

    在这里,我在 Kotlin (here) 中制作了一个完整的示例,如果您愿意,可以在上面启用 swipe-to-dismiss 。这是它的完整代码:

    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 中?非常感谢您的帮助。
    • @MuhammadFarhan 我从未使用过它。谢谢。无论如何,我认为这是可能的。请注意您对操作的操作。 :)
    • 非常感谢您的快速回复。它就像一个魅力,只需要很少的改变,比如需要使用adapter.submitList(newList)。老实说,您的拖放 onMove 代码仅适用于我。非常感谢,伙计。
    • @MuhammadFarhan 不错。 “只为我完美工作”是什么意思?它也对我有用:)
    • 哈哈哈这对每个人都有效。我没有找到 ListApadater 的任何解决方案,只有您的解决方案适用于我在列表中删除和添加项目。每个人都在列表中交换并调用在 ListAdapter 中不起作用的 NotifyItemChange 它显示 janky 动画并将不同的项目设置在不同的位置,我也有一些房间操作更改顺序之类的。再次非常感谢:)
    【解决方案4】:

    我发现Advancedrecyclerview 非常有用。看看!!!

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-05-17
      • 1970-01-01
      • 1970-01-01
      • 2016-02-19
      • 1970-01-01
      • 2016-12-02
      • 1970-01-01
      相关资源
      最近更新 更多