前言
掌握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
的绘制进行梳理分析。