View篇之Measure过程
- 简介
•View的绘制过程分为三部分:measure、layout、draw。
measure用来测量View的宽和高。
layout用来计算View的位置。
draw用来绘制View。
•本章主要对measure过程进行详细的分析。
- measure的始点
measure是从ViewRootImpl的performTraversals()方法开始的:
2.1 ViewRootImpl的performTraversals
private void performTraversals() {
//...
//获得view宽高的测量规格,mWidth和mHeight表示窗口的宽高,lp.widthhe和lp.height表示DecorView根布局宽和高
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//执行测量
//...
}
首先会获取view宽高的测量规格,测量规格在下一节会详细讲述,然后就是调用performMeasure()了:
2.2 ViewRootImpl的performMeasure
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
//...
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
//...
}
performMeasure()中就是调用View的measure()方法开始进行测量。
3.MeasureSpec
了解measure的过程时,我们须先了解MeasureSpec。MeasureSpec,顾名思义,就是测量规格,其决定了一个View的宽和高。
3.1 MeasureSpec组成
MeasureSpec代表一个32位的int值,前2位代表SpecMode,后30位代表SpecSize。其中:SpecMode代表测量的模式,SpecSize值在某种测量模式下的规格大小。来张图解说明:
测量说明.png
3.2 SpecMode测量模式
测量模式分为三种UNSPECIFIED、EXACTLY、AT_MOST,其具体说明如下表所示:
测量模式 | 说明 | 应用场景 |
---|---|---|
UNSPECIFIED (未指定) | 父容器没有对当前View有任何限制,当前View可以任意取尺寸 | 系统内部 |
EXACTLY(精确) | 父容器已经确定当前View的大小,无论View想要多大都会在这范围内 | match_parent 和 体数值 |
AT_MOST(最多) | 当前View不能超过父容器规格大小,具体数值由view去决定 | wrap-content |
3.3 MeasureSpec源码分析
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;//模式移位数
private static final int MODE_MASK = 0x3 << MODE_SHIFT;//模式掩码
//UNSPECIFIED模式:父容器没有对当前View有任何限制,当前View可以任意取尺寸
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
//EXACTLY模式:父容器已经确定当前View的大小,无论View想要多大都会在这范围内
public static final int EXACTLY = 1 << MODE_SHIFT;
//AT_MOST模式:当前View不能超过父容器规格大小,具体数值由view去决定
public static final int AT_MOST = 2 << MODE_SHIFT;
//根据提供的size和mode得到一个测量规格
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
//sUseBrokenMakeMeasureSpec = targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR1;
//即targetSdkVersion<=17时,size与mode是直接相加的;>17则进行位运算
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);//位运算
}
}
//根据提供的size和mode得到一个测量规格
public static int makeSafeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}
//获取测量模式
@MeasureSpecMode
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
//获取测量大小
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
可以看到,MeasureSpec类还是挺简单的,MeasureSpec类通过将mode和size打包成一个32位int值来减少了对象内存分配,并提供了打包和解包的方法。
3.4 确定MeasureSpec值
在measure过程中,系统会将View的LayoutParams和父容器所施加的规则转换成对应的MeasureSpec,然后在onMeasure()方法中根据这个MeasureSpec来确定View的测量宽高。父容器所施加的规则对于DecorView与普通View是不同的,我们分开来看。
3.4.1 确定DecorView的MeasureSpec值
DecorView,作为顶级的View,我们平时setContentView()所设置的布局可能只是DecorView其中的一部分,如下图所示:
DecorView、ContentView、标题栏图解
3.4.1.1 ViewRootImpl的PerformTraveals
在ViewRootImpl的PerformTraveals()方法中会获得DecorView的MeasureSpec值:
private void performTraversals() {
//...
//获得view宽高的测量规格,mWidth和mHeight表示窗口的宽高,lp.widthhe和lp.height表示DecorView根布局宽和高
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);//执行测量
//...
}
3.4.1.2 ViewRootImpl的getRootMeasureSpec
我们再来看看getRootMeasureSpec()方法:
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
//如果View的布局参数为MATCH_PARENT,则其为精确模式,大小为窗口的大小
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
//如果View的布局参数为WRAP_CONTENT,则其为最多模式,大小不定,但不能超过窗口的大小
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
//如果View的布局参数为准确的数值,如100dp等,则其为精确模式,大小为View设定的数值
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
可以看到:DecorView的MeasureSpec值是由窗口大小及其布局参数来决定的。
来张表格总结:
布局参数 | LayoutParams.MATCH_PARENT | LayoutParams.WRAP_CONTENT | 准确数值 |
---|---|---|---|
MeasureSpec值 | EXACTLY windowSize(窗口大小) | AT_MOST windowSize(不能超过窗口大小) | EXACTLY rootDimension(准确数值) |
3.4.2 确定普通View的MeasureSpec值
普通View的MeasureSpec值通过getChildMeasureSpec()方法来获取,getChildMeasureSpec()位于ViewGroup中:
3.4.2.1 ViewGroup的getChildMeasureSpec
/**
*
* @param spec 父容器的测量规格(MeasureSpec)
* @param padding 子view的内边距和外边距(padding,margin)
* @param childDimension 子view的布局参数(宽/高)
* @return 子view的测量规格(MeasureSpec)
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//父容器的测量模式
int specMode = MeasureSpec.getMode(spec);
//父容器的测量大小
int specSize = MeasureSpec.getSize(spec);
//子view大小 = 父容器大小-子view边距(子view只在某些地方地方用到这个值,具体看下面的代码)
int size = Math.max(0, specSize - padding);
//初始化子view的大小和模式(最终的结果需要下面代码计算出来)
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
case MeasureSpec.EXACTLY://当父容器模式为EXACTLY时
if (childDimension >= 0) {// 当子view的LayoutParams>0,即有确切的值
//子view大小为子自身所赋的值,模式大小为EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {// 当子view的LayoutParams为MATCH_PARENT时(-1)
//子view大小为父容器剩余大小,模式为EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {// 当子view的LayoutParams为WRAP_CONTENT时(-2)
//子view决定自己的大小,但最大不能超过父容器剩余大小,模式为AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.AT_MOST: // 当父容器的模式为AT_MOST时
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.UNSPECIFIED:// 当父容器的模式为UNSPECIFIED时
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//这里的sUseZeroUnspecifiedMeasureSpec值为targetSdkVersion < Build.VERSION_CODES.M;即<23为true,否则为false.
//大于等于23时会将size作为提示值,否则为0
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
可以看到:普通View的MeasureSpec值是由父容器的MeasureSpec及其布局参数来决定的。
来张表格总结:
父容器测量模式(右侧)———————————-— 子View布局参数(下侧) | EXACTLY(精确) | AT_MOST(最多) | UNSPECIFIED(未指定) |
---|---|---|---|
具体dp/px | EXACTLY childDimension(子View尺寸) | EXACTLY childDimension(子View尺寸) | EXACTLY childDimension(子View尺寸) |
MATCH_PARENT | EXACTLY parentSize(父容器剩余大小) | AT_MOST parentSize(不能超过父容器剩余大小) | UNSPECIFIED targetSdkVersion<23 ? 0 : size; (大于等于23时会将size作为提示值) |
WRAP_CONTENT | AT_MOST parentSize(不能超过父容器剩余大小) | AT_MOST parentSize(不能超过父容器剩余大小)) | UNSPECIFIED targetSdkVersion<23 ? 0 : size; (大于等于23时会将size作为提示值) |
对上表作一些规律总结:
•以子View布局参数为标准,横向观察:
1.当子View使用具体的dp/px时,无论父容器的测量模式是什么,子View的测量模式都是EXACTLY且大小等于设置的具体数值;
2.当子View使用match_parent时,子View的测量模式与父容器的测量模式保存一致。
另外,UNSPECIFIED模式一般很少用到,可以不用过多关注。
- measure过程分析
measure过程根据View的类型可以分为两种情况:
- 测量单一View时,只需测量自身;
- 测量ViewGroup时,需要对ViewGroup中包含的所有子View都进行测量。
我们对这两种情况分别进行分析。
4.1 单一View的measure过程
单一View的measure过程由View的measure()来实现:
4.1.1 View的measure
/**
*
* @param widthMeasureSpec view的宽测量规格
* @param heightMeasureSpec view的高测量规格
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
//...
onMeasure(widthMeasureSpec, heightMeasureSpec);
//... }
这里传入的widthMeasureSpec/heightMeasureSpec是当前View的测量规格,通过getChildMeasureSpec()来获得的。具体细节可以看上面的分析。
measure()方法为final类型,子类不能对其进行重写。measure()方法中主要就是对基本测量逻辑作判断,最终会调用onMeasure()方法。
4.1.2 View的onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
上面就一行代码,看起来很简单,但是包含了三个方法:getSuggestedMinimumWidth()/getSuggestedMinimumHeight()、getDefaultSize()、setMeasuredDimension(),我们分开来看:
4.1.3 View的getSuggestedMinimumWidth
getSuggestedMinimumHeight()与getSuggestedMinimumWidth()同理,我们这里只看下getSuggestedMinimumWidth():
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
如果View没有设置背景,那么View的宽度为mMinWidth,而mMinWidth就是android:minWidth属性所指定的值,若android:minWidth没有指定,则默认为0;
如果View设置了背景,那么View的宽度为mMinWidth和mBackground.getMinimumWidth()中的最大值。
那么mBackground.getMinimumWidth()的值是多少呢,我们来看看:
4.1.4 Drawable的getMinimumWidth
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();//返回背景图Drawable的原始宽度
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
可以看出:mBackground.getMinimumWidth()返回的是背景图Drawable的原始宽度,若无原始宽度则返回0。
那么Drawable什么情况下有原始宽度?如:ShapeDrawable没有,但BitmapDrawable有。
4.1.5 View的getDefaultSize
我们再来看看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) {
//测量模式为UNSPECIFIED时,View的测量大小为默认大小,即getSuggestedMinimumWidth()/getSuggestedMinimumHeight()返回的结果。
case MeasureSpec.UNSPECIFIED:
result = size;
break;
//测量模式为AT_MOST、EXACTLY时,View的测量大小为测量规格中的测量大小
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
4.1.6 View的setMeasuredDimension
再来看setMeasuredDimension():
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
//...
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
setMeasuredDimension()里会调用setMeasuredDimensionRaw():
4.1.7 View的setMeasuredDimensionRaw
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
setMeasuredDimensionRaw()中就是对测量出的宽高进行保存。
到这里,单一View的measure过程就完成了。
4.1.8 时序图
最后,来张相关的时序图:
单一View测量过程时序图
4.1.9 流程图
以及来张测量过程细节流程图:
单一View测量过程流程图
4.2 ViewGroup的measure过程
由于ViewGroup包含了子View,因此其measure过程是通过遍历所有的子View并对子View测量,然后合并所有子View的尺寸,最后得到ViewGroup的测量值。
ViewGroup测量顺序图如上图所示,要执行ViewGroup的measure,首先从顶部的ViewGroup开始遍历,自上而下递归执行子View的measure。
ViewGroup的measure过程是从measureChildren()开始的。
4.2.1 ViewGroup的measureChildren
/**
*
* @param widthMeasureSpec ViewGroup的宽测量规格
* @param heightMeasureSpec ViewGroup的高测量规格
*/
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {//遍历子View
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
再来看measureChild():
4.2.2 ViewGroup的measureChild
protected void measureChild(View child, int parentWidthMeasureSpec,int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();//获得子view的布局参数
//获得子view的宽/高测量规格
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
//子view开始测量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
4.2.3 View的measure
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
//...
onMeasure(widthMeasureSpec, heightMeasureSpec);
//...
}
这里的measure跟上面单一View的measure方法是一样的。
4.2.4 ViewGroup的onMeasure
如果子View是一个单一的View的话,则直接调用View的onMeasure()方法,跟上面单一View的measure过程是一样大的;
如果子View是ViewGroup的话,理论上应该是应该调用ViewGroup的onMeasure()方法的。
但实际上,ViewGroup中是没有onMeasure()这个方法的,ViewGroup作为一个抽象类,需要其子类去实现测量过程的onMeasure()方法,比如LinearLayout、RelativeLayout等。因为LinearLayout、RelativeLayout这些布局具体不同的特性,其测量细节各有不同,无法进行统一的实现,因此需要其子类去进行具体的实现。因此我们自定义ViewGroup时需要重写onMeasure()方法。
我们这里看下LinearLayout的onMeasure()实现:
4.2.5 LinearLayout的onMeasure
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {//方向判断
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
LinearLayout会区分方向来进行不同的测量方法,我们主要看下竖向的测量measureVertical(),横向的原理差不多这里就不看了。
4.2.6 LinearLayout的measureVertical
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
//获取子view数量
final int count = getVirtualChildCount();
//获取LinearLayout的宽/高测量模式
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//...
for (int i = 0; i < count; ++i) {//遍历子View
final View child = getVirtualChildAt(i);
//..
// 最终会调用子View的measure(),计算出子View的大小
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
// 获取子View的测量高度
final int childHeight = child.getMeasuredHeight();
//..
final int totalLength = mTotalLength;
// 将子View的测量高度以及Margin加到LinearLayout的总高度上
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
}
//..
// 加上LinearLayout设置的Padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
//..
//保存测量值
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),heightSizeAndState);
}
最后会调用setMeasuredDimension()来保存测量好的值,至此ViewGroup的测量过程就完成了。
4.2.7 时序图
最后,来张相关的时序图:
ViewGroup测量过程时序图4.2.8 流程图
以及来张ViewGroup测量过程细节流程图:
ViewGroup测量过程流程图- 自定义View
5.1 自定义单一View
自定义单一View,可以直接使用View中默认定义的onMeasure()方法。如果有时需要更精准的测量,可以重写onMeasure()。
5.2 自定义ViewGroup
由于ViewGroup没实现onMeasure(),所以自定义ViewGroup需要重写onMeasure()方法。这里给个简单的模板:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//定义存放测量后的View的宽和高的变量
int widthMeasure;
int heightMeasure;
//实现测量方法以及具体的测量细节
measureMethod();
//必不可少 保存测量后View宽和高的值
setMeasuredDimension(widthMeasure, heightMeasure);
}
- 其他问题
6.1 获取View的测量宽高
我们可以使用getMeasuredWidth()和getMeasuredHeight()来获取View测量出的宽高。
但是从上面的代码分析可以看到,在调用setMeasuredDimension()方法之后,我们才能使用getMeasuredWidth()和getMeasuredHeight()来获取View测量出的宽高,在这之前去调用这两个方法得到的值都会是0。
结合Activity的启动过程以及生命周期,在onCreate()和onResume()中,都还未开始进行测量的操作,所以这时候调用getMeasuredWidth()和getMeasuredHeight()的值都会是0。
另外,在某些情况下,需要多次测量才能确定View最终宽高;因此这种情况下获得的值可能是不准确的,建议在layout过程中onLayout()去获取最终宽高。
6.2 自定义View时WRAP_CONTENT不生效的问题
从上面getDefaultSize()方法的代码可以看到,无论是AT_MOST模式还是EXACTLY模式,View的测量大小都为测量规格中的测量大小,所以我们就会看到自定义View使用WRAP_CONTENT会起到跟MATCH_PARENT的效果。
为了解决这个问题需要在LayoutParams属性为WRAP_CONTENT时指定一下默认的宽和高,如:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width=100;//设置一个默认宽
int height=100;//设置一个默认高
if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT && getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(width, height);//宽高都为WRAP_CONTENT都设置为默认宽高
} else if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(width, heightSize);//只有宽为WRAP_CONTENT时,只设置默认宽
} else if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(widthSize, height);//只有高为WRAP_CONTENT时,只设置默认高
}
}这里的默认值请根据实际情况去确认。
像系统的TextView这些都支持WRAP_CONTENT,可以去看下其onMeasure()的源码实现。
如果我们自定义的View只需要设置指定的具体宽高或者MATCH_PARENT,可以不用重写onMeasure()方法。