View 和 ViewRoot
从名字来理解,“ViewRoot” 似乎是 “View 树的根” 。这很容易让人产生误解,因为 ViewRoot 并不属于 View 树的一份子。从源码实现上来看,ViewRoot 和 View 对象并没有任何“血缘”关系,它即非 View 的子类,也并非 View 的父类。更确切的说,ViewRoot 可以被理解为“ view 树的管理者”——它有一个 mView 的成员变量,指向的是它所管理的View 树的根。ViewRoot 的核心任务就是与 WindowManagerService 进行通信。
Activity 和 Window
Activity 是支持 UI 显示的,那么它是直接管理 View 树或者 ViewRoot 呢?其实都不是,Activity 内部有一个Window的类型的成员成员 mWindow 。Window 就是“窗口”的意思, Window 是基类,根据不同的产品可以衍生出不同的子类——具体则是由系统在 Activity.attach 中调用 PolicyManager.makeNewWindow 决定的,目前版本的 Android 系统默认生成的都是 PhoneWindow。
ViewRoot 对应于 ViewRootImpl 类,它是连接 WindowManager 和 DecorView 的纽带,View 的三大流程均是通过 ViewRoot 来完成的。在 ActivityThread 中,当 Activity 对象被创建完毕后,会将 DecorView 添加到 Window 中,同时会创建 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 建立关联。
View 的绘制流程是从 ViewRoot 的 performTraversals(执行遍历)方法开始的,它经过 measure、layout 和 draw 三个过程才能最终将一个 View 绘制出来,其中 measure 用来测量 View 的宽和高(尺寸大小),layout 用来确定 View 在父容器中的放置位置,而 draw 则负责将 View 绘制在屏幕上。
MeasureSpec 在 View 的测量过程中起到很大的作用,MeasureSpec 在很大程度上决定了一个 View 的尺寸规格,之所以说是很大程度上是因为这个过程还受父容器的影响,因为父容器影响 View 的 MeasureSpec 的创建过程。在测量的过程中,系统会将 View 的 LayoutParams 根据父容器所施加的规则转换成对应的 MeasureSpec,然后在根据这个MeasureSpec 来测量出 View 的宽/高。
MeasureSpec 代表一个 32 为 int 值,高 2 为代表 SpecMode,低 30 位代表 SpecSize,SpecMode 是指测量模式,而 SpecSize 是指在某种测量模式下 View 的大小。
SpecMode 和 SpecSize 是一个 int 值,一组 SpecMode 和 SpecSize 可以打包成一个 MeasureSpec,而一个 MeasureSpec 可以通过解包的形式来得出原始的 SpecMode 和 SpecSize,需要注意的是这里提到的 MeasureSpec 是指 MeasureSpec 所代表的 int 值,而并非 MeasureSpec 本身。
SpecMode 分为三类,每一个类都表示特殊的含义,分别如下:
系统内部是通过 MeasureSpec 来进行 View 的测量,正常情况下,我们可以使用 View 指定 MeasureSpec ,但是我们还可以给 View 指定 LayoutParams,这使我们指定的 MeasureSpec 是不准确的,所以必须经过系统自己的测量。在 View 测量的时候,系统会将 LayoutParams 在父容器的约束下转换成对应的 MeasureSpec,然后再根据这个 MeasureSpec 来确定 View 测量后的宽/高。需要注意的是,MeasureSpec 不但由 LayoutParams 决定的,而且还和父容器一起才能决定 View 的 MeasureSpec。MeasureSpec 一旦确定后,omMeasure 中就可以确定 View 的测量的宽和高了。
对于普通 View 来说,这里值我们布局中的 View ,View 的 measure 过程由 ViewGroup 传递而来,先看一下 ViewGroup 的 measureChildWithMargins 方法:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
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);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
从代码可以看出,在 ViewGroup 中调用子元素的 measure 方法之前会先通过 getChildMeasureSpec 方法来得到子元素的 MeasureSpec 。从代码来看,很显然,子元素的 MeasureSpec 的值是由父容器的 MeasureSpec 、 子元素本身的 LayoutParams 和 View 的 margin 及 padding (下面有两者的区别)共同决定的。下面根据getChildMeasureSpec 方法的主要内容用表格表现出来:
childLayoutParams \ parentSpecMode | EXACTLY | AT_MOST | UNSPECIFIED |
---|---|---|---|
dp/px | EXACTLY = childSize |
EXACTLY = childSize |
EXACTLY = childSize |
match_parent | EXACTLY = parentSize |
AT_MOST <= parentSize |
UNSPECIFIED = 0 |
warp_content | AT_MOST <= parentSize |
AT_MOST <= parentSize |
UNSPECIFIED = 0 |
简要说明的,对于普通 View ,其 MeasureSpec 由父容器的 MeasureSpec 和 自身的 LayoutParams 来共同决定,那么针对不同的父容器和 View 本身不同的 LayoutParams,View 就可以有多种 MeasureSpec。具体概括就是:
对于 View 类而言,它只有 padding,没有 margin。这是因为 padding 值的是“内容”区域与外围边框的距离,分为 left、right、top、bottom 四个方向。而 margin 则是“内容”内部进一步细化——即“内容”中各元素之间的间距。可想而知,一个 View (非ViewGroup)实例的“内容”本身就是不可分割的,不存在内部对象间距的说法。对于 ViewGroup 由多个子对象组成,它们之间有时需要 margin 属性来将彼此区分开来。
measure 过程要分情况来看,如果只是一个原始的 View ,那么通过 measure 方法就完成了其测量过程,如果是一个 ViewGroup ,除了完成自己的测量之外,还会遍历去调用所有子元素的 measure 方法,各个子元素再递归去执行这个流程,下面针对这两种情况分别讨论。
View 的 measure 过程由其 measure 方法来完成,measure 方法是一个 final 类型的方法,这就意味着不能重写此方法,在 View 的 measure 方法中会去调用 View的 onMeasure 方法,因此只需要看 onMeasure 发实现即可, View 的 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;
}
可以看出,getDefaultSize 这个方法的逻辑很简单,对于我们来说,我们只需要看 AT_MOST 和 EXACTLY 这两种情况。简单的理解就是,getDefaultSize 返回的大小就是 MeasureSpec 中的 specSize,而这个 SpecSize 就是 View 测量后的大小,这里多次提到测量后的大小,是因为 View 最终的大小是在 layout 阶段确定的,所以这里必须要加以区分,但是几乎所有情况下 View 的测量大小和最终大小是相同的。
至于 UNSPECIFIED 这种情况,一般用于系统内部的测量过程,在这种情况下,View 大小为 getDefaultSize 的第一个参数 size,即宽和高为 getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 这两个方法的返回值,它们的源码是:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
我们来分析 getSuggestedMinimumWidth 方法(height方法原理一样)。从代码可以看出如果 View 没有设置背景,那么 View 的宽度为 mMinWidth ,而 mMinWidth 对应于 android:minWidth 这个属性所指定的值。如果这个属性不指定,那么 mMinWidth 则默认为 0 ;如果 View 指定了背景,则 View 的宽度为 max(mMinWidth, mBackground.getMinimumWidth()),mBackground.getMinimumWidth 返回的就是 Drawable 的原始宽度,前提是这个 Drawable 有原始宽度,否则就放回 0 。那么 Drawable 在什么情况下有原始宽度呢?例如,ShapeDrawable 无原始宽和高,而 BitmapDrawable 有原始宽和高(图片的尺寸)。
从 getDefaultSize 方法的实现来看,View 的宽和高由 specSize 决定,所以我们得出结论: 直接继承 View 的自定义控件需要重新写 onMeasure 方法并设置 wrap_content 时的自身大小,否则在布局中使用 wrap_content 就相当于使用 match_parent 。 因为,如果 View 在布局中使用 wrap_content ,那么它的 specMode 是 AT_MOST ,在这种模式下,它的宽和高就等于 specSize。由上面的表格可以看出,View 的 specSize 是 parentSize ,而 parentSize 是父容器中目前可以使用的大小,也就是父容器当前剩余的空间大小。很显然,View 的宽和高就等于父容器当前剩余的空间大小,这种效果和布局中使用 match_parent 完全一致。解决办法为:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int mWidth = 0;//默认的宽,自己定义
int mHeight= 0;//默认的高,自己定义
int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, mHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, heightSpaceSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpaceSize, mHeight);
}
}
对于非 wrap_content 的情形,我们沿用系统的测量值即可,至于这个默认的内部宽和高的大小如何指定,这个没有固定的依据,根据需要灵活指定即可。
对于 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 ,然后再通过 getChildMeasureSpec 来创建子元素的 MeasureSpec,接着将 MeasureSpec 直接传递给 View 的 measure 方法来进行测量。getChildMeasureSpec 就是上述 2.2 中的表格的内容。
我们知道,ViewGroup 并没有定义其测量的具体过程,这是因为 ViewGroup 是一个抽象类,其测量过程的 onMeasure 方法需要各个子类去具体实现,比如 LinearLayout、RelativeLayout 等,为啥 ViewGroup 不像 View 一样对其 onMeasure 方法统一的实现呢?那是因为不同的 ViewGroup 子类有不同的布局特性,这导致它们的测量细节各不相同,比如 LinearLayout 和 RelativeLayout 这两者的布局特性显然不同,因此 ViewGroup 无法做统一实现。
Layout 的作用是 ViewGroup 用来确定子元素的位置,当 ViewGroup 的位置被确定后,它在 onLayout 中会遍历所有的子元素并调用其 layout 方法,在 layout 方法中 onLayout 方法又会被调用。Layout 过程和 measure 过程相比就简单多了,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);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}
layout 方法的大致流程如下:首先会通过 setFrame 方法来设定 View 的四个顶点的位置,即初始化 mLeft、mRight、mTop、和 mBottom 这四个值,View 的四个顶点一旦确定,那么 View 在父容器中的位置也就确定了;接着会调用 onLayout 方法,这个方法的用途是父容器确定子元素的位置,和 onMeasure 方法类似,onLayout 的具体实现同样和具体的布局有关,所以 View 和 ViewGroup 均没有真正实现 onLayout 方法。
Draw 的过程就比较简单了,它的作用是将 View 绘制到屏幕上。View 的绘制过程遵循如下几步:
(1)绘制背景 background.draw(canvas)
(2)绘制自己(onDraw)
(3)绘制 children(dispatchDraw)
(4)绘制装饰(onDrawScrollBars)
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;
}
View 绘制过程的传递是通过 dispatchDraw 来实现的,dispatchDraw 会遍历所有子元素的 draw 方法,如此 draw 事件就一层层的传递下去。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);
}
从 setWillNotDraw 这个方法的注释中可以看出,如果一个 View 不需要绘制任何内容,那么设置这个标记为 true 以后,系统会进行相应的优化。默认情况下,View 没有启用这个优化标记位,但是 ViewGroup 会默认启用这个优化标记位。这个标记位对实际开发的意义是:当我们的自定义控件继承于 ViewGroup 并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。当然,当明确知道一个 ViewGroup 需要通过 onDwon 来绘制内容时,我们需要显示的关闭 WILL_NOT_DRAW 这个标记位。
站在巨人的肩膀上:
《Android 开发艺术探究》——任玉刚