Android 开发艺术探索 - 读书笔记之第四章 View 的工作原理

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 的宽高信息,关于这个问题,书中给出四种方法:

  1. Activity/View#onWindowFocusChanged
  2. view.post(runnable)
  3. 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();
    }
});
  1. 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 的基本流程就是:

  1. setFrame 设定 view 的四个位置属性(mLeft,mTop, mRight, mBottom)
  2. 调用 onLayout 确定子元素位置(View 和 ViewGroup 均没有实现 onLayout)

通过观察 getWidth() 的实现:

public final int getWidth() {
    return mRight - mLeft;
}

可以看出 getWidth() 与 getMeasuredWidth() 的区别,前者是在 layout 过程中形成的,后者是在 measure 过程中形成的。

4.3.3 draw 过程

draw 过程可以说是三大流程中最简单的,绘制过程遵循以下几步:

  1. 绘制背景(background.draw(canvas))
  2. 绘制自身(onDraw)
  3. 绘制子视图(dispatchDraw)
  4. 绘制装饰(onDrawScrollBars)

4.4 自定义 View

4.4.1 自定义 View 的分类

自定义 View 分为:

  • 自定义绘制,例如绘制钟表,贝塞尔曲线之类的
  • 自定义布局,例如瀑布流布局,标签流布局

4.4.2 自定义 View 须知

  • 支持“wrap_content”
  • 支持“padding”
  • 在 onDetachedFromWindow 中终止动画和线程
  • 处理滑动冲突

4.4.3 自定义 View 示例

示例太多了,在了解了原理之后,多浏览 github,技术博客别人分享的源码吧

推荐阅读

Hongyang
郭霖 - Android自定义View的实现方法
stormzhang
View进行自定义UI

推荐阅读更多精彩内容