第四章 View 的工作原理.md
第四章 View 的工作原理
4.1 ViewRoot 与 DecorView
ViewRoot 对应于 ViewRootImpl 类,是连接 WindowManager 和 DecorView 的纽带,View 的三大流程均通过 ViewRoot 完成,在 ActivityThread 中,当 Activity 对象被创建完毕后,会将 DecorView 添加到 Window 中,同时会创建 ViewRootImpl 对象,并将这两个关联。
View 的三大绘制流程是从 ViewRoot 的 performTraversals 方法开始的。经过 measure 、layot 、draw 三个过程才最终显示出来。具体过程在 4.3 将会详细讲解。
DecorView 为顶级 View,其是 WindowManager 持有的 所有 View 的 顶级父容器。在 Activity 中,通常其会包含一个 LinearLayout,并分为两部分,其中上面为 ActionBar,下部分为一个 Id 为 android.R.id.content 的 FrameLayout,当我们调用 setContentView 方法时,设置的 View 其实是作为 FrameLayout 的 子 View。
其中 ActionBar 部分可通过设置主题为 noActionbar 来去除。
4.2 MeasureSpec
4.2.1 MeasureSpec
MeasureSpec 类似一种测量规格,当父布局进行 measure 时,会构造一个 MeasureSpec 对象,其中包含着一些信息,并传送到 子 View中,子 View 在根据 父布局 传进来的该对象来测量自己的 长宽,下面将具体介绍其包含的信息。
MeasureSpec 是一个 32 位的 int 值,其中高二位代表 SpecMode,即测量模式,低 30 位表示 SpecSize,指前面测量模式下的大小,以下为源码中的部分定义:
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
主要为将 size 与 mode 包装为一个 MeasureSpec 的 makeMeasureSpec 方法,与解包获取 mode 与 size 的方法。
同个上面代码可以看出 mode 分为三种,分别为:
- UNSPECTIFIED:父容器不对 View 有任何限制,子 View 的可以随意指定自己的大小。
- EXACTLY:父容易已经测量出子 View 的大小,此时的大小为 size 值,主要用于当 子 View 的长宽为 MATCH_PARENT 时的测量,此时 size 值就为父容器自身的长宽。
- AT_MOST:父容器指定了一个最大长宽,此时 size 中的值为父容器为孩子指定的最大值,主要用于当子 View 的长宽为 wrap_content 时的测量,通常情况下最大值为父容器自身的长宽。
4.2.2 MeasureSpec 与 LayoutParams 的关系
当一个 View 进行 measure 过程时,其父容器会根据其 LayoutParams 生成一个 MeasureSpec ,并作为子 View onMeasure 的依据。因此,决定 MeasureSpec 的因素有两个,一是 父容器,二是 LayoutParams,接下来将介绍 DecorView 与 普通 ViewGroup (安卓中常见 Layout,默认实现) 情况下 MeasureSpec 的生成规则,值得注意的是,这里介绍的是默认实现,如果重写了相关方法,则可能会有出入,同时也可以通过重写相关方法来根据具体业务写入新的生成规则。
DecorView 中的 MeasureSpec 生成规则主要与 getRootMeasureSpec 方法有关,以下为源码:
private static int getRootMeasureSpec(int windowSize, int rootDimension){
int measureSpec;
switch(rootDimension){
case ViewGroup.LayoutParams.MATCH_PARENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
}
return measureSpec;
}
可以很明确的看出, DecorView 的 MeasureSpec 产生规则主要按照 LayoutParams 中宽高来划分:
- LayoutParams.MATCH_PARENT 对应 EXACTLY 模式,size 为 窗口大小
- LayoutParams.WRAP_CONTENT 对应 AT_MOST 模式,size 为窗口大小
- 固定大小同样对应 EXACTLY 模式,size 为该大小
接下来来分析普通 ViewGroup 的生成规则,普通 ViewGroup 与 DecorView 是不同的,因为 ViewGroup 存在多重嵌套,可能父 VireGroup 本身的大小也尚未确定,这里主要来看看 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);
}
其中最后一行 child.measure 为调用孩子的 measure 过程,可以看出其 MeasureSpec 生成主要为调用方法 getChildMeasureSpec 方法,该方法代码较多,可以自行查看,这里直接给出规则。
这里的 MeasureSpec 主要由两个部分决定,一是父容器自身的 MeasureSpec,二是孩子的 LayoutParams,下表中给出其生成规则:
LayoutParams | EXACTLY | AT_MOST | UNSPECTIFIED |
---|---|---|---|
具体尺寸 | EXACTLY childSize | EXACILY childSize | EXACILY childSize |
match_parent | EXACTLY parentSize | AT_MOST parentSize | UNSPECTIFIED 0 |
wrap_content | AT_MOST parentSize | AT_MOST parentSize | UNSPECTIFIED 0 |
4.3 View 的工作流程
View 的工作流程主要指 measure、layout、draw 三个过程,即测量、布局、绘制。对于一个 ViewGroup,在完成自己相应过程的同时,它也会递归调用其孩子的对应过程,最终完成孩子的绘制。
4.3.1 measure 过程
View 的 measure 过程
View 的 measure 过程是在其 measure 方法中完成的,该方法不可重写。在此方法中会调用 onMeasure 方法,来看看 onMeasure 方法的源码:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
可以看到这个方法的默认实现主要与三个方法有关:
- setMeasureedDimension 该方法主要为设置测量结果,测量结果是宽高的像素值,在自定义 View 中,如果我们需要自定义 onMeasure 过程,一般同样需要调用这个方法。
- getDefaultSize 与 getSuggestedMinimumXXX 这两个方法与其默认实现有关,我们分别来看。
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;
}
该方法还是比较简单的,直接根据逻辑走即可。
getSuggestedMinimumXXX ( 以 Width 为例 )
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
可以看到,这个值与 最小宽度以及背景宽度有关。
通过源码可以看出,当给一个默认 View 设置 wrap_content 时,其大小会占满父布局,实际上,我们通常使用的 TextView ImageView 等都对该过程进行了重写。
VioewGroup 的 measure 过程
对于 ViewGroup 来说,它除了完成自己的 measure,还会调用子元素的 measure 方法。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);
}
}
}
这个还是比较好理解的,主要是遍历所有 VISIBILITY 不为 GONE 的孩子,并调用 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);
}
该方法还是比较好理解的,主要是获取孩子的 LayoutParams,然后调用 getChildMeasureSpec 方法构造 MeasureSpec,然后调用孩子的 measure 方法,这里 getChildMeasureSpec 效果在之前已经有介绍。
ViewGroup 的测量过程 onMeasure 需要各自子类具体去实现,因为对于不同的 Layout,其特性不同,因此测量规则也不同。值得注意的是,有的 Layout 因为需要,会对孩子进行多次测量,例如第一次测量为 UNSPECIFIED,获取孩子的大小,然后根据孩子的大小设置自己的大小后在使用 AT_MOST 模式进行二次测量。
4.3.2 layout 过程
该过程的作用主要为确认一个 View 在父容器中的位置,包括左上顶点与右下顶点的坐标共四个数分别是 top,left,bottom,right 。该过程比较简单。首先,layout 方法会被调用,该方法中会调用 setFrame 来设置上面的四个参数。同时会调用 onLayout 方法。值得注意的是,onLayout 在 view 与 viewGroup 中都为空方法。其中第一个参数为是否改变:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
4.3.3 draw 过程
该过程也比较简单,它的作用是将 View 绘制到屏幕上,绘制过程遵循如下几步:
- 绘制背景 background.draw(canvas)
- 绘制自己 onDraw
- 绘制孩子 dispatchDraw
- 绘制装饰 onDrawScrollBars
值得注意的是,在绘制自己的时候,会判断一个 willNotDraw 变量是否为 false,只有该变量为 false 时才会调用 onDraw 方法。对于普通的 View,其默认值为 false,但对于 ViewGroup,其默认值为 true,如果我们需要在 Layout 中绘制自己,则需要显式将其值设置为 false 。
4.4 自定义 View
4.4.1 自定义 View 分类
- 继承 View 重写 onDraw 方法
- 继承 ViewGroup 派生特殊的 Layout
- 继承特定的 View
- 继承特定的 ViewGroup
4.4.2 自定义 View 规则
- 让 View 支持 wrap_content : 默认 View 的实现中,对于 AT_MOST 的模式,是直接占满父布局的。
- 如果有必要,让 View 支持 padding,让 ViewGroup 支持设置孩子的 padding 与 margin
- 尽量不要在 View 中使用 Handler,View 自带 POST 方法
- View 中如果有线程线程或者动画,需要及时停止,可在 onDetachedFromWindow 中停止
- View 带有滑动嵌套情形时,需要处理好滑动冲突