美文网首页Android自定义View
Android 开发艺术探索笔记之四 -- View 的工作原理

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

作者: whd_Alive | 来源:发表于2018-07-26 12:23 被阅读1次

    学习内容

    • 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 示例

    详见原书代码吧。。

    相关文章

      网友评论

        本文标题:Android 开发艺术探索笔记之四 -- View 的工作原理

        本文链接:https://www.haomeiwen.com/subject/xxfbmftx.html