第 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 在屏幕上的绘制
这个流程图需要看源码理解一下。DecorView 继承自 FrameLayout,FrameLayout 继承自 ViewGroup,而 ViewGroup 的 测量、布局、绘制过程都会调用它的子 View 的 测量、布局、绘制。这样 View 树的遍历就完成了。
getMeasuredWidth/getMeasuredHeight 在 measure 完成后,可以获取到 View 测量后的宽高
layout 过程决定了 View 四个顶点的坐标和实际 View 的宽高。这个过程完成后 getLeft、getTop、getRight、getBottom、getWidth、getHeight 就有值了
draw 过程决定了 View 的显示,只有此过程完成后,View 的内容才会呈现在屏幕上
DecorView 的结构:
4.2 理解 MeasureSpec
在测量过程中,系统会将 View 的 LayoutParams 根据父容器所施加的规则转换成对应的 MeasureSpec ,然后再根据这个 MeasureSpec 来测量出 View 的宽高。
4.2.1 MeasureSpec
是一个 32 位的 int 值,高 2 位代表 SpecMode,低 30 位代表 SpecSize
SpecMode 是指测量模式,SpecSize 是指在某种测量模式下的规格大小
SpecMode 的三大类
UNSPECIFIED
父容器不对 View 有任何限制。一般用于系统内部,表示一种测量状态
EXACTLY
父容器已经检测出 View 所需要的精确大小,这个时候 View 的最终大小就是 SpecSize 所指定的值。对应于 LayoutParams 中的
match_parent
和具体的数值
这两种模式AT_MOST
父容器指定了一个可用大小即 SpecSize,View 的大小不能大于这个值。对应于 LayoutParams 中的
wrap_content
4.2.2 MeasureSpec 和 LayoutParams 的对应关系
LayoutParams 和 父容器 一起才能决定 View 的 MeasureSpec,从而进一步决定 View 的宽高
MeasureSpec 确定以后,onMeasure 中就可以确定 View 的测量宽高
4.3 View 的工作流程
View 的工作流程主要是指 measure、layout、draw 这三大流程
4.3.1 measure 过程
View 的 meassure 过程
跟踪源码可以看到 View 里边处理到最后,match_parent
和 wrap_content
最后对应的值都是 父容器的 size
所以如果自定义的 View 直接继承自 View 的话,需要重写 onMeasure
方法,对 wrap_content
情况特殊处理,查看系统的 TextView、ImageView,都对 wrap_content
进行特殊处理了。如下:
ViewGroup 的 meassure 过程
启动某个 Activity 时,就获取某个 View 的宽高,在 onCreate 或 onResume 中为什么获取不到?
View 的 measure 过程跟 Activity 生命周期不是同步执行的
解决方案:
Activity/View#onWindowFocusChanged
此方法含义:View 已经初始化完毕了,可以正确的获取宽高。
这个方法会被调用多次
获得/失去焦点、Activity#onResume、onPause
view.post(runnable)
将 runnable 投递到消息队列的尾部,等待 Looper 调用此 runnable 时,View已经初始化好了
ViewTreeObserver
用它的回调方法可以获取宽高,
OnGlobalLayoutListener
当 View 树的状态发生改变或者 View 树内部的 View 的可见性发生变化。(会调用多次)view.measure(int widthMeasureSpec, int heightMeasureSpec)
手动进行 measure 来获取宽高,分情况(根据 View 的 LayoutParams)
match_parent
放弃,拿不到宽高。原因?它需要知道 parentSize 后去构造 MeasureSpec ,但是此时情况特殊无法知道父容器的剩余空间,所以理论上测量不出 View 的大小
具体的数值(dp/px)
wrap_content
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 的分类
继承 View 重写 onDraw 方法
用于实现一些不规则的效果(不方便通过布局组合达到)
需要自己支持 wrap_content ,自己处理 padding
继承 ViewGroup 派生特殊的 Layout
用于实现自定义的布局组合
需要处理 ViewGroup 的测量、布局;子 View 的测量、布局(方式复杂)
继承特定的 View (比如 TextView)
拓展某种已有的 View 的功能,比较常见的方法
不需要自己支持 wrap_content 和 padding 等
继承特定的 ViewGroup (比如 LinearLayout)
用于实现自定义的布局组合
比直接继承 ViewGroup 去实现的方式简单
与方法 2 相比,都能实现功能,不用自己处理 ViewGroup 的测量、布局。方法 2 更接近 View 的底层
同一个自定义 View 实现方式有很多种,我们需要找到代价最小、最高效的方法去实现。
4.4.2 自定义 View 须知
让 View 支持 wrap_content
如果有必要,让你的 View 支持 padding
直接继承 View 的控件,须在 draw 方法处理 padding
直接继承 ViewGroup 的控件,须在 onMeasure、onLayout 中考虑 padding 和 子元素 margin 产生的影响
尽量不要在 View 中使用 Handler,没必要
View 内部有 post 系列方法
也可以使用 Handler,但是必须你很明确的要用 Handler 发消息
有线程或者动画,需要及时停止(View#onDetachedFromWindow)
onDetachedFromWindow
:包含此 View 的 Activity 退出或当前 View 被 removeonAttachedToWindow
:包含此 View 的 Activity 启动时当 View 不可见时,也需要停止线程和动画,防止可能造成的内存溢出
View 有滑动嵌套的话,要处理好滑动冲突
4.4.3 自定义 View 的示例
继承 View 重写 onDraw 方法
处理 wrap_content、padding
提供自定义属性
继承 ViewGroup 派生特殊的 Layout
4.4.4 自定义 View 的思想
掌握基本功:View 的弹性滑动、滑动冲突、绘制原理等
面对新的自定义 View 的时候,能够对其进行分类并选择合适的实现思路
多积累相关经验