在Android开发中RecyclerView是咱们高频使用的一个组件,用来展现大量的数据。咱们不只要熟练使用它,还要对它的实现有一个认知。本片文章介绍RecyclerView的绘制流程,也就是onMeasure
、onLayout
、onDraw
这三个方法中主要作了些什么工做,let's go!java
OnMeasure
咱们知道该方法是测量当前View及子View的宽高,可是查看RecyclerView的源码发现代码很长,它不只仅作了测量的工做,还作了一些其余的工做,咱们一块儿来看一下缓存
if (mLayout == null) {
//第一种状况
}
if (mLayout.isAutoMeasureEnabled()) {
//第二种状况,一般会进入到这种状况
}else{
// 第三种状况
}
复制代码
根据条件分支将onMeasure
方法分红了三种状况,咱们挨个来讨论一下bash
mLayout == null
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
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));
setMeasuredDimension(width, height);
}
public static int chooseSize(int spec, int desired, int min) {
final int mode = View.MeasureSpec.getMode(spec);
final int size = View.MeasureSpec.getSize(spec);
switch (mode) {
case View.MeasureSpec.EXACTLY:
return size;
case View.MeasureSpec.AT_MOST:
return Math.min(size, Math.max(desired, min));
case View.MeasureSpec.UNSPECIFIED:
default:
return Math.max(desired, min);
}
}
复制代码
mLayout == null
时,从传入的参数能够看出来RecyclerView
没有考虑子View就决定了本身的大小,是一个比较粗糙的测量,具体的大小还须要根据以后屡次测量的结果来定app
mLayout.isAutoMeasureEnabled() == true
mLayout.isAutoMeasureEnabled()
为true
时,将调用LayoutManager.onLayoutChildren(Recycler, State)
来计算子View们所须要的大小,RecyclerView.LayoutManager
的实现类LinearLayoutManager
、StaggeredGridLayoutManager
都重载了该方法并返回true,因此一般都会走入这个分支(列出了部分代码)ide
if (mLayout.isAutoMeasureEnabled()) {
//将测量交给LaytouManager
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
//若是width和height都已是精确值,那么就不用再根据内容进行测量,后面步骤再也不处理
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
dispatchLayoutStep2();
//若是须要二次测量的话
if (mLayout.shouldMeasureTwice()) {
dispatchLayoutStep2();
}
}
复制代码
第一步调用了LayoutManager.onMeasure()
方法源码分析
public void onMeasure(@NonNull Recycler recycler, @NonNull State state, int widthSpec,int heightSpec) {
mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
}
复制代码
该方法直接调用了RecyclerView
的默认测量方法,就是咱们以前分析的第一种状况。阅读该方法的注释咱们能够发现,LayoutoManager
强烈推荐开启自动测量,而且若是开启了自动测量就不要重写该方法,LayoutoManager
的三个默认实现也确实没重写该方法。还介绍了下测量的策略,若是宽高测量模式为UNSPECIFIED. AT_MOST则指定为EXACTLY而且RecyclerView
占用可用的最大空间。布局
第二步 若是宽高的测量模式都为MeasureSpec.EXACTLY
或者没有设置Adapter
直接返回。post
接下来咱们继续看,mState.mLayoutStep
的默认值就是State.STEP_START
,进入条件语句执行dispatchLayoutStep1()
,而后执行dispatchLayoutStep2()
,若是须要执行二次测量的话在执行一次dispatchLayoutStep2()
。动画
咱们重点看dispatchLayoutStep1()
和dispatchLayoutStep2()
,与这两个方法息息相关的一个变量是mState.mLayoutStep
,该变量得意义是决定了dispatchLayoutStep1()
、dispatchLayoutStep1()
、dispatchLayoutStep2()
这三个方法该执行哪一步了,它的取值有三个ui
mLayoutStep | 描述 |
---|---|
STEP_START | 默认值或者dispatchLayoutStep3() 已经执行了 |
STEP_LAYOUT | dispatchLayoutStep1() 已经执行了 |
STEP_ANIMATIONS | dispatchLayoutStep2() 已经执行了 |
对这三个方法的具体分析,咱们放到onLayout
中处理,先说一下结论dispatchLayoutStep1()
处理了Adapter的数据更新以及准备动画前的数据;dispatchLayoutStep2()
进行itemView的布局
状况三: mLayout.isAutoMeasureEnabled() == false
咱们一般使用的LayoutManager
都返回true
,除非咱们自定义,因此暂不分析这种状况
总结一下
onMeasure
所作的工做,假设LayoutManager
为LinearLayoutManager
- 测量一下本身,可能须要屡次测量
- 若是宽高不都为
MeasureSpec.EXACTLY
模式则执行
dispatchLayoutStep1()
,处理Adapter
更新以及准备动画前的数据dispatchLayoutStep2()
进行itemView的布局
OnLayout
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout();
TraceCompat.endSection();
mFirstLayoutComplete = true;
}
复制代码
就是执行了dispatchLayout
void dispatchLayout() {
if (mAdapter == null) {
Log.e(TAG, "No adapter attached; skipping layout");
return;
}
if (mLayout == null) {
Log.e(TAG, "No layout manager attached; skipping layout");
return;
}
mState.mIsMeasuring = false;
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
|| mLayout.getHeight() != getHeight()) {
// First 2 steps are done in onMeasure but looks like we have to run again due to
// changed size.
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
// always make sure we sync them (to ensure mode is exact)
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();
}
复制代码
流程也很清晰
Adapter
和LayoutManager
没设置就不进行布局,RecyclerView
也就只能显示一片空白onMeasure
中执行了dispatchLayoutStep1()
和dispatchLayoutStep2()
则再也不执行这两个方法,不过dispatchLayoutStep2()
可能须要被再次调用dispatchLayoutStep3()
如今咱们来查看下dispatchLayoutStep
系列方法到底干了些什么
dispatchLayoutStep1()
/** * The first step of a layout where we; * - process adapter updates * - decide which animation should run * - save information about current views * - If necessary, run predictive layout and save its information */
private void dispatchLayoutStep1() {
processAdapterUpdatesAndSetAnimationFlags();
if (mState.mRunSimpleAnimations){
//...
}
if (mState.mRunPredictiveAnimations){
//...
}
mState.mLayoutStep = State.STEP_LAYOUT;
}
复制代码
以上代码缩减了不少,从该方法的注释能够看出来,dispatchLayoutStep1()
主要处理了Adapter
更新以及准备动画所需数据,而processAdapterUpdatesAndSetAnimationFlags()
就是用来处理Adapter
更新和动画的Flag
处理,咱们看一下这个方法里面
private void processAdapterUpdatesAndSetAnimationFlags() {
if (mDataSetHasChangedAfterLayout) {
// Processing these items have no value since data set changed unexpectedly.
// Instead, we just reset it.
mAdapterHelper.reset();
if (mDispatchItemsChangedEvent) {
mLayout.onItemsChanged(this);
}
}
为动画的flag进行赋值
mState.mRunSimpleAnimations = ...
mState.mRunPredictiveAnimations = ...
}
复制代码
首先处理Adapter
的更新(Adapter.notifyDataSetChanged()
或者RecyclerView.swapAdapter(Adapter, boolean)
表明Adapter有更新),就是简单的把以前记录的每个item的操做重置一下,由于数据集的更改致使以前存的信息都没有意义了,下边的代码是为动画的标志位赋值,咱们调用RecyclerView.setAdapter
和Adapter.notifyDataSetChanged()
是不会触发动画的,因此咱们先不考虑动画相关的东西。
咱们继续来看dispatchLayoutStep1()
的内容,下面是两个if
条件,涉及两个变量mState.mRunSimpleAnimations
和mState.mRunPredictiveAnimations
这两个变量在要执行动画时才为true
,因此先不考虑里面的内容。 最后执行mState.mLayoutStep = State.STEP_LAYOUT
,表明dispatchLayoutStep1()
已经执行完毕了
总结
dispatchLayoutStep1()
处理了Adapter更新以及准备动画所需数据
private void dispatchLayoutStep2() {
mLayout.onLayoutChildren(mRecycler, mState);
mState.mLayoutStep = State.STEP_ANIMATIONS;
}
复制代码
方法很简洁,调用了LayoutManager.onLayoutChildren(Recycler recycler, State state)
,该方法就是进行子View布局的实质方法,不过是一个空实现,子类必须去实现这个方法,声明以下
public void onLayoutChildren(Recycler recycler, State state) {
Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, Statestate) ");
}
复制代码
最后mState.mLayoutStep = State.STEP_ANIMATIONS
表明dispatchLayoutStep2()
已经执行完毕了
总结
dispatchLayoutStep2()
调用LayoutManager.onLayoutChildren
来进行子View的布局
private void dispatchLayoutStep3() {
mState.mLayoutStep = State.STEP_START;
if (mState.mRunSimpleAnimations) {
//记录layout以后View的信息,并触发动画
//...
}
//...一些清理工做
}
复制代码
首先将mState.mLayoutStep = State.STEP_START
,标志dispatchLayoutStep3()
已经执行了,而后mState.mRunSimpleAnimations
这个变量表示是否执行动画,第一次布局的时候是不须要动画的因此不会进入这个分支,动画咱们以后在讲,最后作一些清理的工做。
总结
dispatchLayoutStep3()
触发动画
总结一下
onLayout
所作的工做,大致流程以下
dispatchLayoutStep1()
处理了Adapter更新以及准备动画所需数据dispatchLayoutStep2()
调用LayoutManager.onLayoutChildren来进行子View的布局dispatchLayoutStep3()
触发动画
draw
public void draw(Canvas c) {
super.draw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDrawOver(c, this, mState);
}
//...处理clipToPadding="false"的状况
}
复制代码
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);
}
}
复制代码
总结:
draw
和onDraw
对RecyclerView
的分割线进行了绘制,固然分割线须要咱们本身去实现具体的绘制内容,同时咱们也知道了ItemDecoration
中onDraw
和onDrawOver
的区别
至此咱们已经完成了RecyclerView
三大流程的源码分析,上面列出的代码大多都通过了精简,省去了不少细节,不过刚开始阅读源码时,咱们只要把握总体的流程就好,抛开细节来看,以上的总体流程并不难理解。可是有一个很重要的方法没有细讲,就是LayoutManager.onLayoutChildren()
,该方法才是布局子View的核心,咱们对该方法进行单独的一波分析,以LinearLayoutManager
(只考虑竖直方向)为例来看一下
LinearLayoutManager.onLayoutChildren()
在进行子View的布局中利用了一些帮助类来帮助布局,咱们须要先了解一下这些帮助类
属性 | 解释 |
---|---|
mOffset |
偏移多少个像素点以后开始布局 |
mAvailable |
当前布局方向上可用的空间 |
mCurrentPosition |
要布局子View在Adapter中的表明的位置 |
mInfinite |
布局的View数量没有限制 |
属性 | 解释 |
---|---|
mPosition |
锚点位置 |
mCoordinate |
锚点坐标信息 |
mValid |
是否可用 |
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// layout algorithm:
// 1) by checking children and other variables, find an anchor coordinate and an anchor
// item position.
// 2) fill towards start, stacking from bottom
// 3) fill towards end, stacking from top
// 4) scroll to fulfill requirements like stack from bottom.
//新建一个LayoutState
ensureLayoutState();
if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION || mPendingSavedState != null) {
mAnchorInfo.reset();
//更新锚点信息
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
mAnchorInfo.mValid = true;
}
//...计算LinearLayoutManager所需的额外空间
//锚点信息准备好了
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
//将现有的子View都缓存起来
detachAndScrapAttachedViews(recycler);
if (mAnchorInfo.mLayoutFromEnd){
//...
}else{
// fill towards end
//将锚点的信息保存到mLayoutState中
updateLayoutStateToFillEnd(mAnchorInfo);
fill(recycler, mLayoutState, state, false);
// fill towards start
updateLayoutStateToFillEnd(mAnchorInfo);
fill(recycler, mLayoutState, state, false);
if (mLayoutState.mAvailable > 0) {
fill(recycler, mLayoutState, state, false);
}
}
}
复制代码
以上代码省略了不少有用信息,包括对LayoutState内部一些有用属性的赋值等。由代码刚开始的注释可了解到该方法内部执行逻辑
关键是锚点,对于LinearLayoutManager
来讲,它不必定是从最高点或者最低点开始布局,有多是从中间某个点开始布局的,如图所示
updateAnchorInfoForLayout(recycler, state, mAnchorInfo)
更新锚点信息private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo) {
// 第一种计算方式
if (updateAnchorFromPendingData(state, anchorInfo)) {
if (DEBUG) {
Log.d(TAG, "updated anchor info from pending information");
}
return;
}
// 第二种计算方式
if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
if (DEBUG) {
Log.d(TAG, "updated anchor info from existing children");
}
return;
}
if (DEBUG) {
Log.d(TAG, "deciding anchor info for fresh state");
}
// 第三种计算方式
anchorInfo.assignCoordinateFromPadding();
anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
}
复制代码
能够看出来有三种计算锚点信息的方法,每一个方法里的代码虽然多却不难理解
private boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo anchorInfo) {
if (state.isPreLayout() || mPendingScrollPosition == RecyclerView.NO_POSITION) {
return false;
}
//...
}
复制代码
mPendingScrollPosition == RecyclerView.NO_POSITION
这个条件判断默认状况是true的,只有在调用scrollToPosition
或者在onRestoreInstanceState
恢复以前记录的mPendingScrollPosition
时才会有其余值(上面省略代码就是计算mPendingScrollPosition
不为默认值的锚点信息,本文没有分析),因此默认状况该方法没有计算锚点信息,往下走
updateAnchorFromChildren
这个方法根据子View来肯定锚点信息
false
,表示没有计算出锚点信息anchorInfo.mCoordinate
被赋值为1号子View上面的Decor的顶部位置该方法的详细分析能够看这篇文章RecyclerView源码解析
最后兜底的方法,直接将anchorInfo.mCoordinate
赋值为padding
,若是没有设置padding
,则anchorInfo.mCoordinate = 0
,anchorInfo.mPosition = 0
(mStackFromEnd == false
的状况,该值默认是false)
锚点信息的计算主要是为
mPosition
、mCoordinate
这两个变量赋值,这样咱们就知道了从哪一个点开始填充子View和子View对应的数据在Adapter
中的位置
更新锚点信息以后,源码中有一长段代码用来计算LinearLayoutManager
须要的“额外空间”,这段代码我也没懂,就不分析了,不过并不影响咱们把握总体布局流程。锚点信息都准备好以后,updateLayoutStateToFillEnd()
将锚点信息保存到mLayoutState
中,而后调用fill()
方法开始填充子View了,mAnchorInfo.mLayoutFromEnd
将填充分为两种状况
true
: 从Adapter
最后一项开始,从下往上布局false
: 从Adapter
第一项开始,从上往下布局(默认状况) 如图所示(虚线表示屏幕外边的ItemView
)
默认状况为false
,从上往下开始布局,而后进入关键的fill()
方法
int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
//可用空间
final int start = layoutState.mAvailable;
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
//保存了每一子View消耗的空间
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
//循环布局子View
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
//核心方法
layoutChunk(recycler, state, layoutState, layoutChunkResult);
if (layoutChunkResult.mFinished) {
break;
}
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null
|| !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
// we keep a separate remaining space because mAvailable is important for recycling
//消费子View占据的空间
remainingSpace -= layoutChunkResult.mConsumed;
}
//...
}
return start - layoutState.mAvailable;
}
复制代码
fill
的核心思路就是在一个循环里不断地进行子View布局,结束条件是没有可用空间或者数据源没有数据了,layoutChunk
负责填充,每填充一个子View,剩余空间就减相对应View占据的空间(竖直方向上来讲就是高度),而后填充下一个,最后返回的是本次布局所填充的区域大小。
咱们进入layoutChunk
来看具体的实现
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
//获取一个子View
View view = layoutState.next(recycler);
if (view == null) {
if (DEBUG && layoutState.mScrapList == null) {
throw new RuntimeException("received null view when unexpected");
}
// if we are laying out views in scrap, this may return null which means there is
// no more items to layout.
result.mFinished = true;
return;
}
//将View添加到RecyclerView中
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
} else {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addDisappearingView(view);
} else {
addDisappearingView(view, 0);
}
}
//测量子View的大小包括margins和decorations
measureChildWithMargins(view, 0, 0);
//将占据的空间保存到LayoutChunkResult之中,供外层循环使用
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
int left, top, right, bottom;
if (mOrientation == VERTICAL) {
if (isLayoutRTL()) {
right = getWidth() - getPaddingRight();
left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
} else {
left = getPaddingLeft();
right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
}
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
bottom = layoutState.mOffset;
top = layoutState.mOffset - result.mConsumed;
} else {
top = layoutState.mOffset;
bottom = layoutState.mOffset + result.mConsumed;
}
} else {
top = getPaddingTop();
bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
right = layoutState.mOffset;
left = layoutState.mOffset - result.mConsumed;
} else {
left = layoutState.mOffset;
right = layoutState.mOffset + result.mConsumed;
}
}
// We calculate everything with View's bounding box (which includes decor and margins)
// To calculate correct layout position, we subtract margins.
//将子View放到合适的位置
layoutDecoratedWithMargins(view, left, top, right, bottom);
if (DEBUG) {
Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
+ (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
+ (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin));
}
// Consume the available space if the view is not removed OR changed
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
result.mFocusable = view.hasFocusable();
}
复制代码
View view = layoutState.next(recycler)
获取一个itemView
,这里涉及到RecyclerView
的缓存机制,咱们后边的篇章在讨论。itemView
添加到RecyclerView
中,又分为两种状况
addView
很好理解,方法内部作了一些位置正确性、避免重复添加等逻辑判断,而后调用ViewGroup
的addView
来实现。addDisappearingView
表明该View即将从屏幕上消失,好比划出屏幕或者调用Adapter.notifyItemRemoved
,该方法和上面的addView
都会调用内部的addViewInt(View child, int index, boolean disappearing)
,只不过是最后一个参数不同而已。itemView
的大小,measureChildWithMargins(view, 0, 0)
这个方法内部除了自身大小以外,还须要考虑margin
和decorations
(咱们常说的分割线)的大小。测量以后把消耗的空间保存到LayoutChunkResult
之中,供外层循环使用。itemView
放到合适的位置,计算位置时layoutState.mOffset
跟咱们以前算的锚点坐标息息相关,若是是第一个itemView
,则layoutState.mOffset
和锚点坐标是同样的,你们能够经过调试来观察数据对应关系。固然布局时还了考虑margin
和decorations
(咱们常说的分割线)以上将fill()
方法分析完成以后,LinearLayoutManager.onLayoutChildren
的核心 已经分析完毕了,最后还有一个layoutForPredictiveAnimations
,从该方法的注释来看,是为了动画作一些布局,也不是必须执行的,就再也不分析了,若是有读者清楚这块内容的,但愿能告知我一下。
至此,LinearLayoutManager.onLayoutChildren
分析完毕,可是该方法注释的最后一条,贴一下原文 4) scroll to fulfill requirements like stack from bottom.
我并无看到它体如今哪,多是上文省略的一些细节中包含,总之这一点我并不明白,若是有读者清楚,但愿能告知我一下。
RecyclerView
的绘制流程咱们分析完了,总结一下
onMeasure
跟LayoutManager
是否开启自动测量是有关系的,若是支持自动测量的话,可能会进行预布局,默认实现的三个LayoutManager
都是支持自动测量的,若是自定义LayoutManager
的话要注意这一点onLayout
中主要是dispatchLayoutStep1()
、dispatchLayoutStep1()
、dispatchLayoutStep1()
这三个方法按顺序调用,第一个和第三个主要处理了动画相关,第二个将布局的任务交给LayoutManager
draw
和onDraw
调用了ItemDecoration
中的方法,咱们实现这些方法来自定义分割线
最后,因为做者水平有限,若是以上分析有出错的地方,欢迎提出,我及时进行改正,以避免误导其余人
相关资料