View的工作原理

这篇主要是我认为《Android开发艺术探索》第四章的重点,所以建议结合任老师的书来看,否则可能会觉得不知所云,没写的并不是说明不重要,而是我没有意识到重要性或者是我已经掌握的知识。

View的流程主要包括测量流程(measure)、布局流程(layout)、绘制流程(draw)。

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

理解MeasureSpec

performTraversals 会依次调用 performMeasure、performLayout 和 performDraw 三个方法,这三个方法分别完成顶级 View 的 measure、layout 和 draw 这三大流程,其中在 performMeasure 中会调用 measure 方法,在 measure 方法中会调用 onMeasure 方法,在 onMeasure 方法中则会对所有的子元素进行 measure 过程,这时 measure 流程就从父容器传递到子元素中了,这样就完成了一次 measure 过程。接着子元素会重复父容器的 measure 过程,如此反复就完成了整个 View 树的遍历。同理, perforLayout 和 performDraw 的传递流程和 perforMeasure 是类似的,唯一不同的是,perforDraw 的传递过程是在 draw 方法中通过 dispatchDraw 来实现的。

SpecMode 有三类

  • UNSPECIFIED

    父容器不对 View 有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。

  • EXACTLY

    父容器已经检测出 View 所需要的精确大小,这个时候 View 的最终大小就是 SpecSize 所指定的值。它对应于 LayoutParams 中的match_parent 和具体的数值这两种模式。

  • AT_MOST

    父容器指定了一个可用大小即 SpecSize,View 的大小不能大于这个值,具体是什么值要看不同 View 的具体实现。它对应于 LayoutParams 中的 wrap_content。

对于 DecorView ,其 MeasureSpec 由窗口的尺寸和其自身的 LayoutParams 共同确定;对于普通 View ,其 MesaureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定,MeasureSpec 一旦确定后,onMeasure 中就可以确定 View 的测量宽/高。

对于普通 View 的测量过程,可以查看 ViewGroup 的 getChildMeasureSpec 方法,如果不知道怎么找的话,可以在AndroidStudio写一下,然后跳转过去就可以了。如果不方便看(就是懒得看源码),可以参考下面的表格,是根据 getChildMeasureSpec 整理的。

View的工作原理_第1张图片
普通 View 的MeasureSpec 创建规则.PNG

View 的工作流程

measure过程

问:直接继承 View 的自定义控件需要重写 onMeasure 方法并设置 wrap_content时的自身大小,否则在布局中使用 wrap_content 就相当于使用 match_parent。

答:如果 View 在布局中使用 wrap_content ,那么它的 specMode 是 AT_MOST 模式,在这种模式下,它的宽/高等于 specSize,而此时 view 的 specSize 是 parentSize,而 parentSize 是父容器中目前可以使用的大小,也就是父容器当前剩余的空间大小,即 View 的宽/高就等于父容器当前剩余的空间大小,这种效果和在布局中使用 match_parent 完全一致。

解决方法: 给 View 指定一个默认的内部宽/高( mWidth,mHeight ),并在 wrap_content 时设置此宽/高即可,对于非 wrap_content 情形,沿用系统的测量值。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, mHeight);
        }
        else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, heightSpecSize);
        }
        else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, mHeight);
        }

    }

建议在 onLayout 方法中去获取 View 的测量宽/高或者最终宽/高。

问:在 Activity 已启动的时候获取某个 View 的宽/高。

答:
Activity/View # onWindowFocusChanged
onWindowFocusChanged 会被调用多次,当 Activity 的窗口得到焦点和失去焦点时均会被调用一次。

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus) {
            int width = mContainer.getMeasuredWidth();
            int height = mContainer.getMeasuredHeight();
        }
    }

mContainer 为需要测量的 View。

view.post(runnable)

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

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

ViewTreeObserver

使用 ViewTreeObserver 的众多回调都可以完成这个功能,例如使用 OnGlobalLayoutListener 这个接口,当 View 树的状态发生改变或者 View 树内部的 View 的可见效发生改变时, onGlobalLayout 方法将被回调,因此这是获取 View 的宽/高一个很好的时机,伴随着 View 树的状态改变等, onGlobalLayout 会被调用多次。


    @Override
    protected void onStart() {
        super.onStart();

        ViewTreeObserver treeObserver = mContainer.getViewTreeObserver();
        treeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                mContainer.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                int width = mContainer.getMeasuredWidth();
                int height = mContainer.getMeasuredHeight();
            }
        });
    }

view.measure(int widthMeasureSpec, int heightMeasureSpec)

通过手动对 View 进行 measure 来得到 View 的宽/高,需要根据 View 的 LayoutParams 来分:

  • match_parent
    无法 measure 出具体的宽/高。根据 view 的 measure 过程,构造此种 MeasureSpec 需要知道 parentSize,即父容器的剩余空间,而此时我们无法知道 parentSize 的大小,所以理论上不可能测量出 View 的大小。

  • 具体数值(dp/px)
    比如宽高都是100px,如下 measure:

        int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
        int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
        mContainer.measure(widthMeasureSpec,heightMeasureSpec);

  • wrap_content
    如下measure:
        int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);
        int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);
        mContainer.measure(widthMeasureSpec,heightMeasureSpec);

注意到 (1 << 30) - 1 ,通过分析 MeasureSpec 的实现可以知道,View 的尺寸使用 30 位二进制表示,也就是说最大时30个1,即2^30 -1,也就是(1 << 30) - 1,在最大化模式下,我们用 View 理论上能支持的最大值去构造 MeasureSpec 是合理的。

layout 过程

Layout 的作用是 ViewGroup 用来确定子元素的位置,当 ViewGroup 的位置被确定后,它在 onLayout 中会遍历所有的子元素并调用其 layout 方法,在 layout 方法中 onLayout 方法又会被调用,layout 方法确定 View 本身的位置,而 onLayout 方法则会确定所有子元素的位置。

问:View 的 getMeasuredWidth 和 getWidth的区别

答:在 View 的默认实现中, View 的测量宽/高和最终宽/高是相等的,只不过测量宽/高形成于 View 的 measure 过程,而最终宽/高形成于 View 的 layout 过程,即两者的赋值时机不同,测量宽/高的赋值时机稍微早一些。

draw 过程

draw 的作用是将 View 绘制到屏幕上面,View 的绘制过程遵循如下步骤:

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

View 中有一个特殊的方法 setWillNotDraw,如果一个 View 不需要绘制任何内容,那么设置这个标记为为 true 以后,系统会进行相应的优化。默认情况下, View 没有启动这个优化标记位,但是 ViewGroup 会默认启用这个优化标记位。这个标记位对实际开发的意义是:当自定义控件继承于 ViewGroup 并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。当然,当明确知道一个 ViewGroup 需要通过 onDraw 来绘制内容时,我们需要显式地关闭 WILL_NOT_DRAW 这个标记位。

你可能感兴趣的:(View的工作原理)