RecyclerView系列|理解RecyclerView(五)—RecyclerView的绘制流程

前言:做人如果没有梦想,那和咸鱼有什么区别。????????——《少林足球》
一、概述 ??上一篇文章对RecyclerView中实现了如何高度自定义点击事件、万能ViewHolder、万能适配器的封装和使用。最开始就提到,RecyclerView支持各种各样的布局效果,其核心关键在于RecyclerView.LayoutManager中,使用时我们是需要setLayoutManager()设置布局管理器的。RecyclerView已经将一部分功能抽离出来,在布局管理器中另外处理,也方便开发者自行拓展。LayoutManager就是负责RecyclerView的测量和布局以及itemView的回收和复用。今天这里主要结合LinearLayoutManager来分析RecyclerView的绘制流程。
RecyclerView提供了三中布局管理器:
  • LinearLayoutManager????? 以列表的方式展示item,有水平方向RecyclerView.HORIZONTAL和垂直方向RecyclerView.VERTICAL;
  • GridLayoutManager?????? 以网格的方式展示item,有水平方向和垂直方向;
  • StaggeredGridLayoutManager ? 以瀑布流的方式展示item,有水平方向和垂直方向。
这里就不一一分析了,前面的文章已经做了详细的介绍,不了解的同学可以回头看一下。这里以LinearLayoutManager为例来分析RecyclerView的绘制流程。
温馨提示:本文源码基于androidx.recyclerview:recyclerview:1.2.0-alpha01
二、RecyclerView的绘制三个步骤 ??RecyclerView设置布局管理器,这一步是必要的,用什么样的LayoutManager来绘制RecyclerView,不然RecyclerView也不知道怎么绘制。
recyclerView.setLayoutManager(manager);

从设置布局管理器方法入手,setLayoutManager()设置布局管理器给RecyclerView使用:
public void setLayoutManager(@Nullable LayoutManager layout) { if (layout == mLayout) {//和之前的管理器一样则直接return return; } stopScroll(); //停止滚动 if (mLayout != null) {//每次设置layoutManager都重新设置recyclerView的初始参数,动画回收view等 if (mItemAnimator != null) { mItemAnimator.endAnimations(); //结束动画 } mLayout.removeAndRecycleAllViews(mRecycler); //移除回收所有itemView mLayout.removeAndRecycleScrapInt(mRecycler); //移除回收所有已经废弃的itemView mRecycler.clear(); //清除所有缓存mLayout.setRecyclerView(null); //重置RecyclerView mLayout = null; } else { mRecycler.clear(); } ······· mLayout.setRecyclerView(this); //LayoutManager与RecyclerView关联 mRecycler.updateViewCacheSize(); //更新缓存大小 requestLayout(); //请求重绘 }

这里首先做了重置回收工作,然后LayoutManager与RecyclerView关联起来,最后请求重绘。这里调用了请求重绘requestLayout()方法,那么说明每次设置layoutManager都会执行View树的绘制,那么就会重走RecyclerView的onMeasure()onLayout()onDraw()绘制三部曲。
public void requestLayout() { if (mRecyclerView != null) { mRecyclerView.requestLayout(); //请求重绘 } }

2.1 onMeasure()
我们来看看RecyclerView的onMeasure()方法:
@Override protected void onMeasure(int widthSpec, int heightSpec) { if (mLayout == null) {//如果mLayout为空则采用默认测量,然后结束 defaultOnMeasure(widthSpec, heightSpec); return; } if (mLayout.mAutoMeasure) {//如果为自动测量,默认为true final int widthMode = MeasureSpec.getMode(widthSpec); final int heightMode = MeasureSpec.getMode(heightSpec); mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); //测量RecyclerView的宽高 //当前RecyclerView的宽高是否为精确值 final boolean measureSpecModeIsExactly = widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY; if (measureSpecModeIsExactly || mAdapter == null) {//如果RecyclerView的宽高为精确值或者mAdapter为空,则结束 return; } //RecyclerView的宽高为wrap_content时,即measureSpecModeIsExactly = false则进行测量 //因为RecyclerView的宽高为wrap_content时,需要先测量itemView的宽高才能知道RecyclerView的宽高 if (mState.mLayoutStep == State.STEP_START) {//还没测量过 dispatchLayoutStep1(); //1.适配器更新、动画运行、保存当前视图的信息、运行预测布局 } dispatchLayoutStep2(); //2.最终实际的布局视图,如果有必要会多次运行 //根据itemView得到RecyclerView的宽高 mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec); } }

onMeasure()主要是RecyclerView宽高测量工作,主要有两种情况:
  • (1)当RecyclerView的宽高为match_parent或者精确值时,即measureSpecModeIsExactly = true,此时只需要测量自身的宽高就知道RecyclerView的宽高,测量方法结束;
  • (2)当RecyclerView的宽高为wrap_content时,即measureSpecModeIsExactly = false,会往下执行dispatchLayoutStep1()dispatchLayoutStep2(),就是遍历测量ItemView的大小从而确定RecyclerView的宽高,这种情况真正的测量操作都是在dispatchLayoutStep2()中完成。
dispatchLayoutStep1()dispatchLayoutStep2()下面会讲解到。
2.2 onLayout()
onLayout()方法中, 直接调用dispatchLayout()方法布局:
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG); dispatchLayout(); //直接调用dispatchLayout()方法布局 TraceCompat.endSection(); mFirstLayoutComplete = true; }

dispatchLayout()layoutChildren()的包装器,它处理由布局引起的动态变化:
void dispatchLayout() { ······ mState.mIsMeasuring = false; //设置RecyclerView布局完成状态,前面已经设置预布局完成了。 if (mState.mLayoutStep == State.STEP_START) {//如果没在OnMeasure阶段提前测量子ItemView dispatchLayoutStep1(); //布局第一步:适配器更新、动画运行、保存当前视图的信息、运行预测布局 mLayout.setExactMeasureSpecsFrom(this); dispatchLayoutStep2(); } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth() || mLayout.getHeight() != getHeight()) {//前两步完成测量,但是因为大小改变不得不再次运行下面的代码 mLayout.setExactMeasureSpecsFrom(this); dispatchLayoutStep2(); //布局第二步:最终实际的布局视图,如果有必要会多次运行 } else { mLayout.setExactMeasureSpecsFrom(this); } dispatchLayoutStep3(); //布局第三步:最后一步的布局,保存视图动画、触发动画和不必要的清理。 }

可以看到dispatchLayout()onMeasure()阶段中一样选择性地进行测量布局的三个步骤:
  • 1、如果没在onMeasure阶段提前测量子ItemView,即RecyclerView宽高为match_parent或者精确值时,调用dispatchLayoutStep1()dispatchLayoutStep2()测量itemView宽高;
  • 2、如果在onMeasure阶段提前测量子ItemView,但是子视图发生了改变或者期望宽高和实际宽高不一致,则会调用dispatchLayoutStep2()重新测量;
  • 3、最后都会执行dispatchLayoutStep3()方法。
(1)我们来看看dispatchLayoutStep1、2、3分发布局的三个步骤:dispatchLayoutStep1()主要是进行预布局,适配器更新、动画运行、保存当前视图的信息等工作;
private void dispatchLayoutStep1() { mState.assertLayoutStep(State.STEP_START); fillRemainingScrollValues(mState); mState.mIsMeasuring = false; startInterceptRequestLayout(); //拦截布局请求 mViewInfoStore.clear(); //itemView信息清除 onEnterLayoutOrScroll(); //测量和分派布局时,更新适配器和计算那种类型要运行的动画 processAdapterUpdatesAndSetAnimationFlags(); saveFocusInfo(); //保存焦点信息 mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged; mItemsAddedOrRemoved = mItemsChanged = false; mState.mInPreLayout = mState.mRunPredictiveAnimations; mState.mItemCount = mAdapter.getItemCount(); findMinMaxChildLayoutPositions(mMinMaxLayoutPositions); //找到可绘制itemView最小最大positionif (mState.mRunSimpleAnimations) { //获得界面上可以显示的个数 int count = mChildHelper.getChildCount(); for (int i = 0; i < count; ++i) { final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); //动画信息 final ItemHolderInfo animationInfo = mItemAnimator .recordPreLayoutInformation(mState, holder, ItemAnimator.buildAdapterChangeFlagsForAnimations(holder), holder.getUnmodifiedPayloads()); //保存holder和动画信息到预布局中 mViewInfoStore.addToPreLayout(holder, animationInfo); } } //运行与布局,将会使用旧的item的position,布局管理器布局所有 if (mState.mRunPredictiveAnimations) { //保存旧的管理器可以运行的逻辑 saveOldPositions(); final boolean didStructureChange = mState.mStructureChanged; mState.mStructureChanged = false; //布局itemView mLayout.onLayoutChildren(mRecycler, mState); mState.mStructureChanged = didStructureChange; } stopInterceptRequestLayout(false); //回复绘制锁定 mState.mLayoutStep = State.STEP_LAYOUT; }

(2)dispatchLayoutStep2()表示对最终状态的视图进行实际布局:
private void dispatchLayoutStep2() { startInterceptRequestLayout(); //拦截请求布局 onEnterLayoutOrScroll(); //设置布局状态和动画状态 mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS); mAdapterHelper.consumeUpdatesInOnePass(); mState.mItemCount = mAdapter.getItemCount(); mState.mDeletedInvisibleItemCountSincePreviousLayout = 0; //预布局完成,开始布局itemView mState.mInPreLayout = false; mLayout.onLayoutChildren(mRecycler, mState); ······ stopInterceptRequestLayout(false); //停止拦截布局请求 }

(3)dispatchLayoutStep3()是布局的最后一步,保存view的动画信息,执行动画,和一些必要的清理工作:
private void dispatchLayoutStep3() { mState.assertLayoutStep(State.STEP_ANIMATIONS); startInterceptRequestLayout(); //开始拦截布局请求mState.mLayoutStep = State.STEP_START; //布局开始状态 if (mState.mRunSimpleAnimations) { //步骤3:找出事情现在的位置,并处理更改动画。 //反向遍历列表,因为我们可能会在循环中调用animateChange,这可能会删除目标视图持有者。 for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) { ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i)); final ItemHolderInfo animationInfo = mItemAnimator.recordPostLayoutInformation(mState, holder); ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key); //运行一个变更动画。如果一个项目被更改,但是更新后的版本正在消失,则会产生冲突的情况。 //由于标记为正在消失的视图可能会超出界限,所以我们运行一个change动画。两个视图都将在动画完成后自动清除。 //另一方面,如果是相同的视图持有者实例,我们将运行一个正在消失的动画,因为我们不会重新绑定更新的VH,除非它是由布局管理器强制执行的。//运行消失动画而不是改变 mViewInfoStore.addToPostLayout(holder, animationInfo); final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout(oldChangeViewHolder); //我们添加和删除,这样任何的布置信息都是合并的 mViewInfoStore.addToPostLayout(holder, animationInfo); ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder); mViewInfoStore.addToPostLayout(holder, animationInfo); }//处理视图信息列表和触发动画 mViewInfoStore.process(mViewInfoProcessCallback); } //回收废弃的视图 mLayout.removeAndRecycleScrapInt(mRecycler); //重置状态 mState.mPreviousLayoutItemCount = mState.mItemCount; mDataSetHasChangedAfterLayout = false; //清除mChangedScrap中的数据 mRecycler.mChangedScrap.clear(); mRecycler.updateViewCacheSize(); //更新缓存大小mLayout.onLayoutCompleted(mState); //布局完成状态 onExitLayoutOrScroll(); stopInterceptRequestLayout(false); //停止拦截布局请求 mViewInfoStore.clear(); //itemView信息清除recoverFocusFromState(); //回复焦点 resetFocusInfo(); //重置焦点信息 }

总结一下这分发布局的三个步骤:
  • dispatchLayoutStep1()??表示进行预布局,适配器更新、动画运行、保存当前视图的信息等工作;
  • dispatchLayoutStep2()??表示对最终状态的视图进行实际布局,有必要时会多次执行;
  • dispatchLayoutStep3()??表示布局最后一步,保存和触发有关动画的信息,相关清理等工作。
2.3 onDraw()
来到最后一步的绘制onDraw()方法中,如果不需要一些特殊的效果,在TextView、ImageView控件中已经绘制完了。
@Override public void onDraw(Canvas c) { super.onDraw(c); //所有itemView先绘制 //分别绘制ItemDecoration final int count = mItemDecorations.size(); for (int i = 0; i < count; i++) { mItemDecorations.get(i).onDraw(c, this, mState); } }

2.4 RecyclerView的绘制三个步骤总结:
1、RecyclerView的itemView可能会被测量多次,如果RecyclerView的宽高是固定值或者match_parent,那么在onMeasure()阶段是不会提前测量ItemView布局,如果RecyclerView的宽高是wrap_content,由于还没有知道RecyclerView的实际宽高,那么会提前在onMeasure()阶段遍历测量itemView布局确定内容显示区域的宽高值来确定RecyclerView的实际宽高;
2、dispatchLayoutStep1()dispatchLayoutStep2()dispatchLayoutStep3()这三个方法一定会执行,在RecyclerView的实际宽高不确定时,会提前多次执行dispatchLayoutStep1()dispatchLayoutStep2()方法,最后在onLayout()阶段执行 dispatchLayoutStep3(),如果有itemView发生改变会再次执行dispatchLayoutStep2()
3、正在的测量和布局itemView实际在dispatchLayoutStep2()方法中。
RecyclerView的绘制三个步骤流程图:
RecyclerView系列|理解RecyclerView(五)—RecyclerView的绘制流程
文章图片

三、LinearLayoutManager填充、测量、布局过程 ??RecyclerView的绘制经过measure、layout、draw三个步骤,但是itemView的真正布局时委托给各个的LayoutManager中处理,上面LinearLayoutManager可以知道dispatchLayoutStep2()是实际布局视图步骤,通过LayoutManager调用onLayoutChildren()方法进行布局itemView,它是绘制itemView的核心方法,表示从给定的适配器中列出所有相关的子视图。
3.1 onLayoutChildren()布局itemView
@Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { // 1) 检查子类和其他变量找到描点坐标和描点位置 // 2) 从开始填补,从底部堆积 // 3) 从底部填补,从顶部堆积 // 4) 从底部堆积来满足需求 // 创建布局状态 if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) { if (state.getItemCount() == 0) { removeAndRecycleAllViews(recycler); //移除所有子View return; } } ensureLayoutState(); mLayoutState.mRecycle = false; //禁止回收 //颠倒绘制布局 resolveShouldLayoutReverse(); final View focused = getFocusedChild(); //获取目前持有焦点的child if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION || mPendingSavedState != null) { mAnchorInfo.reset(); //重置锚点信息 mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd; //1. 计算更新描点位置和坐标 updateAnchorInfoForLayout(recycler, state, mAnchorInfo); mAnchorInfo.mValid = true; } ······· //计算第一布局的方向 int startOffset; int endOffset; final int firstLayoutDirection; onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection); detachAndScrapAttachedViews(recycler); //暂时分离已经附加的view,即将所有child detach并通过Scrap回收 mLayoutState.mInfinite = resolveIsInfinite(); mLayoutState.mIsPreLayout = state.isPreLayout(); mLayoutState.mNoRecycleSpace = 0; //2.开始填充,从底部开始堆叠; if (mAnchorInfo.mLayoutFromEnd) { //描点位置从start位置开始填充ItemView布局 updateLayoutStateToFillStart(mAnchorInfo); fill(recycler, mLayoutState, state, false); //填充所有itemView//描点位置从end位置开始填充ItemView布局 updateLayoutStateToFillEnd(mAnchorInfo); fill(recycler, mLayoutState, state, false); //填充所有itemView endOffset = mLayoutState.mOffset; }else { //3.向底填充,从上往下堆放; //描点位置从end位置开始填充ItemView布局 updateLayoutStateToFillEnd(mAnchorInfo); fill(recycler, mLayoutState, state, false); //描点位置从start位置开始填充ItemView布局 updateLayoutStateToFillStart(mAnchorInfo); fill(recycler, mLayoutState, state, false); startOffset = mLayoutState.mOffset; } //4.计算滚动偏移量,如果有必要会在调用fill方法去填充新的ItemView layoutForPredictiveAnimations(recycler, state, startOffset, endOffset); }

首先是状态判断和一些准备工作,对描点信息选择和更新, detachAndScrapAttachedViews(recycler)暂时将已经附加的view分离,缓存Scrap中,下次重新填充时直接拿出来复用。然后计算是从哪个方向开始布局。布局算法如下:
  • 1.通过检查子元素和其他变量,找到一个锚点坐标和一个锚点项的位置;
  • 2.开始填充,从底部开始堆叠;
  • 3.向底填充,从上往下堆放;
  • 4.滚动以满足要求,如堆栈从底部。
3.2 fill()开始填充itemView
填充布局交给了fill()方法,表示填充由layoutState定义的给定布局。为什么要fill两次呢,我们来看看fill()方法:
//填充方法,返回的是填充itemView的像素,方便后续滚动时使用 int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) { recycleByLayoutState(recycler, layoutState); //回收滑出屏幕的view int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace; LayoutChunkResult layoutChunkResult = mLayoutChunkResult; //核心== while()循环 == while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {//一直循环,知道没有数据 layoutChunkResult.resetInternal(); //填充itemView的核心方法 layoutChunk(recycler, state, layoutState, layoutChunkResult); ······ if (layoutChunkResult.mFinished) {//布局结束,退出循环 break; } layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection; //根据添加的child高度偏移计算 } ······ return start - layoutState.mAvailable; //返回这次填充的区域大小 }

fill()核心就是一个while()循环,循环执行layoutChunk()填充一个itemView到屏幕,同时返回这次填充的区域大小。首先根据屏幕还有多少剩余空间remainingSpace,根据这个数值减去子View所占的空间大小,小于0时布局子View结束,如果当前所有子View还没有超过remainingSpace时,调用layoutChunk()安排View的位置。
3.3 layoutChunk()对itemView创建、填充、测量、布局
layoutChunk()作为最终填充布局itemView的方法,对itemView创建、填充、测量、布局,主要有以下几个步骤:
  • 1.layoutState.next(recycler)从缓存中获取itemView,如果没有则创建itemView;
  • 2.根据实际情况来添加itemView到RecyclerView中,最终调用的还是ViewGroup的addView()方法;
  • 3.measureChildWithMargins()测量itemView大小包括父视图的填充、项目装饰和子视图的边距;
  • 4.根据计算好的left, top, right, bottom通过layoutDecoratedWithMargins()使用坐标在RecyclerView中布局给定的itemView。
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) { //1.从缓存中获取或者创建itemView View view = layoutState.next(recycler); //获取当前postion需要展示的View ······ //2.根据实际情况来添加itemView到RecyclerView中,最终调用的还是ViewGroup的addView()方法 if (layoutState.mScrapList == null) { if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) { addView(view); } else { addView(view, 0); } } //3.测量子View大小包括父视图的填充、项目装饰和子视图的边距 measureChildWithMargins(view, 0, 0); result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view); //计算一个ItemView的left, top, right, bottom坐标值 int left, top, right, bottom; ······ //4.使用坐标在RecyclerView中布局给定的itemView //计算正确的布局位置,减去margin,计算所有视图的边界框(包括margin和装饰) layoutDecoratedWithMargins(view, left, top, right, bottom); //调用child.layout进行布局 }

通过layoutState.next()从缓存中获取itemView如果没有就创建一个新的itemView,然后addView()根据实际情况来添加itemView到RecyclerView中,最终调用的还是ViewGroupaddView()方法,接着通过 measureChildWithMargins()测量子View大小包括父视图的填充、项目装饰和子视图的边距;最后getDecoratedMeasuredWidth()通过计算好的left, top, right, bottom值在RecyclerView坐标中布局给定的itemView,注意这里的宽度是item+decoration的总宽度。
View next(RecyclerView.Recycler recycler) { if (mScrapList != null) { return nextViewFromScrapList(); } final View view = recycler.getViewForPosition(mCurrentPosition); mCurrentPosition += mItemDirection; return view; }

获取itemView,并且如果mScrapList中有缓存的View 则使用缓存的view,如果没有mScrapList 就创建view,并添加到mScrapList 中。接下来getViewForPosition()方法主要是RecyclerView的缓存机制,后续的文章会讲解到。
3.4 LinearLayoutManager填充、测量、布局过程总结:
onLayoutChildren()表示从给定的适配器中列出所有相关的子视图,填充布局交给了fill()方法,填充由layoutState定义的给定布局,while()循环执行layoutChunk()填充一个itemView到屏幕,作为最终填充布局itemView的方法,layoutState.next(recycler)从缓存中获取或者创建itemView,通过addView()添加itemView到RecyclerView中,其实最终调用的还是ViewGroup的addView()方法,measureChildWithMargins()测量itemView大小包括父视图的填充、项目装饰和子视图的边距,最后layoutDecoratedWithMargins()根据计算好的left, top, right, bottom通过使用坐标在RecyclerView中布局给定的itemView。
流程图如下:
RecyclerView系列|理解RecyclerView(五)—RecyclerView的绘制流程
文章图片

至此!本文结束。

请尊重原创者版权,转载请标明出处:https://blog.csdn.net/m0_37796683/article/details/104864318 谢谢!

【RecyclerView系列|理解RecyclerView(五)—RecyclerView的绘制流程】相关文章:

理解RecyclerView(五)

?● RecyclerView的绘制流程

理解RecyclerView(六)

?● RecyclerView的滑动原理

理解RecyclerView(七)

?● RecyclerView的嵌套滑动机制

理解RecyclerView(八)

?● RecyclerView的回收复用缓存机制详解

理解RecyclerView(九)

?● RecyclerView的自定义LayoutManager

    推荐阅读