为什么需要学习View的绘制流程?Android提供了很多的基本View组件,如TextView、EditText、Button等等,这些组件在日常开发中经常要用到,也满足了大部分的需求。然而,有时候用户的要求很特殊,需要的效果基本组件根本实现不了,这时我们就要自定义View,于是View的绘制流程就派上用场了,不学习View的绘制流程,根本写不出符合需求的View,相反,学好了绘制流程,无论什么样的View你都可以实现,只有需求想不出,没有你写不出的自定义View。
简介
View的绘制流程可分为三大阶段,Measure(测量)、Layout(定位)、Draw(绘制),这三个阶段依次进行。Measure的本质就是确定组件的宽和高,Layout的本质是确定组件4个顶点的位置,Draw的本质就是把测量好的、定好位的组件画出来。为什么是这样的顺序?简单举个例子,画画的时候,下笔之前要先想好要画多大、画在哪里吧,画多大就是测量、画在哪就是定位,画这个动作就是绘制。
那么在是在哪调用这3个流程的呢?这里只需要知道入口是ViewRootImpl的performTraversals方法,再上面就是ActivityThread里的代码了,这里不需深究。performTraversals方法会依次调用performMeasure()、performLayout()、performDraw()来完成整个View树的绘制流程。看看其工作流程图。
注意这里并不是说performXXX()方法是ViewGroup里面的,它突出的重点是流程,比如View的measure阶段对应3个方法,preformMeasure->measure->onMeasure,在ViewGroup的onMeasure方法中又遍历子View调用它的measure流程,这样measure就从父View传到子View,子View重复父View的动作,直到反复遍历整个View树,measure阶段结束了就到layout阶段,以此类推。View经历完3个流程才会显示到界面上。
可以看看ViewRootImpl的部分源码,验证上面的内容。
private void performTraversals() {
// 省略代码,通常是一些条件判断
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
// 省略代码,通常是一些条件判断
performLayout(lp, mWidth, mHeight);
// 省略代码,通常是一些条件判断
performDraw();
}
performTraversals()方法的代码很长,全部贴出来不实际,可以自己在AndroidStudio看ViewRootImpl的源码进行验证。前提是你的AndroidStudio下载了源码文件。
下面分别详细讲解三大流程。
measure
要理解measure流程,就要先理解MeasureSpec,因为measure流程其实就是不断的计算MeasureSpec,并传递子View,子View再根据传来的MeasureSpec给某些变量赋值。
MeasureSpec
MeasureSpec是一个32位的int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小。可以把MeasureSpec简单理解为一个对象,里面有两个属性SpecMode和SpecSize,但为了节省空间,把它包装成一个32位的int值。
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
//可以理解为创建对象(传入两个属性size和mode)
public static int makeMeasureSpec(int size,int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
// 下面对应两个get方法
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
需要注意的是,MeasureSpec确实是一个类,但是measure流程使用的MeasureSpec是通过makeMeasureSpec返回的一个int值,并不是MeasureSpec类。
SpecMode
- UNSPECIFIFD
父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部(平时用的基础View和自定义View几乎没有这种情况)。 - EXACTLY
父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值。如下面所示。
android:layout_width="match_parent"
android:layout_width="100dp"
- AT_MOST
父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应与LayoutParams中的wrap_content。
android:layout_width="wrap_content"
注意:
这里并非说一个View在xml文件定义的属性是match_parent,它的SpecMode就是EXACTLY,还有视父View的SpecMode而决定的。所以,一个View的MeasureSpec是由父View的MeasureSpec和自身的LayoutParams属性所决定的。既然如此,那最顶层View的MeasureSpec是怎样得到的呢?下面进行分析。
上面提到measure流程的入口是performTraversals()方法,在里面依次调用了performXXX()函数开始三大流程,可能细心的人已经发现,performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
已经传入了MeasureSpec供后面的View使用。再看ViewRootImpl源码,查看childWidthMeasureSpec是怎么来的(childHeightMeasureSpec与其类似,一个View通常有两个MeasureSpec,分别代表宽和高)。
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// 省略代码
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
可以看到这两个MeasureSpec是通过getRootMeasureSpec()方法得到的,看方法名就知道,这是获取根部MeasureSpec的方法,即顶层View的MeasureSpec。进入该方法查看。
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
通过上面的代码,DecorView的MeasureSpec的产生过程就很明确了,makeMeasureSpec()在上文已经分析过,就是用来创建MeasureSpec的,再总结一下,其遵守如下规则,根据它的LayoutParams中的参数划分。
- LayoutParams.MATCH_PARENT:MeasureSpec的SpecMode为EXACTLY,SpecSize就是窗口的大小。
- LayoutParams.WRAP_CONTENT:MeasureSpec的SpecMode为AT_MOST,SpecSize就是窗口大小(这个SpecSize并不是DecorView的真实大小,这里只是表明了在这种模式下,DecorView的真实大小不能超过SpecSize这个值)。
- 固定大小(比如100dp):MeasureSpec的SpecMode为EXACTLY,SpecSize就是LayoutParams所指定的大小。
顶层View的MeasureSpec后,就调用performMeasure()
了,进入该方法的内部。
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
看到没有,调用了mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
,这样measure流程就由顶层View传递给下层View,并且印证了上面的说法,performMeasure()->measure(),这时进入View.measure(),可以预测它里面肯定调用了onMeasure()。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
//省略代码
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
// 省略代码
}
measure方法确实调用了onMeasure方法,它其实是对MeasureSpec做一些调整,再调用onMeasure进行真正的赋值。它是一个final修饰的方法,意味着子类不能重写这个方法,所以无论当前View是ViewGroup还是View,measure()都可以简单理解为调用了onMeasure()。
所以我们现在的重点onMeasure方法,先看看View的onMeasure()。
View的onMeasure()
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
代码很简洁,但看起来又很复杂。其实就一个函数setMeasuredDimension(int,int)
,只不过它的两个参数都用了函数来获取,但很多情况下,函数得出来的结果就是onMeasure方法中的参数widthMeasureSpec和heightMeasureSpec中的SpecSize值。因此,可以简单理解为setMeasuredDimension(widthMeasureSpec.getSize(),heightMeasureSpec.getSize());
,在这个函数里面就是一些赋值操作,赋值完后,该View的measure流程就结束了。再来分析getDefaultSize(),看完它你就知道为什么上面说可以简化了。
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
从上面的代码可以看出,只有当传进来的MeasureSpec的specMode是UNSPECIFIED的时候,返回的结果才是getSuggestedMinimumWidth()
得到的值,其余情况都是返回specSize,前面又提到UNSPECIFIED是用在系统内部的,自定义或者基本组件很少出现这种模式,一般来说不需要关注此模式,所以知道上面为什么说可以简化了吧。
到这里,View的measure流程就结束了,但上面分析的是最底层的View,是View树的叶子节点,那如果是ViewGroup呢,DecorView就是一个ViewGroup(DecorView继承自FrameLayout,FrameLayout继承自ViewGroup),ViewGroup的onMeasure又是如何工作的呢?
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks
public class FrameLayout extends ViewGroup
ViewGroup的measure流程
在ViewGroup搜索onMeasure方法,没找到?什么情况,没找到那只能说明是继承View的onMeasure方法咯,但View中onMeasure只测量了自己,它没有测量子View,因为如果是View类的话就不存在子View的说法了。然而,ViewGroup可不能只测量自己吧,所以ViewGroup的onMeasure方法是什么回事?答案是,onMeasure的实现在ViewGroup的具体子类中,例如我们常用的布局类RelativeLayout、LinearLayout、FrameLayout等,这些子类都重写了onMeasure方法,每种布局的onMeasure方法都不一样,但肯定都会遍历子View,计算子View的MeasureSpec并调用子View的measure方法,将流程传递下去。所以,如果我们自定义一个ViewGroup,要重写onMeasure方法,如果不重写,就是继承了View的onMeasure方法,这样就测量不了子View导致出现一些异常(一般来说很少自定义一个ViewGroup,多数情况是继承布局类进行功能的扩展)。
你可能看过其他人的文章,他们在讲解ViewGroup的measure过程时,可能会提到ViewGroup类中的一个measureChildren方法,但他们没说这个方法是如何被调用的,搞得不明不白。其实在View和ViewGroup中都没有调用这个方法,这个方法是在ViewGroup的子类如上面提到的几个布局类中被调用,用来测量子View。所以建议大家学习的时候自己多看看源码,看方法是怎样调用的,只看别人的博客很难弄明白整个流程。看看measureChildren的源码。
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
可以看到这里遍历子View,执行measureChild方法,印证上面的说法。进入measureChild方法。
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
在measureChild方法中,先用getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width);
计算子View的MeasureSpec,从参数可以看出子View的MeasureSpec的确跟父View和自身的LayoutParams属性相关(前面有提及过),计算好View的MeasureSpec后传递给child.measure方法,然后子View就根据这个MeasureSpec进行操作了,具体流程上面有详细分析。
除了上面的measureChild方法,ViewGroup还有一个measureChildWithMargins方法,从名字可以看出后者把margins也考虑进来了,其他和measureChild区别不大。看看其源码。
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
值得一提的是,这些方法在ViewGroup的具体实现类中不是一定要使用的,比如measureChildren这个方法在上面提到的几个布局类中都没有使用,它们都自己实现了遍历子View的方式,所以可以理解为,这些方法是提供给ViewGroup子类使用,但使不使用由子类决定。
再来分析getChildMeasureSpec
,看看父View的MeasureSpec和子类LayoutParams具体是怎样共同决定子类MeasureSpec的。
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
上面的代码可以用一个二维表格表示。
childLayoutParams/parentSpecMode | EXACTLY | AT_MOST | UNSPECIFIED |
---|---|---|---|
dp/dx | EXACTLY childSize | EXACTLY childSize | EXACTLY childSize |
MATCH_PARENT | EXACTLY parentSize | AT_MOST parentSize | UNSPECIFIED 0 |
WRAP_CONTENT | AT_MOST parentSize | AT_MOST parentSize | UNSPECIFIED 0 |
需要再次说明,AT_MOST状态下的SpecSize不是真实的的大小,只是表明不能超过这个值。
到此,三大流程中的measure阶段就分析完毕了,接下来就是layout阶段。
layout
和measure类似,layout阶段也是从performLayout开始的,performLayout调用View的layout方法。看看其源码。
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
//省略代码
}
可以看到上面的代码调用了setFrame
或者setOpticalFrame
,setOpticalFrame内部也是调用setFrame,这个方法给View的4个顶点赋值,这里就确定了View在父容器里的位置。赋值完毕后就调用onLayout
,onLayout方法可以理解为父View计算子View的位置,然后调用子View的layout方法确定位置(就是前面说的赋值),这样layout流程就从父View传到子View了。
onLayout在View类中是一个空方法,子类可根据需求重写;在ViewGroup中是一个抽象方法,子类必须给出具体实现。可以这样理解,View的位置在layout方法中已经确定了,而onLayout是父View计算子View位置的方法,叶子节点已经没有子View了,所以onLayout方法为空,是否重写根据具体需求;而ViewGroup下面还有子View,所以必须重写onLayout方法计算子View的位置(ViewGroup子类的onLayout中一定会遍历子View调用其layout方法)。每种ViewGroup的onLayout方法都有不同的逻辑。所以,一般通过继承View实现的自定义组件,很少重写onLayout方法。
draw
Draw过程就相对简单了,它的作用是将View绘制到屏幕上。View的绘制过程遵循下面几步:
(1)绘制背景background.draw(canvas)。
(2)绘制自己 onDraw(canvas)。
(3)绘制children (dispatchDraw)
(4)绘制装饰
这一点可以从源码中明显看出来。
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
drawBackground(canvas);
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
// 省略代码
}
draw过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历子View并调用子View的draw方法。