View的工作流程主要指measure、layout和draw三大流程,其中measure负责确定View的测量宽高,layout负责确定View的最终宽高和四个顶点位置,draw负责将View绘制到屏幕上。
一、measure过程
对于原始View来说,它只需要调用measure方法完成自己的测量;但是对于ViewGroup来说,它不仅要完成自己的测量过程,还要遍历去调用所有子元素的measure方法,这样各个子元素才能完成自己的测量过程。因此measure过程我们要分情况进行讨论。
(1)View的measure过程
View的测量过程由它的measure()方法完成,measure()方法是一个final方法,这就意味着它不能被子类重写。measure()方法中会调用onMeasure()方法,因此我们直接从onMeasure()方法入手。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
setMeasuredDimension方法会设置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:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
对于我们来说,我们只需要看AT_MOST和EXACTLY两种模式,这样getDefaultSize()方法返回的大小就是MeasureSpec中的SpecSize,这个SpecSize就是View的测量后大小。至于UNSPECIFIED这种情况,一般用于系统内部的测量过程,我们暂时可以不考虑它。
从getDefaultSize方法的实现来看,View的宽高由SpecSize决定,所以我们可以得出结论:
直接继承View的控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content相当于使用match_parent。
根据上一节【View的工作原理(一)基本概念】介绍的普通View的MeasureSpec创建规则可以知道:在布局中使用wrap_content的时候,不论父容器是AT_MOST模式还是EXACTLY模式,View都是最大模式并且大小不会超过父容器的剩余空间,也就是说View会填充满父容器的剩余空间,这和使用match_parent达到的效果显然是相同的。这显然不是我们期待的结果,因此我们需要在onMeasure方法内进行处理:
private int defaultWidth = 144;
private int defaultHeight = 144;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(defaultWidth, defaultHeight);
} else if (widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(defaultWidth, heightSize);
} else if (heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSize, defaultHeight);
}
}
从上面的代码可以看到,在布局中使用wrap_content的时候我们为View直接设置一个默认宽高值,这个默认宽高值通常是需要根据需求进行计算得出的,想了解更多细节可以阅读TextView、ImageView等控件,观察它们是如何对测量过程进行特殊处理的。
(2)ViewGroup的measure过程
对于ViewGroup来说,它不仅要完成自己的measure过程,而且会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个过程。ViewGroup是一个抽象类,它会提供一个measureChildren方法:
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
上述代码很简单,通过遍历找出所有的子View,并对其中非GONE状态的View调用measureChild方法。继续看measureChild方法:
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
getChildMeasureSpec方法使用父容器的MeasureSpec和padding参数计算出子元素的最终MeasureSpec,并调用View的measure方法进行测量。getChildMeasureSpec方法在上一节【View的工作原理(一)基本概念】已经提到过,这里就不重复介绍了。
不同的ViewGroup子类有不同的布局特性,因此它的onMeasure方法需要子类根据实际情况去实现。我们来看看LinearLayout是如何实现的:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
首先判断是竖直布局还是水平布局,我们接下来只分析measureVertical方法,measureHorizontal方法类似。由于代码比较长,以下简要概括它的主要逻辑:
- 首先遍历并measure所有子元素,使用mTotalLength变量存储它在竖直方向上的初步高度;
- 子元素测量完成后,开始测量自身大小;
- 水平方向上的测量过程遵循View的测量过程,利用maxWidth变量存储所有子元素的最大宽度;
- 竖直方向上与View有所不同:如果高度采取的是固定值或者match_parent,它的测量过程与View一致,即为SpecSize;如果采取的是wrap_content,那么它的高度是所有子元素所占高度总和,但是仍然不能超过父容器的剩余空间。
二、layout过程
layout方法是View用来确定自身位置的,当位置被确定,它紧接着会调用onLayout方法:如果它有子元素,将会遍历所有的子元素并调用所有子元素的layout方法,这些layout方法内又会调用onLayout方法,如此循环往复。
先看View的layout方法,这里只贴出核心代码:
//为视图及其所有后代指定大小和位置
public void layout(int l, int t, int r, int b) {
……
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);
……
}
……
}
layout方法的大致流程是这样的:
- 首先通过setFrame方法设定View的四个顶点,为顶点变量赋值:
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
四个顶点确定了,那么View在父容器中的位置也就确定了。 - 接着调用onLayout方法,这个方法的作用是父容器确定子元素的位置,这点和onMeasure方法类似。onLayout方法在View和ViewGroup中都是没有实现的,因为不同的父容器对子元素的布局会有不同的要求,因此需要根据具体的布局情况对onLayout方法进行重写。
接下来看看LinearLayout是如何重写onLayout方法的:
@Override
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);
}
}
由于横向和竖直两种布局原理差不多,所以下面只研究竖直布局的情况:
/**
* Position the children during a layout pass if the orientation of this
* LinearLayout is set to {@link #VERTICAL}.
*
* @see #getOrientation()
* @see #setOrientation(int)
* @see #onLayout(boolean, int, int, int, int)
* @param left
* @param top
* @param right
* @param bottom
*/
void layoutVertical(int left, int top, int right, int bottom) {
//需要考虑padding属性的影响
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();
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
//判断不同gravity情况下子元素的childTop值
switch (majorGravity) {
case Gravity.BOTTOM:
// mTotalLength contains the padding already
//mTotalLength是包含padding在内的
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;
}
//遍历所有子元素
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {//view状态为GONE时不占据空间
//获取测量后宽高
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
//获取LayoutParams
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
//计算不同gravity情况下子元素的childLeft
int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
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;
}
//考虑divider占据的空间
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
setChildFrame方法:
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
到此,子元素的四个顶点位置就确定了。
Tips:自定义普通的View不需要重写onLayout方法,定义ViewGroup需要重写onLayout方法
三、draw过程
draw过程的作用是将View绘制到屏幕上,具体体现在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);
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;
}
……
}
通过draw()方法的源码我们可以分析出绘制的主要步骤如下:
- 绘制View的背景 drawBackground(canvas);
- 绘制View的内容 onDraw(canvas);
- 绘制children dispatchDraw(canvas);
- 绘制一些其他的装饰部分,例如foreground, scrollbars等等 onDrawForeground(canvas);
View的绘制过程的传递是通过dispatchDraw方法实现的,dispatchDraw会遍历调用所有子元素的draw方法,这样draw事件就一层一层传递下去了。
参考
《Android开发艺术探索》
网友评论