注:本文分析的源码版本为Android 27
如有转载,请标明出处
(更新时间:2024-11-24)
一、前言
上节我们已经从App启动,到布局的加载,最后分析到ViewRootImpl的performTraversals()方法,而该方法中,就会开始执行View渲染的三部曲:测量、布局、绘制。
这一篇,我们将开始详细分析View绘制的三大流程:
二、测量(Measure)
2.1、前备知识
在介绍Measure之前,我们需要知道一些前备知识。
- 1、
LayoutParam.width和LayoutParam.height
在xml布局文件中,我们描述一个控件的宽高,通常使用android:layout_width和android:layout_height这2个属性。LayoutInflator在解析xml的过程中,会将其赋值给LayoutParam.width和LayoutParam.height。
该属性用于告诉父布局,自身所期望的尺寸,一般父布局会用其计算出子控件的尺寸约束(MeasureSpec)。
- 2、
MeasureSpec
控件在测量自身时,会收到父控件传入的widthMeasureSpec和heightMeasureSpec2个int值,这2个值用于描述父控件给予自身的大小限制,我习惯将其称之为尺寸约束。
MeasureSpec是一个工具类,其主要作用:
从
measureSpec的int类型值中,获得mode、size2个int值。同时也提供将mode、size转换为measureSpec的方法。
其转换规则很简单:
measureSpec是32位的int类型值,其0~30位表示size,30~32位表示mode。
其中,mode有以下3种类型:
| MeasureSpec mode | 含义 |
|---|---|
| MeasureSpec.UNSPECIFIED | 父控件不限制子控件大小,子控件可以无限大 |
| MeasureSpec.AT_MOST | 父控件限制子控件大小,要求子控件不能超过父控件给的size |
| MeasureSpec.EXACTLY | 父控件要求子控件的大小必须是父控件给的size |
- 3、总结
控件的测量,其实就是通过自身的LayoutParam.[width/height],以及父控件给予的MeasureSpec,来确定自身的尺寸大小。
上述这段话是错误的!
布局的测量,并不会通过自身的
LayoutParam.[width/height]去计算尺寸,而只会通过父布局给予的MeasureSpec,结合自身的内容大小、布局方式、子控件大小等情况,综合计算出自身的尺寸。
这段话一定要深刻理解!
2.2、正式分析
有了上述的前备知识,我们就可以开始继续分析ViewRootImpl的performTraversals()方法。
在performTraversals()中,最开始执行performMeasure()方法,测量View尺寸。
//ViewRootImpl.java
public final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
private void performTraversals() {
WindowManager.LayoutParams lp = mWindowAttributes;
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width,
lp.privateFlags);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height,
lp.privateFlags);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
......
}
其中mWidth、mHeight分别表示Window的宽和高,mWindowAttributes类型为WindowManager.LayoutParams,它是从基础布局(DecorView的子布局)中获得。
然后通过getRootMeasureSpec(),来获取子控件的尺寸约束(MeasureSpec)。
//ViewRootImpl.java
private static int getRootMeasureSpec(int windowSize, int measurement, int privateFlags) {
int measureSpec;
final int rootDimension = (privateFlags & PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT) != 0
? MATCH_PARENT : measurement;
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;
}
这里rootDimension为基础布局的size。
- 如果
rootDimension = MATCH_PARENT,则要求子View大小为windowSize。 - 如果
rootDimension = WRAP_CONTENT,则要求子View大小不能超过windowSize。 - 如果
rootDimension为具体值,则要求子View大小为该值。
然后调用performMeasure方法。
//ViewRootImpl.java
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
mMeasuredWidth = mView.getMeasuredWidth();
mMeasuredHeight = mView.getMeasuredHeight();
mViewMeasureDeferred = false;
}
这里mView就是DecorView,这里调用了它的measure()方法,然后通过getMeasuredWidth()和getMeasuredHeight()获得测量结果。
DecorView继承至FrameLayout->ViewGroup->View,所以这里直接调用到View.measure()方法中。
//View.java
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
......
onMeasure(widthMeasureSpec, heightMeasureSpec);
......
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
......
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
View.measure()方法是一个final方法,不允许子类进行重写,其主要作用是通过一系列判断,来决定是否调用onMeasure()方法。
在onMeasure()的默认的实现中,调用了getSuggestedMinimumWidth()和getDefaultSize()方法。
//View.java
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
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;
}
其中getSuggestedMinimumWidth()是根据控件和背景的最小尺寸来确定的。getDefaultSize()则通过size和measureSpec来返回控件的大小,代码很简单,就不进行赘述。
需要注意的是,onMeasure()的默认的实现存在以下问题:
- 1、
specMode为AT_MOST情况下,返回specSize。
一般情况下,
AT_MOST表示子控件不能超过父控件允许的最大size。子控件应该根据自身的内容,返回最佳尺寸,而不是直接返回父控件允许的最大size。
- 2、未适配ViewGroup
如果是自定义ViewGroup,控件在测量时,还需综合布局方式、子控件的测量结果,才能得到自身的大小。
所以我们一般在自定义View和ViewGroup时,都会要求重写onMeasure()方法。在计算出尺寸大小后,调用setMeasuredDimension(),来保存测量结果。
接下来,我们开始介绍自定义View和ViewGroup时,重写onMeasure方法的一般步骤。
2.3、重写View的onMeasure方法
由于View没有子控件,在测量自身时,只需要考虑自身的内容即可。
一般流程:
- 确定自身最小尺寸,可通过内容、背景、padding等因素确定,也可以直接指定一个最小值。
- 根据父布局传入的尺寸约束,计算出自身尺寸,一般规则如下:
- 当mode为
UNSPECIFIED和AT_MOST时,返回最小尺寸 - 当mode为
EXACTLY,返回size
- 当mode为
- 调用
setMeasuredDimension保存测量结果。
注意:该计算方式并不是固定的,需要根据实际情况进行调整。
代码如下:
public int getContentMinWidth() {
//返回内容最小区域
}
public int getContentMinHeight() {
//返回内容最小区域
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int minWidht = getContentMinWidth();
int minHeight = getContentMinHeight();
int width = getViewSize(minWidht, widthMeasureSpec);
int height = getViewSize(minHeight, heightMeasureSpec);
setMeasuredDimension(width, height);
}
public static int getViewSize(int minSize, int measureSpec) {
int result = minSize;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
case MeasureSpec.AT_MOST:
result = minSize;
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
2.4、重写ViewGroup的onMeasure方法
ViewGroup的测量的一般流程:
- 遍历所有子控件。
- 调用子控件的measure方法,让子控件测量自身大小。
- 通过子控件的测量结果,结合布局方式等因素,计算出自身的大小。
- 调用
setMeasuredDimension保存测量结果。
需要注意一点:
调用子控件的measure方法时,需要传入measureSpec参数。而这个参数,并非父控件传递给自身的measureSpec参数,而是由父控件传递给自身的measureSpec,结合布局方式、子控件的margin、自身的padding等因素,综合计算所得。
这里我们举个栗子:
假设我们有3个控件:ViewGroup A,ViewGroup B,View C;其中A为B的父控件,B为C的父控件。
我们现在只考虑B的onMeasure()方法中,调用C的measure()方法的情况。
其中B的onMeasure()方法的measureSpec参数是A给的限制。而传入给C的measure()方法的measureSpec参数,则应该是B给C的限制,它们可能相同,也可能不同,因为这是根据B的实际应用场景进行决定的。
其中ViewGroup给我们提供了几个现成的方法,方便我们测量子控件。
//ViewGroup.java
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);
}
}
}
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);
}
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);
}
这几个方法都很简单,我就不一一解释了。需要注意的是,它们都调用了getChildMeasureSpec()方法:
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);
}
该方法的主要作用的是:
通过父控件给予自身的MeasureSpec限制,以及自己的子控件的LayoutParam.[width/height],来构造子控件的MeasureSpec限制。
其内容有点多,通过表格进行记录:
| childDimension \ specMode | EXACTLY | AT_MOST | UNSPECIFIED |
|---|---|---|---|
| 精确值 | childSize = 精确值 childMode = EXACTLY | childSize = 精确值 childMode = EXACTLY | childSize = 精确值 childMode = EXACTLY |
| LayoutParams.MATCH_PARENT | childSize = parentSize childMode = EXACTLY | childSize = parentSize childMode = AT_MOST | childSize = 0 childMode = UNSPECIFIED |
| LayoutParams.WRAP_CONTENT | childSize = parentSize childMode = AT_MOST | childSize = parentSize childMode = AT_MOST | childSize = 0 childMode = UNSPECIFIED |
通过getChildMeasureSpec()方法,我们就能获得调用子控件的measure()方法所需要的measureSpec参数。
注意:我们不一定非要调用getChildMeasureSpec()方法,才能获得子控件测量所需要的measureSpec参数。我们可以根据自身情况进行修改。只不过,官方提供的getChildMeasureSpec()方法一般能够满足大部分情况。
子控件执行完measure()方法后,就可以通过getMeasuredWidth()和getMeasuredHeight()拿到子控件的测量结果。
然后当前控件,根据子控件的测量结果,结合自身情况(如最小尺寸、背景大小、子控件大小、布局方式等),计算出自身的尺寸,并通过setMeasuredDimension保存测量结果。
同样的,父控件之后便能拿到本次的测量计算结果。
可见这是一个递归的过程,请仔细体会!
2.5、注意
在xml布局文件中,
layout_width和layout_height一定会影响到控件的宽高吗?
答案是否定的!
我们举个栗子:
public class MyFrameLayout extends FrameLayout {
public MyFrameLayout(Context context) {
super(context);
}
public MyFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(800, MeasureSpec.EXACTLY);
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(800, MeasureSpec.EXACTLY);
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
setMeasuredDimension(width, height);
}
}
这里自定义了一个ViewGroup,继承至FrameLayout,并重写了其onMeasure方法。
其核心修改:将子控件的测量约束改为固定为800像素。
布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<View
android:layout_width="200dp"
android:layout_height="200dp"
android:background="#fcc" />
</FrameLayout>
<com.pujh.demo.MyFrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<View
android:layout_width="200dp"
android:layout_height="200dp"
android:background="#3F51B5" />
</com.pujh.demo.MyFrameLayout>
</LinearLayout>
显示结果:
明显可以发现,下面的View,layout_width和layout_height设置的200dp变得无效。