1. 简介
-
View
的绘制过程分为三部分:measure
、layout
、draw
。
measure
用来测量View的宽和高。
layout
用来计算View的位置。
draw
用来绘制View。
- 本章主要对
measure
过程进行详细的分析。 - 本文源码基于android 27。
2. 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
值在某种测量模式下的规格大小。来张图解说明:
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
,可以看看我的另一篇文章: 从setContentView揭开DecorView。
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布局参数为标准,横向观察:
- 当子View使用具体的dp/px时,无论父容器的测量模式是什么,子View的测量模式都是EXACTLY且大小等于设置的具体数值;
- 当子View使用match_parent时,子View的测量模式与父容器的测量模式保存一致。
另外,UNSPECIFIED
模式一般很少用到,可以不用过多关注。
4. 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 时序图
最后,来张相关的时序图:
4.1.9 流程图
以及来张测量过程细节流程图:
4.2 ViewGroup的measure过程
由于ViewGroup
包含了子View
,因此其measure
过程是通过遍历所有的子View
并对子View
测量,然后合并所有子View
的尺寸,最后得到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);
}
getChildMeasureSpec()
的代码请看上面的分析。
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 时序图
最后,来张相关的时序图:
4.2.8 流程图
以及来张ViewGroup
测量过程细节流程图:
5. 自定义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. 其他问题
6.1 获取View的测量宽高
我们可以使用getMeasuredWidth()
和getMeasuredHeight()
来获取View
测量出的宽高。
但是从上面的代码分析可以看到,在调用setMeasuredDimension()
方法之后,我们才能使用getMeasuredWidth()
和getMeasuredHeight()
来获取View测量出的宽高,在这之前去调用这两个方法得到的值都会是0。
结合Activity的启动过程以及生命周期,在onCreate()
和onResume()
中,都还未开始进行测量的操作,所以这时候调用getMeasuredWidth()
和getMeasuredHeight()
的值都会是0。开始测量的相关代码最早可以追涉到ActivityThread
的handleResumeActivity()
方法,可以看下这篇文章的介绍:Activity启动过程详解。
另外,在某些情况下,需要多次测量才能确定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()
方法。