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

学习内容

  • View 基础概念
  • 自定义 View
  • View 的底层工作原理
    • 测量流程
    • 布局流程
    • 绘制流程
  • View 常见回调
  • 自定义 View
    • 类型
    • 滑动效果

初识 ViewRoot 和 DecorView

基本概念

  1. ViewRoot

    1. 对应于 ViewRootImpl 类,是连接 Window Manager 和 DecorView 的纽带。

    2. view 的三大流程均是通过 ViewRoot 来完成的,在 ActivityThread 中,当 Activity 对象创建完毕后,会将 DecorView 添加到 Window 中,同时会创建 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 建立关联。

    3. View 的绘制流程从 ViewRoot 的 performTraversals 方法开始,经过 measure,layout 和 draw 三个过程最终将一个 View 绘制出来,其中 measure 用来测量 View 的宽和高,layout 用来确定 View 在父容器中的放置位置,而 draw 负责讲 View 绘制在屏幕上。

    4. performTraversals 调用流程如下:

      img

      onMeasure 方法中会对所有子元素进行 measure 过程。子元素重复父容器的 measure 过程,反复完成整个 View 树的遍历。preformLayout 和 preformDraw 方法类似,只不过 preformDraw 的传递过程是在 draw 方法总通过 dispatchDraw 实现的。

    5. Measure 过程决定了 View 的宽 / 高,完成后可通过 getMeasuredWidth / getMeasureHeight 方法或许 View 测量后的 宽 / 高(绝大多数时等同于 View 最终的宽 / 高);

    6. Layout 过程决定了 View 的四个顶点坐标和实际的 View 的宽 / 高,完成后可通过 getTop、getBottom、getLeft、getRight 拿到 View 的四个顶点坐标,以及 getWidth 和 getHeight 方法拿到 View 的最终宽 / 高。

    7. Draw 过程决定了 View 的显示,只有 draw 方法完成以后 View 的内容才能显示在屏幕上。

  2. DecorView

    1. 如下图,DecorView 作为顶级 View,一般情况下包含一个竖直方向的 LinearLayout。

    2. 在 Activity 中通过 setContentView 设置的布局文件实际上就是被加到内容栏中。

      //获取内容栏 content
      ViewGroup content = (ViewGroup) findViewById(android.R.id.content);
      
      //获取设置的 View
      content.getChild(0);
      
      img
    3. DecorView 实际是一个 FrameLyout,View 层的事件都先经过 DecorView,然后才传递给我们的 View。

    4. DecorView 其实是一个 FrameLayout,View 层的事件都先经过 DecorView,之后才传递给我们的 View。


理解 MeasureSpec

MeasureSpec

1.基础

  • MeasureSpec 很大程度上决定一个 View 的尺寸规格,"很大程度"是因为父容器影响 View 的 MeasureSpec 的创建过程。
  • 测量过程中,系统将 View 的LayoutParams 根据父容器的规则转换成相应的 measureSpec,然后据此测量出 View 的宽高。
  • MeasureSpec 代表一个 32 位 int 值,高 2 位代表 SpecMode(测量模式),低 30 位 代表 SpecSize(某种测量模式下的规格大小)

2.类别

(此处的 "类别" 指的是 SpecMode 的类别。)

  1. UNSPECIFIED:父容器不对 View 有任何限制,要毒打给多大,一般用于系统内部,表示一种测量的状态
  2. EXACTLY:父容器检测到 View 所需要的精确大小,此时 View 的最终大小就是 SpecSize 所指定的值。对应于 LayooutParams 中的 match_parent 和具体的数值。
  3. AT_MOST:父容器制定了一个可用大小 specSize,View 的大小不能大于这个值。对应于 LayoutParams 中的 wrap_content

MeasureSpec 和 LayoutParams 的对应关系

1.结论

2.说明

  1. DecorView vs 普通 View
    1. DecorView 的 MeasureSpec 由窗口的尺寸和其自身的LayoutParams 确定;普通 View 的 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来确定。
  2. 普通 View 的 measureSpec 的确定规则
    1. View 固定宽高时,无视父容器的 MeasureSpec,View 的MeasureSpec 始终是精准模式 EXACTLY,并且大小遵循 LayoutParams 中的大小
    2. View 的 MeasureSpec 是 match_parent 时
      1. 如果父容器的模式也是 EXACTLY 精准模式,那么 View 也是 EXACTLY 精准模式并且大小是父容器的剩余空间
      2. 如果父容器的模式是 AT_MOST 最大模式,那么 View 也是 AT_MOST 最大模式并且大小不会超过父容器的剩余空间
    3. 当 View 的宽高是 wrap_content 时,不管父容器的模式是 EXACTLY 精准模式还是 AT_MOST 最大化,View 的模式总是 AT_MOST 最大化模式,并且不能超过父容器的剩余空间
    4. 关于 UNSPECIFIED 这个模式,勿需关注。

View 的工作流程

measure 过程

1.View 的 measure 过程

(通过 measure 方法即完成了测量过程)


说明:

  1. setMeasuredDimension 方法设置 View 的宽/高的测量值

  2. getDefault 方法返回值有两种情况:

    1. AT_MOST 和 EXACTLY:返回值就是 measureSpec 中的 specSize,这个 sepcSize 就是 View 测量后的大小(View 最终的大小是在 layout 阶段确定的,几乎所有情况下 View 的测量大小和最终大小是相等的)

    2. (对于我们而言,这个情况我们可以直接跳过,因为这是系统内部的测量过程)UNSPECIFIED:该情况下,View 的大小是第一个参数 size,即高宽分别为View.getSuggestMinimumHeight / View.getSuggestMinimumWidth 的返回值。

      而对于 getSuggestMinimunWidth 方法,又有两种情况:

      1. View 没有设置背景:View 的宽度为 mMinWidth,对应为 android:minWidth 属性所指定的值,默认为0
      2. View 指定了背景:View 的宽度为 android:minWidth 和 背景的最小宽度这两者中的最大值。

结论

  • 直接继承 View 的自定义控件需要重写 onMeasure 方法并设置 wrap_content 时的自身大小,否则在布局中使用 wrap_content 就相当于使用 match_parent(这也是为什么 自定义 View 时,需要重写 onMeasure 的原因)。

  • 解决方案:给 View 制定一个 默认的内部宽 / 高(mWidth / mHeight),并在 wrap_content 时设置此高 / 宽即可。

2.ViewGroup 的 measure 过程

(不仅需要完成自己的 measure 过程,还会遍历调用所有子元素的 measure 过程,各个子元素再递归执行这个过程)

说明

  1. ViewGroup 是一个抽象类,没有重写 View 的 onMeasure 方法,而是提供了 measureChildren 方法
  2. measureChildren 方法会对每一个子元素进行 measure,即调用 measureChild 方法
  3. measureChild 的思想就是取出子元素的 LayoutParams,然后通过 getChildMeasureSpec 方法来创建子元素的 MeasureSpec,接着讲 Measurespec 直接传递给 View 的 measure 方法来进行测量
  4. ViewGroup 并没有定义其测量的具体流程,其测量过程的 onMeasure 方法需要各个子类具体实现。
    1. 不统一实现的原因在于不同的 ViewGroup 子类有不同的特性,导致其测量细节不相同。
  5. measure 过程完成后,通过 getMeasuredWidth/Height 方法可以正确的获取到 View 的测量宽/高。需要注意的是,在某些极端情况下(WTF?),系统可能需要多次 measure 才能确定最终的测量宽/高,此时 onMeasure 拿到的测量宽/高可能不准确,比较好的习惯是再 onLayout 方法中获取 View 的测量宽/高或者最终宽/高。

具体问题:如何保证 Activity 启动的时候获取某个 View 的宽/高?

  1. 1个典型错误

    1. onCreate 或者 onResume 方法中直接获取 这个 View 的宽/高
    2. 原因:View 的测量过程和 Activity 的生命周期不同步。
  2. 4种正确解决方案:

    1. View.onWindowFocusChanged

      该方法的含义是:View 已经初始化完毕了,宽/高已经准备好了,这个时候去获取宽高是没有问题的。当 Activity 的窗口得到和失去焦点的时候均会被调用一次。

      @Override
          public void onWindowFocusChanged(boolean hasWindowFocus) {
              super.onWindowFocusChanged(hasWindowFocus);
              if (hasWindowFocus){
                  int width = this.getMeasuredWidth();
                  int height = this.getMeasuredHeight();
              }
          }
      
    2. View.post(runnable)

      通过 post 将一个 runnable 投递到消息队列的尾部,然后等待 Looper 调用此 runnable 的时候,View 也已经初始化好了。

      @Override
          protected void onStart() {
              super.onStart();
              view.post(new Runnable() {
                  @Override
                  public void run() {
                      int width = view.getMeasuredWidth();
                      int height = view.getMeasuredHeight();
                  }
              });
          }
      
    3. ViewTreeObserver

      ViewTreeObserver 的众多回调可以完成这个功能,比如使用 OnGlobalLayoutListener 这个接口,当 View 树的状态发生改变或者 View 树内部的 View 的可见性发生改变时,onGlobalLayout 方法将被回调。需要注意的是,伴随着 View 树改变的改变,onGlobalLayout 会被回调多次。

      @Override
          protected void onStart() {
              super.onStart();
              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();
                  }
              });
          }
      
    4. view.measure(int widthMeasureSpec,int heightMeasureSpec)

      手动进行 measure 过程来得到 View 的宽/高。

      需要考虑 View 的 LayoutParams 分为三种情况:

      1. match_parent:这种情况获取具体宽高是不存在的。因为不知道父容器的剩余空间,所以理论上不可能测量 View 的大小。

      2. 具体的数值(dp/px):

        int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
        int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
        view.measure(widthMeasureSpec,heightMeasureSpec);
        
      3. wrap_content:

        int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
        int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
        view.measure(widthMeasureSpec,heightMeasureSpec);
        

layout 过程

大致流程

  1. 首先通过 setFrame 方法设定 View 的四个顶点位置,此时 View 在父容器中的位置就确定了。
  2. 接着调用该 onLayout 方法,确定子元素的位置,和 onMeasure 方法类似,同样没有真正实现。

说明

  • Layout 的作用是 ViewGroup 用来确定子元素的位置。
  • ViewGroup 的位置确定后,它再 onLayout 种遍历所有的子元素并调用其 layout 方法,在 layout 方法种又会嗲用 onLayout 方法。
  • layout 方法确定 View 把本身的位置,onLayout 方法确定所有子元素的位置。
  • View 的默认实现中,View 的测量宽/高和最终宽/高是相等的,测量宽/高形成于 View 的 measure 过程,最终宽高/形成于 layout 过程

draw 过程

流程

  1. 绘制背景 -- background.draw(canvas)
  2. 绘制自己 -- onDraw
  3. 绘制 Children -- dispatchDraw
  4. 绘制装饰 -- onDrawScrollBars

说明

  1. View 绘制过程的传递是通过 dispatchDraw 来实现的,dispatchDraw 会遍历调用所有子元素的 draw 方法。
  2. setWillNotDraw 方法:当我们的自定义控件继承于 ViewGroup 并且本身不具备绘制功能时,可以开启此标记位从而便于系统进行后续的优化。当明确知道一个 ViewGroup 需要通过 onDraw 来绘制内容时们需要显式地关闭 WILL_NOT_DRAW 这个标记位。

自定义 View

自定义 View 的分类

分为四类:

  1. 继承 View 重写 onDraw 方法
    • 实现一些不规则的效果,重写 onDraw 方法,通过绘制的方法实现。
    • 需要自己支持 wrap_content,并且自己处理 padding
  2. 继承 ViewGroup 派生特殊的 Layout
    • 实现自定义的布局
    • 需要合理处理 ViewGroup 的测量、布局两个过程,同时处理子元素的测量和布局过程
  3. 继承特定的 View(比如 TextView)
    • 扩展某种已有 View 的功能
    • 不需要自己支持 wrap_content 和 padding 等
  4. 继承特定的 ViewGroup(比如 LinearLayout)
    • 实现像几种 View 组合在一起的某种效果。
    • 不需要自己处理 View 的测量和布局这两个过程

自定义 View 须知

具体如下:

  1. 让 View 支持 wrap_content
    • 直接继承 View 或者 ViewGroup 的控件,需要在 onMeasure 中对 wrap_content 做特殊处理,否则 wrap_content 将失效
  2. 如果有必要,让 View 支持 padding
    • 直接继承 View 的控件:在 draw 方法中处理 padding
    • 直接继承 ViewGroup 的控件:在 onMeasure 和 onLayout 中考虑 padding 和子元素的 margin 对其造成的影响,否则二者失效。
  3. 尽量不在 View 中能够使用 Handler,没必要
    • 使用 View.post 系列方法代替 Handler,除非明确要使用 Handler 来发送消息
  4. View 中如果有线程或者动画,及时停止,参考 View.onDetachedFromWindow
    • 如果不处理线程或者动画的停止,那么可能造成内存泄漏
  5. View 带有滑动嵌套情形时,需要处理好滑动冲突。
    • 参见 第3章 如何处理滑动冲突。

自定义 View 示例

详见原书代码吧。。

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

推荐阅读更多精彩内容