学习内容
- View 基础概念
- 自定义 View
- View 的底层工作原理
- 测量流程
- 布局流程
- 绘制流程
- View 常见回调
- 自定义 View
- 类型
- 滑动效果
初识 ViewRoot 和 DecorView
基本概念
-
ViewRoot
对应于 ViewRootImpl 类,是连接 Window Manager 和 DecorView 的纽带。
view 的三大流程均是通过 ViewRoot 来完成的,在 ActivityThread 中,当 Activity 对象创建完毕后,会将 DecorView 添加到 Window 中,同时会创建 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 建立关联。
View 的绘制流程从 ViewRoot 的 performTraversals 方法开始,经过 measure,layout 和 draw 三个过程最终将一个 View 绘制出来,其中 measure 用来测量 View 的宽和高,layout 用来确定 View 在父容器中的放置位置,而 draw 负责讲 View 绘制在屏幕上。
-
performTraversals 调用流程如下:
onMeasure 方法中会对所有子元素进行 measure 过程。子元素重复父容器的 measure 过程,反复完成整个 View 树的遍历。preformLayout 和 preformDraw 方法类似,只不过 preformDraw 的传递过程是在 draw 方法总通过 dispatchDraw 实现的。
Measure 过程决定了 View 的宽 / 高,完成后可通过 getMeasuredWidth / getMeasureHeight 方法或许 View 测量后的 宽 / 高(绝大多数时等同于 View 最终的宽 / 高);
Layout 过程决定了 View 的四个顶点坐标和实际的 View 的宽 / 高,完成后可通过 getTop、getBottom、getLeft、getRight 拿到 View 的四个顶点坐标,以及 getWidth 和 getHeight 方法拿到 View 的最终宽 / 高。
Draw 过程决定了 View 的显示,只有 draw 方法完成以后 View 的内容才能显示在屏幕上。
-
DecorView
如下图,DecorView 作为顶级 View,一般情况下包含一个竖直方向的 LinearLayout。
-
在 Activity 中通过 setContentView 设置的布局文件实际上就是被加到内容栏中。
//获取内容栏 content ViewGroup content = (ViewGroup) findViewById(android.R.id.content); //获取设置的 View content.getChild(0);
DecorView 实际是一个 FrameLyout,View 层的事件都先经过 DecorView,然后才传递给我们的 View。
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 的类别。)
- UNSPECIFIED:父容器不对 View 有任何限制,要毒打给多大,一般用于系统内部,表示一种测量的状态
- EXACTLY:父容器检测到 View 所需要的精确大小,此时 View 的最终大小就是 SpecSize 所指定的值。对应于 LayooutParams 中的 match_parent 和具体的数值。
- AT_MOST:父容器制定了一个可用大小 specSize,View 的大小不能大于这个值。对应于 LayoutParams 中的 wrap_content
MeasureSpec 和 LayoutParams 的对应关系
1.结论
2.说明
- DecorView vs 普通 View
- DecorView 的 MeasureSpec 由窗口的尺寸和其自身的LayoutParams 确定;普通 View 的 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来确定。
- 普通 View 的 measureSpec 的确定规则
- View 固定宽高时,无视父容器的 MeasureSpec,View 的MeasureSpec 始终是精准模式 EXACTLY,并且大小遵循 LayoutParams 中的大小
- View 的 MeasureSpec 是 match_parent 时
- 如果父容器的模式也是 EXACTLY 精准模式,那么 View 也是 EXACTLY 精准模式并且大小是父容器的剩余空间
- 如果父容器的模式是 AT_MOST 最大模式,那么 View 也是 AT_MOST 最大模式并且大小不会超过父容器的剩余空间
- 当 View 的宽高是 wrap_content 时,不管父容器的模式是 EXACTLY 精准模式还是 AT_MOST 最大化,View 的模式总是 AT_MOST 最大化模式,并且不能超过父容器的剩余空间
- 关于 UNSPECIFIED 这个模式,勿需关注。
View 的工作流程
measure 过程
1.View 的 measure 过程
(通过 measure 方法即完成了测量过程)
说明:
setMeasuredDimension 方法设置 View 的宽/高的测量值
-
getDefault 方法返回值有两种情况:
AT_MOST 和 EXACTLY:返回值就是 measureSpec 中的 specSize,这个 sepcSize 就是 View 测量后的大小(View 最终的大小是在 layout 阶段确定的,几乎所有情况下 View 的测量大小和最终大小是相等的)
-
(对于我们而言,这个情况我们可以直接跳过,因为这是系统内部的测量过程)UNSPECIFIED:该情况下,View 的大小是第一个参数 size,即高宽分别为View.getSuggestMinimumHeight / View.getSuggestMinimumWidth 的返回值。
而对于 getSuggestMinimunWidth 方法,又有两种情况:
- View 没有设置背景:View 的宽度为 mMinWidth,对应为 android:minWidth 属性所指定的值,默认为0
- View 指定了背景:View 的宽度为 android:minWidth 和 背景的最小宽度这两者中的最大值。
结论:
直接继承 View 的自定义控件需要重写 onMeasure 方法并设置 wrap_content 时的自身大小,否则在布局中使用 wrap_content 就相当于使用 match_parent(这也是为什么 自定义 View 时,需要重写 onMeasure 的原因)。
解决方案:给 View 制定一个 默认的内部宽 / 高(mWidth / mHeight),并在 wrap_content 时设置此高 / 宽即可。
2.ViewGroup 的 measure 过程
(不仅需要完成自己的 measure 过程,还会遍历调用所有子元素的 measure 过程,各个子元素再递归执行这个过程)
说明:
- ViewGroup 是一个抽象类,没有重写 View 的 onMeasure 方法,而是提供了 measureChildren 方法
- measureChildren 方法会对每一个子元素进行 measure,即调用 measureChild 方法
- measureChild 的思想就是取出子元素的 LayoutParams,然后通过 getChildMeasureSpec 方法来创建子元素的 MeasureSpec,接着讲 Measurespec 直接传递给 View 的 measure 方法来进行测量
- ViewGroup 并没有定义其测量的具体流程,其测量过程的 onMeasure 方法需要各个子类具体实现。
- 不统一实现的原因在于不同的 ViewGroup 子类有不同的特性,导致其测量细节不相同。
- measure 过程完成后,通过 getMeasuredWidth/Height 方法可以正确的获取到 View 的测量宽/高。需要注意的是,在某些极端情况下(WTF?),系统可能需要多次 measure 才能确定最终的测量宽/高,此时 onMeasure 拿到的测量宽/高可能不准确,比较好的习惯是再 onLayout 方法中获取 View 的测量宽/高或者最终宽/高。
具体问题:如何保证 Activity 启动的时候获取某个 View 的宽/高?
-
1个典型错误:
- onCreate 或者 onResume 方法中直接获取 这个 View 的宽/高
- 原因:View 的测量过程和 Activity 的生命周期不同步。
-
4种正确解决方案:
-
View.onWindowFocusChanged
该方法的含义是:View 已经初始化完毕了,宽/高已经准备好了,这个时候去获取宽高是没有问题的。当 Activity 的窗口得到和失去焦点的时候均会被调用一次。
@Override public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); if (hasWindowFocus){ int width = this.getMeasuredWidth(); int height = this.getMeasuredHeight(); } }
-
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(); } }); }
-
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(); } }); }
-
view.measure(int widthMeasureSpec,int heightMeasureSpec)
手动进行 measure 过程来得到 View 的宽/高。
需要考虑 View 的 LayoutParams 分为三种情况:
match_parent:这种情况获取具体宽高是不存在的。因为不知道父容器的剩余空间,所以理论上不可能测量 View 的大小。
-
具体的数值(dp/px):
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY); int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY); view.measure(widthMeasureSpec,heightMeasureSpec);
-
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 过程
大致流程
- 首先通过 setFrame 方法设定 View 的四个顶点位置,此时 View 在父容器中的位置就确定了。
- 接着调用该 onLayout 方法,确定子元素的位置,和 onMeasure 方法类似,同样没有真正实现。
说明
- Layout 的作用是 ViewGroup 用来确定子元素的位置。
- ViewGroup 的位置确定后,它再 onLayout 种遍历所有的子元素并调用其 layout 方法,在 layout 方法种又会嗲用 onLayout 方法。
- layout 方法确定 View 把本身的位置,onLayout 方法确定所有子元素的位置。
- View 的默认实现中,View 的测量宽/高和最终宽/高是相等的,测量宽/高形成于 View 的 measure 过程,最终宽高/形成于 layout 过程
draw 过程
流程
- 绘制背景 -- background.draw(canvas)
- 绘制自己 -- onDraw
- 绘制 Children -- dispatchDraw
- 绘制装饰 -- onDrawScrollBars
说明
- View 绘制过程的传递是通过 dispatchDraw 来实现的,dispatchDraw 会遍历调用所有子元素的 draw 方法。
- setWillNotDraw 方法:当我们的自定义控件继承于 ViewGroup 并且本身不具备绘制功能时,可以开启此标记位从而便于系统进行后续的优化。当明确知道一个 ViewGroup 需要通过 onDraw 来绘制内容时们需要显式地关闭 WILL_NOT_DRAW 这个标记位。
自定义 View
自定义 View 的分类
分为四类:
-
继承 View 重写 onDraw 方法
- 实现一些不规则的效果,重写 onDraw 方法,通过绘制的方法实现。
- 需要自己支持 wrap_content,并且自己处理 padding
-
继承 ViewGroup 派生特殊的 Layout
- 实现自定义的布局
- 需要合理处理 ViewGroup 的测量、布局两个过程,同时处理子元素的测量和布局过程
-
继承特定的 View(比如 TextView)
- 扩展某种已有 View 的功能
- 不需要自己支持 wrap_content 和 padding 等
-
继承特定的 ViewGroup(比如 LinearLayout)
- 实现像几种 View 组合在一起的某种效果。
- 不需要自己处理 View 的测量和布局这两个过程
自定义 View 须知
具体如下:
-
让 View 支持 wrap_content
- 直接继承 View 或者 ViewGroup 的控件,需要在 onMeasure 中对 wrap_content 做特殊处理,否则 wrap_content 将失效
-
如果有必要,让 View 支持 padding
- 直接继承 View 的控件:在 draw 方法中处理 padding
- 直接继承 ViewGroup 的控件:在 onMeasure 和 onLayout 中考虑 padding 和子元素的 margin 对其造成的影响,否则二者失效。
-
尽量不在 View 中能够使用 Handler,没必要
- 使用 View.post 系列方法代替 Handler,除非明确要使用 Handler 来发送消息
-
View 中如果有线程或者动画,及时停止,参考 View.onDetachedFromWindow
- 如果不处理线程或者动画的停止,那么可能造成内存泄漏
-
View 带有滑动嵌套情形时,需要处理好滑动冲突。
- 参见 第3章 如何处理滑动冲突。
自定义 View 示例
详见原书代码吧。。