mvvm架构中RecyclerView拖拽抖动,item错乱问题
对于RecyclerView的侧滑和拖拽功能,实现并不复杂,网上一搜就能出来,这里主要记录一下在mvvm中遇到的问题
大体实现步骤:
新建监听类继承ItemTouchHelper.Callback
public class ItemTouchHelperCallback extends ItemTouchHelper.Callback
覆写方法
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
//返回侧滑和拖拽的标志
dragFlag = ItemTouchHelper.DOWN | ItemTouchHelper.UP;
swipeFlag = ItemTouchHelper.LEFT;
return makeMovementFlags(dragFlag, swipeFlag);
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder from, @NonNull RecyclerView.ViewHolder to) {
//实现拖拽交换逻辑,刷新adapter,此方法会不停的调用
return true;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int i) {
//实现侧滑删除逻辑,刷新adapter
}
然后绑定到RecyclerView也就算完成了
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new ItemTouchHelperCallback(adapter));
itemTouchHelper.attachToRecyclerView(activityMainBinding.recycler);
mvvm中遇到的问题
一般情况下,只要根据自己的需求实现好拖拽和侧滑的逻辑是没有问题的,但是很多情况下我们会在adapter中使用 ObservableArrayList + ObservableArrayList.OnListChangedCallback 实现UI自动刷新。此时会出现无法拖拽item,拖拽抖动,item错乱等问题。
其实在我强调ObservableArrayList + ObservableArrayList.OnListChangedCallback的时候你应该已经知道,问题就出在这里,是的,但我在将问题定位到这里之前是一头雾水...
ObservableArrayList.OnListChangedCallback主要实现以下方法
@Override
public void onItemRangeChanged(ObservableArrayList<T> sender, int positionStart, int itemCount) {
if (!isEnable) {//为什么加这个标记后面解释
adapter.notifyItemRangeChanged(positionStart + adapter.getHeadersCount(), itemCount);
}
}
@Override
public void onItemRangeInserted(ObservableArrayList<T> sender, int positionStart, int itemCount) {
//-----------省略
}
@Override
public void onItemRangeMoved(ObservableArrayList<T> sender, int fromPosition, int toPosition, int itemCount) {
if (itemCount == 1) {
adapter.notifyItemMoved(fromPosition + adapter.getHeadersCount(), toPosition);
} else {
adapter.notifyDataSetChanged();
}
}
@Override
public void onItemRangeRemoved(ObservableArrayList<T> sender, int positionStart, int itemCount) {
//-----------省略
}
在ItemTouchHelper.Callback中onMove我们一般的拖拽交换逻辑是这样的
if (fromListPosition < toListPosition) {
for (int i = fromListPosition; i < toListPosition; i++) {
Collections.swap(getList(), i, i + 1);
}
} else {
for (int i = fromListPosition; i > toListPosition; i--) {
Collections.swap(getList(), i, i - 1);
}
}
由于此时UI具有自动刷新功能,所以你认为它应该会走上面的onItemRangeMoved回调,那么一切都很美好,然而实际通过打Log你发现并没有走到onItemRangeMoved回调,也就是说adapter.notifyItemMoved
没有调用,自然没有交换成功。那么我们在实现交换逻辑后手动调用adapter.notifyItemMoved呢,结果仍然是出现item闪烁和错乱。
查看ObservableArrayList源码,发现它内部有一个ListChangeRegistry变量,回调主要是通过调用ListChangeRegistry的notify**一系列方法,最终调用ListChangeRegistry的notifyCallbacks,然后根据标志触发回调。
private static final int ALL = 0;
private static final int CHANGED = 1;
private static final int INSERTED = 2;
private static final int MOVED = 3;
private static final int REMOVED = 4;
private static final CallbackRegistry.NotifierCallback<ObservableList.OnListChangedCallback,
ObservableList, ListChanges> NOTIFIER_CALLBACK = new CallbackRegistry.NotifierCallback<
ObservableList.OnListChangedCallback, ObservableList, ListChanges>() {
@Override
public void onNotifyCallback(ObservableList.OnListChangedCallback callback,
ObservableList sender, int notificationType, ListChanges listChanges) {
switch (notificationType) {
case CHANGED:
callback.onItemRangeChanged(sender, listChanges.start, listChanges.count);
break;
case INSERTED:
callback.onItemRangeInserted(sender, listChanges.start, listChanges.count);
break;
case MOVED:
callback.onItemRangeMoved(sender, listChanges.start, listChanges.to,
listChanges.count);
break;
case REMOVED:
callback.onItemRangeRemoved(sender, listChanges.start, listChanges.count);
break;
default:
callback.onChanged(sender);
break;
}
}
};
也不知道是我太菜还是确实没有,我在ObservableArrayList只看到CHANGED,INSERTED,MOVED三种,也就是说ObservableArrayList根本无法触发onItemRangeMoved回调,那抖动和错乱是怎么回事呢,在看看拖拽交换逻辑:
if (fromListPosition < toListPosition) {
for (int i = fromListPosition; i < toListPosition; i++) {
Collections.swap(getList(), i, i + 1);
}
} else {
for (int i = fromListPosition; i > toListPosition; i--) {
Collections.swap(getList(), i, i - 1);
}
}
我估计问题在于
Collections.swap(getList(), i, i + 1);
查看它的源码
public static void swap(List<?> list, int i, int j) {
// instead of using a raw type here, it's possible to capture
// the wildcard but it will require a call to a supplementary
// private method
final List l = list;
l.set(i, l.set(j, l.get(i)));
}
它的内部是通过list的两次调用set方法实现的,在回到ObservableArrayList的源码中
@Override
public T set(int index, T object) {
T val = super.set(index, object);
if (mListeners != null) {
mListeners.notifyChanged(this, index, 1);
}
return val;
}
set方法触发了mListeners.notifyChanged,那么它最终会通过CHANGED标志触发onItemRangeChanged回调,通过log发现的确要交换的两个item走了两次onItemRangeChanged回调,那么问题应该就是这了,拖动过程不断回调onItemRangeChanged更新item导致了抖动错乱等问题。
那怎么解决呢?修改源码是不可能修改的,这辈子都不可能改!不用自动更新又不行,那就只能想办法在需要的时候屏蔽掉onItemRangeChanged中的更新逻辑,手动调用adapter.notifyItemMoved,不需要的时候恢复原本的更新逻辑咯。于是就有了之前提到的isEnable标志
@Override
public void onItemRangeChanged(ObservableArrayList<T> sender, int positionStart, int itemCount) {
if (!isEnable) {//为什么加这个标记后面解释
adapter.notifyItemRangeChanged(positionStart + adapter.getHeadersCount(), itemCount);
}
}
如何控制这个标志?肯定是在拖动开始和结束的时候,在拖拽回调类ItemTouchHelper.Callback中实现
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG && isListItem(viewHolder.getAdapterPosition())) {
//拖拽时设置自动刷新监听器状态
adapter.enableChangedCallback(true);
}
super.onSelectedChanged(viewHolder, actionState);
}
@Override
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
//设置自动刷新监听器状态
adapter.enableChangedCallback(false);
}
通过onSelectedChanged判断拖拽开始设置开始标志,通过clearView设置结束标志,ObservableArrayList.OnListChangedCallback实现类变量一般定义在adapter中,我们通过adapter控制ObservableArrayList.OnListChangedCallback的isEnable标志达到目的。
注意标志一定要恢复!注意标志屏蔽后拖拽要手动调用adapter.notifyItemMoved
具体的实现在我的项目中有源码
https://github.com/wbaizx/BaseBindingRecyclerViewAdapter
写的不好或者有错的地方希望提出,谢谢。