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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容