View 的工作原理

理解 View 的工作原理,熟悉自定义 View 的各种套路。

1. ViewRoot 和 DecorView

  1. ViewRoot 对应于 ViewRootImpl 类,它是连接 WindowManager 和DecorView 的纽带。
  2. View 的绘制流程是从 ViewRoot 的 performtraversals 方法开始的:
    image
  3. measure 的过程决定了 View 的宽高,完成后可以通过 getMeasuredWidth 和 getMeasuredHeight 获取。
  4. layout 的过程决定了 View 的四个顶点的坐标和实际宽高,完成后可以通过getTopgetBottomgetLeftgetRight 获取。
  5. draw 的过程决定了 View 的显示,完成后才能呈现在屏幕上。
  6. 在 Activity 中通过 setContentView 的布局可以通过以下方式获取到:
    1
    ViewGroup content = findViewById(android.R.id.content);

2. MeasureSpec

  1. MeasureSpec 和父容器决定了 View 的尺寸规格。
  2. MeasureSpec 是一个 32 位的 int 值,高 2 位代表 SpecMode,低 30 位代表 SpecSize

    1
    2
    3
    > int mode = MeasureSpec.getMode(spec);
    > int size = MeasureSpec.getSize(spec);
    >
  3. SpecMode 有三类:

    • UNSPECIFIED:父容器不对 View 有任何限制,要多大给多大。
    • EXACTLY:父容器已经检测出 View 所需的精确代销,这时 View 的最终大小就是 SpecSize 的值,它对应于 LayoutParams 中的 match_parent 或具体大小数值。
    • AT_MOST:父容器指定了一个大小 SpecSize,View 不能大于这个值,最终大小取决于 View 的具体实现,它对应于 LayoutParams 中的 wrap_content。
  4. 我们在给 View 设置 LayoutParams 后,系统会在父容器的约束下将 LayoutParams 转换成对应的 MeasureSpec。
    • 顶级 DecorView,其 MeasureSpec 由窗口尺寸和自身 LayoutParams 决定。
    • 普通 View,其 MeasureSpec 由父容器的 MeasureSpec 和自身 LayoutParams 决定。
  5. 普通 View 的 MeasureSpec 创建规则:
childLayoutParams \ parentSpecMode EXACTLY AT_MOST UNSPECIFIED
dp/px EXACTLY
childSize
EXACTLY
childSize
EXACTLY
childSize
match_parent EXACTLY
parentSize
AT_MOST
parentSize
UNSPECIFIED
0
wrap_content AT_MOST
parentSize
AT_MOST
parentSize
UNSPECIFIED
0

3. View 的工作流程

3.1 measure 过程
  1. 分两种情况,如果是一个原始 View,通过 measure 方法就完成了测量过程;如果是 ViewGroup,除了完成自己的测量,还要遍历子元素的 measure 方法。
  2. 直接继承 View 的自定义控件需要重写 onMeasure 方法并设置 wrap_content 时的自身大小,否则在布局中使用 wrap_content 时就相当于使用 match_parent;
  3. ViewGroup 是一个抽象类,并没有定义其具体的测量过程,但它提供了一个 measureChildren(int widthMeasureSpec, int heightMeasureSpec) 的方法来测量子元素,该方法里循环调用 measureChild(child, widthMeasureSpec, heightMeasureSpec) 方法,measureChild 的思想就是取出元素的 LayoutParams,然后通过 getChildMeasureSpec 方法创建子元素的 MeasureSpec,最后将 MeasureSpec 传递给 View 的 measure 方法来进行测量。
  4. 简化版的流程图如下:
    image
  5. 由于在一些极端情况下,系统要在多次 measure 后才能确定测量的结果,所以在 onMeasure 中拿到的宽高可能不准确,推荐在 onLayout 中获取。
  6. 准确获取 View 宽高的四种方式:

    • Activity/View#onWindowFocusChanged
    • view.post(runnable)
    • ViewTreeObserver

      1
      2
      3
      4
      ...
      ViewTreeObserver observer = view.getViewTreeObserver();
      observer.addOnGlobalLayoutListener
      ...
    • view.measure(int widthMeasureSpec, int heightMeasureSpec)

3.2 layout 过程
  1. View 的 onLayout 方法为空实现,而 ViewGroup 继承自 View 也没有实现该方法,所以 onLayout 具体实现由父容器的特性决定,比如 LinearLayout。
  2. 简易的流程图如下:
    image
  3. 在 View 的默认实现中,View 的测量宽高和最终宽高是相等的,只不过测量宽高形成于 measure 过程,而最终宽高形成于 layout 过程,两者赋值时机不同。
3.3 draw 过程
  1. draw 的绘制过程遵循如下几步:
    1. 绘制背景 background.draw(canvas)
    2. 绘制自己(onDraw
    3. 绘制 children(dispatchDraw
    4. 绘制装饰(onDrawScrollBars
  2. ViewGroup 默认启用绘制优化标记位,如果 ViewGroup 需要通过 onDraw 来绘制内容,我们可以通过 setWillNotDraw(false); 来关闭该标记位。

4. 自定义 View

  1. 自定义 View 大体可以分为四类:
    1. 继承 View 重写 onDraw 方法
    2. 继承 ViewGroup 派生特殊的 Layout
    3. 继承特定的 View(比如 TextView)
    4. 继承特定的 ViewGroup(比如 LinearLayout)
  2. 自定义 View 须知:
    1. 让 View 支持 wrap_content
    2. 如果有必要让你的 View 支持 padding
    3. 尽量不要在 View 中使用 Handler,没必要,因为其内部就提供了 post 方法
    4. View 中如果有线程或者动画,需要及时停止,参考 View#onDetachedFromWindow
    5. View 带有滑动嵌套情形时,需要处理好滑动冲突

本章节配套源码戳我