Android View的工作流程 - measure过程
1. View的工作流程概述
View的绘制流程主要是指View的 measure
、 layout
、 draw
过程:
-
measure
:确定View的测量宽/高 -
layout
:确定View的最终宽高和四个顶点的位置 -
draw
:将View绘制到屏幕上
View的绘制流程是从ViewRootImpl
的performTraversals
方法开始的,performTraversals
会依次调用performMeasure
、performLayout
、performDraw
三个方法:
-
performMeasure
: performMeasure会调用DecorView的measure方法,将measure流程传递到根View(DecorView)。因为DecorView继承自FrameLayout,FrameLayout继承自ViewGroup,ViewGroup继承自View,那么到DecorView之后的measure过程主要就是ViewGroup和View的measure过程。 -
performLayout
: 同performMeasure一样,调用DecorView的layout方法,将layout流程传递到根View(DecorView)。 -
performDraw
: 同performMeasure一样,调用DecorView的draw方法,将draw流程传递到根View(DecorView)。
2. measure过程概述
measure过程主要是为了确定View的测量宽/高,理解View测量过程的重点在于理解MeasureSpec和onMeasure方法。
-
measure
方法:
measure方法是一个final方法(不能重写measure方法),measure方法会调用自己的onMeasure方法 -
View的
onMeasure
方法:
根据自己的MeasureSpec
确定测量宽/高。 -
ViewGroup
的onMeasure
方法:
需要去遍历子元素,并调用子元素的measure方法,将measure过程传递到子元素。
等子元素完成测量后,ViewGroup会根据自己的MeasureSpec
和子元素的测量宽/高,来确定自身的测量宽/高。 -
MeasureSpec
:
View测量宽/高的确定是和MeasureSpec
有关的
measure的传递过程:
-
performMeasure
调用DecorView
的measure
方法; -
DecorView
的measure
方法调用onMeasure
方法,在onMeasure
方法中遍历子元素,并调用子元素的measure
方法 - 子元素
measure
方法被调用后,重复过程2,如此反复,完成整个View
树的遍历。
3. MeasureSpec
View的Measure过程主要是为了确定View的测量宽/高,而View的测量宽/高的确定是和MeasureSpec
有关的,所以在了解View的测量过程前,需要先了解MeasureSpec
。
MeasureSpec
代表一个32位的int值,高2位代表SpecMode
,低30位代表SpecSize
,SpecMode
是指测量模式,而SpecSize
是指在某种模式下的规格大小。MeasureSpec
通过将SpecMode
和SpecSize
打包成一个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;
/**
* 将SpecMode和SpecSize打包成一个int值,避免过多的对象内存分配
* */
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
/**
* 将打包后的int值解包,获取SpecMode
* */
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
/**
* 将打包后的int值解包,获取SpecSize
* */
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
SpecMode有三类,如下:
-
UNSPECIFIED
父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态 -
EXACTLY
父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams
中的match_parent
和具体的数值这两种模式。 -
AT_MOST
父容器指定了一个可用大小即SpecSize
,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于LayoutParams
中的wrap_content
。
4. MeasureSpec和LayoutParams的对应关系
View的MeasureSpec
由父容器和View自身的LayoutParams
共同决定:
- 对于
DecorView
,其MeasuceSpec由窗口的尺寸和其自身的LayoutParams来共同决定 - 对于
普通View
,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定
4.1. DecorView的MeasureSpec
先来看下DecorView的MeasureSpec
的创建过程,在ViewRootImpl
中的measureHierarchy
方法中有如下一段代码:
/**
* @param desiredWindowWidth 屏幕宽度
* @param desiredWindowHeight 屏幕高度
* @param lp WindowManager.LayoutParams
*/
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
接着看getRootMeasureSpec
方法的实现:
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.
//窗口无法调整大小。强制根视图为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
的产生过程就很明确了,其由它的LayoutParams
和屏幕尺寸
共同确定,如下:
-
LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小
-
LayoutParams.WRAP_CONTENT:最大模式,大小不确定,但不能超过窗口的大小
-
固定大小(比如100dp):精确模式,大小为LayoutParams中指定的大小
4.2. 普通View的MeasureSpec
对于普通View来说,View的measure
过程由ViewGroup传递而来,先看下ViewGroup的measureChildWithMargins
方法:
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);
}
measureChildWithMargins
方法会对子元素进行measure
,在调用子元素的measure
方法之前会先通过getChildMeasureSpec
方法来得到子元素的MeasureSpec
。从代码来看,子元素MeasureSpec
的创建与父容器的MeasureSpec
和子元素本身的LayoutParams
有关,此外还和View的margin
及padding
有关,具体情况在ViewGroup的getChildMeasureSpec
方法中,如下:
/**
* 根据父容器的MeasureSpec和View自身的LayoutParams来确定子元素的MeasureSpec
* @param spec 父容器的MeasureSpec
* @param padding 父容器已占用的空间大小
* @param childDimension View本身的LayoutParams,如MATCH_PARENT、WRAP_CONTENT、100dp
* @return 返回View的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);
//View最终的SpecSize
int resultSize = 0;
//View最终的SpecMode
int resultMode = 0;
//判断父容器的SpecMode,EXACTLY、AT_MOST、UNSPECIFIED
switch (specMode) {
case MeasureSpec.EXACTLY:
//判断View自身的LayoutParams,MATCH_PARENT、WRAP_CONTENT、固定值(如100dp)
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == ViewGroup.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 == ViewGroup.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 == ViewGroup.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 them have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == ViewGroup.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 == ViewGroup.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);
}
getChildMeasureSpec
方法清楚的展示了普通View的MeasureSpec的创建规则。最后附上getChildMeasureSpec
对应的一个表格梳理,其中parentSize
是指父容器中目前可用的大小:
父容器的SpecMode-EXACTLY | 父容器的SpecMode-AT_MOST | 父容器的SpecMode-UNSPECIFIED | |
---|---|---|---|
自身LayoutParams-固定值 | EXACTLY | ||
childSize | EXACTLY | ||
childSize | EXACTLY | ||
childSize | |||
自身LayoutParams-match_parent | EXACTLY | ||
parentSize | AT_MOST | ||
parentSize | UNSPECIFIED | ||
0 | |||
自身LayoutParams-wrap_cotent | AT_MOST | ||
parentSize | AT_MOST | ||
parentSize | UNSPECIFIED | ||
0 |
5. LinearLayout的measure过程
ViewGroup没有实现onMeasure方法,它提供了一个叫measureChildren的方法,measureChildren的实现比较简单,就是遍历所有的子元素,取出子元素的LayoutParams,然后再通过getChilMeasureSpec来创建子元素的MeasureSpec,最后将子元素的MeasureSpec直接传递给子元素的measure方法来进行测量。getChildMeasureSpec方法也在分析MeasureSpec的时候介绍过了。
下面我们主要分析下LinearLayout的onMeasure方法,如下所示:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
LinearLayout竖直布局和水平布局的测量过程是类似的,分析其中一个就可以了,如下:
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
//记录LinearLayout的总高度
mTotalLength = 0;
...
//遍历所有子元素
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
//获取子元素的LayoutParams
final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
...
//measureChildBeforeLayout方法调用了measureChildWithMargins方法,
//measureChildWithMargins方法在介绍MeasureSpec的时候已经介绍过了,
//就是根据LinearLayout的margin、MeasureSpec和子元素本身的LayoutParams
//来确定子元素的MeasureSpec,并调用子元素的measure方法
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
//measureChildBeforeLayout已经调用了子元素的measure方法,此时可以获取到子元素的测量高
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
//最后使用mTotalLength累加子元素的高度
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
...
}
...
//加上LinearLayout自身的padding,
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
//计算LinearLayout的最终大小,
// resolveSizeAndState方法就是根据LinearLayout的MeasureSpec和记录的mTotalLength
// 得到LinearLayout最终的测量高
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
...
//设置LinearLayout的测量宽/高
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
}
在measureVertical方法中,LinearLayout遍历所有子元素,通过measureChildBeforeLayout确定子元素的MeasureSpec并调用子元素的measure方法,完成子元素的测量。在遍历过程中,还会根据子元素的高度来测量自己的大小,即mTotalLength,最后LinearLayout根据自身的MeasureSpec和mTotalLength确定自己的测量高,并使用setMeasuredDimension方法设置自己的测量高。
6. View的Measure过程
View的measure过程由其measure方法来完成,measure方法是一个final类型的方法,这意味着子类不能重写此方法,在View的measure方法中会调用View的onMeasure方法,onMeasure方法如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
setMeasuredDimension方法会设置View宽/高的测量值,因此我们只需要看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;
//如果是AT_MOST或者EXACTLY,返回specSize。
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
getDefaultSize方法中主要根据View的specMode来确定View的测量宽/高,对于我们来说,只需要看AT_MOST和EXACTLY这两种情况就可以了。在specMode是AT_MOST或者EXACTLY的时候,getDefaultSize返回的大小就是specSize,即View的测量宽/高就是specSize。在EXACTLY模式下,specSize是一个固定大小,而在AT_MOST模式下,specSize是多少?回顾上面根据MeasureSpec的创建规则:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//父容器的specMode
int specMode = MeasureSpec.getMode(spec);
//父容器的specSize
int specSize = MeasureSpec.getSize(spec);
//父容器可用大小
int size = Math.max(0, specSize - padding);
//判断父容器的SpecMode,EXACTLY、AT_MOST、UNSPECIFIED
switch (specMode) {
case MeasureSpec.EXACTLY:
//判断View自身的LayoutParams,MATCH_PARENT、WRAP_CONTENT、固定值(如100dp)
...
else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.AT_MOST:
...
else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
可以发现,如果View在布局中使用wrap_cotent,,它的大小就是父容器可用大小,这种效果和在布局中使用match_parent一样,所以我们在自定义View的时候,要处理View宽/高是wrap_content的情况,重写View的onMeasure方法,如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getMode(heightMeasureSpec);
//对于wrap_content情况,设置一个默认的宽/高
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(mDefaultWidth, mDefaultHeight);
}else if (widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(mDefaultWidth, heightMeasureSpec);
}else if (heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthMeasureSpec, mDefaultHeight);
}
//其它情况还是沿用系统的测量值
}