mvvm架构中RecyclerView拖拽抖动,item错乱问题

2018-09-29  本文已影响0人  肆无忌惮_c9a2

对于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.CallbackonMove我们一般的拖拽交换逻辑是这样的

 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

写的不好或者有错的地方希望提出,谢谢。

上一篇下一篇

猜你喜欢

热点阅读