基本使用
- # RecyclerView GridLayoutManager 等分间距
- # 基于RecyclerView实现网格分页LayoutManager——PagerGridLayoutManager
- # GridLayoutManager这么用,你可能还真没尝试过
开源库
明天待看
- www.jianshu.com/p/f2a5764a3…
- www.jianshu.com/p/5735db176…
- www.jianshu.com/p/60aa2fc17…
- mp.weixin.qq.com/s/08LpubdLT…
- mp.weixin.qq.com/s/S31bHWLtU…
- mp.weixin.qq.com/s/R0w-UBB1f…
- juejin.im/post/684490…
- juejin.im/post/685457…
- www.jianshu.com/p/3e9aa4bda…
- www.jianshu.com/p/5034a4eea…
- www.jianshu.com/p/40820ea48…
- juejin.im/post/684490…
- github.com/yangchong21…
- juejin.im/post/684490…
- juejin.im/post/684490…
- juejin.im/post/684490…
- juejin.im/post/684490…
- juejin.im/post/684490…
- juejin.im/post/684490…
- www.jianshu.com/p/7684fc2d4…
- www.jianshu.com/p/1d2213f30…
- www.wanandroid.com/wxarticle/l…
文章
- Android中RecyclerView出现java.lang.IndexOutOfBoundsException
- 非常牛逼,刷新指定ITEM中的指定View
- 当RecyclerView遇到Inconsistency detected崩溃时
- Inconsistency detected Invalid view holder adapter position"
- Android 复杂的列表视图新写法 MultiType (v3.1.0 修订版)
- Android 复杂的多类型列表视图新写法:MultiType
- MultiType
- RecyclerView系列文章 RecyclerView缓存机制(咋复用?)
RecyclerView动画
源码解析
0. 待看
mp.weixin.qq.com/s/08LpubdLT…
www.jianshu.com/p/1ae2f2fcf…
www.jianshu.com/p/f2a5764a3…
www.jianshu.com/p/5735db176…
1. RecyclerView缓存机制
- RecyclerView缓存机制(咋复用?)
- RecyclerView缓存机制(回收些啥?)
- RecyclerView缓存机制(回收去哪?)
- RecyclerView缓存机制(scrap view)
- LinearLayoutManager中滑动触发的列表项回收,回收哪些列表项 没想明白,看代码好像没有移出屏幕也会被回收?
- 明天继续看其他文章,把'没有移出屏幕也会被回收'的问题搞清楚,然后把缓存机制整理好,继续看源码其他方面.
2. RecyclerView测量,布局,绘制流程,Recycler,Adapter
onMeasure
-->
onLayout
--> dispatchLayout
-->
case mState.mLayoutStep == State.STEP_START:
dispatchLayoutStep1();//D1
dispatchLayoutStep2();//D2
dispatchLayoutStep3();//D3
case D1,D2在onMeasure中已经执行过,但是此时RecyclerView的宽高发生变化:
dispatchLayoutStep2();
dispatchLayoutStep3();
default:
dispatchLayoutStep3();
-->
onDraw
- dispatchLayoutStep1
- 记录ViewHolder状态及子View信息
- 如有必要/mState.mRunPredictiveAnimations是true,则执行'预布局'
mState.mStructureChanged = false; // temporarily disable flag because we are asking for previous layout mLayout.onLayoutChildren(mRecycler, mState);
- dispatchLayoutStep2
- 使用LayoutManager对子View真正进行布局
- 如有必要,dispatchLayoutStep2会执行多次
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
- dispatchLayoutStep3
- 将之前保存的ViewHolder和其对应的Item动画信息进行绑定/存储
- 执行Item动画.
- 真正执行动画逻辑的是RecyclerView.ItemAnimator
- 将布局,动画交给LayoutManager,ItemAnimator,可以由用户自定义,实现不同的布局方式及布局动画.
- onDraw:单纯绘制分隔线
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
final int count = mItemDecorations.size();
//绘制分隔线
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
- Recycler
public final class Recycler {
static final int DEFAULT_CACHE_SIZE = 2;
private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;//就是2
int mViewCacheMax = DEFAULT_CACHE_SIZE;//就是2
//一级缓存: mAttachedScrap + mChangedScrap
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
ArrayList<ViewHolder> mChangedScrap = null;
//二级缓存: mCachedViews
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
//三级缓存
private ViewCacheExtension mViewCacheExtension;
//四级缓存
RecycledViewPool mRecyclerPool;
****
}
- 缓存存储的是ViewHolder实例
- 一级缓存: mChangedScrap + mAttachedScrap
- mChangedScrap
- 仅参与预布局
- mAttachedScrap
- 存放还会被复用的ViewHolder
- mChangedScrap
- 二级缓存: mCachedViews
- 最多存放2个ViewHolder
- 三级缓存: mViewCacheExtension,需要开发者自行实现ViewCacheExtension
- 四级缓存: mRecyclerPool
- 可以将RecycledViewPool当做1个SparseArray. key是要存储的ViewHolder的viewType, value是ArrayList.
- 每个ArrayList最多存放5个ViewHolder实例,已满则不再存放
public static class RecycledViewPool { private static final int DEFAULT_MAX_SCRAP = 5; static class ScrapData { //真正存放ViewHolder实例的位置 final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>(); int mMaxScrap = DEFAULT_MAX_SCRAP;//5 long mCreateRunningAverageNs = 0; long mBindRunningAverageNs = 0; } //每个存储的ViewHolder实例都被放在 ScrapData 中的 mScrapHeap 中. SparseArray<ScrapData> mScrap = new SparseArray<>(); //向RecycledViewPool中存储ViewHolder实例 public void putRecycledView(ViewHolder scrap) { final int viewType = scrap.getItemViewType(); final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap; //最多存放5个ViewHolder实例,已满则不再存放 if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) { return; } //之前已经存放了,报异常 if (DEBUG && scrapHeap.contains(scrap)) { throw new IllegalArgumentException("this scrap item already exists"); } //将ViewHolder实例添加到scrapHeap中 scrap.resetInternal(); scrapHeap.add(scrap); } //通过ViewHolder的viewType,获取存储过的/新创建的ScrapData实例 private ScrapData getScrapDataForType(int viewType) { ScrapData scrapData = mScrap.get(viewType); if (scrapData == null) { scrapData = new ScrapData(); mScrap.put(viewType, scrapData); } return scrapData; } } - 通过Recycler获取ViewHolder实例
public View getViewForPosition(int position) { return getViewForPosition(position, false); } View getViewForPosition(int position, boolean dryRun) { return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView; } ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) { if (position < 0 || position >= mState.getItemCount()) { //这儿就是RecyclerView内部的BUG抛出异常的位置 throw new IndexOutOfBoundsException("Invalid item position " + position + "(" + position + "). Item count:" + mState.getItemCount() + exceptionLabel()); } boolean fromScrapOrHiddenOrCache = false; ViewHolder holder = null; // 0) 如果处于'预布局',则从mChangedScrap中获取ViewHolder实例 if (mState.isPreLayout()) { holder = getChangedScrapViewForPosition(position); fromScrapOrHiddenOrCache = holder != null; } // 1) 通过position,从mAttachedScrap,已移出屏幕的视图(隐藏列表项),及mCachedViews中获取ViewHolder实例 if (holder == null) { holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); *** } if (holder == null) { *** final int type = mAdapter.getItemViewType(offsetPosition); // 2) Find from scrap/cache via stable ids, if exists if (mAdapter.hasStableIds()) { //通过id,从mAttachedScrap及mCachedViews中获取ViewHolder实例 holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun); *** } if (holder == null && mViewCacheExtension != null) { //从自定义缓存中尝试获取ViewHolder实例 // We are NOT sending the offsetPosition because LayoutManager does not // know it. *** } if (holder == null) { // fallback to pool //通过RecycledViewPool在缓存池中获取ViewHolder实例 holder = getRecycledViewPool().getRecycledView(type); *** } if (holder == null) { *** //所有缓存都没有命中,只能通过Adapter创建ViewHolder实例 holder = mAdapter.createViewHolder(RecyclerView.this, type); *** } } *** boolean bound = false; *** //只有invalid的viewHolder才能绑定视图数据 //未绑定 || 需要更新 || 已失效 if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) { *** //对于需要绑定数据的ViewHolder执行tryBindViewHolderByDeadline. final int offsetPosition = mAdapterHelper.findPositionOffset(position); bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs); } *** return holder; } private boolean tryBindViewHolderByDeadline(@NonNull ViewHolder holder, int offsetPosition, int position, long deadlineNs) { *** mAdapter.bindViewHolder(holder, offsetPosition); *** return true; } public abstract static class Adapter<VH extends ViewHolder> { public final void bindViewHolder(@NonNull VH holder, int position) { *** //熟悉的味道,会调用我们自定义Adapter的: //onBindViewHolder(@NonNull VH holder,int position,@NonNull List<Object> payloads) onBindViewHolder(holder, position, holder.getUnmodifiedPayloads()); holder.clearPayload(); *** } }- RecyClerView通过Recycler获取下一个待绘制列表项.
- Recycler有4层缓存用于缓存ViewHolder实例.
- 优先级依次为: mChangedScrap --> mAttachedScrap --> mCachedViews --> mViewCacheExtension --> mRecyclerPool
- mCachedViews用于缓存指定位置的ViewHolder,默认最多存储2个,已满情况,按照先进先出原则将第一个ViewHolder实例存入mRecyclerPool.
- RecycledViewPool 对ViewHolder按viewType分类存储,同类ViewHolder被存放在最大长度为5的ArrayList中.
- 若4层缓存都未命中,则通过Adapter实例创建ViewHolder实例,并执行数据绑定.
- Recycler获取到ViewHolder实例,要不要执行数据绑定?
- 通过mChangedScrap,mAttachedScrap,mCachedViews中获取的ViewHolder实例不需要
- 通过mRecyclerPool 及 Adapter 获取的ViewHolder实例需要执行数据绑定
- ViewHolder的回收
- ViewHolder回收的场景很多,以最常见的滑动为例.滑动涉及事件处理,看onTouchEvent的MOVE事件.
@Override public boolean onTouchEvent(MotionEvent e) { *** switch (action) { *** case MotionEvent.ACTION_MOVE: scrollByInternal(canScrollHorizontally ? dx : 0,canScrollVertically ? dy : 0,vtev) } } boolean scrollByInternal(int x, int y, MotionEvent ev) { *** if (mAdapter != null) { scrollStep(x, y, mScrollStepConsumed); *** } } void scrollStep(int dx, int dy, @Nullable int[] consumed) { if (dx != 0) { consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState); } if (dy != 0) { consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState); } } androidx/recyclerview/widget/LinearLayoutManager.java @Override public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { *** return scrollBy(dy, recycler, state); } int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { *** updateLayoutState(layoutDirection, absDy, true, state); final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false); *** } int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) { *** while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { //通过Recycler获取ViewHolder实例 layoutChunk(recycler, state, layoutState, layoutChunkResult); *** //通过Recycler回收ViewHolder recycleByLayoutState(recycler, layoutState); } } void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) { //通过Recycler获取ViewHolder,并进一步获取到View View view = layoutState.next(recycler); *** //将获取到的Item的View摆放到正确的位置 layoutDecoratedWithMargins(view, left, top, right, bottom); } View next(RecyclerView.Recycler recycler) { *** //通过Recycler获取ViewHolder,并进一步获取到View final View view = recycler.getViewForPosition(mCurrentPosition); mCurrentPosition += mItemDirection; return view; } public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, int right, int bottom) { //将获取到的Item的View摆放到正确的位置 child.layout(***); } //通过Recycler回收ViewHolde实例 private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) { *** recycleViewsFromStart(recycler, layoutState.mScrollingOffset); *** } private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) { *** final int limit = dt; final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View child = getChildAt(i); if (mOrientationHelper.getDecoratedEnd(child) > limit || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) { //回收指定index以上的所有ViewHolder recycleChildren(recycler, 0, i); return; } } } private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) { *** if (endIndex > startIndex) { for (int i = endIndex - 1; i >= startIndex; i--) { removeAndRecycleViewAt(i, recycler); } } else { *** } } public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) { final View view = getChildAt(index); //将对应的View从RecyclerView中移除 removeViewAt(index); //Recycler进行回收 recycler.recycleView(view); } public void recycleView(@NonNull View view) { //通过View获取到ViewHolder实例 ViewHolder holder = getChildViewHolderInt(view); *** //Recycler真正回收ViewHolder实例的逻辑 recycleViewHolderInternal(holder); } void recycleViewHolderInternal(ViewHolder holder) { if (forceRecycle || holder.isRecyclable()) { //这里的判断条件决定了复用mCachedViews中的ViewHolder时不需要重新绑定数据 //重新执行数据绑定的条件: !holder.isBound() || holder.needsUpdate() || holder.isInvalid() //!holder.isBound()对于mCachedViews 中的ViewHolder来说必然为false, //只有当调用ViewHolder.resetInternal()重置ViewHolder后,才会将其设置为未绑定状态. //而只有存入回收池时才会重置ViewHolder if (mViewCacheMax > 0 && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) { // 如果mCachedViews已经存满了2个,则执行 recycleCachedViewAt(0) int cachedViewSize = mCachedViews.size(); if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) { //将cachedViewSize中最早加入的ViewHolder实例先存入mRecyclerPool中 //再将其从mCachedViews中移除 recycleCachedViewAt(0); cachedViewSize--; } int targetCacheIndex = cachedViewSize; if (ALLOW_THREAD_GAP_WORK && cachedViewSize > 0 && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) { // when adding the view, skip past most recently prefetched views int cacheIndex = cachedViewSize - 1; while (cacheIndex >= 0) { int cachedPos = mCachedViews.get(cacheIndex).mPosition; if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) { break; } cacheIndex--; } targetCacheIndex = cacheIndex + 1; } mCachedViews.add(targetCacheIndex, holder); cached = true; } if (!cached) { addViewHolderToRecycledViewPool(holder, true); recycled = true; } } else { *** } // even if the holder is not removed, we still call this method so that it is removed // from view holder lists. mViewInfoStore.removeViewHolder(holder); if (!cached && !recycled && transientStatePreventsRecycling) { holder.mOwnerRecyclerView = null; } } void recycleCachedViewAt(int cachedViewIndex) { *** //获取mCachedViews中最先存放的ViewHolder实例 ViewHolder viewHolder = mCachedViews.get(cachedViewIndex); *** //将该ViewHolder实例放入mRecyclerPool中 addViewHolderToRecycledViewPool(viewHolder, true); //将该ViewHolder实例从mCachedViews中移除 mCachedViews.remove(cachedViewIndex); } void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) { clearNestedRecyclerViewIfNotNested(holder); *** getRecycledViewPool().putRecycledView(holder); } public void putRecycledView(ViewHolder scrap) { final int viewType = scrap.getItemViewType(); final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap; //最多存储5个同类型的ViewHolder实例 if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) { return; } *** scrap.resetInternal(); //将ViewHolder实例加入scrapHeap scrapHeap.add(scrap); } - 从上述代码可知,在使用LayoutManager执行视图填充/ViewHolder获取过程中,也伴随着ViewHolder的回收.回收指定position范围内的ViewHolder实例
- 通过索引值找到Item对应的View,将其从RecyclerView中移除
- 将对应的ViewHolder实例通过Recycler进行回收
- 优先尝试将ViewHolder实例存入mCachedViews,可能伴随老的ViewHolder实例被移出并存入mRecyclerPool
- 尝试存入 mCachedViews 失败,则直接将ViewHolder实例存入mRecyclerPool
- LayoutManager
- 抽丝剥茧RecyclerView - LayoutManager
- 这么用GridLayoutManager,你可能还真没尝试过
- Orient-Ui 上面文章对应的代码
- LayoutManager主要作用
- 通过Recycler,回收和复用ViewHolder
- 关于滑动的处理,涉及滑动过程中调用 ==> fill ==> ViewHolder的获取及回收
- 测量及布局子View.
- 重点解释:
- 测量及布局子View
- Recycler中 mAttachedScrap 的数据何时存储
- fill中什么时候会涉及ViewHolder的回收
- 测量
androidx/recyclerview/widget/RecyclerView.java public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 { LayoutManager mLayout; public void onMeasure(@NonNull Recycler recycler, @NonNull State state, int widthSpec, int heightSpec) { mRecyclerView.defaultOnMeasure(widthSpec, heightSpec); } void defaultOnMeasure(int widthSpec, int heightSpec) { final int width = LayoutManager.chooseSize(widthSpec, getPaddingLeft() + getPaddingRight(), ViewCompat.getMinimumWidth(this)); final int height = LayoutManager.chooseSize(heightSpec, getPaddingTop() + getPaddingBottom(), ViewCompat.getMinimumHeight(this)); //单纯设置RecyclerView自身宽高 setMeasuredDimension(width, height); } //计算宽高 void setMeasureSpecs(int wSpec, int hSpec) { mWidth = MeasureSpec.getSize(wSpec); mWidthMode = MeasureSpec.getMode(wSpec); if (mWidthMode == MeasureSpec.UNSPECIFIED && !ALLOW_SIZE_IN_UNSPECIFIED_SPEC) { mWidth = 0; } mHeight = MeasureSpec.getSize(hSpec); mHeightMode = MeasureSpec.getMode(hSpec); if (mHeightMode == MeasureSpec.UNSPECIFIED && !ALLOW_SIZE_IN_UNSPECIFIED_SPEC) { mHeight = 0; } } @Override protected void onMeasure(int widthSpec, int heightSpec) { *** if (mLayout.isAutoMeasureEnabled()) { //LayoutManage的isAutoMeasureEnabled默认返回值是true final int widthMode = MeasureSpec.getMode(widthSpec); final int heightMode = MeasureSpec.getMode(heightSpec); *** //默认实现就是单纯设置RecyclerView自身宽高 mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); //当RecyclerView的宽高都是MATCH_PARENT或精确数值,return final boolean measureSpecModeIsExactly = widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY; if (measureSpecModeIsExactly || mAdapter == null) { return; } //mLayoutStep默认值就是State.STEP_START,会走dispatchLayoutStep1. //会走dispatchLayoutStep1 可能会执行'预布局' if (mState.mLayoutStep == State.STEP_START) { dispatchLayoutStep1(); } //1:计算宽高 mLayout.setMeasureSpecs(widthSpec, heightSpec); mState.mIsMeasuring = true; //2:真正执行布局子View //会调用 mLayout.onLayoutChildren(mRecycler, mState); dispatchLayoutStep2(); //3:获取尺寸最大的子View,将其范围/Rect作为参数得到RecyclerView的尺寸,设置为RecyclerView的宽高 mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec); //若需要二次测量,则再执行一次 1->2->3 //shouldMeasureTwice 默认返回false. //LinearLayoutManager重写了该方法,通常情况也会返回false /* LinearLayoutManager: @Override boolean shouldMeasureTwice() { return getHeightMode() != View.MeasureSpec.EXACTLY && getWidthMode() != View.MeasureSpec.EXACTLY && hasFlexibleChildInBothOrientations(); } //lp.width < 0 && lp.height < 0 ,不知道什么情况会小于0 boolean hasFlexibleChildInBothOrientations() { final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); final ViewGroup.LayoutParams lp = child.getLayoutParams(); if (lp.width < 0 && lp.height < 0) { return true; } } return false; } */ if (mLayout.shouldMeasureTwice()) { //4:需要二次测量,则再执行一次 1->2->3 mLayout.setMeasureSpecs( MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY)); mState.mIsMeasuring = true; dispatchLayoutStep2(); mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec); } } else { *** } } }- 测量总结:
- isAutoMeasureEnabled方法,LayoutManager及3个实现类都返回true.
- LayoutManager.onMeasure默认实现就是单纯设置RecyclerView自身尺寸. 且3个实现类未重写.
- 上述代码中的1,2,3,4(再次1-2-3)暂且命名为'自动测量逻辑'.只有LayoutManager实例的isAutoMeasureEnabled返回true,且RecyclerView的宽高至少1个不是MeasureSpec.EXACTLY,'自动测量逻辑'才被执行.
- 不要禁用自动测量机制,官方说如果不使用自动测量,需要重写LayoutManage,并在其onMeasure中包含自动测量逻辑.所以自定义LayoutManager不要重写isAutoMeasureEnabled.
- dispatchLayoutStep2真正执行了子View的布局.
- 测量总结:
- 布局
private void dispatchLayoutStep2() { *** mState.mInPreLayout = false; mLayout.onLayoutChildren(mRecycler, mState); *** } androidx/recyclerview/widget/LinearLayoutManager.java public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { //1:初始化LayoutState实例 ensureLayoutState(); mLayoutState.mRecycle = false; //2:获取持有焦点的子View final View focused = getFocusedChild(); //3:将界面中已存在的子View从RecyclerView中移除,并保存对应的ViewHolder实例到Recycler中 //Recycler中 mChangedScrap, mAttachedScrap 中存储的ViewHolder实例就是在这儿村上的 detachAndScrapAttachedViews(recycler); if (mAnchorInfo.mLayoutFromEnd){ *** }else{ //4:根据锚点位置,分2次更新mLayoutState属性,分别从锚点处向结束方向 及 从锚点处向开始方向 填充子View. //根据锚点位置,更新mLayoutState的可用空间等属性,并设置:mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN //mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN非常重要,保证了在fill中不执行ViewHolder的回收 updateLayoutStateToFillEnd(mAnchorInfo); *** //从锚点位置,向结束方向填充子View fill(recycler, mLayoutState, state, false); *** updateLayoutStateToFillStart(mAnchorInfo); *** //从锚点位置,向开始方向填充子View fill(recycler, mLayoutState, state, false); *** if (mLayoutState.mAvailable > 0) { //如果还有剩余空间,则再次填充子View updateLayoutStateToFillEnd(lastElement, endOffset); *** fill(recycler, mLayoutState, state, false); *** } } *** //5:设置参数值 //不是预布局,则记录RecyclerView的宽/高: mLastTotalSpace = getTotalSpace(); //是预布局,则重置锚点信息 if (!state.isPreLayout()) { mOrientationHelper.onLayoutComplete(); } else { mAnchorInfo.reset(); } *** } 之前的疑问点1: updateLayoutStateToFillEnd(mAnchorInfo) 及 updateLayoutStateToFillStart(mAnchorInfo) 中设置的 mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN 决定了 fill 中不会执行ViewHolder的回收, 原因如下: int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) { while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { layoutChunk(recycler, state, layoutState, layoutChunkResult); //这里会对mScrollingOffset进行校验, mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN 才会执行ViewHolder实例的回收 if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) { *** //这里执行ViewHolder实例的回收 recycleByLayoutState(recycler, layoutState); } } } 而之前提到的因为滑动执行的fill,会触发ViewHolder实例的回收,原因如下: onTouchEvent -> scrollByInternal -> scrollStep -> scrollVerticallyBy -> scrollBy scrollBy: int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { *** //这儿会对mLayoutState计算并设置属性,其中 mScrollingOffset是计算得到的值,不是 LayoutState.SCROLLING_OFFSET_NaN updateLayoutState(layoutDirection, absDy, true, state); //fill方法中,对mScrollingOffset进行校验会通过,触发ViewHolder实例的回收 final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false); *** return scrolled; } private void updateLayoutState(int layoutDirection, int requiredSpace, boolean canUseExistingSpace, RecyclerView.State state) { //mScrollingOffset是计算得到的值,不是 LayoutState.SCROLLING_OFFSET_NaN mLayoutState.mScrollingOffset = scrollingOffset; } 之前的疑问点2: Recycler中 ArrayList<ViewHolder> mAttachedScrap 和 ArrayList<ViewHolder> mChangedScrap 是何时存入的. 源码可见,是scrapView方法存入的: void scrapView(View view) { final ViewHolder holder = getChildViewHolderInt(view); if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID) || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) { //非预布局情况,存入mAttachedScrap mAttachedScrap.add(holder); } else { //预布局情况,存入mChangedScrap mChangedScrap.add(holder); } } scrapView调用栈: 1.tryGetViewHolderForPositionByDeadline -> getScrapOrHiddenOrCachedHolderForPosition -> scrapView View view = mChildHelper.findHiddenNonRemovedView(position); scrapView(view); 2.detachAndScrapAttachedViews -> scrapOrRecycleView -> scrapView recycler.scrapView(view); 通过调用栈可知: - 在通过Recycler获取ViewHolder实例,若是从'已隐藏但未移除的Views'中获取到ViewHolder实例,则将此ViewHolder实例存入. - 在执行layout/布局过程中,会将当前页面中的子View对应的ViewHolder实例存入. - 处理预布局,都存入mAttachedScrap.- onLayoutChildren分5步:
- 创建LayoutState实例
- 获取焦点子View
- 将界面中已存在的子View从RecyclerView中移除,并将其对应的ViewHolder实例存入Recycler中
- 预布局则存入 mChangedScrap , 否则存入 mAttachedScrap
- 根据锚点位置,分2次更新 LayoutState实例 属性,分别从锚点处向结束方向 及 从锚点处向开始方向 填充子View.
- 更新 LayoutState实例 属性这一步非常重要,其中 mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN 保证了后面执行fill不会执行ViewHolder实例的回收.
- 设置参数值
- 之前疑问点解释
- ViewHolder实例在滑动触发的fill中会回收,在layout触发的fill中不会回收
- Recycler中的mChangedScrap和mAttachedScrap:
- 在通过Recycler获取ViewHolder实例,若是从'已隐藏但未移除的Views'中获取到ViewHolder实例,则将此ViewHolder实例存入.
- 在执行layout/布局过程中,会将当前页面中的子View对应的ViewHolder实例存入.
- 处理预布局,都存入mAttachedScrap.
- onLayoutChildren图示:
- onLayoutChildren分5步:
- 自定义LayoutManager没有看懂,先过,收集LayoutManager待看文章
- 这么用GridLayoutManager,你可能还真没尝试过
- 看完这篇文章你还不会自定义LayoutManager,我吃X!
- 【Android】掌握自定义LayoutManager(一) 系列开篇 常见误区、问题、注意事项,常用API
- Android自定义LayoutManager第十一式之飞龙在天
- Android自定义控件进阶篇,自定义LayoutManager
- 由旋转画廊,看自定义RecyclerView.LayoutManager
- RecyclerView 里的自定义 LayoutManager 的一种设计与实现
- Android RecyclerView自定义LayoutManager
- RecyclerView库中的遗珠
- LayoutManager
2. DiffUtil
零散的点
1. java.lang.IndexOutOfBoundsException
java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionViewHolder{f82aa71 position=11 id=-1, oldPos=6, pLpos:6 scrap [attachedScrap] tmpDetached not recyclable(1) no parent}
at android.support.v7.widget.RecyclerView$Recycler.validateViewHolderForOffsetPosition(RecyclerView.java:5297)
at android.support.v7.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:5479)
at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:5440)
at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:5436)
at android.support.v7.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:2224)
at android.support.v7.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1551)
at android.support.v7.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1511)
at android.support.v7.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:595)
at android.support.v7.widget.RecyclerView.dispatchLayoutStep1(RecyclerView.java:3534)
at android.support.v7.widget.RecyclerView.dispatchLayout(RecyclerView.java:3310)
- 原因
- 综合解决方案
- 重写LinearLayoutManager
@Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { try { super.onLayoutChildren(recycler, state); } catch (IndexOutOfBoundsException e) { e.printStackTrace(); } } mRecyclerView.setLayoutManager(new CustomLayoutManager(this, LinearLayoutManager.VERTICAL, false)); - 不要直接设置Adapter实例中的'data list',要使用新的List
private List<Item> mItems; public CustomAdapter(List<Item> items){ this.mItems = new ArrayList<>(); try{ this.mItems.addAll(items); }catch(Exception e){ *** } } - 对'data list'进行更新,尽量避免调用notifyDataSetChanged,应该使用notifyItem~进行替代.
notifyDataSetChanged没有动画效果,且更新数据的性能低
/** * 'data lsit' */ private List<Item> mItems; /** * refresh 'data lsit' * * @param items */ public void refreshList(List<Item> items) { int preSize = this.mItems.size(); this.mItems.clear(); notifyItemRangeRemoved(0, preSize); this.mItems.addAll(items); notifyItemRangeInserted(0, this.mItems.size()); } /** * load more * * @param moreItems */ public void loadMore(List<Item> moreItems) { int curIndex = this.mItems.size(); this.mItems.addAll(moreItems); notifyItemRangeInserted(curIndex, this.mItems.size()); } /** * refresh an Item * * @param item */ public void refreshItem(Item item) { if (this.mItems.contains(item)) { int index = this.mItems.indexOf(item); notifyItemChanged(index); } } /** * add an Item * * @param item */ public void add(Item item) { this.mItems.add(item); notifyItemInserted(this.mItems.size() - 1); } /** * delete an Item * * @param item */ public void delete(Item item) { if (this.mItems.contains(item)) { int index = this.mItems.indexOf(item); this.mItems.remove(item); notifyItemRemoved(index); } } - 在RecyclerView滑动的时候不要更新'data-list',或更新'data-list'时候不要滑动,这种方案没有试
2. RecyclerView刷新指定Item中的指定View,真正的局部刷新
- 重写LinearLayoutManager
源码中指出实现Item局部刷新的方法:
androidx/recyclerview/widget/RecyclerView.java
public abstract static class Adapter<VH extends ViewHolder> {
/**
* 1:调用notifyItemChanged时候,传入payload,不为null即可
* Client can optionally pass a payload for partial change.
* 2:最终会调用到我们重写的 Adapter中的 onBindViewHolder(@NonNull VH holder, int position, @NonNull List<Object> payloads)
* These payloads will be merged and may be passed to adapter's {@link #onBindViewHolder(ViewHolder, int, List)} if the
* item is already represented by a ViewHolder and it will be rebound to the same
* ViewHolder.
*/
public final void notifyItemChanged(int position, @Nullable Object payload) {
mObservable.notifyItemRangeChanged(position, 1, payload);
}
/**
* 局部刷新对比整体刷新:
* Partial bind vs full bind:
* <p>
* payloads是从 notifyItemChanged 或 notifyItemRangeChanged 传入的.
* The payloads parameter is a merge list from {@link #notifyItemChanged(int, Object)} or
* {@link #notifyItemRangeChanged(int, int, Object)}.
* payloads不为空,Adapter应该通过解析payloads数据执行局部刷新,如果payloads是空,则执行整体刷新.
* If the payloads list is not empty,
* the ViewHolder is currently bound to old data and Adapter may run an efficient partial
* update using the payload info. If the payload is empty, Adapter must run a full bind.
* Adapter should not assume that the payload passed in notify methods will be received by
* onBindViewHolder(). For example when the view is not attached to the screen, the
* payload in notifyItemChange() will be simply dropped.
*
* @param payloads A non-null list of merged payloads. Can be empty list if requires full
* update.
*/
public void onBindViewHolder(@NonNull VH holder, int position,
@NonNull List<Object> payloads) {
//源码默认情况下直接执行了onBindViewHolder. 所以我们在自定义Adapter中要重写onBindViewHolder(@NonNull VH holder, int position, @NonNull List<Object> payloads)
//根据payloads是否为空,payloads具体数据执行不同的逻辑
onBindViewHolder(holder, position);
}
}
实际代码实现Item局部刷新的方法:
1:自定义Adapter重写onBindViewHolder(@NonNull VH holder, int position,@NonNull List<Object> payloads)
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads) {
//如果payloads是空,则执行Item整体刷新
if(payloads == null || payloads.isEmpty()){
//Item整体刷新
onBindViewHolder(holder,position);
return;
}
//如果payloads非空,则根据payloads具体的值,执行不同的Item局部刷新逻辑
final int pos = getRealPosition(holder);
if (viewHolder instanceof **ViewHolder) {
**ViewHolder holder = (**ViewHolder) holder;
String refreshType = (String)payloads.get(0);
switch(refreshType){
case "refresh1":
holder.refresh1(pos);
break;
case "refresh2":
holder.refresh2(pos);
break;
default:
holder.refreshX(pos);
break;
}
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, final int position) {
final int pos = getRealPosition(holder);
if (viewHolder instanceof (**ViewHolder) {
**ViewHolder holder = (**ViewHolder) holder;
//Item整体刷新
holder.setData(pos);
}
}
public int getRealPosition(RecyclerView.ViewHolder holder) {
return holder.getLayoutPosition();
}
2:调用 notifyItemChanged 和 notifyItemRangeChanged 时候传入对应参数
rvAdapter.notifyItemChanged(10,"refresh1");
rvAdapter.notifyItemRangeChanged(10,5,"refresh2");
3. 如何为Item项设置点击监听器,不产生N个冗余的OnClickListener实例
- 读源码长知识 | 更好的 RecyclerView 表项点击监听器 自定义RecyclerView,类似ListView,解析MotionEvent坐标,获取该坐标所在Item的索引值,调用OnItemClickListener.onItemClick方法,比较简单
- 更好的 RecyclerView 表项子控件点击监听器 更进一步,设置Item中子View的点击监听
4. 类型