0%

View 的工作原理(三):View 的工作流程

1. View 的工作流程的概念

  • View 的工作流程就是 measurelayoutdraw 这三大流程,即测量布局绘制
  • measure 确定 View 的测量宽高,layout 确定 View 的最终宽高四个顶点的位置,draw 将 View 绘制到屏幕上

2. measure 过程的分类

  • 对于原始的 View: 通过 measure 方法就完成了其测量过程(measure 过程是三大流程中最复杂的一)
  • 对于 ViewGroup: 除了完成自己的测量过程外,还会遍历去调用所有子元素的 measure 方法,各个子元素再递归去执行这个流程

3. View 的 measure 过程的大致流程

  1. View 的 measure 过程由其 measure() 方法完成,measure() 方法是一个 final 方法,即子类不能重写该方法。在 View 的 measure() 方法中会去调用 View 的 onMeasure() 方法:

    1
    2
    3
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
  2. setMeasuredDimension() 方法会设置 View 宽高的测量值,只需关注 getDefaultSize() 方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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 MeasureSpce.EXACTLY:
    result = specSize;
    break;
    }
    return result;
    }
    • 只需要看 AT_MOST 和 EXACTLY 两种情况。简单理解,getDefaultSize() 方法返回的大小就是 measureSpec 中的 specSize,而这个 specSize 就是 View 测量后的大小(这里多次提到测量后的大小,是因为 View 最终的大小是在 layout 阶段确定的,不过几乎所有情况下 View 的测量大小和最终大小是相等的
    • UNSPECIFIED 的情况,一般用于系统内部的测量过程,此时 View 的大小为 getDefaultSize() 方法的第一个参数 size,即宽高分别为 getSuggestedMinimumWidth()getSuggestedMinimumHeight() 这两个方法的返回值
  3. getSuggestedMinimumWidth()getSuggestedMinimumHeight() 这两个方法的源码:

    1
    2
    3
    protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }
    1
    2
    3
    protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
    }
    • 只分析 getSuggestedMinimumWidth() 方法,getSuggestedMinimumHeight() 同理
    • 如果 View 没有设置背景,那么 View 的宽度即为 mMinWidth,mMinWidth 对应于 android:minWidth 属性所指定的值。这个属性如果不指定,则 mMinWidth 默认为 0
    • 如果 View 指定了背景,则 View 的宽度为 max(mMinWidth, mBackground.getMinimumWidth()),即 android:minWidth 和背景的最小宽度这两者中的最大值
  4. Drawable 的 getMinimumWidth() 方法源码:

    1
    2
    3
    4
    public int getMinimumWidth() {
    final int intrinsicWidth = getIntrinsicWidth();
    return intrinsicWidth > 0 ? intrinsicWidth : 0;
    }
    • 返回值就是 Drawable 的原始宽度,前提是这个 Drawable 有原始宽度,否则返回 0
    • 举例,ShapeDrawable 没有原始宽高BitmapDrawable 有原始宽高(图片的尺寸)
  5. getDefaultSize() 方法的实现来看,View 的宽高由 specSize 决定,可以得出结论:直接继承 View 的自定义控件需要重写 onMeasure() 方法并设置 wrap_content 时的自身大小,否则在布局中使用 wrap_content 就相当于使用 match_parent(具体解释可查看 P185)

4. ViewGroup 的 measure 过程的大致流程

  1. ViewGroup 除了完成自己的 measure 过程以外,还会遍历去调用所有子元素的 measure 方法,各个子元素再递归去执行这个过程

  2. ViewGroup 是一个抽象类,没有重写 View 的 onMeasure() 方法,但提供了 measureChildren() 方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;

    for (int i = 0; i < size; ++i) {
    final View child = children[i];
    if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
    measureChild(child, widthMeasureSpec, heightMeasureSpec);
    }
    }
    }
    • 从上面代码可看出,ViewGroup 在 measure 时,会对每一个子元素进行 measure
  3. measureChild() 方法实现:

    1
    2
    3
    4
    5
    6
    7
    8
    protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parenWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    • 从上面代码可看出,measureChild() 的思想就是取出子元素的 LayoutParams,然后再通过 getChildMeasureSpec() 来创建子元素的 MeasureSpec,接着将 MeasureSpec 直接传递给 View 的 measure 方法来进行测量
  4. ViewGroup 并没有定义其测量的具体过程,这是因为 ViewGroup 是一个抽象类,其测量过程的 onMeasure() 方法需要各个子类去具体实现,onMeasure() 方法没有统一实现的原因是因为不同的 ViewGroup 子类有不同的布局特性,导致测量细节各不相同(LinearLayout 的 onMeasure() 方法的源码分析请查看 P187)

  5. 实际使用中,在 Activity 的 onCreate()onStart()onResume() 方法中均无法正确得到某个 View 的宽高信息

    • 这是因为 View 的 measure 过程和 Activity 的生命周期方法不是同步执行的
    • 因此无法保证 Activity 执行了 onCreate()onStart()onResume() 方法时某个 View 已经测量完毕,如果 View 还没有测量完毕,那么获得的宽高就是 0
  6. 实际开发中,解决问题 5 的四种方法:

    1. Activity/View#onWindowFocusChanged():

      • onWindowFocusChanged() 方法的含义:View 已经初始化完毕了,宽高已经准备好了,这个时候去获取宽高是没问题的

      • onWindowFocusChanged() 方法会被调用多次,当 Activity 的窗口得到焦点和失去焦点时均会被调用一次

      • Activity 继续执行和暂停执行时,onWindowFocusChanged() 方法均会被调用,如果频繁得进行 onResume()onPause(),那么 onWindowFocusChanged() 也会被频繁调用

      • 典型代码:

        1
        2
        3
        4
        5
        6
        7
        public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus) {
        int width = view.getMeasuredWidth();
        int height = view.getMeasuredHeight();
        }
        }
    2. view.post(runnable):

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

      • 典型代码:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        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 宽高的一个很好的时机

      • 需要注意的是,伴随着 View 的状态改变等,onGlobalLayout() 会被调用多次

      • 典型代码:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        protected void onStart() {
        super.onStart();

        ViewTreeObserver observer = view.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
        @SuppressWarnings("deprecation")
        @Override
        public void onGlobalLayout() {
        view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
        int width = view.getMeasuredWidth();
        int height = view.getMeasuredHeight();
        }
        });
        }
    4. view.measure(int widthMeasureSpec, int heightMeasureSpec): 通过手动对 View 进行 measure 来得到 View 的宽高。这种方法比较复杂,要分情况处理,根据 View 的 LayoutParams 来分:

      • mach_parent: not work,因为构造此种 MeasureSpec 需要知道 parentSize,即父容器的剩余空间,而此时无法知道 parentSize 的大小,所以理论上不可能测量出 View 的大小

      • wrap_content:

        1
        2
        3
        int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
        int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
        view.measure(widthMeasureSpec, heightMeasureSpec);
        • 注意到 (1 << 30)-1,由 MeasureSpec 的实现可以知道,View 的尺寸使用 30 位二进制表示,也就是最大是 30 个 1(2^30-1),即 (1 << 30)-1
        • 在最大化模式下,用 View 理论上能支持的最大值去构造 MeasureSpec 是合理的
      • 具体的数值(dp/px):

        1
        2
        3
        4
        // 举例,比如宽高都是 100px
        int widthMesaureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
        int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
        view.measure(widthMeasureSpec, heightMeasureSpec);
      • 关于 View 的 measure,有两个错误的用法。首先其违背了系统的内部实现规范(因为无法通过错误的 MeasureSpec 去得出合法的 SpecMode,从而导致 measure 过程出错),其次不能保证一定能 measure 出正确的结果

        • 第一种错误用法

          1
          2
          3
          int widthMeasureSpec = MeasureSpec.makeMeasureSpec(-1, MeasureSpec.UNSPECIFIED);
          int heightMeasureSpec = MeasureSpec.makeMeasureSpec(-1, MeasureSpec.UNSPECIFIED);
          view.measure(widthMeasureSpec, heightMeasureSpec);
        • 第二种错误用法

          1
          view.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);

5. View 的 layout 过程的大致流程

  1. layout() 方法确定 View 本身的位置layout() 方法中的 onLayout() 方法确定所有子元素的位置

  2. layout() 方法的大致流程:

    1. 首先会通过 setFrame() 方法来设定 View 的四个顶点的位置,即初始化 mLeft、mRight、mTop 和 mBottom 这四个值。View 的四个顶点一旦确定,那么 View 在父容器中的位置也就确定了
    2. 接着会调用 onLayout() 方法,这个方法的作用是父容器确定子元素的位置
    3. onMeasure() 方法类似,onLayout() 方法的具体实现和具体的布局有关,所以 View 和 ViewGroup 均没有真正实现 onLayout() 方法(LinearLayout 的 onLayout() 方法的源码分析请查看 P194)
  3. 问题:View 的测量宽高和最终宽高的区别?(具体为:View 的 getMeasuredWidth()getWidth() 这两个方法的区别?)

    • 在 View 的默认实现中,View 的测量宽高和最终宽高是相等的,只不过测量宽高形成于 View 的 measure 过程,最终宽高形成于 View 的 layout 过程,即两者的赋值时机不同,测量宽高的赋值时机稍早一些
    • 日常开发中,可以认为 View 的测量宽高就等于最终宽高
    • 特殊情况下,测量宽高可以不等于最终宽高
      • 一种情况是重写 View 的 layout() 方法加入自己的逻辑可能导致不等
      • 一种情况是某些场景下,View 需要多次 measure 才能确定自己的测量宽高,在前几次测量过程中可能导致不等,但最终测量宽高和最终宽高相同

6. View 的 draw 过程的大致流程

  1. draw 过程的作用是将 View 绘制到屏幕上

  2. 分析 draw 方法源码可以看出,View 的绘制过程遵循如下几步

    1. 绘制背景(background.draw(canvas)
    2. 绘制自己(onDraw()
    3. 绘制 children(dispatchDraw()
    4. 绘制装饰(onDrawScrollBars()
  3. View 绘制过程的传递是通过 dispatchDraw() 方法来实现的,dispatchDraw() 方法会遍历调用所有子元素的 draw() 方法

  4. View 有一个特殊的方法 setWillNotDraw(),源码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /**
    * If this view doesn't do any drawing on its own, set this flag to
    * allow further optimizations. By default, this flag is not set on
    * View, but could be set on some View subclasses such as ViewGroup.
    *
    * Typically, if you override (@link #onDraw(android.graphics.Canvas))
    * you shoul clear this flag.
    *
    * @param willNotDraw whether or not this View draw on its own
    */
    public void setWillNotDraw(boolean willNotDraw) {
    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }
    • 由注释可知,如果一个 View 不需要绘制任何内容,那么设置这个标记位为 true 以后,系统会进行相应的优化
    • 默认情况下,View 没有启用这个优化标记位,但是 ViewGroup 会默认启用这个优化标记位
    • 这个标记位对实际开发的意义是:当自定义的控件继承自 ViewGroup 并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化
    • 当明确知道一个 ViewGroup 需要通过 onDraw() 方法来绘制内容时,需要显示关闭 WILL_NOT_DRAW 这个标记位
-------------------- 本文结束感谢您的阅读 --------------------