前言
View的测量过程是View绘制三大步骤(测量、布局、绘制)中的第一步。
整个View树的测量涉及的流程很多,我们先看一些必要的前置知识:
首先来思考一下测量过程,测量的目的是确定View的尺寸,而Android中View是以树状结构组成的,那么:
- 每一个View(除去根节点以外)都存在于某一个ViewGroup中,子View的尺寸受到父View的限制,这就涉及到父View对子View的尺寸要求;
- 每一个ViewGroup又可能包含多个子View,所以ViewGroup不仅要考虑其父View对自己的尺寸要求,还要考虑到自己对多个子View的尺寸;
由此可见,测量过程中View之间不是孤立的,父View对子View存在着尺寸要求,那么代码中如何表述这种“要求”呢?答案是仅需要一个普通的int。具体的操作来看MeasureSpec类。
一、MeasureSpec:父View对子View的布局要求
MeasureSpec是View的一个静态内部类,用途是:将 父View对子View的布局要求 封装成一个int(目的是减少频繁的创建对象引起的性能问题)
MeasureSpec 将两部分信息填充到int中,一是模式,二是尺寸
一个int通常占用32位,MeasureSpec将其中的2位用于存储模式,剩余30位用于存储尺寸。
frameworks/base/core/java/android/view/View.java
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
模式一共有3种,分别是:UNSPECIFIED
、EXACTLY
、AT_MOST
三种模式从名字也能看出,UNSPECIFIED
是尺寸未指定,EXACTLY
则是代表尺寸有具体的要求,AT_MOST
是规定了能够达到的最大尺寸。
此外MeasureSpec提供了静态方法makeMeasureSpec,用于将模式、尺寸组装成int,以及从int中解析模式和尺寸的getMode、getSize静态方法。
了解了父容器传递给子容器的测量要求MeasureSpec,接下来看一下具体代码中,ViewGroup怎样对子View进行测量
二、measureChildWithMargins:ViewGroup对子View的测量
ViewGroup中有measureChildWithMargins
方法对子View之一进行测量,根据注释,核心的工作是在getChildMeasureSpec
方法中完成的。
参数中:
child
是要测量的子ViewparentWidthMeasureSpec
、parentHeightMeasureSpec
是本ViewGroup的父容器传入的 对本ViewGroup的尺寸要求,可以认为是child
的“祖父View”对父View(也就是调用该方法的ViewGroup)的尺寸要求widthUsed
、heightUsed
是已经被父容器使用的额外空间(可能被父级的其他子View使用)
frameworks/base/core/java/android/view/ViewGroup.java
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
/* 从子View的xml中获取参数 */
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
/* 获取对子View的宽、高要求 */
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);
/* 要求子View测量 */
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
整个方法体很简单,核心工作都是在getChildMeasureSpec
方法中,获取对子View的宽、高要求后继续传入子View,调用子View的measure
方法测量。
调用getChildMeasureSpec
方法时:
- 第一个参数
spec
:“祖父View”对该ViewGroup(也就是this
)的尺寸要求MeasureSpec; - 第二个参数
padding
:将水平和垂直方向上的 本ViewGroup设置的内部填充(mPadding*
)、子View设置的左右边距(lp.*Margin
)、其他子View已用掉的尺寸(*Used
) 累加起来传入;我们可以推测,除去padding
,剩余的尺寸才是子View能够使用的尺寸; - 第三个参数
childDimension
:从子View的xml中获取的宽高参数 lp.width 和 lp.height;
frameworks/base/core/java/android/view/ViewGroup.java
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
// 解析“祖父View”对该ViewGroup的尺寸限制
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
// 这里验证了我们的猜测,用“祖父View”对该ViewGroup的尺寸限制,减去本ViewGroup设置的padding,
// 减去子View设置的margin,减去其他子View已经使用的尺寸,就是子View能够使用的最大尺寸 size
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 them 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);
}
getChildMeasureSpec方法中的代码看似很多,实际上结构很简单,首先根据“祖父View”对该ViewGroup的尺寸限制和参数padding,获得子View最大允许使用的尺寸size
然后根据“祖父View”对该ViewGroup的尺寸限制模式(EXACTLY、AT_MOST、UNSPECIFIED),以及子View在xml中约束的3种情况(具体尺寸、MATCH_PARENT、WRAP_CONTENT),组合为9种不同的情况分别确定好 尺寸和模式,再组合成MeasureSpec即可。
- “祖父View”对该ViewGroup的尺寸要求为EXACTLY模式,代表该ViewGroup尺寸已确定
- 如子View的xml中写明了具体尺寸,则无视该ViewGroup的尺寸要求
- 如子View的xml中规定了MATCH_PARENT,则子View需要占用剩余所有空间,那么在该ViewGroup尺寸明确的情况下,子View自然也有明确的尺寸,所以子View的模式也设置为EXACTLY
- 如子View的xml中规定了WRAP_CONTENT,那么在该ViewGroup尺寸明确、子View尺寸未知的情况下,将子View的模式设置为AT_MOST,即限制最大尺寸。
- “祖父View”对该ViewGroup的尺寸要求为AT_MOST,代表该ViewGroup尺寸不确定,但存在确定的尺寸限制
- 如子View的xml中写明了具体尺寸,依然是无视该ViewGroup的要求
- 如子View的xml中规定了MATCH_PARENT或者WRAP_CONTENT,那么在该ViewGroup尺寸暂不确定的情况下,子View的尺寸也无法确定,只能跟随父View的限制设置AT_MOST
- “祖父View”对该ViewGroup的尺寸要求为UNSPECIFIED,没有明确尺寸和限制
- 如子View的xml中写明了具体尺寸,依然是无视该ViewGroup的要求
- 如子View的xml中规定了MATCH_PARENT或者WRAP_CONTENT,那么子View也跟随该ViewGroup的没有明确的尺寸限制
利用MeasureSpec.makeMeasureSpec
方法组合好该ViewGroup对子View的尺寸要求后,再回到measureChildWithMargins
方法,就可以调用child.measure(childWidthMeasureSpec, childHeightMeasureSpec)
继续对子View的测量。
三、measure、onMeasure:View对自身的测量
在ViewGroup的measureChildWithMargins
方法中,获取对子View的宽高要求后,会对子View调用measure(int, int)
方法
frameworks/base/core/java/android/view/View.java
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// ......
onMeasure(widthMeasureSpec, heightMeasureSpec);
// flag未设置意味着没有调用 setMeasuredDimension
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException(/*.......*/);
}
// ......
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
measure
方法是一个final方法,不允许重写,其核心是调用onMeasure
方法,应由View的子类重写onMeasure
方法来完成测量过程
setMeasuredDimension
方法的注释中有说明:onMeasure
方法中必须使用setMeasuredDimension
保存测量结果,不然measure
方法中会抛出IllegalStateException
异常
先不管具体的测量结果怎样得出的,setMeasuredDimension
方法是怎样把测量结果保存的呢?
frameworks/base/core/java/android/view/View.java
int mMeasuredWidth;
int mMeasuredHeight;
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
// ......Optical Bounds 模式下的小修正
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
可以看到最终调用setMeasuredDimensionRaw
将计算出的宽高数据存放到 mMeasuredWidth
和 mMeasuredHeight
两个成员变量中,这就是View测量步骤的目的了。
小节
至此,已经了解了 ViewGroup如何要求子View测量、View的测量 这些前置的知识,接下来看View树的测量流程。
四、帧绘制的时机
位于前台的Activity会拥有一个ViewRootImpl实例,ViewRootImpl构造时会拥有一个Choreographer实例,在允许绘制时,ViewRootImpl会调用scheduleTraversals()
方法安排一次“Traversal”(遍历)。
frameworks/base/core/java/android/view/ViewRootImpl.java
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
经过Choreographer的安排,下一帧绘制时执行了doTraversal()
方法,其中移除了同步屏障,修改了mTraversalScheduled
的状态,然后执行performTraversals()
方法
frameworks/base/core/java/android/view/ViewRootImpl.java
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
performTraversals();
}
}
performTraversals()
方法中进行了一帧绘制的全部工作,包含 动画、测量、布局、绘制 等,代码量很大,我们这里可以暂时认为其关键逻辑是:
frameworks/base/core/java/android/view/ViewRootImpl.java
private void performTraversals() {
final View host = mView;
WindowManager.LayoutParams lp = mWindowAttributes;
// ......
// desiredWindowWidth = XXX;
// desiredWindowHeight = XXX;
// 测量
windowSizeMayChange |= measureHierarchy(host, lp, mView.getContext().getResources(),
desiredWindowWidth, desiredWindowHeight);
// ......
// 布局
performLayout(lp, mWidth, mHeight);
// ......
// 绘制
performDraw();
}
其中measureHierarchy
方法是对整个View树的测量,最终是调用到mView.measure
,通常情况下ViewRootImpl的mView就是在setView时赋值的 DecorView。
五、DecorView的测量
DecorView就是View树的根,getRootMeasureSpec
方法获取的是用于DecorView测量的“根MeasureSpec”,通常情况下,getRootMeasureSpec
获取到的 根MeasureSpec 是 EXACTLY模式,宽高与屏幕的分辨率一致。
frameworks/base/core/java/android/view/ViewRootImpl.java
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
int childWidthMeasureSpec;
int childHeightMeasureSpec;
// ......
childWidthMeasureSpec = getRootMeasureSpec(/*.......*/);
childHeightMeasureSpec = getRootMeasureSpec(/*.......*/);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
// ......
return windowSizeMayChange;
}
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
DecorView也是View的子类,因此其measure方法就是View的measure
方法,根据上述对View测量的介绍,其核心逻辑也是在重写的onMeasure
方法里
frameworks/base/core/java/com/android/internal/policy/DecorView.java
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
/*
* 处理根MeasureSpec是AT_MOST的情况
*/
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
其实DecorView的onMeasure
方法并没有这么简洁,但多数逻辑用于处理 根MeasureSpec是AT_MOST的情况,在大多数情况下根MeasureSpec还是 EXACTLY模式 ,所以这里核心是调用super.onMeasure
方法。
由于DecorView继承自FrameLayout,所以我们看帧布局的onMeasure
方法
FrameLayout的onMeasure
方法也有一些 非EXACTLY情况 的处理,但其核心思想很简单,遍历其中所有的子View,并调用上面介绍过的measureChildWithMargins
方法对子View逐个进行测量。
frameworks/base/core/java/android/widget/FrameLayout.java
@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);
// ......
}
}
setMeasuredDimension(/*...*/);
// ......
}
可以看到FrameLayout在对子View调用measureChildWithMargins
方法时,已使用的尺寸widthUsed
和heightUsed
传入的都是0,这也是FrameLayout的特性决定的,子View之间互不影响,可以重叠。
DecorView在对子View的遍历中,通过measureChildWithMargins
方法令子View也调用measure
、onMeasure
方法,这样从上到下的遍历完成后,整个View树的测量也就完成了。
不同的ViewGroup和View都有各自重写的onMeasure
方法,具体的测量细节都在里面,可以深入源码去了解。
需要注意:
View测量后的尺寸可以通过getMeasuredHeight()
、getMeasuredWidth()
获取,但这不代表View的真正尺寸,在布局后可能会发生改变。
如果你看到了这里,觉得文章写得不错就给个赞呗?
更多Android进阶指南 可以扫码 解锁更多Android进阶资料
敲代码不易,关注一下吧。ღ( ´・ᴗ・` )