View—View绘制的三大流程
2018-01-18 本文已影响40人
SharryChoo
View 三大流程的发起点
从 Window 机制探索中, 我们看到在 WindowManagerGlobal 调用 addView 方法时, 会走到 ViewRootImpl.setView 方法中去, 从而触发 performTraversals 方法, 下面就来看看这个方法做了哪些处理
/**
* ViewRootImpl.performTraversals 方法
*/
private void performTraversals() {
final View host = mView;
if (host == null || !mAdded)
return;
// 更新标记位, 正在 Traversal
mIsInTraversal = true;
// ... 这里省略了数百行代码
WindowManager.LayoutParams lp = mWindowAttributes;
if (!mStopped || mReportNextDraw) {
boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
(relayoutResult & WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
|| mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
updatedConfiguration) {
// 这个 host 为 Window 下的第一个 View, 它的宽高一般就是制定的 Window 的宽高
// ViewRootImpl, 其实就是用于处理 Window 下最直接的 View, 用 ViewRootImpl 来命名就显而易见了
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// 1. 调用 performMeasure 开启 View 树的测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
// Implementation of weights from WindowManager.LayoutParams
// We just grow the dimensions as needed and re-measure if
// needs be
int width = host.getMeasuredWidth();
int height = host.getMeasuredHeight();
boolean measureAgain = false;
// 判断是否需要重新 performMeasure
// 保证宽度权重大于0
if (lp.horizontalWeight > 0.0f) {
width += (int) ((mWidth - width) * lp.horizontalWeight);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
MeasureSpec.EXACTLY);
measureAgain = true;
}
// 保证高度权重大于0
if (lp.verticalWeight > 0.0f) {
height += (int) ((mHeight - height) * lp.verticalWeight);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
MeasureSpec.EXACTLY);
measureAgain = true;
}
if (measureAgain) {
// 从新测量一波
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
layoutRequested = true;
}
}
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
if (didLayout) {
// 2. 调用 performLayout 确定 View 的位置
performLayout(lp, mWidth, mHeight);
}
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
if (!cancelDraw && !newSurface) {
// 3. 调用 performDraw 开启 View 的绘制
performDraw();
} else {
if (isViewVisible) {
// Try again
scheduleTraversals();
}
}
mIsInTraversal = false;
}
ViewRootImpl.performTraversals 方法一共有 700 多行代码, 这里只分析与 View 相关的核心部分, 这个方法做了如下的事情
- ViewRootImpl.performMeasure, 测量 View 的大小
- 若宽高的权重不大于0, 则需要重新测量 View 大小
- ViewRootImpl.performLayout 确定 View 摆放的位置
- ViewRootImpl.performDraw 开启 View 的绘制
下面逐个分析 View 的三大流程
View 的测量
/**
* ViewRootImpl.performMeasure
*/
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
try {
// 很简单直接调用了 mView 的 measure 方法
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
/**
* View.measure
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int oWidth = insets.left + insets.right;
int oHeight = insets.top + insets.bottom;
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
}
// 1 判断是否开启了强制 Layout
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
// 2 与缓存的 mOldMeasureSpec 进行一系列的比较判断得到最终的Flag needsLayout
// 2.1 判断 MeasureSpec 是否与缓存的一致
final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
|| heightMeasureSpec != mOldHeightMeasureSpec;
// 2.2.1 判断 MeasureSpec 的测量模式是否为 MeasureSpec.EXACTLY
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
// 2.2.2 判断 MeasureSpec 的测量值是否与当前View测量值一致
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
// 2.3 得出最终 Flag 的值
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
// 若添加了强制测量的 Flag 或者 needsLayout = true 才会走下面的代码
if (forceLayout || needsLayout) {
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// 3.1 无缓存: 这里回调用我们最最熟悉的 onMeasure
onMeasure(widthMeasureSpec, heightMeasureSpec);
} else {
// 3.2 有缓存: 直接调用 setMeasuredDimensionRaw
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
}
}
// 更新缓存的 MesureSpec
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |(long) mMeasuredHeight & 0xffffffffL);
}
可以看到 measure 方法做了如下的事情:
- 获取 forceLayout 和 needLayout 这两个 Flag 的值
- 可以看到 needLayout 这里做了很多的判断, 可以根据其判断流程做一些优化, 防止过度 Measure
- 判断 XXXMeasureSpec 是否与缓存的 mOldXXXMeasureSpec 一致
- 判断 MeasureSpec 的测量模式是否为 MeasureSpec.EXACTLY
- 判断 MeasureSpec 的测量值是否与当前View测量值一致
- 根据这个两个 Flag 来决定是否需要启动 View 的测量
- 无缓存: onMeasure
- 有缓存: setMeasuredDimensionRaw
注意:
ViewGroup extends View, ViewGroup 中默认是没有重写 onMeasure 的
所以当我们继承 ViewGroup 写自定义 View 的时候必须要重写 onMeasure 方法, 一般步骤如下
/**
* 重写 View 的 onMeasure 方法
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 1. 获取从上层传递下来的 Mode 和 Size
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
// 2. 遍历测量子 View 大小
int count = getChildCount();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
// 调用了这个 measureChildWithMargins
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
}
// 3. 根据子 View 大小来设置自己的大小
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
}
/**
* ViewGroup.measureChildWithMargins
*/
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
// 测量 View 的大小, 连同其设置的 margin 参数
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
// 可以看到最终又回调了 View 的 measure 方法, 这样就一层一层的将 View 的 measure 传递下去了
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
可以看到 ViewGroup 中的 Measure 主要做了两件事情:
-
遍历子 View, 将 measure 操作向下分发
-
子 View 全部测量好之后, 根据自身布局的特性, 设置自身大小
接下来看看 View.onMeasure 方法
/**
* View.onMeasure
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
);
}
/**
* View.getSuggestedMinimumWidth
*/
protected int getSuggestedMinimumWidth() {
// 获取建议的最小宽度, 即背景的宽度
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
/**
* View.getDefaultSize
*/
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:// 未指定的
// 当未指定的时候, View 的大小默认为 size, 通过上面得知为其 Background 的宽高
result = size;
break;
// 可见如果不重写 View 的 onMeasure 方法的话, 其 wrap_content 的作用于 match_parent 是一样的
case MeasureSpec.AT_MOST:// 最大的: wrap_content
case MeasureSpec.EXACTLY:// 精确的: match_parent 和 10dp
result = specSize;
break;
}
return result;
}
/**
* View.setMeasureDimension
*/
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
// 调用者这个方法
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
/**
* View.setMeasuredDimensionRaw
*/
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
// 给成员变量赋值
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
总结:
Measure 操作的过程
- 将 MeasureSpec 经过自身处理后, 分发到下层子 View
- 子 View 确定了大小之后, 再回溯到父容器中
- 父容器结合子 View 大小和 自身布局特性 来确定自己的大小, 一直回溯到顶层
可以看到 View 的 onMeasure 中还是有一些细节的, 在我们自定义 View 中起到至关重要的作用:
- 当测量模式为 MeasureSpec.UNSPECIFIED 时, View 的大小默认为 getSuggestedMinimumWidth() 即背景的宽高
- View 默认是不支持 wrap_content 属性的, 源码中它直接走到了下面的 MeasureSpec.EXACTLY 才得以 break, 我们需要重写 onMeasure, 让其支持 wrap_content
- 当 View 的 setMeasureDimension() 执行后, 我们可以通过 getMeasureWidth/Height() 获取其初步的宽高值
View 位置的确定
/**
* ViewRootImpl.performLayout
*/
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mInLayout = true;
final View host = mView;
if (host == null) {
return;
}
try {
// 调用了 View.layout 方法, 将他的边界传入
// 因为 ViewRootImpl 关联的是 Window 下的直接 View, 所以他的起始位置就是 0, 0
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
mInLayout = false;
// ... 省略了部分代码
}
}
/**
* ViewGroup.layout
*/
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
// 回调了 View.layout 方法, 可见 ViewGroup 的 layout 方法并没有什么实际作用
super.layout(l, t, r, b);
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}
/**
* View.layout
*/
public void layout(int l, int t, int r, int b) {
// ...
// 1. 很重要的方法 View.setFrame()
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
// 2. 调用了 onLayout 方法
onLayout(changed, l, t, r, b);
// ...
}
}
/**
* View.setFrame
*/
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;
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);
// 1. 更新当前 View 四个顶点的位置, 更新了顶点位置之后, 便可以通过 getWidth/Height 来获取宽高了
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
}
}
/**
* View.onLayout
*/
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
/**
* ViewGroup.onLayout
*/
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
看到这里, 我们就可以解开心中的疑惑了
- performLayout 调用了顶级 View 的 layout 方法
- View.layout 方法会先调用 View.setFrame() 方法, 更新当前 View 四个顶点的坐标,
- 注意这个时候, 就已经可以通过 getWidth/getHeight 获取自身的宽高了, 但是还不能获取子 View 的宽高
- Vew.layout 再调用 View.onLayout() 方法,
- 多肽性————若当前为 ViewGroup 实例, 则会调用 ViewGroup 的 layout 方法
- 这也就揭示了为什么 ViewGroup 必须重写 onLayout 的原因
- 调用了 ViewGroup 中的 onLayout 又会遍历子 View, 调用其 layout 方法, 这样每个 View 就确定了自己的位置, 是一个从上往下分发的过程
View 的绘制
/**
* ViewRootImpl.performDraw
*/
private void performDraw() {
// 只关注核心部分
final boolean fullRedrawNeeded = mFullRedrawNeeded;
mFullRedrawNeeded = false;
mIsDrawing = true;
try {
draw(fullRedrawNeeded);
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
/**
* ViewRootImpl.draw
*/
private void draw(boolean fullRedrawNeeded) {
// 调用了drawSoftware
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
}
/**
* ViewRootImpl.drawSoftware
*/
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
mView.draw(cavans)
}
/**
* View.draw
*/
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
// 这个 dirtyOpaque 非常重要, 它是回调 onDraw 方法的必要条件
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
int saveCount;
// 1. 绘制背景, 如果需要的话
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
// 2. 若满足条件则调用自身的 View.onDraw 方法
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
// 3. ViewGroup 中实现了该方法, 它会调用 drawChild
// 从而回调 View.draw, 又回到该方法, 不断的往下分发
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
好了, 代码里注释很清晰, 不过还是有一个值得关注的点:
- 如果 dirtyOpaque = true 的话是不会回调 onDraw 方法的
- 这也解释了为什么我们常常在 ViewGroup 中的 onDraw() 中绘图但无法得以执行的原因
- 通过 setWillNotDraw(false) 来可以开启绘制
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
至此对 View 绘制的三大流程就有一个系统的认识了