使用 GridLayoutManager 在 RecyclerView 中拖放项目
Drag and drop items in RecyclerView with GridLayoutManager
我想达到的目标:
有一个带有 GridLayoutManager 的 RecyclerView,它支持拖放并在拖动时重新排列项目。
旁注:第一次使用拖放进行开发。
关于如何使用 ListView 实现此功能的主题有很多,例如:https://raw.githubusercontent.com/btownrippleman/FurthestProgress/master/FurthestProgress/src/com/anappforthat/android/languagelineup/DynamicListView.java
然而,示例通常包含大量代码,创建拖动视图的位图,感觉应该可以使用 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);
这段代码有点工作,我得到了交换,但非常 unstable/shaky 有时当它刷新时,整个网格重新排列回原始顺序或随机排列。无论如何,上面的代码只是我的第一次快速尝试,我真正更感兴趣的是,是否有一些 standard/best 使用 ReyclerView 进行拖放的实践方法,或者解决它的正确方法是否仍然是多年来一直用于 ListViews 的相同内容?
实际上有更好的方法来实现这一点。你可以使用一些 RecyclerView
的 "companion" 类:
ItemTouchHelper
,也就是
a utility class to add swipe to dismiss and drag & drop support to RecyclerView.
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);
}
};
有关详细信息,请查看他们的文档。
这是我的数据库重新排序解决方案:
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();
}
我发现 Advancedrecyclerview 非常有用。看看吧!!!
在这里,我用 Kotlin (here) 制作了一个完整示例,如果您愿意,可以在其上启用滑动关闭功能。这是它的完整代码:
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)
}
}
我想达到的目标: 有一个带有 GridLayoutManager 的 RecyclerView,它支持拖放并在拖动时重新排列项目。
旁注:第一次使用拖放进行开发。
关于如何使用 ListView 实现此功能的主题有很多,例如:https://raw.githubusercontent.com/btownrippleman/FurthestProgress/master/FurthestProgress/src/com/anappforthat/android/languagelineup/DynamicListView.java
然而,示例通常包含大量代码,创建拖动视图的位图,感觉应该可以使用 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);
这段代码有点工作,我得到了交换,但非常 unstable/shaky 有时当它刷新时,整个网格重新排列回原始顺序或随机排列。无论如何,上面的代码只是我的第一次快速尝试,我真正更感兴趣的是,是否有一些 standard/best 使用 ReyclerView 进行拖放的实践方法,或者解决它的正确方法是否仍然是多年来一直用于 ListViews 的相同内容?
实际上有更好的方法来实现这一点。你可以使用一些 RecyclerView
的 "companion" 类:
ItemTouchHelper
,也就是
a utility class to add swipe to dismiss and drag & drop support to RecyclerView.
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);
}
};
有关详细信息,请查看他们的文档。
这是我的数据库重新排序解决方案:
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();
}
我发现 Advancedrecyclerview 非常有用。看看吧!!!
在这里,我用 Kotlin (here) 制作了一个完整示例,如果您愿意,可以在其上启用滑动关闭功能。这是它的完整代码:
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)
}
}