前言
通过图像渲染的准备的学习, 我们知道在 onResume 之后我们的 ViewRootImpl 会通过 addToDisplay 与 WMS 建立联系, 之后就会执行 requestLayout 发送的消息执行 View 的遍历操作
这里我们就 requestLayout 入手分析 View 的变量
一. requestLayout
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
// 用于区分是否需要进行 View 的布局
boolean mLayoutRequested;
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
// 1. 检查请求 requestLayout 的线程
checkThread();
// 2. 标记为请求遍历的状态
mLayoutRequested = true;
// 3. 调用了 scheduleTraversals
scheduleTraversals();
}
}
final Thread mThread;
public ViewRootImpl(Context context, Display display) {
......
mThread = Thread.currentThread();
......
}
// 检查当前的线程是否为 ViewRootImpl 创建的线程
void checkThread() {
// 1. 若非 ViewRootImpl 创建的线程会扔出异常
// 因此 View 只能在主线程更新 UI 是伪命题, 它只能在其 ViewRootImpl 创建的线程更新 UI
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
}
好的, 可以看到 ViewRootImpl 的 requestLayout 方法中做了如下几件事情
- 检查请求 requestLayout 的线程是否为 ViewRootImpl 创建的线程, 若非 ViewRootImpl 创建的线程会扔出异常
- 因此 View 只能在主线程更新 UI 是伪命题, 它只能在其 ViewRootImpl 创建的线程更新 UI
- 将 mLayoutRequested flag 标记为 true
- 这个标记开启说名 View 是否需要重新布局
- 调用了 ViewRootImpl.scheduleTraversals
从 requestLayout 中, 我们明确了为什么 View 只能在主线程更新 UI 是个伪命题, 结下来继续分析 scheduleTraversals, 看看它为 View 的遍历做了哪些计划
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
public boolean mTraversalScheduled;
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void scheduleTraversals() {
if (!mTraversalScheduled) {
// 表示当前正在计划 View 的遍历
mTraversalScheduled = true;
// 插入同步屏障
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// post 到 Looper 的 MessageQueue 中
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
......
}
}
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
if (mTraversalScheduled) {
// 走到这里, 说明遍历的计划阶段已经完成, 即将进入执行遍历的阶段
mTraversalScheduled = false;
// 移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
......
// 执行 View 的遍历
performTraversals();
......
}
}
}
从这里我们可以看到, 我们的 Traversals 操作是添加到 Choreographer 的 CallbackQueue 中统一调度的, 当 VSYNC 到来时会自动执行
最有趣的是, 它为了保证 traversal 操作的优先执行, 插入了一个同步屏障
接下来我们看看 ViewRootImpl 是如何遍历 View 的
ViewRootImpl.performTraversals
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
@NonNull Display mDisplay; // 描述屏幕的尺寸
boolean mFirst; // 描述是否是第一次进行 performTraversals
final Rect mWinFrame; // frame given by window manager.
public ViewRootImpl(Context context, Display display) {
mDisplay = display;
......
mWinFrame = new Rect();
......
mFirst = true;
......
}
private void performTraversals() {
// 获取 DecorView
// 使用局部变量保存, 防止变更
final View host = mView;
WindowManager.LayoutParams lp = mWindowAttributes; // 获取这个 DecorView 的布局参数
boolean windowSizeMayChange = false; // 用于判断窗体大小是否有可能发生改变
// 1. View 的测量
int desiredWindowWidth;
int desiredWindowHeight;
Rect frame = mWinFrame;
// 若为第一次 Traversals
if (mFirst) {
......
mLayoutRequested = true;
final Configuration config = mContext.getResources().getConfiguration();
// 使用屏幕物理宽高作为窗体期望尺寸
if (shouldUseDisplaySize(lp)) {
Point size = new Point();
mDisplay.getRealSize(size);
desiredWindowWidth = size.x;
desiredWindowHeight = size.y;
} else {
// 使用 mWindFrame 中的宽高作为窗体期望尺寸
desiredWindowWidth = mWinFrame.width();
desiredWindowHeight = mWinFrame.height();
......
}
......
} else {
// 若非第一次进行 Traversals, 那么窗体期望尺寸直接使用 frame(mWinFrame) 中记录的宽高
desiredWindowWidth = frame.width();
desiredWindowHeight = frame.height();
......
}
......
boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
if (layoutRequested) {
final Resources res = mView.getContext().getResources();
......
if (mFirst) {
......// 第一次的尺寸已经在上面设置过了, 这里不会进行窗体期望尺寸的设置操作
} else {
......
// 若 LayoutParams 为 WRAP_CONTENT, 那么会再次进行窗体期望尺寸的判断
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
|| lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
windowSizeMayChange = true;
// 使用屏幕物理宽高作为窗体期望尺寸
if (shouldUseDisplaySize(lp)) {
Point size = new Point();
mDisplay.getRealSize(size);
desiredWindowWidth = size.x;
desiredWindowHeight = size.y;
} else {
// 使用屏幕可用宽高作为窗体期望尺寸
Configuration config = res.getConfiguration();
desiredWindowWidth = dipToPx(config.screenWidthDp);
desiredWindowHeight = dipToPx(config.screenHeightDp);
}
}
}
// 1.1 调用 measureHierarchy 方法, 为了确定其布局位置, 先确定它的大小
windowSizeMayChange |= measureHierarchy(host, lp, res,
desiredWindowWidth, desiredWindowHeight);
}
......
// 1.2 若测量尺寸变更, 则重置 Surface
......
if (mFirst || windowShouldResize || insetsChanged ||
viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
......
boolean hadSurface = mSurface.isValid();
try {
......
relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
......
}
}
......
if (didLayout) {
// 2. View 的布局
performLayout(lp, mWidth, mHeight);
......
}
// 3. View 的绘制
if (!cancelDraw && !newSurface) {
......
performDraw();
}
}
// LayoutParams 若为如下三种, 则直接使用屏幕的大小
private static boolean shouldUseDisplaySize(final WindowManager.LayoutParams lp) {
return lp.type == TYPE_STATUS_BAR_PANEL
|| lp.type == TYPE_INPUT_METHOD
|| lp.type == TYPE_VOLUME_OVERLAY;
}
}
好的, 可以看到 performTraversals 中便是 View 绘制的流程了, 中间简化了很多代码, 主要有以下几步
- View 的测量
- 调用 measureHierarchy 进行 View 测量
- 测量之后窗体变更则调用 relayoutWindow 重置 Surface
- View 的布局
- 调用 performLayout 进行 View 的摆放
- View 的绘制
- 调用 performDraw 进行 View 的绘制
由于篇幅限制, 这里我们主要学习 View 的测量和布局, 关于 Surface 的重置和 View 的绘制流程都较为复杂, 我们放到后面进行分析
二. View 的测量
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
int childWidthMeasureSpec; // 描述 Window 的宽度测量说明书
int childHeightMeasureSpec; // 描述 Window 的高度测量说明书
boolean windowSizeMayChange = false; // 描述 Window 尺寸是否变化
boolean goodMeasure = false; // 判断是否是好的测量
// 1. 对 Window 布局参数为 WRAP_CONTENT 的情况进行特殊处理
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
// 则分配一个默认的宽度
final DisplayMetrics packageMetrics = res.getDisplayMetrics();
// 1.1 从系统文件 config_prefDialogWidth 中读取这个预制的宽度
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);
}
// 1.2 获取系统提供的宽度成功了, 但期望的窗体宽度比这个大, 则优先使用 baseSize
// 若期望的比它小, 则直接使用期望的宽高即可
if (baseSize != 0 && desiredWindowWidth > baseSize) {
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width); // 构建宽度测量说明书
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); // 构建高度测量说明书
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); // 进行子 View 的测量操作
// 1.4 若 DecorView 的状态值中发现大小合适, 说明是好的尺寸
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
goodMeasure = true;
} else {
// 走到这里, 说明上面的测量不太合适, 通过下面的算法, 再次构建 baseSize
baseSize = (baseSize+desiredWindowWidth)/2;
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);// 构建测量说明书
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); // 执行测量操作
// 再次判断是否是好的尺寸
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
goodMeasure = true;
}
}
}
}
// 2. 若非好的测量尺寸
if (!goodMeasure) {
// 2.1 通过 Window 期望的宽高和布局参数构建测量说明书
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
// 2.2 执行测量操作
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
windowSizeMayChange = true;
}
}
return windowSizeMayChange;
}
}
measureHierarchy 中主要有两步
- 首先是构建根 View 的测量说明书
- 然后调用 performMeasure 进行测量分发
其说明书构建过程如下
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// 若为 MATCH_PARENT, 说明书的类型为 MeasureSpec.EXACTLY
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// 若为 WRAP_CONTENT, 说明书的类型为 MeasureSpec.AT_MOST
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// 若为精确值, 说明书的类型也为 MeasureSpec.EXACTLY
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
}
- 若为 MATCH_PARENT
- 测量说明书类型为 MeasureSpec.EXACTLY, 值为窗体的大小
- 若为 WRAP_CONTENT
- 测量说明书类型为 MeasureSpec.AT_MOST, 值为窗体的大小
- 若设置了精确值
- 测量说明书类型为 MeasureSpec.EXACTLY, 值为精确值大小
当窗体的测量说明书已经构建好了, 便调用 performMeasure 真正进行 View 树的测量操作了
二) View 树的测量分发
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
......
try {
// 对 DecorView 进行测量, 它虽然是 ViewGroup, 但并没有重写 measure 方法, 因此我们直接去 View 中查看
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
......
}
}
}
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
int mOldWidthMeasureSpec = Integer.MIN_VALUE;
int mOldHeightMeasureSpec = Integer.MIN_VALUE;
private LongSparseLongArray mMeasureCache;
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
......
// 1. 构建测量缓存的 key
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
if (mMeasureCache == null) {
mMeasureCache = new LongSparseLongArray(2);
}
// 2. 根据 Flag 判断是否要强制重新布局
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
// 3. 判断是否需要重新测量
final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
|| heightMeasureSpec != mOldHeightMeasureSpec; // 3.1 判断测量说明书是否有变化
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY; // 3.2 判断是否是精确值测量
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec); // 3.3 判断说明书中给予的值与当前 View 已经测量的宽高是否是一致的
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize); // 3.4 通过上述条件判断是否需要测量
// 4. 进行测量操作
if (forceLayout || needsLayout) {
// 获取缓存的索引
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// 4.1 执行测量操作
onMeasure(widthMeasureSpec, heightMeasureSpec);
......
} else {
// 4.2 直接从缓存中读取数据, 并且重新赋值
long value = mMeasureCache.valueAt(cacheIndex);
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
......
}
......
}
// 5. 将最新的测量说明书更新到成员变量
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
// 6. 将 key 与 val 存入缓存
// val 的构建: 宽度测量说明书左移 32 位占用 long 类型的高 32 位
// 高度测量说明书保存在 long 类型的低 32 位
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
}
可以看到 View 的 measure 中做了如下的操作
- 构建当前 View 测量说明书缓存的 key
- mMeasureCache 未创建, 先进行创建
- 根据 Flag 判断是否要强制重新布局
- 根据一些条件判断是否需要重新测量
- 若无缓存, 则使用 onMeasure 执行测量操作
- 若存在缓存, 则直接使用缓存中的测量好的数据
- 将最新的测量说明书更新到成员变量
- 将 key 与测量的宽高存入缓存
- 测量的宽高是将 int 类型合并到了 long 类型中进行存储
好的, 因为 DecorView 是 FrameLayout 的容器, 我们就以 FrameLayout 举例, 看看一次测量的过程是到底是怎样的
public class FrameLayout extends ViewGroup {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
......
int maxHeight = 0;
int maxWidth = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
// 1. 测量子孩子
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);
......
}
}
......// 对测量出来的子视图大小做一些验证
// 2. 根据子视图的大小来确定自己的大小
setMeasuredDimension(
resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT)
);
......
}
}
可以看到 FrameLayout 容器的 onMeasyre 设计如下
- 首先是调用了 measureChildWithMargins 测量子孩子
- 然后再来确定自身的大小
1. 测量子 View
我们看看这个确认子 View 大小的过程
public class ViewGroup extends View {
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
// 获取子 View 的 LP
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// 1. 构建子视图的测量说明书
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);
// 2. 将 measure 操作分发给子 View
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
好的可以看到测量子 View 的过程中, 主要有两个操作
- 第一个是构建子 View 的测量参考说明书
- 第二个便是将测量参考说明书下发给子 view, 让其内部去处理测量操作
下面我们一一查看
1) 构建子 View 测量说明书
public class View {
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
// 1. 解析当前容器的测量模式与大小
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
// 2. 去除 padding, 即获取当前容器可供 View 使用的空间
int size = Math.max(0, specSize - padding);
// 3. 用于构建子 View 说明书
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// 3.1 当前容器是 match_parent 或者精确值
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 3.2 当前容器是 wrap_content
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 3.3 当前容器是尺寸是未指明的(子视图可以随意发挥)
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
// 构建子视图说明书
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
}
上面终点介绍了子 View 说明书的构建过程, 接下来看看 View 拿到了自己的测量说明书之后, 做了什么操作
2) 测量子 View
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 通过测量说明书来构建 View 的宽高, 以宽度举例
// 1. 首先获取当前 View 建议的最小宽度
// 2. 获取默认的大小
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
private Drawable mBackground;
protected int getSuggestedMinimumWidth() {
// 1. 若 View 没有设置背景, 则使用 mMinWidth, 它由解析 "android: minWidth" 标签获取到
// 2. 若设置了背景, 则使用 mMinWidth 和背景宽度中最大的
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
public static int getDefaultSize(int size, int measureSpec) {
// 先将 result 置为最小的尺寸
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
// UNSPECIFIED 该情况默认是 View 最小的尺寸
result = size;
break;
case MeasureSpec.AT_MOST:
// 从 ViewRootImpl.getRootMeasureSpec 方法可知, AT_MOST 对应 wrap_content
// View 默认实现中, 并没有对 AT_MOST 做任何处理
case MeasureSpec.EXACTLY:
// 从 ViewRootImpl.getRootMeasureSpec 说明书构建的过程中可知
result = specSize;
break;
}
return result;
}
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
......
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
int mMeasuredWidth;
int mMeasuredHeight;
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
// 至此便获得了 Measured 的宽高
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
......
}
}
可以看到在 View 大小的确定中
- 首先确定 View 的最小尺寸
- 选择 android:minWidth = “xx” 和 background 中最大的作为最小的尺寸
- 根据测量说明书的类型, 确认 View 最终的大小
- UNSPECIFIED: 系统的实现为最小的尺寸
- AT_MOST(wrap_content): View 中没有做任何处理, 它的效果与 EXACTLY 一致
- 因此一个 View 想要实现 wrap_content 效果, 需要自己手动重写实现
- EXACTLY(match_parent 和 数值): 说明书中的值
至此子 View 的测量就完成了, 接下来看看容器根据子 View 确认自身大小的过程
2. 测量容器自身
先回顾一下 FrameLayout 的 onMeasure 中, 确定自身大小的代码
public class FrameLayout extends ViewGroup {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 记录子 View 的最大宽高
int maxHeight = 0;
int maxWidth = 0;
......// 进行子 View 的测量
// 2. 根据子视图的大小来确定自己的大小
setMeasuredDimension(
resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT)
);
......
}
}
好的, 测量完子 View 之后, 便知道他们之间最大的宽高了, 接下来我们看看 resolveSizeAndState 这个方法, 看看它是如何确认 FrameLayout 的尺寸的
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
// 1. 获取父容器传递下来的 MeasureSpec
// 若为 DecorView 则为 ViewRootImpl 中根据 Window 大小构建的说明书
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
// 2. 确认自身大小
final int result;
switch (specMode) {
// 若为 wrap_content, 则将子 View 的大小截断值说明书的最大值
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
// 若为 match_parent 和精确值, 则直接使用说明书中的尺寸
case MeasureSpec.EXACTLY:
result = specSize;
break;
// 若为未确认的, 则忽略说明书中的值, 直接使用子 View 测量结果
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
// 返回测量尺寸
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
}
好的, 到这里容器的大小就确认好了, 之后便可以通过 getMeasuredWidth 和 getMeasuredHeight 获取测量的宽高了
回顾
View 的测量流程如下
- 构建 DecorView 的测量说明书
- 遍历 View 树进行测量(以 FrameLayout 为例)
- 测量子 View
- 构建子 View 测量说明书
- 分发到子 View 执行测量操作
- 确定 View 的最小尺寸
- 根据测量说明书的类型, 确认 View 最终的大小
- 测量容器自身
- 根据容器的特性进行测量即可
- 测量子 View
三. View 的布局
public final class ViewRootImpl implements ViewParent,
View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
// 这里已经开始执行布局请求了, 因此置为 false
mLayoutRequested = false;
// 1. 第一阶段, 执行第一次 Layout 操作
// 表示当前正在执行 layout 操作
mInLayout = true;
final View host = mView;
......
try {
// 1.1 执行 DecorView 的 layout
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
// layout 完毕了, 标记为 false
mInLayout = false;
// 2. 第二阶段, 处理在第一次 layout 的过程中, 请求的调用了 requestLayout 的 view 集合
int numViewsRequestingLayout = mLayoutRequesters.size();
if (numViewsRequestingLayout > 0) {
// 获取有效的请求者
ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, false);
if (validLayoutRequesters != null) {
// 表示正在处理第一次 layout 过程中调用 requestLayout 的 view
mHandlingLayoutInLayoutRequest = true;
int numValidRequests = validLayoutRequesters.size();
// 2.1 调用再次请求 view 的 reqeustLayout 操作, 重新发起 Traversals
for (int i = 0; i < numValidRequests; ++i) {
final View view = validLayoutRequesters.get(i);
view.requestLayout();
}
// 2.2 进行测量操作
measureHierarchy(host, lp, mView.getContext().getResources(),
desiredWindowWidth, desiredWindowHeight);
// 2.3 进行布局操作
mInLayout = true;
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
// 表示第一次 layout 过程中调用 requestLayout 的 view 已经处理结束了
mHandlingLayoutInLayoutRequest = false;
// 3. 第三阶段: 处理在第二次 layout 过程中, 调用了 requestLayout 的 view 集合
validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, true);
if (validLayoutRequesters != null) {
final ArrayList<View> finalRequesters = validLayoutRequesters;
getRunQueue().post(new Runnable() {
@Override
public void run() {
// 若在第二次 layout 时, 又有新的请求添加进来, 那么直接 post 到下一帧去执行
int numValidRequests = finalRequesters.size();
for (int i = 0; i < numValidRequests; ++i) {
final View view = finalRequesters.get(i);
view.requestLayout();
}
}
});
}
}
}
} finally {
......
}
mInLayout = false;
}
}
好的, 可以看到 ViewRootImpl.performLayout 主要三个阶段, 注释中也标注的很清楚
接下来我们就分析一下 View 的 layout 过程
public class View {
public void layout(int l, int t, int r, int b) {
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
// 1. 调用 setFrame 保存其新的坐标位置
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
// 2. 若新的位置比起之前有所变更, 则调用 onLayout 去确定其子 View 的布局
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
// 调用了 onLayout 操作
onLayout(changed, l, t, r, b);
......
}
......
}
}
可以看到主要做了两个操作
- 更新当前 View 的位置
- 更新子 View 的位置
一) 更新当前 View 的位置
public class View {
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
// 1.1 判断其新的位置与老的位置是否有变更
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
// 1.2 计算之前窗体的尺寸
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
// 判断尺寸是否变更了
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
invalidate(sizeChanged);// 若尺寸变更了, 则说明之前绘制的区域一定无效了, 因此调用该方法重新绘制
// 1.3 更新当前成员变量的值
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
// 确定当前 View 渲染器结点的大小(在 View 渲染的过程中介绍)
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
......
}
return changed;
}
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
}
好的, 可以看到在 layout 中主要做了两个操作
- 调用 setFrame 来更新当前 View 的坐标值
- 调用了 onLayout 操作去确定其子视图的位置
- 这个方法在 View 中个空实现, 因为 View 的摆放的位置, 是由与其容器的特性相关的
二) 更新子 View 的位置
DecorView 是 FrameLayout, 这里以它来举例, 看看它 onLayout 的实现
public class FrameLayout extends ViewGroup {
@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) {
// 1. 获取子孩子的数量
final int count = getChildCount();
// 2. 计算当前容器的可用空间
final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
// 3. 循环操作子 View
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 3.1 获取子 View 的宽高
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
// 3.2 根据 gravity 参数来设置确定子 View 的 left, top 坐标
int childLeft;
int childTop;
......// 省略了计算坐标的代码
// 3.3 确定了子 View 的坐标之后, 将其向下分发
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
}
可以看到, FrameLayout 计算好子视图位置之后, 调用了其 layout 方法, 将新的坐标位置传递给它
三) 回顾
layout 的过程比起 measure 是简单很多的
- 调用 setFrame 来更新当前 View 的坐标值
- 调用了 onLayout 操作去确定其子视图的位置
总结
View 的测量流程如下
- 构建 DecorView 的测量说明书
- 遍历 View 树进行测量(以 FrameLayout 为例)
- 测量子 View
- 构建子 View 测量说明书
- 分发到子 View 执行测量操作
- 确定 View 的最小尺寸
- 根据测量说明书的类型, 确认 View 最终的大小
- 测量容器自身
- 根据容器的特性进行测量即可
- 测量子 View
View 的布局方式如下
- 调用 setFrame 来更新当前 View 的坐标值
- 调用了 onLayout 操作去确定其子视图的位置
至此 View 的 measure 过程就完成了, 其中不乏有很多值得我们思考的细节, 代码中注释的也很详细
思考
在整个策略过程中, 我们发现了一个特殊的说明书类型, 它是 UNSPECIFIED, 其他的 AT_MOST 对应 wrap_content, EXACTLY 对应着 match_parent 和具体的数值, 这个 UNSPECIFIED 设计它的目的是什么, 它的作用又是什么呢?
一) 问题描述
UNSPECIFIED 这个类型的说明书在 Android 使用的非常少, 有迹可寻的地方是在 ScrollView 中, 我们先看看一个特殊的现象
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="300dp"
android:gravity="center"
android:text="TextView"
/>
</ScrollView>
这个 xml 文件我们设想的展示效果是 ScrollView 撑满整个屏幕, 然后 TextView 占用 300 dp 的高度, 并且文本是居中显示的, 然而实际上, TextView 仅仅只有一行文字的高度, 如下图所示
这是为什么呢? 不可思议, 我们去 ScrollView 中看看它做了什么操作
二) 问题探究
1. ScrollView 的测量策略
public class ScrollView extends FrameLayout {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 调用了 FrameLayout 的 onMeasure 对子 View 进行测量
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 我们的 .xml 文件中, 没有使用 android:fillViewport = true, 因此这里会 return
if (!mFillViewport) {
return;
}
......
}
}
可以看到在没有开启 fillViewport 的情况下, 它会调用 FrameLayout 的 onMeasure, 很奇怪我们的 TextView 在 FrameLayout 中展示的效果是没有问题的, 为什么 ScrollView 中出现了偏差呢, 那是因为虽然它没有破坏 FrameLayout onMeasure 的流程, 但它重写了 measureChild 和 measureChildWithMargins, 影响了最终测量的结果
我们选择 measureChildWithMargins 看看它内部做了哪些操作
public class ScrollView extends FrameLayout {
@Override
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 usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
heightUsed;
// 可以看到他将子 View 的说明书类型, 强制传入了 UNSPECIFIED
final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
很有意思, ScrollView 在测量子孩子的时候, 将其测量说明书强行改写成了 UNSPECIFIED 类型, size 为其最大可用空间, 因此我们得去 TextView 的 onMeasure 中寻找答案
2. TextView 的测量策略
我们回顾一下在 View 测量的过程中, 在面对 UNSPECIFIED 策略说明书时, 它会使用自己最小的高度, 但 TextView 不同, 它重写了 onMeasure, 于是我们去看看 TextView 中做了哪些处理
public class TextView extends View {
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
......
// 这里是对高度的测量策略
if (heightMode == MeasureSpec.EXACTLY) {
// 若为 EXACTLY 的, 则使用策略说明书中分配的
height = heightSize;
mDesiredHeightAtMeasure = -1;
} else {
// 1. 若为 AT_MOST/UNSPECIFIED 类型
// 获取期望的高度, 很显然是 wrap_content 的效果
int desired = getDesiredHeight();
height = desired;
......
}
......
setMeasuredDimension(width, height);
}
}
可以看到, TextView 中对于 UNSPECIFIED 类型的高度说明书, 直接使用了 wrap_content 的效果, 这也就很好的解释了, 为什么会呈现出上图中的结果
三) 问题解析
通过了对 ScrollView 和 TextView 组合的分析, 我们看到了 UNSPECIFIED 的使用, 不过似乎通过对它的使用, 我们的视图效果却变得更差了, 这个 UNSPECIFIED 存在有什么意义呢?
我们知道 MeasureSpec 在整个 View 的测量过程中起到了一个非常重要的作用, 子 View 获取到父容器传递给其的策略说明书后, 就可以进行测量了, 它其中有三个模式, 分别是 EXACTLY, AT_MOST, 我们通常理解为:
- EXACTLY: 对应 match_parent 和设置的 dp 值
- AT_MOST: 对应 wrap_content
于是 UNSPECIFIED 就找不到映射关系了, 从而产生了对这个类型的存在产生了怀疑, 但其实并非如此, 对于 MeasureSpec 中的 mode, 并非是为了映射 match_parent 和 wrap_content 等 xml 元素而存在的, 它的作用是用来规范子 View 的测量行为
- EXACTLY: 指的是子 View 必须严格按照说明书中 size 的值来设置自己的尺寸
- AT_MOST: 指的是子 View 可以在说明书中 size 之内来设置自己的尺寸
- UNSPECIFIED: 指的是子 View 可以忽略说明书中 size 的大小, 随意的设置自己的尺寸
当我们正确的认识了 MeasureSpec 的作用之后, 一切就迎刃而解了, 之前理解的映射关系其实仅仅是 Android Framework 开发工程师遵守 MeasureSpec 中 mode 的约束而产生的, 若是忽略 mode 的规范, 我们自然是可以随心所欲的吧 UNSPECIFIED 当成 match_parent 的映射, 但这样的自定义组件, 显然是不会受到大众认可的
因此 UNSPECIFIED 存在的意义是为了应对 View 的大小不需要受到父容器限制的场景, ScrollView 中对子 View 使用 UNSPECIFIED 策略模式, 是因为它是可以滚动的, 它不希望子 View 受到当前父容器尺寸的影响, 否则我们何来滚动的效果呢?