View的工作原理分析
View的工作流程主要是指measure、layout、draw这三大流程,即测量、布局和绘制,其中measure确定View的测量宽/高,layout确定View的最终宽/高和四个顶点的位置,而draw则将View绘制到屏幕上。
measure过程
measure过程分两种,第一种是view,只需通过measure方法就可以完成测量过程。还有一种是ViewGroup,除了完成自己的测量过程外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个流程。
View的measure过程
View的measure过程由measure方法来完成,measure方法是一个final类型的方法,子类不能重写,在View的measure方法中会去调用View的onMeasure方法,因此只需要看onMeasure的实现:
@Override
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;
}
getDefaultSize逻辑很简单,通过MeasureSpec获取specMode和specSize,specMode对于开发者来说只需要看AT_MOST和EXACTLY这两种情况,specSize就是view测量后的大小,view最终大小是在layout阶段确定的,但是view测量大小与view最终大小几乎等同。至于UNSPECIFIED这种情况,一般用于系统内部的测量过程,View的宽高分别为getSuggestedMinimumWidth和getSuggestedMinimumHeight这两个方法的返回值:
private Drawable mBackground;
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
从getSuggestedMinimumWidth的代码可以看出,如果View没有设置背景,View的宽度为mMinWidth,而mMinwidth对应于android:minWidth这个属性所指定的值,如果不指定,那么MinWidth则默认为0;如果View指定了背景,则View的宽度为mMinWidth与mBackground.getMinimumWidth两者间的最大值
mBackground是Drawable类型,我们看一下Drawable的getMinimumWidth方法:
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
可以看出,getMinimumWidth返回的就是Drawable的原始宽度,前提是这个Drawable有原始宽度,比如BitmapDrawable的原始宽/高(图片的尺寸),否则就返回0(ShapeDrawable无原始宽/高)。
从getDefaulSize方法的实现来看,如果specMode是AT_MOST与EXACTLY时,view的宽高都等于specSize,通过Android View 工作原理基础
结尾的表可知,这种情况下View的specSize是parentSize,而parentSize是父容器当前剩余的空间大小。所以设置wrap_content的效果与match_parent会一样,要解决这个问题的话需要重写onMeasure,代码如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getMode(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, mHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, heightSpecSize);
} else if (eightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, mHeight);
}
}
在上面代码中,给View指定一个默认的内部宽高(mWidth和mHeight),并在wrap_content时设置此宽高即可。对于其他情形沿用系统的测量值,至于这个默认的内部宽/高的大小根据需要灵活指定即可。
view Measure 过程流程图
ViewGroup的measure过程
对于ViewGroup来说,除了完成自己的measure过程以外,还会遍历去调用所有子元素的measure方法,各个子元素再通归去执行这个过程。和View不同的是,ViewGroup是一个抽象类,因此它没有重写View的onMeasure方法,但是它提供了一个叫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);
}
}
}
从上述代码中看到,ViewGroup的measure时,会对每一个子元素进行measure,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);
}
measureChild的思想就是取出子元素的LayoutParams,然后再通过getChidMeasureSpec来创建子元素的MeasureSpec,接着将MeasureSpec直接传递给View的measure方法来进行测量。getChildMeasureSpec是根据父容器的MeasureSpec结合View本身LayoutParams来确定子元素的MeasureSpec,它的工作过程已经在Android View 工作原理基础进行了详细分析。
LinearLayout的measure过程
ViewGroup并没有定义其测量的具体过程,因为ViewGroup是一个抽象类,不同的ViewGroup子类有不同的布局特性,这导致它们的测量细节各不相同。下面就通过LinearLayout的onMeasure方法来分析ViewGroup的measure过程。
首先看一下LinearLayout的onMeasure方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
看一下measureVertical的源码,源码比较长,下面只描述大概逻辑:
// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
...
// Determine how big this child would like to be. If this or
// previous children have given a weight, then we allow it to
// use all available space (and we will shrink things later
// if needed).
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
final int childHeight = child.getMeasuredHeight();
...
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
...
}
从上面的代码可以看出,系统会遍历子元素并对每一个子元素执行measureChildBeforeLayout
方法,这个方法最终会调用child.measure
方法,这样各个子元素就开始依次进入measure过程,测量结果包括子元素的高度以及竖直方向上的margin等,最后通过mTotalLength
这个变量来存储LinearLayout在竖直方向上的初步高度。当子元素测量完毕之后,LinearLayout会测量自己的大小,看源码:
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
// Reconcile our calculated size with the heightMeasureSpec
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
...
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
当子元素测量完毕之后,LinearLayout会根据子元素的情况来测量自己的大小,它的最终高度还需要加上padding。
竖直的LinearLayout在水平方向的测量过程遵循View的测量过程,而竖直方向的测量过程和View有些不同,具体可以参考resolveSizeAndState的源码:
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
如果布局中height用的是match_parent或者具体值,那么绘制过程和View一致,即高度为specSize。如果布局中height采用wrap_content,那么它的高度是所有的子元素所占用的高度总和,但不会超过它的父容器剩余空间(heightMeasureSpec中获取的specSize)。
measure完成以后,通过getMeasureWidth/Height就可以正确地获取到View的测量宽/高。需要注意的是,在某些极端情况下系统可能要多次调用measure方法进行测量,在这种情形下onMeasure方法中拿到的测量值很可能是不准确的。
一个比较好的习惯是在onLayout方法中去获取View的测量宽/高或者最终宽/高。
LinearLayout垂直方向measure流程图
layout过程
Layout过程是用于确定View位置的,layout方法确定了View本身的位置,而onLayout方法则会遍历子元素的layout方法确定所有子元素的位置,先看View的layout方法:
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);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
layout的方法的大致流程如下,首先会通过setFrame方法来设定View的四个顶点的位置(View本身的位置),即初始化mLeft,mTop,mRight,mBottom这四个值。接着会调用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);
}
}
LinearLayout中onLayout逻辑和onMeasure类似,这里选择layoutVertical讲解,下面是主要代码:
void layoutVertical(int left, int top, int right, int bottom) {
...
final int count = getVirtualChildCount();
...
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
...
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);
}
}
}
可以看到,此方法会遍历所有子元素,childTop会逐渐累加子元素height和margin等,意味着下面的子元素会被放置在上面的子元素下方,符合竖直方向的线性布局原理。setChildFrame中的传递的width和height实际上就是子元素测量宽高。
setChildFrame实现如下:
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
setChildFrame方法会调用子元素的layout方法,这样父容器在layout方法中完成自己的定位,然后通过onLayout方法调用子元素的layout方法,确定子元素自己的位置,这样一层一层传递下去完成整个View树的layout过程。
最后我们回到Layout方法中的setFrame方法,该方法中有如下几句赋值语句,这样子元素的位置就确定了。
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
LinearLayout垂直方向layout流程图
draw过程
Draw过程就比较简单,它的作用是将View绘制到屏幕上面,View的绘制过程遵循如下几步:
- 绘制背景 drawBackground(canvas)
- 绘制自己 onDraw(canvas)
- 绘制子元素 dispatchDraw(canvas)
- 绘制装饰 onDrawForeground(canvas)
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;
}
}
viewGroup中,draw过程传递是通过dispatchDraw来实现的,它会遍历调用所有子元素的draw方法,这样draw事件就一层层传递了下去,相关代码如下:
@Override
protected void dispatchDraw(Canvas canvas) {
...
for (int i = 0; i < childrenCount; i++) {
...
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
...
}
View有一个特殊的方法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);
}
从这个方法的注释中可以看出,如果一个View不需要绘制任何内容,那么设置这个标记位为true以后,系统会进行相应的优化。默认情况下,View没有启用这个校化标记位,但是ViewGroup会默认启用这个优化标记位。这个标记位对实际开发的意义是。当我们的自定义控件继承于ViewGroup并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。当然,当明确知道一个ViewGroup需要通过onDraw来绘制内容时,我们需要显式地关闭WILL_NOT_DRAW这个标记位。