4.1 初识 ViewRoot 和 DecorView
ViewRootImpl
注意 ViewRootImpl 并不是一个 View,但它实现了 ViewParent 接口,WindowManager 通过它来指挥 DecorView 的运作;View 的 measure/layout/draw 三大流程都是在 ViewRoot 的 performTraversals 方法中依次调用 performMeausre/performLayout/performDraw 方法,并遍历 View 树的。DecorView
是整个视图树的根节点;是 Window 的实现 PhoneWindow 类的私有内部类,是一个 FrameLayout,默认包含标题栏和内容栏,内容栏是一个 id 为“andorid.R.id.content”的 FrameLayout,我们调用 Activity 的 setContentView,就是向这个内容栏中添加视图。
4.2 理解 MeasureSpec
“测量规格”或“测量说明”,见 View 下的 MeasureSpec 公共静态内部类
4.2.1 MeasureSpec
先看一个最普通的 View 的布局定义:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_world" />
“layout_”开头的属性是跟测量有关系的属性,它可以是“wrap_content”,可以是“match_parent”,也可以指定精确的值例如“18dp”。
在代码中,这个属性封装在 LayoutParams 中,并最终转化为一个 MeasureSpec,是一个 32 位 int 值,高2位代表 SpecMode,低30位代表 SpecSize。
SpecMode 有三类:
- UNSPECIFIED,要多大给多大,出现在系统内部,一般不常见
- EXACTLY,精确值,对应布局定义中的 “match_parent” 和具体数值的情况。
- AT_MOST,最大不能超过一个大小,对应布局中的“wrap_content”
4.2.2 MeasureSpec 和 LayoutParams 的对应关系
当我们在指定一个 View 的布局属性(LayoutParams)时,最终的测量结果有可能有所不同,这是因为测量结果会受到父容器的约束。当为 View 指定了 LayoutParams 后,交给父容器计算后得到的 MeasureSpec 才是真正的测量结果。例如指定“match_parent”,经过父容器计算返回“100px”,View 就可以拿这个结果确定大小,并进行绘制了:
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 的创建规则,跟父容器的 MeasureSpec 与子 View 的 LayoutParams 都有关系。
例如,若父容器是“match_parent”,子视图也是“match_parent”,那子视图的 SpecMode 就是 “EXACTLY”,SpecSize 就是父容器的大小去掉 padding;若父容器是“wrap_content”,子视图是“10px”,那子视图的 SpecMode 就是 “EXACTLY”,SpecSize 就是“10”;等等,不一而足,具体请看源码,这部分逻辑还蛮简单的。
当然作为顶级 View 的 DecorView 来说,它没有父容器了,它的 MeasureSpec 自然也只受窗口(Window)和自身的 LayoutParams 决定了。在 ViewRootImpl 的 measureHierarchy 方法的源码中有所体现,这里就不赘述。
4.3 View 的工作流程
在 ViewRootImpl 的 performMeausre 方法中,从根节点开始执行深度优先遍历,依次调用每一个节点的 measure 方法。每个 ViewGroup 通过遍历调用所有的非 GONE 的子视图的 measure 方法来测量自身。
4.3.1 measure 过程
measure 方法是一个 final 方法,它把测量的具体实现交给了 onMeasure 方法。View 类中提供了该方法的默认实现。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
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 被一视同仁,后果是使用“wrap_content”就相当于“match_parent”。
所以要自定义 View 支持 “wrap_content” 的话,就必须要重写 onMeasure 方法了。
对于一个 ViewGroup 而言,它的测量工作不仅取决于子视图的测量结果,也与它自身的布局特性有关,所以自定义布局也必须重写 onMeasure 方法。
由于 View 的 Measure 过程与 Activity 的生命周期方法不是同步执行的, 所以在 Activity 生命周期方法中无法获取一个 View 的宽高信息,关于这个问题,书中给出四种方法:
- Activity/View#onWindowFocusChanged
- view.post(runnable)
- ViewTreeObserver
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
- view.measure(a, b)
这种方法要根据 view 的布局属性来分情况讨论:
match_parent
无法计算,直接放弃此方法wrap_content
使用理论最大值构造 MeasureSpec
int widthMeasureSpect =
MeasureSpec.makeMeasureSpec((1 << 30), View.MeasureSpec.AT_MOST);
int heightMeasureSpect =
MeasureSpec.makeMeasureSpec((1 << 30), View.MeasureSpec.AT_MOST);
view.measure(widthMeasureSpect, heightMeasureSpect);
- 具体的值
int widthMeasureSpect =
MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
int heightMeasureSpect =
MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
view.measure(widthMeasureSpect, heightMeasureSpect);
4.3.2 layout 过程
Layout 的作用就是确定视图的位置,具体就是通过 layout 方法,确定自身相对父容器的位置以及大小的信息。
对于 ViewGroup 而言,就是通过 onLayout 进行一定的计算遍历调用所有子视图的 layout 方法。
layout 的基本流程就是:
- setFrame 设定 view 的四个位置属性(mLeft,mTop, mRight, mBottom)
- 调用 onLayout 确定子元素位置(View 和 ViewGroup 均没有实现 onLayout)
通过观察 getWidth() 的实现:
public final int getWidth() {
return mRight - mLeft;
}
可以看出 getWidth() 与 getMeasuredWidth() 的区别,前者是在 layout 过程中形成的,后者是在 measure 过程中形成的。
4.3.3 draw 过程
draw 过程可以说是三大流程中最简单的,绘制过程遵循以下几步:
- 绘制背景(background.draw(canvas))
- 绘制自身(onDraw)
- 绘制子视图(dispatchDraw)
- 绘制装饰(onDrawScrollBars)
4.4 自定义 View
4.4.1 自定义 View 的分类
自定义 View 分为:
- 自定义绘制,例如绘制钟表,贝塞尔曲线之类的
- 自定义布局,例如瀑布流布局,标签流布局
4.4.2 自定义 View 须知
- 支持“wrap_content”
- 支持“padding”
- 在 onDetachedFromWindow 中终止动画和线程
- 处理滑动冲突
4.4.3 自定义 View 示例
示例太多了,在了解了原理之后,多浏览 github,技术博客别人分享的源码吧