美文网首页
mvvm架构中RecyclerView拖拽抖动,item错乱问题

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

作者: 肆无忌惮_c9a2 | 来源:发表于2018-09-29 16:17 被阅读0次

    对于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

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

    相关文章

      网友评论

          本文标题:mvvm架构中RecyclerView拖拽抖动,item错乱问题

          本文链接:https://www.haomeiwen.com/subject/auavoftx.html