如何复制现有的 ListAdapter 列表,以便可以使用它来移动项目,然后通过将新复制的列表与原始列表进行比较来更新 UI?
也许Android开发人员在设计用于RecyclerView列表的ListAdapter的源代码时只想到了CRUD操作。如果是这样,他们就忽略了其他重要的 recyclerView 操作,例如过滤、排序和移动列表项。这些操作唯一改变的是排序顺序(排序和移动项目)或从原始列表中删除一些项目(用于过滤),但这现在需要 CustomAdapter?此外,对于非常大的 RecyclerView 列表,需要将列表复制到内存中,以便 ListAdapter“看到”新列表,这似乎违背了 ListAdapter 在后台线程上提供效率提升的最初目的。
我欣然接受将现有项目转换为 ListAdapter,因为我认为后台线程的更新对于稳定性和 UI 效率非常有价值。我的 CRUD 操作运行良好,但现在艰巨的任务是弄清楚如何进行其他过滤、排序和移动列表项。
我尝试移动 RecyclerView 列表中的项目,但拖放不起作用。以下是我迄今为止尝试过的不同方法的各种组合。与此同时,我阅读了 stackoverflow 上的所有其他 ListAdapter 问题...不幸的是我对 Kotlin 的了解为零,所以我正在寻找 Java 解决方案。我在这里缺少什么?
MainActivity 方法#1:
ItemTouchHelper.SimpleCallback itemTouchHelperCallback1 = new ItemTouchHelper.SimpleCallback(
(ItemTouchHelper.UP | ItemTouchHelper.DOWN),0) {
int dragFrom = -1;
int dragTo = -1;
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
int fromPos = viewHolder.getBindingAdapterPosition();
int toPos = target.getBindingAdapterPosition();
if (dragFrom == -1) {
dragFrom = fromPos;
}
dragTo = toPos;
cardsAdapter.moveItem(fromPos, toPos);
return true;
}
};
new ItemTouchHelper(itemTouchHelperCallback1).attachToRecyclerView(recyclerView);
MainActivity 方法#2,针对 onMove() 方法:
List newList = adapter.getCurrentList();
Quickcard quickcardItem = newList.get(fromPos);
newList.remove(fromPos);
newList.add(toPos, quickcardItem);
cardsAdapter.moveItem(fromPos, toPos);
cardsAdapter.notifyItemMoved(fromPos, toPos);
cardsAdapter.submitList(newList);
return true;
ListAdapter 方法#1:
public class CardsAdapter extends ListAdapter<Quickcard, CardsAdapter.ItemHolder> {
private final LayoutInflater layoutInflater;
protected CardsAdapter(@NonNull LayoutInflater layoutInflater, DiffUtil.ItemCallback<Quickcard> diffCallback) {
super(diffCallback);
this.layoutInflater = layoutInflater;
}
... // ViewHolder code
public void moveItem(int fromOldPos, int toNewPos) {
// Copy the ListAdapter's existing list
ArrayList<Quickcard> currList = new ArrayList<>(getCurrentList());
if (fromOldPos == toNewPos) {
return;
}
Collections.swap(currList, toNewPos, fromOldPos);
notifyItemMoved(fromOldPos, toNewPos);
submitList(currList);
}
}
ListAdapter 方法#2,用于 moveItem() 方法:
List<Quickcard> currList = getCurrentList();
List<Quickcard> copiedList = new ArrayList<>(currList);
if (fromOldPos == toNewPos) {
return;
}
if (fromOldPos < toNewPos) { // the q. is being moved down the q.list
for (int i = fromOldPos; i < toNewPos; i++) {
Collections.swap(copiedList, i,i + 1);
}
} else { // the q. is being moved up the q.list
for (int i = fromOldPos; i > toNewPos; i--) {
Collections.swap(copiedList, i,i - 1);
}
}
notifyItemMoved(fromOldPos, toNewPos);
submitList(copiedList);
不幸的是,
ListAdapter
本身在拖动/滑动操作方面存在问题,但submitList
方法的大多数优秀功能都可以在常规适配器中使用DiffUtil
方法来复制。您可以通过在其他地方(例如在 ViewModel 中)过滤列表并使用 submitList
发布它来处理过滤。通过这样做,您可以访问显示的列表,并可以轻松修改它以进行拖动/滑动事件,同时在发布新列表或重新排序的列表时仍然获得 ListAdapter
的一些好处。
这里有一个示例,说明如何执行此操作,并通过使用
submitList
工具的 DiffUtil
获得“类似 ListAdapter”行为的有效拖动/滑动行为。您确实丢失了 diff 回调的后台线程。这可以稍微困难一点地添加回 submitList
(或者您可以使用 Kotlin 和协程更轻松地添加它)。
适配器
由于适配器可以访问显示的列表,因此当项目被移动/刷开时,它可以在其中移动项目或将其删除。当使用
submitList
发布新列表时,它使用 DiffUtil 方法将新列表与当前显示的列表进行比较,更新显示的列表,并将更改分派到要动画化的回收器视图。
如果您需要将订单更改/刷回 ViewModel 或其他层,请将接口传递给适配器并根据需要在
onItemMove
和 onItemDismiss
中调用它,让后端知道项目位置已更改或项目已被删除。此类更改不应导致后端向 submitList
提交新列表,因为显示的列表已更新。
public class RecyclerViewAdapter
extends RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder>
implements ItemTouchHelperAdapter
{
// Define a diff callback, similar to what is used in ListAdapter,
// but with a couple other methods.
private static class DiffCallback extends DiffUtil.Callback {
DiffCallback(List<RecyclerViewData> old) {
oldList = old;
}
@Override
public int getOldListSize() {
return oldList.size();
}
@Override
public int getNewListSize() {
return newList.size();
}
// Each entry in the example has a UUID, this compares only
// the UUID, not the name or birthday, to determine if an
// item moved
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return oldList.get(oldItemPosition).isSameEntry(newList.get(newItemPosition));
}
// Compare the displayed data (name and birthday) to decide
// if it should be redrawn/rebound
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return oldList.get(oldItemPosition).isSameAs(newList.get(newItemPosition));
}
List<RecyclerViewData> newList = new ArrayList<>();
private final List<RecyclerViewData> oldList;
}
// Basic ViewHolder
public static class ViewHolder extends RecyclerView.ViewHolder {
TextView name;
TextView birthday;
public ViewHolder(@NonNull View itemView) {
super(itemView);
name = itemView.findViewById(R.id.name);
birthday = itemView.findViewById(R.id.birthday);
}
}
private final LayoutInflater inflater;
private final ArrayList<RecyclerViewData> displayedList = new ArrayList<>();
private final DiffCallback diffCallback = new DiffCallback(displayedList);
public RecyclerViewAdapter (Context ctx) {
this.inflater = LayoutInflater.from(ctx);
}
// Implement methods from ItemTouchHelperAdapter interface, to be called
// from the ItemTouchHelper.SimpleCallback
@Override
public boolean onItemMove(RecyclerView.ViewHolder from, RecyclerView.ViewHolder to) {
int fromP = from.getAdapterPosition();
int toP = to.getAdapterPosition();
if( fromP == toP ) {
return false;
}
else {
Collections.swap(displayedList, fromP, toP);
notifyItemMoved(fromP, toP);
return true;
}
}
@Override
public void onItemDismiss(int position, int direction) {
displayedList.remove(position);
notifyItemRemoved(position);
}
// Mimic the ListAdapter "submitList" method, but without the benefit
// of a background thread. Could be added if needed, but adds complexity.
public void submitList(List<RecyclerViewData> newList) {
diffCallback.newList = newList;
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback);
displayedList.clear();
displayedList.addAll(newList);
diffResult.dispatchUpdatesTo(this);
}
// Basic RecyclerView stuff
@NonNull
@Override
public RecyclerViewAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = inflater.inflate(R.layout.profile_row,parent,false);
return new RecyclerViewAdapter.ViewHolder(view);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public void onBindViewHolder(@NonNull RecyclerViewAdapter.ViewHolder holder, int position) {
RecyclerViewData data = displayedList.get(position);
holder.name.setText(data.getName());
holder.birthday.setText(data.getBirthday());
}
@Override
public int getItemCount() {
return displayedList.size();
}
}
活动
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView recyclerView = findViewById(R.id.profile_list);
RecyclerViewAdapter adapter = new RecyclerViewAdapter(this);
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
ItemTouchHelper ith = new ItemTouchHelper(new SimpleItemTouchHelper(adapter));
ith.attachToRecyclerView(recyclerView);
// this should come from a ViewModel, just here for simplicity
ArrayList<RecyclerViewData> data = new ArrayList<>();
data.add(new RecyclerViewData("Bob", "Jan 12"));
data.add(new RecyclerViewData("Frank", "Feb 12"));
data.add(new RecyclerViewData("Herbert", "Mar 22"));
data.add(new RecyclerViewData("Joe", "Apr 15"));
data.add(new RecyclerViewData("Bob", "Jan 12"));
// when the ViewModel gets data, or filters the list to show it in a new order,
// it calls "submitList" and the adapter figures out what to show/move
adapter.submitList(data);
}
物品触摸界面
(不是绝对必要的,但很好地将适配器与项目触摸助手分离)
public interface ItemTouchHelperAdapter {
boolean onItemMove(RecyclerView.ViewHolder from, RecyclerView.ViewHolder to);
void onItemDismiss(int position, int direction);
}
item触摸回调
简单的回调,只需调用适配器上的
onItemMove
和 onItemDismiss
。
public class SimpleItemTouchHelper extends ItemTouchHelper.SimpleCallback {
private final ItemTouchHelperAdapter adapter;
SimpleItemTouchHelper(ItemTouchHelperAdapter adapter) {
super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.RIGHT);
this.adapter = adapter;
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
return adapter.onItemMove(viewHolder, target);
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
adapter.onItemDismiss(viewHolder.getAdapterPosition(), direction);
}
}
数据类
public class RecyclerViewData {
private final String uuid;
private final String name;
private final String birthday;
public RecyclerViewData(String name, String birthday) {
this.uuid = UUID.randomUUID().toString();
this.name = name;
this.birthday = birthday;
}
public boolean isSameEntry(RecyclerViewData other) {
return this.uuid.equals(other.uuid);
}
public boolean isSameAs(RecyclerViewData other) {
return this.name.equals(other.name) &&
this.birthday.equals(other.birthday);
}
public String getName() {
return name;
}
public String getBirthday () {
return birthday;
}
}