上篇文章View 的测量分析了 View 的工作原理中最复杂的测量过程,接着测量过程的是布局和绘制的过程,这里两个过程相对比较简单,所以放到一篇文章中来写
View 的测量过程中,确定了 View 的测量宽高的信息,布局过程则是为了确定 View 在其父 View 中的位置以及 ViewGroup 确定其所有子 View 元素的位置;布局结束后会执行绘制过程,绘制过程将 View 需要显示的内容绘制到屏幕上
一、layout (布局)过程
依旧从 ViewRootImpl 的 performLayout 方法开始,其中调用 DecorView 的 layout 方法,layout 方法是在 View 类中定义和实现的,其中 getMeasuredWidth、getMeasuredHeight 方法得到的是 DecorView 在测量过程中确定的测量宽高
// ViewRootImpl
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
// View
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);
...
}
...
}
// View
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
...
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
...
}
layout 方法中调用 setFrame 方法,可以看到 setFrame 方法中保存了 View 四个顶点在父 View 中的位置,四个顶点确定,View在父容器中的位置也就确定,View 的实际宽高也就确定,接着就会调用 onLayout 方法
View 的 onLayout 方法是个空实现,说明 View 在布局过程中的任务就是确定自己在父 View 中的位置,确定了在父 View 中的位置后也就确定了子 View 的最终宽高,如果不重写 View 的 layout 方法,其最终宽高与测量宽高相等,如果重写了 layout 方法,并且子 View 的位置不以测量宽高来确定,此时 View 的最终宽高将不等于测量宽高。测量宽高赋值于测量过程,最终宽高赋值于布局过程,两者赋值时机不同
ViewGroup 中 onLayout 是个抽象方法,其子类必须重写该方法以确定其所有子 View 的位置,下面重点分析 ViewGroup 的 onLayout 方法
1、ViewGroup 的 onLayout 方法
由于 ViewGroup 的 onLayout 方法是个抽象方法,所以我们选一个特定的 ViewGroup 实现类来分析,这里分析 LinearLayout 的 onLayout() 方法
// LinearLayout
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
// LinearLayout
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;
int childTop;
int childLeft;
// Where right end of child should go
final int width = right - left;
int childRight = width - mPaddingRight;
// Space available for child
int childSpace = width - paddingLeft - mPaddingRight;
final int count = getVirtualChildCount(); // 子 View 数量
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
switch (majorGravity) {
case Gravity.BOTTOM:
// mTotalLength contains the padding already
childTop = mPaddingTop + bottom - top - mTotalLength;
break;
// mTotalLength contains the padding already
case Gravity.CENTER_VERTICAL:
childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
break;
case Gravity.TOP:
default:
childTop = mPaddingTop;
break;
}
// 遍历所有子 View
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
// 获取子 View 由测量过程确定的的测量宽高
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
// 确定子 View 的 left 位置
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;
case Gravity.LEFT:
default:
childLeft = paddingLeft + lp.leftMargin;
break;
}
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin; // 确定子 View 的 top 位置
//
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
// 确定下一子 View 的 top 位置
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
// LinearLayout
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
LinearLayout 的 onLayout 方法中根据设置的 View 线性排列的方向确定如果实现布局,以竖直排列的 情况为例,会调用 layoutVertical 方法
layoutVertical 中会遍历所有的子 View,由子 View 的测量宽高和 ViewGroup 自身业务逻辑确定子 View 的四个顶点在 ViewGroup 中的位置,并通过 setChildFrame 方法来调用子 View 的 layout 方法。从而将布局过程从 ViewGroup 传递到 View 中,View 中的 layout 方法上面已经分析了,作用为确定自己四个顶点在父 View 中的位置。这样一层一层传递下去就完成了 View 视图树的 layout 过程。
1. ViewGroup 的布局过程的作用为先确定自己在父容器的位置,再确定子 View 在该 ViewGroup 中的位置,子 View 的 layout 结果不会影响 ViewGroup 的layout
2. view 的布局过程的作用为确定自己四个顶点在父 View 中的位置
2、View 的 getMeasuredWidth() 和 getWidth() 的区别
-
子元素的layout() 方法中会根据父容器中传递的顶点位置为 mLeft , mTop , mRight , mBottom 等属性赋值,View 的 getWidth() 方法得到的值为 mRight - mLeft
-
View 的 getMeasuredWidth() 方法得到的值是 View 的 mMeasuredWidth 参数的值,该参数的赋值是在 onMeasure() 方法中
-
这两个方法得到的值不是同一个参数的值,两个参数的赋值时间是不同的,如果View 重写 layout 方法,修改四个顶点的位置,这样两个方法得到的值就是不同的
-
所以不能说这两值一定相等。getWidth() 方法得到的是 View 的最终宽高,getMeasuredWidth() 方法得到的是 View 的测量宽高
- getHeight() 和 getMeasuredHeight() 方法同理。
二、View 的 draw() 绘制过程
测量和布局过程完成之后,ViewRootImpl 会接着调用 performDraw 方法,该方法最终会调用 DecorView 的 draw() 方法
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
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)
*/
// Step 1, draw the background, if needed
int saveCount;
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
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(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);
// we're done...
return;
}
/*
* Here we do the full fledged routine...
* (this is an uncommon case where speed matters less,
* this is why we repeat some of the tests that have been
* done above)
*/
boolean drawTop = false;
boolean drawBottom = false;
boolean drawLeft = false;
boolean drawRight = false;
float topFadeStrength = 0.0f;
float bottomFadeStrength = 0.0f;
float leftFadeStrength = 0.0f;
float rightFadeStrength = 0.0f;
// Step 2, save the canvas' layers
int paddingLeft = mPaddingLeft;
final boolean offsetRequired = isPaddingOffsetRequired();
if (offsetRequired) {
paddingLeft += getLeftPaddingOffset();
}
int left = mScrollX + paddingLeft;
int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
int top = mScrollY + getFadeTop(offsetRequired);
int bottom = top + getFadeHeight(offsetRequired);
if (offsetRequired) {
right += getRightPaddingOffset();
bottom += getBottomPaddingOffset();
}
final ScrollabilityCache scrollabilityCache = mScrollCache;
final float fadeHeight = scrollabilityCache.fadingEdgeLength;
int length = (int) fadeHeight;
// clip the fade length if top and bottom fades overlap
// overlapping fades produce odd-looking artifacts
if (verticalEdges && (top + length > bottom - length)) {
length = (bottom - top) / 2;
}
// also clip horizontal fades if necessary
if (horizontalEdges && (left + length > right - length)) {
length = (right - left) / 2;
}
if (verticalEdges) {
topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
drawTop = topFadeStrength * fadeHeight > 1.0f;
bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
}
if (horizontalEdges) {
leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
drawLeft = leftFadeStrength * fadeHeight > 1.0f;
rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
drawRight = rightFadeStrength * fadeHeight > 1.0f;
}
saveCount = canvas.getSaveCount();
int solidColor = getSolidColor();
if (solidColor == 0) {
final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;
if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}
if (drawBottom) {
canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
}
if (drawLeft) {
canvas.saveLayer(left, top, left + length, bottom, null, flags);
}
if (drawRight) {
canvas.saveLayer(right - length, top, right, bottom, null, flags);
}
} else {
scrollabilityCache.setFadeColor(solidColor);
}
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
final Paint p = scrollabilityCache.paint;
final Matrix matrix = scrollabilityCache.matrix;
final Shader fade = scrollabilityCache.shader;
if (drawTop) {
matrix.setScale(1, fadeHeight * topFadeStrength);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, right, top + length, p);
}
if (drawBottom) {
matrix.setScale(1, fadeHeight * bottomFadeStrength);
matrix.postRotate(180);
matrix.postTranslate(left, bottom);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, bottom - length, right, bottom, p);
}
if (drawLeft) {
matrix.setScale(1, fadeHeight * leftFadeStrength);
matrix.postRotate(-90);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, left + length, bottom, p);
}
if (drawRight) {
matrix.setScale(1, fadeHeight * rightFadeStrength);
matrix.postRotate(90);
matrix.postTranslate(right, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(right - length, top, right, bottom, p);
}
canvas.restoreToCount(saveCount);
// 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);
}
// View
protected void onDraw(Canvas canvas) {}
// ViewGroup
protected void dispatchDraw(Canvas canvas) {
for (int i = 0; i < childrenCount; i++) {
...
drawChild(canvas, transientChild, drawingTime);
...
}
}
// ViewGroup
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
由 View 的 draw 方法可分析得出以下绘制流程
1. View 绘制的流程
- 绘制背景 background.draw(canvas)
- 保存 canvas 图层
- 绘制自己 调用 onDraw(canvas) 方法
- 绘制 children (dispatchDraw)
- 绘制渐变效果和恢复 canvas 图层
- 绘制装饰 (onDrawScrollBars)
onDraw() 为空实现,需要子类根据需要显示的内容重写此方法
dispatchDraw() 方法也是空实现,ViewGroup 中重写了此方法,dispatchDraw 方法中遍历所有子 View,并调用其 draw() 方法,将绘制过程一层层传递,完成了 View 树的绘制过程。
2. setWillNotDraw() 方法
/**
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
*
* Typically, if you override {@link #onDraw(android.graphics.Canvas)}
* you should clear this flag.
*
* @param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
setWillNotDraw() 方法的作用可以从其注释中看出,如果一个 View 不需要绘制任何内容,那么设置这个标记位 true 后,系统会进行相应的优化。
默认情况下 View 没有开启这个标记,而 ViewGroup 则开启了这个标记。
在开发过程中,如果我们的 View 继承自 ViewGroup 且没有进行绘制时就可以开启这个标记以便于系统对其进行优化,如果该 ViewGroup 需要通过 onDraw 来绘制内容,则需要通过调用 setWillNotDraw() 方法来关闭此标记
好啦,到这里 View 的工作过程中测量、布局、绘制三大过程的分析就结束啦,接下来将是 View 的事件分发机制和自定义 View 的文章,敬请期待
网友评论