本文主要android View绘制三大流程(measure、layout、draw)中的measuer流程。
如何通过measure来确定自己的宽高?
主要问以下三部分:
- 名词定义
- measure过程是什么样的,它是如何确定一个View/ViewGroup的宽高
- 总结
一、名词定义
View是一个矩形区域,它有自己的位置、大小与边距。
View位置
View位置:有左上角坐标(getLeft(), getTop())决定,该坐标是以它的父View的左上角为坐标原点,单位是pixels。
View大小
View大小:View的大小有两对值来表示。getMeasuredWidth()/getMeasuredHeight()这组值表示了该View在它的父View里期望的大小值,在measure()方法完成后可获得。 getWidth()/getHeight()这组值表示了该View在屏幕上的实际大小,在draw()方法完成后可获得。
View内边距
View内边距:View的内边距用padding来表示,它表示View的内容距离View边缘的距离。通过getPaddingXXX()方法获取。需要注意的是我们在自定义View的时候需要单独处理 padding,否则它不会生效,这一块的内容我们会在View自定义实践系列的文章中展开。
View外边距
View内边距:View的外边距用margin来表示,它表示View的边缘离它相邻的View的距离。
Measure过程决定了View的宽高,该过程完成后,通常都可以通过getMeasuredWith()/getMeasuredHeight()获得宽高。
MeasureSpec
MeasureSpec从父View生成传递给子View。View的大小最终由子View的LayoutParams与传递给子View的MeasureSpec共同决定。它包含两部分:
高2位:SpecMode,测量模式
低30位:SpecSize,某种测量模式下的规格大小
SpecMode的三种模式
UNSPECIFIED:这种模式表明parent对它的child的大小没有限制,child可以告诉parent它自己所希望的尺寸。
EXACTLY:这种模式表明parent
给child
设置了一个确切的值,child
必须使用这个值,并且需要保证child
的后代节点都要符合这个值的设置。
AT_MOST:这种模式表明parent
给child
设置了一个最大值,child
可以是它想要的任何值,但child
以及它的后代节点的尺寸大小都必须保证在这个最大值内。
二、measure过程

<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">View树(自上而下遍历)</figcaption>
具体分析
measure 过程由measure(int, int)
方法发起,从上到下有序的测量 View,在 measure 过程的最后,每个视图存储了自己的尺寸大小和测量规格。 layout 过程由layout(int, int, int, int)
方法发起,也是自上而下进行遍历。在该过程中,每个父视图会根据 measure 过程得到的尺寸来摆放自己的子视图。
measure 过程会为一个 View 及所有子节点的 mMeasuredWidth 和 mMeasuredHeight 变量赋值,该值可以通过 getMeasuredWidth()
和getMeasuredHeight()
方法获得。而且这两个值必须在父视图约束范围之内,这样才可以保证所有的父视图都接收所有子视图的测量。如果子视图对于 Measure 得到的大小不满意的时候,父视图会介入并设置测量规则进行第二次 measure。比如,父视图可以先根据未给定的 dimension 去测量每一个子视图,如果最终子视图的未约束尺寸太大或者太小的时候,父视图就会使用一个确切的大小再次对子视图进行 measure。
下面我们对ViewGoup的 Measure 流程做一个分析

<figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">流程图</figcaption>
核心方法1:ViewGroup.getChildMeasureSpec(int spec, int padding, int childDimension)
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
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);
}
}
该方法用来获取子View的MeasureSpec,由参数我们就可以知道子View的MeasureSpec由父容器的spec,父容器中已占用的的空间大小 padding,以及子View自身大小childDimension共同来决定的。
通过上述方法,我们可以总结出普通View的MeasureSpec的创建规则。
- 当View采用固定宽高的时候,不管父容器的MeasureSpec是什么,resultSize都是指定的宽高,resultMode都是MeasureSpec.EXACTLY。
- 当View的宽高是match_parent,当父容器是MeasureSpec.EXACTLY,则View也是MeasureSpec.EXACTLY,并且其大小就是父容器的剩余空间。当父容器是MeasureSpec.AT_MOST 则View也是MeasureSpec.AT_MOST,并且大小不会超过父容器的剩余空间。
- 当View的宽高是wrap_content时,不管父容器的模式是MeasureSpec.EXACTLY还是MeasureSpec.AT_MOST,View的模式总是MeasureSpec.AT_MOST,并且大小都不会超过父类的剩余空间。
了解了MeasureSpec的概念之后,我就就可以开始分析测量流程了。
- 对于顶级View(DecorView)其MeasureSpec由窗口的尺寸和自身的LayoutParams共同确定的。
- 对于普通View其MeasureSpec由父容器的Measure和自身的LayoutParams共同确定的。
View的绘制会先调用View的measure()方法,measure()方法用来测量View的大小,实际的测量工作是由View的onMeasure()来完成的。我们来看看 onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法的实现。
核心方法2:View.onMeasure(int widthMeasureSpec, int heightMeasureSpec)
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
//设置View宽高的测量值
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
//measureSpec指的是View测量后的大小
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
//MeasureSpec.UNSPECIFIED一般用来系统的内部测量流程
case MeasureSpec.UNSPECIFIED:
result = size;
break;
//我们主要关注着两种情况,它们返回的是View测量后的大小
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
//如果View没有设置背景,那么返回android:minWidth这个属性的值,这个值可以为0
//如果View设置了背景,那么返回android:minWidth和背景最小宽度两者中的最大值。
protected int getSuggestedMinimumHeight() {
int suggestedMinHeight = mMinHeight;
if (mBGDrawable != null) {
final int bgMinHeight = mBGDrawable.getMinimumHeight();
if (suggestedMinHeight < bgMinHeight) {
suggestedMinHeight = bgMinHeight;
}
}
return suggestedMinHeight;
}
}
View的onMeasure()方法实现比较简单,它调用setMeasuredDimension()方法来设置View的测量大小,测量的大小通过getDefaultSize()方法来获取。
如果我们直接继承View来自定义View时,需要重写onMeasure()方法,并设置wrap_content时的大小。为什么呢?
通过上面的描述我们知道,当LayoutParams为wrap_content时,SpecMode为AT_MOST,而在关于getDefaultSize(int size, int measureSpec) 方法需要说明一下,通过上面的描述我们知道getDefaultSize()方法中AT_MOST与EXACTLY模式下,返回的 都是specSize,这个specSize是父View当前可以使用的大小,如果不处理,那wrap_content就相当于match_parent。
如何处理?
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.d(TAG, "widthMeasureSpec = " + widthMeasureSpec + " heightMeasureSpec = " + heightMeasureSpec);
//指定一组默认宽高,至于具体的值是多少,这就要看你希望在wrap_cotent模式下
//控件的大小应该设置多大了
int mWidth = 200;
int mHeight = 200;
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightMeasureSpec == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, mHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, mHeight);
}
}
注:你可以自己尝试一下自定义一个View,然后不重写onMeasure()方法,你会发现只有设置match_parent和wrap_content效果是一样的,事实上TextView、ImageView 等系统组件都在wrap_content上有自己的处理,可以去翻一翻源码。
看完了View的measure过程,我们再来看看ViewGroup的measure过程。ViewGroup继承于View,是一个抽象类,它并没有重写onMeasure()方法,因为不同布局类型的测量 流程各不相同,因此onMeasure()方法由它的子类来实现。
三、总结
Measure过程是对View尺寸的测量过程,View通过onMeasure方法确定自己的尺寸,ViewGroup在确定自己尺寸的同时,要正确调用子View的measure()方法,让子View正确测量。自定义View和ViewGroup的时候,也是通过onMeasure方法完成measure过程。
网友评论