您现在的位置是:首页 >学无止境 >【RecyclerView】同时刷新和滚动导致,滚动位置异常(一)网站首页学无止境

【RecyclerView】同时刷新和滚动导致,滚动位置异常(一)

花尾巴狼_ 2023-07-07 16:00:02
简介【RecyclerView】同时刷新和滚动导致,滚动位置异常(一)

前置:

被选中item高度与非选中item高度不一致,且硬件有点卡,运行会有一定卡顿。

可视界面的item为三个,总数据为十个。

期望效果:

=》

 实际上效果:

 

 代码:

        mListAdapter.setSelectedPosition(position);
        mListView.post(() -> {
            mListView.smoothScrollToPosition(position);
        });
    public void setSelectedPosition(int position) {
        if (mSelectedPosition == position) {
            return;
        }
        mSelectedPosition = position;
        mSelectedSubPoiPosition = -1;
        if (0 > mSelectedPosition) {
            return;
        }
        if (mSelectedPosition >= getItemCount()) {
            return;
        }
        notifyDataSetChanged();
    }

代码很简单,先notify在进行scroll,看代码是不会出现这个问题,后来进行初步调试,推测为view未渲染完毕,进行滚动,导致滚动的位置与实际偏差。(选中和未选中的item高度不一致)

一.解决方案

    public void setSelectedPosition(int position) {
        if (mSelectedPosition == position) {
            return;
        }
        int mLastSelectPos=mSelectedPosition;
        mSelectedPosition = position;
        mSelectedSubPoiPosition = -1;
        if (0 > mSelectedPosition) {
            return;
        }
        if (mSelectedPosition >= getItemCount()) {
            return;
        }
      //修改为:范围刷新
      notifyItemRangeChanged(Math.min(mLastSelectPos, mSelectedPosition), Math.abs(mLastSelectPos - mSelectedPosition) + 1);//受影响的itemd都刷新下

    }
   mListView.post(() -> {
            //TODO 添加代码 把刷新动画end,设置为null也会失败,但是end有用
            RecyclerView.ItemAnimator itemAnimator = mListView.getItemAnimator();
            if (itemAnimator != null) {
                itemAnimator.endAnimations();
            }
            mListView.smoothScrollToPosition(scrollPosition);
        });

二.recyclerView的刷新

我的思路是,首先对控件的加载进行优化,现针对notify刷新,其次为滚动动画。

1. 刷新全部的item,notifyDataSetChanged()   
2. 刷新指定的item,notifyItemChanged(int)   
3. 从指定的位置开始刷新指定个item,notifyItemRangeChanged(int,int) 这个刷新onBindViewHolder方法,position才能保持一直
4. 局部刷新指定的数据,notifyItemChanged(int, Object)

根据浏览器和源码的结果,主要分为以上四种刷新方式。

1.notifyItemChanged

1.1 notifyItemChanged(position)

想先使用notifyItemChanged,指定刷新本次选中和上次选中的item,精准打击,这样刷新是最节约资源的。

        notifyItemChanged(mLastSelectPos);
        notifyItemChanged(mSelectedPosition);

onBindViewHolder也只会调用俩次。

但是加上滚动,且滚动的目标位置距离过远,导致中间加载的view数量多,滚动的位置也会出现偏差。并未解决到BUG。

1.2 notifyItemChanged(position,payloads)

payloads我理解为一个标识符,在onBindViewHolder的方法中进行判断,针对item的控件单独刷新,其余并无区别。

    @Override
    public final void onBindViewHolder(VH holder, int position, List<Object> payloads) {
      
    }

用法举个栗子:

        //"a"和"b"都是标识符,可以换成其他的
        notifyItemChanged(mLastSelectPos,"a");
        notifyItemChanged(mSelectedPosition,"b");
    @Override
    public final void onBindViewHolder(VH holder, int position, List<Object> payloads) {
      
     if (payloads.get(0).equals("a")){
            ((Item) holder).getmNameTv().setText("我是A");
        }else if (payloads.get(0).equals("b")){
            ((Item) holder).getmNameTv().setText("我是B");
        }
    }

1.3 它们终归指向何处?

 

查看源码都指向了 notifyItemRangeChanged

 2.notifyItemRangeChanged

 先看参数源码解释:

positionStart :刷新开始的位置/下标

itemCount:从positionStart开始数itemCount个数需要刷新。

用法:

notifyItemRangeChanged(Math.min(mLastSelectPos, mSelectedPosition), Math.abs(mLastSelectPos - mSelectedPosition) + 1)

 举个栗子:

notifyItemRangeChanged(0,3),刷新下标:012

notifyItemRangeChanged(3,5),刷新下标:23456

3.刷新流程=》Observable

我们现在知道是由Observable促成刷新,来看下如何进行注册。

首先进行注册

RecyclerView 会一开始new一个RecyclerViewDataObserver进行注册。

private final RecyclerViewDataObserver mObserver = new RecyclerViewDataObserver();

    ....
public void setAdapter(@Nullable Adapter adapter) {
        // bail out if layout is frozen
        setLayoutFrozen(false);
        setAdapterInternal(adapter, false, true);
        processDataSetCompletelyChanged(false);
        requestLayout();
    }

    ....

private void setAdapterInternal(@Nullable Adapter adapter, boolean compatibleWithPrevious,
            boolean removeAndRecycleViews) {
        if (mAdapter != null) {
            mAdapter.unregisterAdapterDataObserver(mObserver);
            mAdapter.onDetachedFromRecyclerView(this);
        }
        if (!compatibleWithPrevious || removeAndRecycleViews) {
            removeAndRecycleViews();
        }
        mAdapterHelper.reset();
        final Adapter oldAdapter = mAdapter;
        mAdapter = adapter;
        if (adapter != null) {
            adapter.registerAdapterDataObserver(mObserver);
            adapter.onAttachedToRecyclerView(this);
        }
        if (mLayout != null) {
            mLayout.onAdapterChanged(oldAdapter, mAdapter);
        }
        mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
        mState.mStructureChanged = true;
    }
    ...

我们来看下RecyclerViewDataObserver里面的方法对应RecyclerView的哪些调用。

notifyDataSetChanged() => 
RecyclerViewDataObserver.onChanged()
notifyItemChange()/notifyItemRangeChanged()  =>
RecyclerViewDataObserver.onItemRangeChanged();

先看下最开始调用的RecyclerViewDataObserver.onChanged()里做了什么事。


    private class RecyclerViewDataObserver extends AdapterDataObserver {
       

        @Override
        public void onChanged() {
            assertNotInLayoutOrScroll(null);
       
            mState.mStructureChanged = true;
         // 清空mCachedViews
            processDataSetCompletelyChanged(true);
        // 刷新UI
            if (!mAdapterHelper.hasPendingUpdates()) {
                requestLayout();
            }
        }
        }    
    void processDataSetCompletelyChanged(boolean dispatchItemsChanged) {
        mDispatchItemsChangedEvent |= dispatchItemsChanged;
        mDataSetHasChangedAfterLayout = true;
    //添加标识符
        markKnownViewsInvalid();
    }

调用markKnownViewsInvalid()使用for循环给mCachedViews 的每个viewHolder添加标识符FLAG_UPDATE或者FLAG_INVALID。

 // RecyclerView.java/Recycler
 void markKnownViewsInvalid() {
 	// 遍历缓存的view设置标志
     int cachedCount = this.mCachedViews.size();
     for(int i = 0; i < cachedCount; ++i) {
         RecyclerView.ViewHolder holder = (RecyclerView.ViewHolder)this.mCachedViews.get(i);
         if (holder != null) {
             holder.addFlags(6);
             holder.addChangePayload((Object)null);
         }
     }
	 // 决定回收和清除view的关键是 是否设置的stableIds
     if (RecyclerView.this.mAdapter == null || !RecyclerView.this.mAdapter.hasStableIds()) {
     	 // 将mCacheViews中缓存的viewHolder全部移入mRecyclerPool中
         this.recycleAndClearCachedViews();
     }
 }

下一步查看了网上的相关博客,我们可以倒回去看requestLayout()刷新布局,会执行onMeasure()
onLayout(),那么势必就涉及到viewHolder的复用问题,而在填充fill之前,会在之前
执行 detachAndScrapAttachedViews 方法进行缓存。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。