这是Android视图绘制系列文章的第一篇,系列文章目录如下:
View绘制就好比画画,先抛开Android概念,如果要画一张图,首先会想到哪几个基本问题:
- 画多大?
- 画哪儿?
- 怎么画?
Android绘制系统也是按照这个思路对View进行绘制,上面这些问题的答案分别藏在
- 测量(measure)
- 定位(layout)
- 绘制(draw)
这一篇将以源码中的几个关键函数为线索分析“测量(measure)”。
View.measure()
“测量”要解决的问题是确定待绘制View的尺寸,以View.measure()
为入口,一探究竟:
public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
/**
* <p>
* This is called to find out how big a view should be. The parent
* supplies constraint information in the width and height parameters.
* 这个方法用于决定当前view到底有多大,父亲提供宽高参数起到限制大小的作用
*
* The actual measurement work of a view is performed in
* {@link #onMeasure(int, int)}, called by this method. Therefore, only
* {@link #onMeasure(int, int)} can and must be overridden by subclasses.
* 真正的测量工作在onMeasure()中进行
*
* @param widthMeasureSpec Horizontal space requirements as imposed by the
* parent(父亲施加的宽度要求)
* @param heightMeasureSpec Vertical space requirements as imposed by the
* parent(父亲施加的高度要求)
*
* @see #onMeasure(int, int)
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
/**
* Measure the view and its content to determine the measured width and the
* measured height. This method is invoked by {@link #measure(int, int)} and
* should be overridden by subclasses to provide accurate and efficient
* measurement of their contents.
* View子类应该重载这个方法以定义自己尺寸
*
* <strong>CONTRACT:</strong> When overriding this method, you
* <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
* measured width and height of this view. Failure to do so will trigger an
* <code>IllegalStateException</code>, thrown by
* {@link #measure(int, int)}. Calling the superclass'
* {@link #onMeasure(int, int)} is a valid use.
* 重载方法必须调用 setMeasuredDimension()
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
}
从注释中得知这么几个信息:
- 真正的测量工作在
onMeasure()
中进行,View
的子类应该重载这个方法以定义自己尺寸。 - 在
onMeasure()
中必须调用setMeasuredDimension()
。 - 父
View
会通过传入的宽高参数对子View
的尺寸施加限制。
顺带便看了一下常见控件如何重载onMeasure()
,其实套路都一样,不管是TextView
还是ImageView
,在一系列计算得出宽高值后将传入setMeasuredDimension()
。所以,整个测量过程的终点是View.setMeasuredDimension()
的调用,它表示着视图大小已经有确定值。
ViewGroup.onMeasure()
View
必然依附于一棵“View树”,那父View
是如何对子View
的尺寸施加影响的?全局搜索View.measure()
被调用的地方,在很多ViewGroup
类型的控件中发现类似child.measure()
的调用,以最简单的FrameLayout
为例:
public class FrameLayout extends ViewGroup {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//获得孩子数量
int count = getChildCount();
...
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
//遍历可见孩子或者强制遍历所有孩子
if (mMeasureAllChildren || child.getVisibility() != GONE) {
//测量孩子
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//记忆孩子中最大宽度
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
//记忆孩子中最大高度
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
...
//以最孩子中最大的尺寸作为自己的尺寸
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
...
}
}
FrameLayout
会遍历所有可见的孩子记忆其中最大宽度和最大高度,并以此作为自己的宽和高(这是FrameLayout
的测量算法,其他的ViewGroup
应该也有自己独特的测量算法。)
ViewGroup.measureChildWithMargins()
父控件在遍历每个孩子时会调用measureChildWithMargins()
来测量孩子:
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
/**
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding
* and margins. The child must have MarginLayoutParams The heavy lifting is
* done in getChildMeasureSpec.
* 要求孩子自己测量自己(考虑父亲的要求和自己的边距)
*
* @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view(来自父亲的宽度要求)
* @param widthUsed Extra space that has been used up by the parent
* horizontally (possibly by other children of the parent)
* @param parentHeightMeasureSpec The height requirements for this view(来自父亲的高度要求)
* @param heightUsed Extra space that has been used up by the parent
* vertically (possibly by other children of the parent)
*/
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);
}
}
读到这里应该可以总结出ViewGroup
的测量过程: 遍历所有的孩子,通过调用View.measure()
触发孩子们测量自己。测量完所有孩子之后,按照自有的测量算法将孩子们的尺寸转换成自己的尺寸并传入View.setMeasuredDimension()
。
ViewGroup.getChildMeasureSpec()
触发孩子测量自己的时候传入了宽高两个参数,它们是通过ViewGroup.getChildMeasureSpec()
产生的:
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);
...
switch (specMode) {
case MeasureSpec.EXACTLY:
...
break;
case MeasureSpec.AT_MOST:
...
break;
case MeasureSpec.UNSPECIFIED:
...
break;
}
...
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
}
这个函数中有一个陌生的类MeasureSpec
,点进去看看:
/**
* A MeasureSpec encapsulates the layout requirements passed from parent to child.
* Each MeasureSpec represents a requirement for either the width or the height.
* A MeasureSpec is comprised of a size and a mode. There are three possible
* modes:
* MeasureSpec包装了父亲对孩子的布局要求,它是尺寸和模式的混合,它包含三种模式
*
* MeasureSpecs are implemented as ints to reduce object allocation.
* MeasureSpec被实现成一个int值为了节约空间
*/
public static class MeasureSpec {
//前2位是模式,后30位是尺寸
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
* 散养父亲:随便孩子多大
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* Measure specification mode: The parent has determined an exact size
* for the child. The child is going to be given those bounds regardless
* of how big it wants to be.
* 圈养父亲:强制指定孩子尺寸
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
* 折中父亲:在有限范围内允许孩子想多大就多大
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
...
}
MeasureSpec
用于在View
测量过程中描述尺寸,它是一个包含了布局模式和布局尺寸的int
值(32位),其中最高的2位代表布局模式,后30位代表布局尺寸。它包含三种布局模式分别是UNSPECIFIED
、EXACTLY
、AT_MOST
。
结合刚才的ViewGroup.getChildMeasureSpec()
来探究下这些模式到底是什么意思:
/**
* Does the hard part of measureChildren: figuring out the MeasureSpec to
* pass to a particular child. This method figures out the right MeasureSpec
* for one dimension (height or width) of one child view.
* 获得孩子布局参数(宽或高):混合父亲要求和孩子诉求
*
* The goal is to combine information from our MeasureSpec with the
* LayoutParams of the child to get the best possible results. For example,
* if the this view knows its size (because its MeasureSpec has a mode of
* EXACTLY), and the child has indicated in its LayoutParams that it wants
* to be the same size as the parent, the parent should ask the child to
* layout given an exact size.
*
* @param spec The requirements for this view 父亲要求:要求孩子多宽多高
* @param padding The padding of this view for the current dimension and
* margins, if applicable
* @param childDimension How big the child wants to be in the current
* dimension孩子诉求:想要多宽多高
* @return a MeasureSpec integer for the child
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//获得父亲测量模式
int specMode = MeasureSpec.getMode(spec);
//获得父亲尺寸
int specSize = MeasureSpec.getSize(spec);
//从父亲尺寸中去除padding
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
//结合父亲要求和孩子诉求计算出孩子尺寸,父亲有三种类型的要求,孩子有三种类型的诉求,孩子尺寸一共有9种结果。
switch (specMode) {
// Parent has imposed an exact size on us(父亲有明确尺寸)
case MeasureSpec.EXACTLY:
//如果孩子对自己尺寸有明确要求,只能满足它,不考虑padding
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
}
//如果孩子要求和父亲一样大且父亲有明确尺寸,则孩子尺寸有确定,考虑padding
else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
}
//如果孩子要求完全显示自己内容,但它不能超过父亲,考虑padding
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:
//如果孩子对自己尺寸有明确要求,只能满足它,不考虑padding
if (childDimension >= 0) {
// Child wants a specific size... so be it(父亲其实很无奈)
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
}
//如果孩子要求和父亲一样大,但父亲只有明确最大尺寸,则孩子也能有明确最大尺寸,考虑padding
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;
}
//如果孩子要求完全显示自己内容,但它不能超过父亲,考虑padding
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:
//如果孩子对自己尺寸有明确要求,只能满足它,不考虑padding
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);
}
这个函数揭示了一个“人间真相”:父亲总是对孩子有要求,但孩子也总是有自己的诉求。最圆满的结局莫过于充分考量两方面的需求并调和之。ViewGroup.getChildMeasureSpec()
将3种父亲的要求和3种孩子的诉求进行了调和(详见上述代码及注释)
总结
- 父控件在测量自己的时候会先遍历所有子控件,并触发子控件测量自己。完成孩子测量后,根据孩子的尺寸来确定自己的尺寸。
- 父控件会将自己的布局要求和子控件的布局诉求结合成一个
MeasureSpec
对象传递给子控件以指导子控件测量自己。 -
MeasureSpec
用于在View
测量过程中描述尺寸,它是一个包含了布局模式和布局尺寸的int
值(32位),其中最高的2位代表布局模式,后30位代表布局尺寸。 - 整个测量过程的终点是
View.setMeasuredDimension()
的调用,它表示着视图大小已经有确定值。