前言
掌握Android中View的工作机制有助于日常的UI开发工作,实现具有不同样式和交互的UI界面。如何在屏幕上呈现各种各样的视图元素 正是Android中的View工作机制解决的问题,主要包括:View的大小如何确定、View的位置如何确定以及View内容对应的渲染数据如何生成。
本文围绕前两个问题(View大小确定和View位置确定),对Android中View的工作机制进行分析,在知其然的同时,也要知其所以然,这样才能实现出自定义的UI效果。此外,View渲染数据的生成涉及到的内容较多,后面再单独分析。
View工作流程的触发时机
之前在《浅析Android中的Choreographer工作原理》中分析得出,每一次App进程在请求调度VSYNC信号之后,VSYNC信号会通过Socket发送到App进程,此时会触发View的工作流程,即测量、布局以及绘制。
一般来说,应用进程请求VSYNC信号分为两种情况,一种是启动一个Activity之后默认请求,一种是调用API更新View时主动请求。无论是哪种情况,最终在接收到VSYNC信号之后的处理流程是一样的。下面将结合源码对View的工作流程进行分析。
确定View的尺寸(onMeasure)
测量环节用于计算
View的宽和高,即View的成员变量mMeasuredWidth和mMeasuredHeight的值。测量环节分为两部分,一部分是在
View树上分发测量任务,一部分是对单个View进行具体测量工作。
测量流程
首先,看下测量流程开始的入口方法performTraversals,通过一系列的条件判断和处理最终得到了窗口的宽desiredWindowWidth和高desiredWindowHeight,并将其传入measureHierarchy方法。
java
// android.view.ViewRootImpl
private void performTraversals() {
// cache mView since it is used so much below...
final View host = mView;
if (host == null || !mAdded) {
return;
}
mIsInTraversal = true;
mWillDrawSoon = true;
boolean windowSizeMayChange = false;
WindowManager.LayoutParams lp = mWindowAttributes;
int desiredWindowWidth;
int desiredWindowHeight;
WindowManager.LayoutParams params = null;
CompatibilityInfo compatibilityInfo =
mDisplay.getDisplayAdjustments().getCompatibilityInfo();
if (compatibilityInfo.supportsScreen() == mLastInCompatMode) {
params = lp;
mFullRedrawNeeded = true;
mLayoutRequested = true;
// ...
}
Rect frame = mWinFrame;
if (mFirst) {
mFullRedrawNeeded = true;
mLayoutRequested = true;
final Configuration config = getConfiguration();
if (shouldUseDisplaySize(lp)) {
// NOTE -- system code, won't try to do compat mode.
Point size = new Point();
mDisplay.getRealSize(size);
desiredWindowWidth = size.x;
desiredWindowHeight = size.y;
} else if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
|| lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
// For wrap content, we have to remeasure later on anyways. Use size consistent with
// below so we get best use of the measure cache.
final Rect bounds = getWindowBoundsInsetSystemBars();
desiredWindowWidth = bounds.width();
desiredWindowHeight = bounds.height();
} else {
// After addToDisplay, the frame contains the frameHint from window manager, which
// for most windows is going to be the same size as the result of relayoutWindow.
// Using this here allows us to avoid remeasuring after relayoutWindow
desiredWindowWidth = frame.width();
desiredWindowHeight = frame.height();
}
// ...
host.dispatchAttachedToWindow(mAttachInfo, 0);
mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
dispatchApplyInsets(host);
} else {
desiredWindowWidth = frame.width();
desiredWindowHeight = frame.height();
if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
if (DEBUG_ORIENTATION) Log.v(mTag, "View " + host + " resized to: " + frame);
mFullRedrawNeeded = true;
mLayoutRequested = true;
windowSizeMayChange = true;
}
}
// ...
// Execute enqueued actions on every traversal in case a detached view enqueued an action
getRunQueue().executeActions(mAttachInfo.mHandler);
// ...
boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
if (layoutRequested) {
if (!mFirst) {
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
|| lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
windowSizeMayChange = true;
if (shouldUseDisplaySize(lp)) {
// NOTE -- system code, won't try to do compat mode.
Point size = new Point();
mDisplay.getRealSize(size);
desiredWindowWidth = size.x;
desiredWindowHeight = size.y;
} else {
final Rect bounds = getWindowBoundsInsetSystemBars();
desiredWindowWidth = bounds.width();
desiredWindowHeight = bounds.height();
}
}
}
// 开始计算DecorView以及其子View的宽高
windowSizeMayChange |= measureHierarchy(host, lp, mView.getContext().getResources(),
desiredWindowWidth, desiredWindowHeight);
}
}
而measureHierarchy方法中又通过getRootMeasureSpec方法构建了用于约束根View的测量规格childWidthMeasureSpec和childHeightMeasureSpec,最后调用performMeasure方法开始对根View进行实际的测量工作。当通过各种情况的处理之后得到了DecorView的可用宽高之后,通过调用measureHierarchy方法从DecorView开始分发测量工作。
到这里可以看到主要方法有getRootMeasureSpec和performMeasure,getRootMeasureSpec方法后面再分析,下面先对performMeasure方法进行分析,主要梳理出测量过程的大致流程是什么样的。
java
// android.view.ViewRootImpl
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
int childWidthMeasureSpec;
int childHeightMeasureSpec;
boolean windowSizeMayChange = false;
boolean goodMeasure = false;
// 宽屏适配逻辑
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
final DisplayMetrics packageMetrics = res.getDisplayMetrics();
res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
int baseSize = 0;
if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
baseSize = (int)mTmpValue.getDimension(packageMetrics);
}
if (baseSize != 0 && desiredWindowWidth > baseSize) {
// 结合窗口可用的宽高以及窗口的布局参数来计算测量规格,用于后续子View的测量
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width, lp.privateFlags);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height, lp.privateFlags);
// 开始具体的测量以及分发工作
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
goodMeasure = true;
} else {
// Didn't fit in that size... try expanding a bit.
baseSize = (baseSize+desiredWindowWidth)/2;
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width, lp.privateFlags);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
goodMeasure = true;
}
}
}
}
if (!goodMeasure) {
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width,
lp.privateFlags);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height,
lp.privateFlags);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
windowSizeMayChange = true;
}
}
return windowSizeMayChange;
}
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
// 调用DecorView#measure方法在View树上从根节点开始进行递归测量
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
可以看到performMeasure方法直接调用了View#measure方法来对DecorView的宽高进行测量,这里需要注意的是,View#measure是一个final方法,从方法的注释也可以看出一个View的实际测量工作是在onMeasure方法中实现的,而onMeasure方法是允许子类进行重写的,其实就是基于模版方法模式实现的。
java
/**
* 此方法用于确定View应该占用多大的区域,父View在宽高上对View进行了限制。
* 实际的测量工作交由onMeasure方法进行实现,子类可以对onMeasure进行重写来实现自定义的测量逻辑。
* @param widthMeasureSpec 父View对当前View施加的宽度约束
* @param heightMeasureSpec 父View对当前View施加的高度约束
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// Suppress sign extension for the low bytes
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
// Optimize layout by avoiding an extra EXACTLY pass when the view is
// already measured as the correct size. In API 23 and below, this
// extra pass is required to make LinearLayout re-distribute weight.
final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec;
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
final boolean needsLayout = specChanged && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
// 如果强制布局或者需要布局时,对View尝试进行测量
// 1. 是否强制布局取决于是否对View的标记位进行设置
// 2. 是否需要布局取决于测量规格是否发生变化以及是否为EXACTLY模式或者尺寸约束是否发生变化
if (forceLayout || needsLayout) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
// 如果强制布局或者缓存中没有之前的测量结果或者忽略测量缓存,则调用onMeasure进行测量;
// 否则,直接从缓存中取之前的测量结果,
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
// flag not set, setMeasuredDimension() was not invoked, we raise
// an exception to warn the developer
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("View with id " + getId() + ": "
+ getClass().getName() + "#onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
// 更新测量规格约束
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
// 更新测量缓存
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 | (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
onMeasure方法是protected修饰的,因此运行时的逻辑由子类是否重写决定。我们先看下DecorView中的onMeasure方法的实现,该方法是最终执行的方法。
java
// com.android.internal.policy.DecorView
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final Resources res = getContext().getResources();
final DisplayMetrics metrics = res.getDisplayMetrics();
final boolean isPortrait = getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT;
final int widthMode = getMode(widthMeasureSpec);
final int heightMode = getMode(heightMeasureSpec);
boolean fixedWidth = false;
mApplyFloatingHorizontalInsets = false;
// 处理窗口的布局宽度为wrap_content模式的情况,视情况重新生成widthMeasureSpec
if (widthMode == AT_MOST) {
final TypedValue tvw = isPortrait ? mWindow.mFixedWidthMinor : mWindow.mFixedWidthMajor;
if (tvw != null && tvw.type != TypedValue.TYPE_NULL) {
final int w;
if (tvw.type == TypedValue.TYPE_DIMENSION) {
w = (int) tvw.getDimension(metrics);
} else if (tvw.type == TypedValue.TYPE_FRACTION) {
w = (int) tvw.getFraction(metrics.widthPixels, metrics.widthPixels);
} else {
w = 0;
}
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if (w > 0) {
widthMeasureSpec = MeasureSpec.makeMeasureSpec(
Math.min(w, widthSize), EXACTLY);
fixedWidth = true;
} else {
widthMeasureSpec = MeasureSpec.makeMeasureSpec(
widthSize - mFloatingInsets.left - mFloatingInsets.right,
AT_MOST);
mApplyFloatingHorizontalInsets = true;
}
}
}
mApplyFloatingVerticalInsets = false;
// 处理窗口的布局高度为wrap_content模式的情况,视情况重新生成heightMeasureSpec
if (heightMode == AT_MOST) {
final TypedValue tvh = isPortrait ? mWindow.mFixedHeightMajor
: mWindow.mFixedHeightMinor;
if (tvh != null && tvh.type != TypedValue.TYPE_NULL) {
final int h;
if (tvh.type == TypedValue.TYPE_DIMENSION) {
h = (int) tvh.getDimension(metrics);
} else if (tvh.type == TypedValue.TYPE_FRACTION) {
h = (int) tvh.getFraction(metrics.heightPixels, metrics.heightPixels);
} else {
h = 0;
}
final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (h > 0) {
heightMeasureSpec = MeasureSpec.makeMeasureSpec(
Math.min(h, heightSize), EXACTLY);
} else if ((mWindow.getAttributes().flags & FLAG_LAYOUT_IN_SCREEN) == 0) {
heightMeasureSpec = MeasureSpec.makeMeasureSpec(
heightSize - mFloatingInsets.top - mFloatingInsets.bottom, AT_MOST);
mApplyFloatingVerticalInsets = true;
}
}
}
// 1.调用FrameLayout的onMeasure,遍历所有子View并对其进行测量(调用子View的measure方法)
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredWidth();
boolean measure = false;
widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, EXACTLY);
if (!fixedWidth && widthMode == AT_MOST) {
final TypedValue tv = isPortrait ? mWindow.mMinWidthMinor : mWindow.mMinWidthMajor;
final float availableWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
res.getConfiguration().screenWidthDp, metrics);
if (tv.type != TypedValue.TYPE_NULL) {
final int min;
if (tv.type == TypedValue.TYPE_DIMENSION) {
min = (int) tv.getDimension(metrics);
} else if (tv.type == TypedValue.TYPE_FRACTION) {
min = (int) tv.getFraction(availableWidth, availableWidth);
} else {
min = 0;
}
if (width < min) {
widthMeasureSpec = MeasureSpec.makeMeasureSpec(min, EXACTLY);
measure = true;
}
}
}
// TODO: Support height?
if (measure) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
可以看到DecorView#onMeasure中调用了FrameLayout#onMeasure来遍历DecorView下所有子View并对其进行测量(调用子View的measure方法),这样也就开启了新一轮的测量流程。当所有子View完成测量之后,将会计算DecorView自身的宽高。
java
// android.widget.FrameLayout
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren.clear();
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
// 测量子View,每个子View的可用宽高是一样的,因为父布局是FrameLayout
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 更新子View中宽度(包括自身的margin)的最大值
maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
// 更新子View中高度(包括自身的margin)的最大值
maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
// 如果开启对布局宽或高为match_parent模式的测量时,需要在完成所有子View测量之后,对这些match_parent的子View再次遍历测量
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT || lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
// Account for padding too
maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
// Check against our minimum height and width
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
// Check against our foreground's minimum height and width
final Drawable drawable = getForeground();
if (drawable != null) {
maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
}
// 确定DecorView的测量宽高
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));
count = mMatchParentChildren.size();
if (count > 1) {
for (int i = 0; i < count; i++) {
final View child = mMatchParentChildren.get(i);
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec;
if (lp.width == LayoutParams.MATCH_PARENT) {
final int width = Math.max(0, getMeasuredWidth() - getPaddingLeftWithForeground() - getPaddingRightWithForeground() - lp.leftMargin - lp.rightMargin);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
} else {
childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, getPaddingLeftWithForeground() + getPaddingRightWithForeground() + lp.leftMargin + lp.rightMargin, lp.width);
}
final int childHeightMeasureSpec;
if (lp.height == LayoutParams.MATCH_PARENT) {
final int height = Math.max(0, getMeasuredHeight() - getPaddingTopWithForeground() - getPaddingBottomWithForeground() - lp.topMargin - lp.bottomMargin);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
} else {
childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTopWithForeground() + getPaddingBottomWithForeground() + lp.topMargin + lp.bottomMargin, lp.height);
}
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
// android.view.ViewGroup
/**
* 结合父容器的测量约束以及自身的padding和margin来测量child,必须在getChildMeasureSpec中处理布局参数为权重的情况
*
* @param child 待测量的子View
* @param parentWidthMeasureSpec 父容器施加的宽度约束
* @param widthUsed 父容器已经使用的宽度
* @param parentHeightMeasureSpec 父容器施加的高度约束
* @param heightUsed 父容器已经使用的高度
*/
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// 减去了父容器的padding以及子View的margin
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);
}
测量流程的整体时序图如下:

测量流程的整体流程图如下:

总结一下,View的测量过程其实就是从DecorView开始调用其measure方法,如果当前View包含子View,则在onMeasure方法中遍历所有的子View,对每一个子View调用measure方法进行测量。以此往复,一层一层遍历下去,直至所有View都完成测量。对于包含子View的容器来说,其自身的宽高在所有子View完成遍历测量之后进行确定。
其实View的测量流程与View的触摸事件分发流程有相似之处,都是采用类似深度遍历的算法进行逻辑的分发,毕竟作用的目标都是同一个View树。
测量计算
MeasureSpec(测量规格)
上面通过源码分析了测量分发的流程,其中每一次遍历父容器中的子View进行测量时,都会有MeasureSpec出现,源码注释说明了MeasureSpec是父容器对子View施加的测量约束。因此在分析具体的测量计算逻辑之前,需要了解MeasureSpec的组成以及创建过程。
MeasureSpec本身是一个工具类,通过MeasureSpec可以对测量约束进行装包和拆包,装包就是将测量规格中的尺寸和模式封装成一个int值,拆包就是将测量规格的尺寸和模式从int值中提取出来。
java
/**
* 一个MeasureSpec封装了父容器传递下来的布局约束,每一个MeasureSpec代表了一个宽度或者高度的约束.
* 一个MeasureSpec由size和mode组成. mode取值如下:
* 1. UNSPECIFIED:父容器没有对子View施加任何限制,子View自由决定自己的尺寸。
* 2. EXACTLY:父容器确定了子View的具体尺寸,无论子View想要占用多大的空间,最终都会受到父容器的限制。
* 3. AT_MOST:子View决定自己的尺寸,但是不能超过父容器给定的尺寸限制。
*/
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << 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) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
上面源码分析的过程中涉及的MeasureSpec相关的方法主要有两个:ViewRootImpl#getRootMeasureSpec和ViewGroup#getChildMeasureSpec。前者用于计算施加于DecorView下的直接子View的测量约束,后者则用于计算非DecorView下的直接子View的测量约束。两者的逻辑存在部分差异,下面根据源码看下测量约束生成的具体逻辑。
java
/**
* 基于窗口的布局参数为窗口中的DecorView计算其测量规格。
*
* @param windowSize 窗口可用的宽/高.
* @param measurement 窗口的布局参数中的宽/高.
* @param privateFlags 窗口的布局参数中的标记位.
* @return 用于测量根View(DecorView)的测量规格.
*/
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;
}
DecorView的测量约束的生成由窗口的尺寸和窗口的LayoutParams共同决定:
- 布局参数为
MATCH_PARENT:测量约束的size为窗口大小,测量规格的mode为EXACTLY; - 布局参数为
WRAP_CONTENT:测量约束的size为窗口大小,测量规格的mode为AT_MOST; - 布局参数为固定的数值:测量约束的
size为布局参数对应的具体值,测量规格的mode为EXACTLY;
java
/**
* 负责处理measureChildren的复杂部分:计算出测量规格并传递给具体的子View。这个方法为测量具体子View的宽/高计算出正确的测量规格。
*
* 结合父容器施加的测量约束以及子View的布局参数来获取最合适的测量约束。例如,如果子View明确自己的宽/高或者声明了MATCH_PARENT来要求占满父容器的剩余空间,则父容器应该要求子View的尺寸为一个具体值。
*
* @param spec 父容器对当前View的约束
* @param padding 已用的大小,其包括了父容器的padding以及子View的margin
* @param childDimension 子View的布局参数
* @return View的测量规格
*/
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) {
// 父容器约束子View的尺寸为具体值
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 子View要求占满父容器剩余的可用空间
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 子View希望自己确定大小,但是大小不能超过父容器剩余的可用空间
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父容器约束子View的尺寸不能超过父容器的剩余可用空间
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 子View想要占满父容器的剩余可用空间,但是父容器自己的尺寸也还没有确定下来.
// 只能约束子View不能超过父容器的剩余可用空间.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 子View希望自己确定大小,但是大小不能超过父容器剩余的可用空间
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父容器对子View的尺寸没有任何约束
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
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(View或ViewGroup)的测量规格的计算规则如下:
- 如果子
View的布局参数为具体值,则无论父View施加的测量规格是什么,子View的测量规格的size都是子View布局参数中指定的值,测量规格的mode都是EXACTLY; - 如果子
View的布局参数为MATCH_PARENT,则子View的测量规格的size都是父View测量规格约束下的剩余可用空间,测量规格的mode与父View的测量规格的mode保持一致; - 如果子
View的布局参数为WRAP_CONTENT,则子View的测量规格的size都是父View测量规格约束下的剩余可用空间,测量规格的mode都是AT_MOST;
总结一下:
- 子
View如果指定自身布局参数的值为具体数值,则父View施加的测量规格对子View的测量规格没有任何影响; - 子
View如果指定自身布局参数为WRAP_CONTENT,则子View的测量规格的mode为AT_MOST,子View的测量规格的size为父View的剩余可用空间; - 子
View如果指定自身布局参数为MATCH_PARENT,则子View的测量规格的mode与父View的测量规格的mode保持一致(毕竟是match_parent了嘛),子View的测量规格的size为父View的剩余可用空间(毕竟是match_parent了嘛);
需要注意的是,
DecorView的测量规格的生成过程只和窗口的尺寸和布局参数有关,而子View的测量规格的生成过程则和父View的测量规格以及子View自身的布局参数有关;
最后根据测量规格对View的测量宽高进行确定,View的测量宽高取决于测量规格以及最小值。
java
/**
* 对view及其内容进行测量并确定测量后的宽高,这个方法是在measure(int, int)方法中调用的,子类应该重写这个方法来提供准确高效的测量实现。
*
* 当重写此方法时必须调用setMeasuredDimension(int, int)来存储此View测量之后的宽高,如果没有这么做会导致measure(int, int)方法抛出IllegalStateException异常。
*
* 基类的实现默认是:当父View传递下来的MeasureSpec没有对View的宽高进行约束时将背景的宽高或者mMinWidth/mMinHeight作为默认的宽高,否则将父View传递下来的MeasureSpec的大小作为默认的宽高。
*
* 如果重写此方法,必须保证测量的宽高不能比view的最小宽高小,即必须大于等于getSuggestedMinimumWidth()和getSuggestedMinimumHeight()方法的返回值。
*
* @param widthMeasureSpec horizontal space requirements as imposed by the parent.
* The requirements are encoded with
* {@link android.view.View.MeasureSpec}.
* @param heightMeasureSpec vertical space requirements as imposed by the parent.
* The requirements are encoded with
* {@link android.view.View.MeasureSpec}.
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
/**
* 返回一个默认大小。如果MeasureSpec没有施加约束的话,则直接返回参数size,否则,返回MeasureSpec中的size。
*
* @param size Default size for this view
* @param measureSpec Constraints imposed by the parent
* @return The size this view should be.
*/
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: // wrap_content模式的处理和match_parent以及具体值的处理是一样的。
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
/**
* 返回view应该设置的最小高度,最小高度是背景和最小高度参数两者中的最大值。
*/
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
/**
* 返回view应该设置的最小宽度,最小宽度是背景和最小宽度度参数两者中的最大值。
*/
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
上面可以看到getDefaultSize方法中,对于MeasureSpec.AT_MOST的处理和MeasureSpec.EXACTLY的处理是一样的,因此,对于wrap_content来说,需要特殊处理否则将会等同于match_parent。
自定义View需要解决的问题
从测量流程的分析中,总结出自定义View需要注意的事项:
-
自定义
View的wrap_content从上面的源码分析中可以得知,
onMeasure的默认实现是将wrap_content等同于match_parent进行处理,因此自定义View时需要注意对这种情况的处理。解决办法是通过对
onMeasure方法进行重写,实现对wrap_content模式的特殊处理,即判断布局参数是否为wrap_content,是的话则调用setMeasuredDimension方法并传入具体数值。 -
自定义
View的padding参数通过上面源码的分析可以知道,容器类型的
View比如FrameLayout、LinearLayout都会通过ViewGroup的measureChildWithMargins方法将自身的padding和子View的margin所占用的空间作为已经使用的空间,因此传递给子View的可用空间可以完全用于子View自身内容。如果自定义
View不是容器类型的View,则只需要在绘制时候考虑自身的padding即可。如果自定义
View是容器类型的View,则需要在onMeasure方法中处理自身的padding和子View的margin。
确定View的位置(onLayout)
布局流程用于确定
View在其父容器中的位置,即View的成员变量mLeft、mTop、mRight以及mBottom的值。完成布局之后,View最终的宽和高也就随之确定了,即View的成员变量mWidth、mHeight。
从上面的源码中可以看到,performTraversals方法中在完成测量过程之后会调用performLayout方法开始进行布局,通过布局来对这个界面中所有的View的位置进行确定。performLayout方法中直接调用了DecorView#layout开启了布局过程。由于DecorView及其父类FrameLayout都没有重写layout方法,因此调用的是View#layout方法。
java
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
// ...
try {
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
// ...
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
mInLayout = false;
}
在View#layout中调用setFrame方法将View的四个坐标确定之后,接着调用onLayout来对内部的子View进行布局的分发。一般来说,参数r为参数l和View的测量宽度的和,参数b等于参数t加上View的测量高度。
java
/**
* Assign a size and position to a view and all of its
* descendants
*
* <p>This is the second phase of the layout mechanism.
* (The first is measuring). In this phase, each parent calls
* layout on all of its children to position them.
* This is typically done using the child measurements
* that were stored in the measure pass().</p>
*
* <p>Derived classes should not override this method.
* Derived classes with children should override
* onLayout. In that method, they should
* call layout on each of their children.</p>
*
* @param l Left position, relative to parent
* @param t Top position, relative to parent
* @param r Right position, relative to parent
* @param b Bottom position, relative to parent
*/
@SuppressWarnings({"unchecked"})
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); // 确定坐标
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
// ...
}
// ...
}
/**
* Assign a size and position to this view.
*
* This is called from layout.
*
* @param left Left position, relative to parent
* @param top Top position, relative to parent
* @param right Right position, relative to parent
* @param bottom Bottom position, relative to parent
* @return true if the new size and position are different than the
* previous ones
* {@hide}
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
// Remember our drawn bit
int drawn = mPrivateFlags & PFLAG_DRAWN;
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
// Invalidate our old position
invalidate(sizeChanged);
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
mPrivateFlags |= PFLAG_HAS_BOUNDS;
// ...
}
return changed;
}
而View#onLayout是空实现,从注释可以看出这个方法是给自定义View进行重写的,注释说明了这个方法中需要对根据需要对各个子View进行布局处理。
java
/**
* Called from layout when this view should
* assign a size and position to each of its children.
*
* Derived classes with children should override
* this method and call layout on each of
* their children.
* @param changed This is a new size or position for this view
* @param left Left position, relative to parent
* @param top Top position, relative to parent
* @param right Right position, relative to parent
* @param bottom Bottom position, relative to parent
*/
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
可以看下FrameLayout中的onLayout的实现,并根据布局的特性进行了特殊处理,即结合布局的特点计算每个子View的实际位置。
java
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
final int count = getChildCount();
final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft;
int childTop;
int gravity = lp.gravity;
if (gravity == -1) {
gravity = DEFAULT_CHILD_GRAVITY;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}
case Gravity.LEFT:
default:
childLeft = parentLeft + lp.leftMargin;
}
switch (verticalGravity) {
case Gravity.TOP:
childTop = parentTop + lp.topMargin;
break;
case Gravity.CENTER_VERTICAL:
childTop = parentTop + (parentBottom - parentTop - height) / 2 +
lp.topMargin - lp.bottomMargin;
break;
case Gravity.BOTTOM:
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
childTop = parentTop + lp.topMargin;
}
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
总结来看,布局流程相比测量流程要简单很多,对于容器类型的自定义View需要根据布局的特点计算各个子View的位置,并将布局分发给各个子View即可。

结语
到这里,我们从源码角度对每一次VSYNC信号到达之后的测量布局流程进行了梳理和理解,总结起来就是,测量是用于确定View的宽高,布局是为了确定View在屏幕上展示的位置,而测量和布局都需要在View树进行分发处理,从而将所有View最终在屏幕上展示的位置以及大小确定下来。但是,仅仅完成测量和布局,用户还是不能看到视图元素,还需要对View的绘制流程进行处理,最终将生成的视图数据交由屏幕进行刷新之后才能被用户看见。
对于View的绘制,Android不同版本的实现也不同,分为软件绘制和硬件绘制,后面将会结合源码对View的绘制进行梳理分析。