【艺术探索笔记】第 4 章 View 的工作原理
第 4 章 View 的工作原理
- 测量、布局、绘制
- 熟练掌握回调方法:
- onAttach、onVisibilityChanged、onDetach 等
- 自定义 View 的固定类型:
- 直接继承 View 和 ViewGroup
- 继承现有的系统控件
- 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.1 MeasureSpec
- 是一个 32 位的 int 值,高 2 位代表 SpecMode,低 30 位代表 SpecSize
- SpecMode 是指测量模式,SpecSize 是指在某种测量模式下的规格大小
UNSPECIFIED
父容器不对 View 有任何限制。一般用于系统内部,表示一种测量状态
EXACTLY
父容器已经检测出 View 所需要的精确大小,这个时候 View 的最终大小就是 SpecSize 所指定的值。对应于 LayoutParams 中的match_parent
和具体的数值
这两种模式
AT_MOST
父容器指定了一个可用大小即 SpecSize,View 的大小不能大于这个值。对应于 LayoutParams 中的wrap_content
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
文章图片
- match_parent
- Activity/View#onWindowFocusChanged
研究源码后
* layout 方法(View 中)
1. setFrame 设定 View 的四个定点位置(mLeft、mRight、mTop、mBottom),这时候 View 在父容器中的位置就确定了2. 调用 onLayout,用于父容器确定子元素的位置
- onLayout 方法(LinearLayout 为例)
遍历所有子元素并调用 setChildFrame 方法来为子元素指定对应的位置,setLayoutFrame 方法调用了子元素的 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 绘制内容时,需要显式的关闭这个标记位
- 继承 View 重写 onDraw 方法
- 用于实现一些不规则的效果(不方便通过布局组合达到)
- 需要自己支持 wrap_content ,自己处理 padding
- 用于实现一些不规则的效果(不方便通过布局组合达到)
- 继承 ViewGroup 派生特殊的 Layout
- 用于实现自定义的布局组合
- 需要处理 ViewGroup 的测量、布局;子 View 的测量、布局(方式复杂)
- 用于实现自定义的布局组合
- 继承特定的 View (比如 TextView)
- 拓展某种已有的 View 的功能,比较常见的方法
- 不需要自己支持 wrap_content 和 padding 等
- 拓展某种已有的 View 的功能,比较常见的方法
- 继承特定的 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 的控件,须在 draw 方法处理 padding
- 尽量不要在 View 中使用 Handler,没必要
- View 内部有 post 系列方法
- 也可以使用 Handler,但是必须你很明确的要用 Handler 发消息
- View 内部有 post 系列方法
- 有线程或者动画,需要及时停止(View#onDetachedFromWindow)
onDetachedFromWindow
:包含此 View 的 Activity 退出或当前 View 被 remove
onAttachedToWindow
:包含此 View 的 Activity 启动时
- 当 View 不可见时,也需要停止线程和动画,防止可能造成的内存溢出
- View 有滑动嵌套的话,要处理好滑动冲突
- 继承 View 重写 onDraw 方法
- 处理 wrap_content、padding
提供自定义属性
文章图片
- 处理 wrap_content、padding
- 继承 ViewGroup 派生特殊的 Layout
- 掌握基本功:View 的弹性滑动、滑动冲突、绘制原理等
- 面对新的自定义 View 的时候,能够对其进行分类并选择合适的实现思路
- 【【艺术探索笔记】第 4 章 View 的工作原理】多积累相关经验
推荐阅读
- 宽容谁
- 我要做大厨
- 增长黑客的海盗法则
- 画画吗()
- 2019-02-13——今天谈梦想()
- 远去的风筝
- 三十年后的广场舞大爷
- 叙述作文
- 20190302|20190302 复盘翻盘
- 学无止境,人生还很长