说到安卓的测绘流程,肯定会想到安卓View绘制三大流程,measure、layout、draw。通过分析View的这三大流程,就可以大概洞悉一个View是怎么从无到有的。万变不离其踪,所以分析RecycleView也按照这个思路进行。
Measure
分析Measure过程,我们直接查看OnMeasure方法。这里我们由高层到低层逐步分析每一个部分。
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
if (mLayout.isAutoMeasureEnabled()) {
。。。
} else {
。。。
}
}
部分细节代码被省略了,我们可以清晰的看到一个流程
- 类型为LayoutManager的mLayout为空的情况下
对应我们没有设置LayoutManager,这是measure过程会直接执行defaultOnMeasure方法
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:
//对应固定宽高 和 match_parent
return size;
case View.MeasureSpec.AT_MOST:
//对应wrap_content
return Math.min(size, Math.max(desired, min));
case View.MeasureSpec.UNSPECIFIED:
default:
return Math.max(desired, min);
}
}
这里直接根据设置的padding和最小宽高,结合布局参数的值算出默认的尺寸。如果我们没有设置LayoutManger,那么就是这样的测量逻辑。
- LayoutManager的isAutoMeasureEnabled判断
/**
* This field is only set via the deprecated {@link #setAutoMeasureEnabled(boolean)} and is
* only accessed via {@link #isAutoMeasureEnabled()} for backwards compatability reasons.
*/
boolean mAutoMeasure = false;
isAutoMeasureEnabled是LayoutManager的属性,表示是否开启自动测试。我们发现系统提供的LayoutManager,不管是常用的 LinearLayoutManager还是GridLayoutManager都通过冲重写isAutoMeasureEnabled方法把它设置成为true。
所以使用系统的控件,都是走的这里面的逻辑。
接下来分别看下开启与否自动测量这两种情况的代码。
isAutoMeasureEnabled为true情况
if (mLayout.isAutoMeasureEnabled()) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
dispatchLayoutStep2();
// now we can get the width and height from the children.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
// if RecyclerView has non-exact width and height and if there is at least one child
// which also has non-exact width & height, we have to re-measure.
if (mLayout.shouldMeasureTwice()) {
mLayout.setMeasureSpecs(
MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
mState.mIsMeasuring = true;
dispatchLayoutStep2();
// now we can get the width and height from the children.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
}
- 调用LayoutManager的onMeasure方法。
内部会直接调用RecyclerView#defaultOnMeasure
,这个方法上面也分析过。按照RecyclerView的设计,如果采用自动测量(mAutoMeasure为true),那么就不要继承LayoutManager的onMeasure方法。可以看出写开源控件每一个方法的调用都应该有清晰的职责和抽象。 - 如果是measureSpecMode是Exactly,也就是尺寸都是EXACTLY,也就是固定尺寸会match_parent,这时候recycleView的宽高已经确认了,不需要再测量了,直接return。或者没有设置Adatper,也就是没有数据,也直接return。这里我们大概猜到开启自动测量的不同之处了,自动测量会内部根据各个item的宽高算出外部RecyclerView的宽高,如果是wrap_content模式的。
- 接下来,因为mState.mLayoutStep 的初始状态就是State.STEP_START。所以会分别执行dispatchLayoutStep1和dispatchLayoutStep2。看方法名就知道这是这是一个连续的步骤,这个一共分三步,还有dispatchLayoutStep3。共同完成了RV的测量布局工作。下面会详细的讲解这三个步骤。经过这两个步骤,我们就已经完成了布局,因为要支持wrap_content嘛,需要提前知道各个item的宽高。调用
mLayout.setMeasuredDimensionFromChildren
设置RecyclerView的布局参数。 - 根据shouldMeasureTwice判断是否会测量两次,看下LinearManger里怎么实现的。从下面的方法可以看出,如果RV的宽高都是wrap_content并且子view至少有一个宽高设置的都不是固定尺寸。才会进行二次测量。这里对应的场景就是子view需要依赖外部的RecyclerView的宽高。比如子View的宽是match_parent,依赖了父View的宽。
@Override
boolean shouldMeasureTwice() {
return getHeightMode() != View.MeasureSpec.EXACTLY
&& getWidthMode() != View.MeasureSpec.EXACTLY
&& hasFlexibleChildInBothOrientations();
}
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;
}
isAutoMeasureEnabled为false情况
if (mHasFixedSize) {
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
return;
}
// custom onMeasure
if (mAdapterUpdateDuringMeasure) {
startInterceptRequestLayout();
();
processAdapterUpdatesAndSetAnimationFlags();
onExitLayoutOrScroll();
if (mState.mRunPredictiveAnimations) {
mState.mInPreLayout = true;
} else {
mAdapterHelper.consumeUpdatesInOnePass();
mState.mInPreLayout = false;
}
mAdapterUpdateDuringMeasure = false;
stopInterceptRequestLayout(false);
} else if (mState.mRunPredictiveAnimations) {
setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight());
return;
}
if (mAdapter != null) {
mState.mItemCount = mAdapter.getItemCount();
} else {
mState.mItemCount = 0;
}
startInterceptRequestLayout();
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
stopInterceptRequestLayout(false);
mState.mInPreLayout = false; // clear
- 看到了常用的mHasFixedSize的使用,他表示如果RV的大小不受适配器内容的影响。那么直接调用LayoutManager的onMeasure方法测量。对应系统提供的LinearLayoutManager,就是直接调用了上面提到的defaultOnMeasure方法。
- 又出现了mAdapterUpdateDuringMeasure和mRunPredictiveAnimations两种情况,表示测量期间刷新了Adapter和预测动画部分。mAdapterUpdateDuringMeasure在调用局部刷新的时候会设置成true。这块和动画相关,后面会进行讲解。
- 看到了熟悉的mAdapter.getItemCount(),这里获取的item的数量。并直接调用了LayoutManager的onMeasure方法进行测量,可见如果不需要自动刷新,那么我们需要实现onMeasure方法,实现我们自己的测绘工作,后面我们会自己实现一个LayoutManager。
- startInterceptRequestLayout/stopInterceptRequestLayout和onEnterLayoutOrScroll/onExitLayoutOrScroll一对类似开关的方法。startInterceptRequestLayout会设置mInterceptRequestLayoutDepth变量,阻止RequestLayout的调用。onEnterLayoutOrScroll会通过mLayoutOrScrollCounter加一,通过isComputingLayout方法判断是否正在测量中。可以理解为一组开关。
public boolean isComputingLayout() {
return mLayoutOrScrollCounter > 0;
}
@Override
public void requestLayout() {
if (mInterceptRequestLayoutDepth == 0 && !mLayoutSuppressed) {
super.requestLayout();
} else {
mLayoutWasDefered = true;
}
}
通过分析没有开启自动测量的代码,发现主要的区别在于是否支持了wrap_content的功能。没有开启自动测量,我们就需要在自己的LayoutManager中通过重写LayoutManager的onMeasure自己实现自己的测量逻辑。而开启了自动测量,我们就不需要,也不要重写LayoutManager的onMeasure了。
Layout
我们看下onLayout的源码
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout();
TraceCompat.endSection();
mFirstLayoutComplete = true;
}
void dispatchLayout() {
if (mAdapter == null) {
Log.e(TAG, "No adapter attached; skipping layout");
// leave the state in START
return;
}
if (mLayout == null) {
Log.e(TAG, "No layout manager attached; skipping layout");
// leave the state in START
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()) {
//前 2 个步骤是在 onMeasure 中完成的,但由于大小的变化,我们似乎必须再次运行。
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
// always make sure we sync them (to ensure mode is exact)
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();
}
onLayout方法内部直接调用了dispatchLayout方法,重头戏在dispatchLayout方法中。
- 先看到了两个判断,这也验证了没有设置LayoutManger,不显示任何东西的。在onMeasure种也进行了类似的判断,不设置LayoutManager,onMeasure会直接进行defaultOnMeasure,而onLayout会直接return,不进行任何操作。
- 又看到了我们熟悉的dispatchLayoutStep系列方法,只是多了一个dispatchLayoutStep3(),这里主要是根据mState.mLayoutStep的状态进行操作。我看先看下这个状态的意义。
取值 | 意义 |
---|---|
STEP_START | 还未开始自动测量,是默认值,这种情况下,会执行dispatchLayoutStep1 |
STEP_LAYOUT | 执行完成dispatchLayoutStep1后,进行设置。表示已经执行完成dispatchLayoutStep1 |
STEP_ANIMATIONS | 执行完成dispatchLayoutStep2后,进行设置。表示已经执行dispatchLayoutStep2,将要执行dispatchLayoutStep3 |
通过对上面状态的分析,可以看到dispatchLayout中也会执行onMeasure中的dispatchLayout系列分方法,并执行单独的dispatchLayout3。
什么情况下执行onMeasure后,mState.mLayoutStep还未STEP_START呢。
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}
为什么这种情况下,就不需要在onMeasure中提前进行dispatchLayoutStep呢。分析下这是什么状态,在宽高方向有任何一个是wrap_content时,都需要提前进行dispatchLayoutStep。
RecycleView怎么支持wrap_content呢,上面也提到过。当然需要知道里面显示的item的显示细节,才能设置自己的自适应。所以这里可以看出,如果RV需要支持wrap_content,需要在onMeasure中提前进行layout,如果不需要也就是宽高都是确定的,那么就在onLayout在进行dispatchLayoutStep即可。
布局的重头戏在dispatchLayoutStep系列方法里。这个系列有3个方法,我们现大致了解下这几个方法。
方法 | 大致用途 |
---|---|
dispatchLayoutStep1 | 预布局阶段,也保存一些当先item view的一些状态 |
dispatchLayoutStep2 | 进行真正的测量和布局 |
dispatchLayoutStep3 | 布局完成后,根据dispatchLayoutStep1中保存的状态,进行动画 |
可以看出1、3方法,主要进行了提前的状态保存和使用,发生在2操作的前后。2进行真正的测量后,3根据1的存储的信息,进行动画和清理工作。这就是大致的整体工作流程。1和3的内部实现主要和动画有关。以后介绍动画的时候,测量和布局的核心逻辑都在dispatchLayoutStep系列方法中,这部分由于篇幅原因,放到下一章分析。
Draw
@Override
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);
}
}
先看RecycleView的draw,首先调用了super.draw(c),这里执行父级绘制默认的绘制流程。先绘制背景,再执行onDraw,再执行dispatchDraw。
因为在dispatchLayoutStep2中已经通过attachViewToParent加入了父类viewGroup的children里面了,所以draw方法内也会直接通过dispatchDraw绘制children内部每个item。
这里我们看下onDraw方法。这里主要绘制RecycleView本身。
@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中都看到了mItemDecorations的使用。
先调用了onDraw内部的mItemDecorations的onDraw方法,这时候还没绘制child,所以是在child的下层绘制的。
再执行draw内部的onDrawOver方法。这时执行完super.draw()已经绘制完child了,所以会绘制在child的上层。
总结
- RecyclerView的measure分两种情况,LayoutManager开没开启autoMeasure模式,系统自带的LayoutManager都是开启了这个模式的。如果开启了autoMeasure模式,长宽有一个不是EXACTLY模式的情况下。在measure过程会提前进行dispatchLayoutStep的前两部计算,以支持wrap_content,也就是需要提前计算出子View的高度,以判断自己需要自适应的尺寸。如果没有开启这个模式,会根据LayoutManager的onMeasure方法进行测量。这就要看我们自己的实现了。需要自己实现wrap_content的效果。
- RecyclerView的layout也是执行了dispatchLayoutStep系列方法,他会确保执行1-3各个步骤的方法,通过当前的状态判断执行到哪儿个状态了。
- RecyclerView的draw通过父类viewgroup的dispatchDraw完成子类的绘制,并且实现了自己的ItemDecorations绘制功能。
- LayoutManager的onMeasure()和isAutoMeasureEnabled()方法,不要同时重写。在和isAutoMeasureEnabled()返回true时,会走自动测绘逻辑。这时执行的onMeasure时使用默认的实现,如果这时候也自己实现了onMeasure,是不符合系统的设计的。我们不重写isAutoMeasureEnabled(),返回false,那么测量的工作就交给我们自己实现的onMeasure方法了。
通过这篇文章,我们知道了RV时怎么绘制布局绘画各个item的。这时RV重要的基础,也是我们接下里分析各个功能控件的基石。
下一篇会介绍dispatchLayoutStep系列方法,介绍最核心的绘制流程。本篇相当于一个壳,下一篇讲解内部的核。
网友评论