【艺术探索笔记】第 4 章 View 的工作原理

第 4 章 View 的工作原理

  • 测量、布局、绘制
  • 熟练掌握回调方法:
    • onAttach、onVisibilityChanged、onDetach 等
  • 自定义 View 的固定类型:
    • 直接继承 View 和 ViewGroup
    • 继承现有的系统控件
4.1 初识 ViewRoot 和 DecorView
  • ViewRoot 对应于 ViewRootImpl 类,它是连接 WindowManager 和 DecorView 的纽带,View 的三大流程均是通过 ViewRoot 来完成的。
  • 在 ActivityThread 中,当 Activity 对象被创建完毕后,会将 DecorView 添加到 Window 中,同时会创建 ViewRootImpl 对象,并将ViewRootImpl 对象和 DecorView 建立关联:
    //ActivityThread#handleResumeActivity -> WindowManagerImpl#addView -> WindowManagerGlobal#addView root = new ViewRootImpl(view.getContext(), display); root.setView(view, wparams, panelParentView);

  • View 的绘制流程是从 ViewRoot 的 performTraversals 方法开始的,经过 measure、layout、draw 最终绘制出 View
    • measure 测量 View 宽高
    • layout 确定 View 在父容器中的放置位置
    • draw 负责 View 在屏幕上的绘制
    【艺术探索笔记】第 4 章 View 的工作原理
    文章图片

    这个流程图需要看源码理解一下。DecorView 继承自 FrameLayout,FrameLayout 继承自 ViewGroup,而 ViewGroup 的 测量、布局、绘制过程都会调用它的子 View 的 测量、布局、绘制。这样 View 树的遍历就完成了。
  • getMeasuredWidth/getMeasuredHeight 在 measure 完成后,可以获取到 View 测量后的宽高
  • layout 过程决定了 View 四个顶点的坐标和实际 View 的宽高。这个过程完成后 getLeft、getTop、getRight、getBottom、getWidth、getHeight 就有值了
  • draw 过程决定了 View 的显示,只有此过程完成后,View 的内容才会呈现在屏幕上
  • DecorView 的结构:
    【艺术探索笔记】第 4 章 View 的工作原理
    文章图片

4.2 理解 MeasureSpec 在测量过程中,系统会将 View 的 LayoutParams 根据父容器所施加的规则转换成对应的 MeasureSpec ,然后再根据这个 MeasureSpec 来测量出 View 的宽高。
4.2.1 MeasureSpec
  • 是一个 32 位的 int 值,高 2 位代表 SpecMode,低 30 位代表 SpecSize
  • SpecMode 是指测量模式,SpecSize 是指在某种测量模式下的规格大小
SpecMode 的三大类
  1. UNSPECIFIED
    父容器不对 View 有任何限制。一般用于系统内部,表示一种测量状态
  2. EXACTLY
    父容器已经检测出 View 所需要的精确大小,这个时候 View 的最终大小就是 SpecSize 所指定的值。对应于 LayoutParams 中的 match_parent具体的数值这两种模式
  3. AT_MOST
    父容器指定了一个可用大小即 SpecSize,View 的大小不能大于这个值。对应于 LayoutParams 中的 wrap_content
4.2.2 MeasureSpec 和 LayoutParams 的对应关系
LayoutParams 和 父容器 一起才能决定 View 的 MeasureSpec,从而进一步决定 View 的宽高
MeasureSpec 确定以后,onMeasure 中就可以确定 View 的测量宽高
【艺术探索笔记】第 4 章 View 的工作原理
文章图片

4.3 View 的工作流程 View 的工作流程主要是指 measure、layout、draw 这三大流程
4.3.1 measure 过程
View 的 meassure 过程 跟踪源码可以看到 View 里边处理到最后,match_parentwrap_content 最后对应的值都是 父容器的 size
所以如果自定义的 View 直接继承自 View 的话,需要重写 onMeasure 方法,对 wrap_content 情况特殊处理,查看系统的 TextView、ImageView,都对 wrap_content 进行特殊处理了。如下:
【艺术探索笔记】第 4 章 View 的工作原理
文章图片

ViewGroup 的 meassure 过程
  • 启动某个 Activity 时,就获取某个 View 的宽高,在 onCreate 或 onResume 中为什么获取不到?
    View 的 measure 过程跟 Activity 生命周期不是同步执行的
    解决方案:
    1. Activity/View#onWindowFocusChanged
      此方法含义:View 已经初始化完毕了,可以正确的获取宽高。
      这个方法会被调用多次 获得/失去焦点、Activity#onResume、onPause
    2. view.post(runnable)
      将 runnable 投递到消息队列的尾部,等待 Looper 调用此 runnable 时,View已经初始化好了
    3. ViewTreeObserver
      用它的回调方法可以获取宽高, OnGlobalLayoutListener 当 View 树的状态发生改变或者 View 树内部的 View 的可见性发生变化。(会调用多次)
    4. view.measure(int widthMeasureSpec, int heightMeasureSpec)
      手动进行 measure 来获取宽高,分情况(根据 View 的 LayoutParams)
      • match_parent
        放弃,拿不到宽高。原因?它需要知道 parentSize 后去构造 MeasureSpec ,但是此时情况特殊无法知道父容器的剩余空间,所以理论上测量不出 View 的大小
      • 具体的数值(dp/px)
        【艺术探索笔记】第 4 章 View 的工作原理
        文章图片

      • wrap_content
        【艺术探索笔记】第 4 章 View 的工作原理
        文章图片

4.3.2 layout 过程
研究源码后
* layout 方法(View 中)
1. setFrame 设定 View 的四个定点位置(mLeft、mRight、mTop、mBottom),这时候 View 在父容器中的位置就确定了2. 调用 onLayout,用于父容器确定子元素的位置

  • onLayout 方法(LinearLayout 为例)
    遍历所有子元素并调用 setChildFrame 方法来为子元素指定对应的位置,setLayoutFrame 方法调用了子元素的 layout 方法。
一层一层传递后就完成了整个 View 树的 layout 过程
测量宽高 和 最终宽高的区别:
系统默认实现中,这两个方式获取的值是相等的,只是形成的时机不一样。测量宽高形成于 measure 过程;最终宽高形成于 layout 过程。
一般来说这两个值是相等的,除非你重写 layout 方法,在调用父类 layout 方法时把宽高值改变。(但是这样做好像没啥实际意义)
4.3.3 draw 过程
View 的绘制过程遵循如下几步(查看 View 源码):
1. 绘制背景 background.draw(canvas)
2. 绘制自己(onDraw)
3. 绘制 children (dispatchDraw)
4. 绘制装饰(onDrawForeground)
View 绘制过程的传递是通过 dispatchDraw 来实现的。遍历所有子元素的 Draw 方法
setWillNotDraw(boolean willNotDraw)
  • 如果一个 View 不需要绘制任何内容,可以设置 true ,系统会进行相应的优化。
  • View 默认不启用此标记位,但 ViewGroup 默认会启用此标记位。
  • 实际开发的意义:继承自 ViewGroup 的自定义 View 不具备绘制功能时,开启这个标记位便于系统进行后续优化。当明确知道一个 ViewGroup 需要通过 onDraw 绘制内容时,需要显式的关闭这个标记位
4.4 自定义 View 4.4.1 自定义 View 的分类
  1. 继承 View 重写 onDraw 方法
    • 用于实现一些不规则的效果(不方便通过布局组合达到)
    • 需要自己支持 wrap_content ,自己处理 padding
  2. 继承 ViewGroup 派生特殊的 Layout
    • 用于实现自定义的布局组合
    • 需要处理 ViewGroup 的测量、布局;子 View 的测量、布局(方式复杂)
  3. 继承特定的 View (比如 TextView)
    • 拓展某种已有的 View 的功能,比较常见的方法
    • 不需要自己支持 wrap_content 和 padding 等
  4. 继承特定的 ViewGroup (比如 LinearLayout)
    • 用于实现自定义的布局组合
    • 比直接继承 ViewGroup 去实现的方式简单
    • 与方法 2 相比,都能实现功能,不用自己处理 ViewGroup 的测量、布局。方法 2 更接近 View 的底层
同一个自定义 View 实现方式有很多种,我们需要找到代价最小、最高效的方法去实现。
4.4.2 自定义 View 须知
  1. 让 View 支持 wrap_content
  2. 如果有必要,让你的 View 支持 padding
    • 直接继承 View 的控件,须在 draw 方法处理 padding
    • 直接继承 ViewGroup 的控件,须在 onMeasure、onLayout 中考虑 padding 和 子元素 margin 产生的影响
  3. 尽量不要在 View 中使用 Handler,没必要
    • View 内部有 post 系列方法
    • 也可以使用 Handler,但是必须你很明确的要用 Handler 发消息
  4. 有线程或者动画,需要及时停止(View#onDetachedFromWindow)
    • onDetachedFromWindow:包含此 View 的 Activity 退出或当前 View 被 remove
    • onAttachedToWindow:包含此 View 的 Activity 启动时
    • 当 View 不可见时,也需要停止线程和动画,防止可能造成的内存溢出
  5. View 有滑动嵌套的话,要处理好滑动冲突
4.4.3 自定义 View 的示例
  1. 继承 View 重写 onDraw 方法
    • 处理 wrap_content、padding
    • 提供自定义属性
      【艺术探索笔记】第 4 章 View 的工作原理
      文章图片

  2. 继承 ViewGroup 派生特殊的 Layout
4.4.4 自定义 View 的思想
  • 掌握基本功:View 的弹性滑动、滑动冲突、绘制原理等
  • 面对新的自定义 View 的时候,能够对其进行分类并选择合适的实现思路
  • 【【艺术探索笔记】第 4 章 View 的工作原理】多积累相关经验

    推荐阅读