这篇文章主要整理一下Android系统中,View的工作流程。主要就是measure、layout、draw三个过程。
我们知道,在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立联系。View的绘制流程从ViewRootImpl的performTraversals方法开始,方法中会调用performMeasure、performLayout和performDraw方法分别开启measure、layout及draw过程。其中measure用来测量View的宽和高,layout用来确定View在父容器中放置的位置,而draw则负责将View绘制在屏幕上。大致流程如下图所示:
另外,在分析代码原理之前,我们先要理解一下MeasureSpec。MeasureSpec在很大程度上决定了一个View的尺寸规格。在测量的过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个MeasureSpec来测量出View的宽和高。
MeasureSpec是一个32位的int值,高2位代表SpecMode,表示测量模式;低30位代表SpecSize,是指在某种测量模式下的规格大小。SpecMode有三种类型:
UNSPECIFIED
父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量状态。
EXACTLY
父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体数值这两种模式。
AT_MOST
父容器指定了一个可用大小SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于LayoutParams中的wrap_content。
一、measure过程
如果这个View只是一个View,那么通过measure方法就可以完成测量过程;如果是一个ViewGroup,除了要测量自身之外,还会遍历调用所有子View的measure方法,各个子元素再递归去执行这个流程。
View的measure方法是一个final的方法,也就是子类无法重写这个方法。在measure方法中会调用View的onMeasure方法,这个方法是可以重写的方法。View的onMeasure方法如下所示:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
在setMeasuredDimension() 里最终调用了setMeasureDimensionRaw。用来将最终测量的宽、高的值存入本View的成员变量中。
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);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
接下来我们回头看看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;
}
这个方法虽然有三个case,但是事实上只有两种情况,第一种是在UNSPECIFIED时;第二种是在AT_MOST和EXACTLY时。
先来看在UNSPECIFIED情况,在这种情况下直接返回传入这个方法的第一个参数,我们看一下第一个参数的由来
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
如果View没有设置背景,那么返回的是mMinWidth,而这个值对应于android:minWidth这个属性所指的值,如果不指定这个属性,它的默认值为0。如果设定了View的背景,则View的宽度为max(mMinWidth,mBackground.getMinimumWidth()),即android:minWidth和背景的最小宽度中两者的最大值。
接下来是第二种情况,在AT_MOST和EXACTLY时,直接返回measureSpec的specSize的值。
由上述内容可以看出来,View的宽、高由specSize决定,所以直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent。因为在布局中使用wrap_content,那么它的specMode是AT_MOST模式,在这种模式下,它的宽高等于specSize。在这种情况下View的specSize就是parentSize,即父容器中剩余可用空间大小,这种布局效果和使用match_parent完全一致。解决这个问题的方法只需要在wrap_content时指定一个内部默认的宽、高值即可,这个值可以灵活指定。TextView、ImageView针对wrap_content情形在onMeasure方法中均做了特殊处理。下面是示例
对于ViewGroup来说,除了完成自己的measure过程以外,还会遍历调用子View的measure方法,各个子View再去递归执行这个过程。和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的过程中,会遍历所有的子View,并调用measureChid方法,接下来我们看一下这个方法:
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);
}
这个方法的意思就是获取子View的LayoutParams,然后再通过getChildMeasureSpec方法来创建子元素的MeasureSpec,接着讲MeasureSpec直接传递给子View的Measure方法来进行测量。
ViewGroup没有直接定义测量的具体过程,这是因为每一个ViewGroup都有其特有的测量过程。接下来我们分析一下LinearLayout的Measure的过程。
LinearLayout的onMeasure方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
这个方法就是以布局方向将测量过程分离开。我们看一下纵向布局下的测量过程measureVertical方法的主要逻辑:
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
...省略处处理了child为null,为GONE等几种情况的测量
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
// Optimization: don't bother measuring children who are only
// laid out using excess space. These views will get measured
// later if we have space to distribute.
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else {
if (useExcessSpace) {
// The heightMode is either UNSPECIFIED or AT_MOST, and
// this child is only laid out using excess space. Measure
// using WRAP_CONTENT so that we can find out the view's
// optimal height. We'll restore the original height of 0
// after measurement.
lp.height = LayoutParams.WRAP_CONTENT;
}
// 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();
if (useExcessSpace) {
// Restore the original height and record how much space
// we've allocated to excess-only children so that we can
// match the behavior of EXACTLY measurement.
lp.height = 0;
consumedExcessSpace += childHeight;
}
在当前的heightMode为AT_MOST的情况下。会对每一个子View执行measureChildBeforeLayout方法,这个方法内部会调用子View的measure方法,这样每个子元素就开始依次进入measure过程。并且会通过mTotalLength这个变量来存储LinearLayout在竖直方向的高度,每测量一个子View,mTotalLength就会增加。当子元素测量完毕之后,LinearLayout会测量自己的大小。针对竖直的LinearLayout而言,它在水平方向的测量过程遵循View的测量过程,在竖直方向的测量过程有所不同,具体来说,如果它的布局中高度采用的是match_parent或者具体数值,那么它的测量过程和View一致,即高度为specSize;如果它的布局中高度采用的是wrap_content,那么它的高度是所有子元素所占用的高度的总和,但是仍然不能超过父容器的剩余空间,同时还要考虑在竖直方向的padding。
measure完成后,通过getMesuredWidth/Height方法就可以获取View的测量宽\高。
二、Layout过程
Layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用其layout方法,在layout方法中onLayout方法又会被调用。layout方法确定View本身的位置,而onLayout方法则会确定所有子元素的位置。先看一下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);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
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);
}
}
}
final boolean wasLayoutValid = isLayoutValid();
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
if (!wasLayoutValid && isFocused()) {
mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
if (canTakeFocus()) {
// We have a robust focus, so parents should no longer be wanting focus.
clearParentsWantFocus();
} else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) {
// This is a weird case. Most-likely the user, rather than ViewRootImpl, called
// layout. In this case, there's no guarantee that parent layouts will be evaluated
// and thus the safest action is to clear focus here.
clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
clearParentsWantFocus();
} else if (!hasParentWantsFocus()) {
// original requestFocus was likely on this view directly, so just clear focus
clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
}
// otherwise, we let parents handle re-assigning focus during their layout passes.
} else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) {
mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
View focused = findFocus();
if (focused != null) {
// Try to restore focus as close as possible to our starting focus.
if (!restoreDefaultFocus() && !hasParentWantsFocus()) {
// Give up and clear focus once we've reached the top-most parent which wants
// focus.
focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
}
}
}
if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}
layout大致流程:首相会通过setFrame方法设置View的四个顶点的位置,即初始化mLeft、mRight、mTop、mBotton这四个值,View的这四个顶点一旦确定下来,那么View在父容器中的位置也就确定下来了。接着会调用onLayout方法,这个方法的用途是父容器确定子View的位置。
在View中,onLayout方法为空方法,因为View是没有子View的。
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
ViewGroup中layout方法重点也是调用了父类的layout方法
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}
ViewGroup中的onLayout是一个抽象的方法,迫使其子类必须实现这个方法。
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
接下来我们重点看一下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);
}
}
我们还是看竖直布局的layoutVertical中的重点部分
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();
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;
}
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);
}
}
这个逻辑主要是遍历所有子View,根据你当前的显示情况,Gravity设置情况等计算子View的位置,然后调用setChildFrame方法来为子View指定对应的位置。其中childTop会逐渐增大,也就是下面的子View会方法更考下的位置。看一下setChildFrame方法:
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
这个只是调用了子View的layout方法。如果这个child是一个View,那么通过layout的方法就可以设置好它的位置,如果它是一个ViewGroup就会继续调用onLayout设置其子View的位置。这样一层层传递下去就可以完成整个View树的layout过程。最终将位置信息保存成当前View的成员信息。
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
draw过程
draw过程是一个比较简单的过程,主要作用就是将View绘制到屏幕上。
/*
* 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)
*/
以上是源码中的注解,View的绘制过程主要遵循以下步骤:
1.绘制背景。由drawBackground方法完成,如果未设定Drawable对象,则会直接返回。
2.绘制自身内容。由onDraw方法完成。
protected void onDraw(Canvas canvas) {
}
View的onDraw是空方法,所以自定义的view都要实现这个方法。
3.绘制子视图。由dispatchDraw方法完成。View的dispatchDraw是空方法,ViewGroup的dispatchDraw方法有具体的实现,主要是调用子视图的draw方法,这样只要最顶层的View调用了draw方法,底层的所有View的draw方法都可以被调用。下面是dispatchDraw的一部分代码,主要是遍历了所有的子View,并通过drawChild方法来调用子View的draw方法。
for (int i = 0; i < childrenCount; i++) {
while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
more |= drawChild(canvas, transientChild, drawingTime);
}
transientIndex++;
if (transientIndex >= transientCount) {
transientIndex = -1;
}
}
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);
}
}
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
4.绘制装饰。主要是foreground和滚动条。
此文仅为学习笔记
参考:《Android开发艺术探索》