【艺术探索笔记】第 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 在屏幕上的绘制

    image
    这个流程图需要看源码理解一下。DecorView 继承自 FrameLayout,FrameLayout 继承自 ViewGroup,而 ViewGroup 的 测量、布局、绘制过程都会调用它的子 View 的 测量、布局、绘制。这样 View 树的遍历就完成了。

  • getMeasuredWidth/getMeasuredHeight 在 measure 完成后,可以获取到 View 测量后的宽高

  • layout 过程决定了 View 四个顶点的坐标和实际 View 的宽高。这个过程完成后 getLeft、getTop、getRight、getBottom、getWidth、getHeight 就有值了

  • draw 过程决定了 View 的显示,只有此过程完成后,View 的内容才会呈现在屏幕上

  • DecorView 的结构:
    image


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 的测量宽高

image


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 进行特殊处理了。如下:

image

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)

        image

      • wrap_content

        image

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

    • 提供自定义属性

      image

  2. 继承 ViewGroup 派生特殊的 Layout

4.4.4 自定义 View 的思想

  • 掌握基本功:View 的弹性滑动、滑动冲突、绘制原理等

  • 面对新的自定义 View 的时候,能够对其进行分类并选择合适的实现思路

  • 多积累相关经验


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值