-文章来源:itsCoder 的 WeeklyBolg 项目
- itsCoder主页:http://itscoder.com/
- 作者:yongyu0102
- 审阅者:hymane
1 前言
笔记内容源于Android 开发艺术探索。
View 是 Android 中所有控件的基类,例如 Button 和 TextView、ViewGroup 等常见控件他们的基类都是 View,View 是一种界面层的控件的一种抽象,代表了一个控件。View 本身可以是单个控件也可以是由多个控件组成的一组控件,通过这种关系就形成了 View 树的结构。Android 系统本身就提供好了很多好用的 View,你也可以自己根据需求去自定义一个 View,拿最简单的一个例子来说,当我们想在界面上显示一行文字的时候,我们会在 xml 文件中写好布局然后在 Activity 中的 onCreate 方法中使用 setContentView 方法来加载布局就可以显示出我们想要的文字,这时候你是否有思考过这个过程是怎么完成的, View 是如何被显示到界面上的;还有一个我们经常遇到的问题是:当我们在一个 ScrollView 控件内部嵌套一个 ListView 的时候 ListView 只会显示一行;当使用自定义的 View 的时候,View 可以显示到界面,但是当使用 WrapContent 属性的时候不起作用,这些问题笔者就曾都遇到过,如果你也曾有过这样的疑问,可以阅读以下这篇文章。
1.1 主要内容简介
View 的工作原理主要包含 View 的三大流程 onMeasure()、onLayout()和onDraw() ,而由于一次性全部写完内容会有点长,所以本次主要先介绍关于 View 的工作流程的整体梳理和 Measure 过程相关知识,而下一篇笔记会把剩下的部分写完。
2 初识 ViewRoot 和 DecorView
在正式介绍 View 的三大流程 onMeasure()、onLayout()和onDraw() 之前,先简单介绍一下当我们在 Activity 方法 onCreate 里执行 setContentView 之后 View 是如何显示到屏幕上的,这里我们就不分析源码过程了,因为这个过程不是我们要分析的重点,只是辅助我们去理解,有助于我们对整个流程有更好的理解和把握。
当调用 Activity 的 setContentView 方法后会调用 PhoneWindow 类 的 setContentView 方法,PhoneWindow 类是抽象类Window的实现类,Window 类用来描述 Activity 视图最顶端的窗口显示和行为操作,PhoneWindow 类 的 setContentView 方法中最终会 生成一个 DecorView 对象,DecorView 是 PhoneWindow类的内部类,继承自FrameLayout ,所以调用 Activity 方法 setContetnView 后最终会生成一个 FrameLayout 类型的 DecorView 组件,该组件将作为整个应用窗口的顶层图,然后在 DecorView 容器中添加根布局,根布局中包含一个 id 为 contnet 的 FrameLayout 内容布局,我们的 Activity 加载的布局 xml 最后通过LayoutInflater 将 xml 内容布局解析成 View 树形结构,最后添加到 id 为 content 的 FrameLayout布局当中,至此,View 最终就会显示到手机屏幕上,如果想详细了解出门右转从ViewRootImpl类分析View绘制的流程——废墟的树。整理流程梳理可以参考下面这张图片:
我们了解了上面得到流程后下面梳理一下如何进入到 view 的绘制流程:
ViewRoot 对应的实现类是 ViewRootImpl 类,他是连接 WindowManager 和DecorView 的纽带,view 的三大 流程均是通过 ViewRoot 来完成的。在 ActivityThread 中,当 activity 对象被创建完毕后,会将 DecorView 添加到Window 中,同时会创建 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 建立关联。这个流程可以参考下图,图片来自从ViewRootImpl类分析View绘制的流程——废墟的树:
View 的绘制流程是从 ViewRoot 的 performTraversals 方法开始的,它经过 measure、layout、draw 三个过程才能最终将一个 View 绘制出来,其中 measure 用来测量 View 的宽和高,layout 用来确定 View 在父容器的放置位置,而 draw 则负责将 View 绘制在屏幕上,参考下图(来源艺术探索截图) :
performTraversals 会依次调用 performMeasure、performLayout、performDraw 三个方法,这三个方法分别完成顶级 View 的 measure、layout 和 draw 这三大流程,其中 performMeasure 会调用 measure 方法,在measure 方法中又会调用 onMeasure 方法,在 onMeasure 方法中对所有的子元素进行 measure 过程,这个时候 measure 流程就会从父容器传递到子元素中了,这样就完成了一次 measure 过程。接着子元素就会重复父容器的 measure 过程,如此反复就完成了整个 View 树的遍历,同理 perFormLayout 和 performDraw 的流程也是类似。
measure 过程决定了 view 的宽高,在几乎所有的情况下这个宽高都等同于 view 最终的宽高,但特殊情况除外。layout 过程决定了 view 的四个顶点的坐标和 view实 际的宽高,通过 getWidth
和 getHeight
方法可以得到最终的宽高。draw过程决定了view的显示。
DecorView 其实是一个 FrameLayout,其中包含了一个竖直方向的 LinearLayout,上面是标题栏,下面是内容栏(id为android.R.id.content
)。
3 理解MeasureSpec
MeasureSpec 是 View 测量过程中的一个关键参数,很大程度上决定了 View 的宽高,父容器会影响 View 的 MeasureSpec 的创建,MeasureSpec 不是唯一由 LayoutParams 决定的,LayoutParams 需要和父容器一起才能决定 View 的MeasureSpec,从而进一步确定 View 的宽高,在 View 测量过程中,系统会将该 View 的 LayoutParams 参数在父容器的约束下转换成对应的 MeasureSpec ,然后再根据这个 measureSpec 来测量 View 的宽高。
MeasureSpec 代表一个32位 int 值,高2位代表 SpecMode(测量模式),低30位代表 SpecSize(在某个测量模式下的规格大小),MeasureSpec 通过将 SpecMode 和 SpecSize 打包成一个 int 值来避免过多的内存分配,为了方便操作,其提供了打包和解包方法源码如下:
|
|
SpecMode 有三类,每一类都表示特殊的含义:
- UNSPECIFIED 父容器不对 View 有任何的限制,要多大给多大,这种情况下一般用于系统内部,表示一种测量的状态。
- EXACTLY 父容器已经检测出 View 所需要的精确大小,这个时候 View 的最终大小就是 SpecSize 所指定的值,它对应于LayoutParams 中的 match_parent 和具体的数值这两种模式
- AT_MOST 父容器指定了一个可用大小即 SpecSize,View 的大小不能大于这个值,具体是什么值要看不同 View 的具体实现。它对应于 LayoutParams 中的 wrap_content。
4 MeasureSpec 和 LayoutParams 的对应关系
对于DecorView,它的 MeasureSpec 由窗口的尺寸和其自身的 LayoutParams 来决定;对于普通 View,它的MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定。
对普通的 View 的 measure 方法的调用,是由其父容器传递而来的,这里先看一下 ViewGroup 的 measureChildWithMargins 方法:
|
|
从上代码第二步可以看出,子 view 的 MeasureSpec 的创建与父容器的 MeasureSpec 、子 view 本身的 LayoutParams 有关,此外还与 view 本身的 margin 和 padding 值有关,具体看一下 getChildMeasureSpec 方法:
|
|
以上代码主要作用就是根据父容器的 MeasureSpec 和 view 本身的 LayoutParams 来确定子元素的 MeasureSpec 的整个过程,这个过程清楚的展示了普通 view 的 MeasureSpec 的创建规则,整理一下可得到如下表格(来源艺术探索截图):
总结:
- 当 View 采用固定宽高时,不管父容器的 MeasureSpec 是什么,View 的 MeasureSpec 都是精确模式,并且大小是LayoutParams 中的大小。
- 当 View 的宽高是 match_parent 时,如果父容器的模式是精确模式,那么 View 也是精确模式,并且大小是父容器的剩余空间;如果父容器是最大模式,那么 View 也是最大模式,并且大小是不会超过父容器的剩余空间。
- 当 View 的宽高是 wrap_content 时,不管父容器的模式是精确模式还是最大模式,View 的模式总是最大模式,并且大小不超过父容器的剩余空间。
5 View 的工作流程
View 的工作流程主要是指 measure、layout、draw 这三大流程,即测量、布局和绘制,其中 measure 确定 View 的测量宽和高,layout 确定 View 的最终宽和高及 View 的四个顶点位置,而 draw 是将 View 绘制到屏幕上。
5.1 measure 过程
分两种情况:
- 如果只是一个原始的 View,通过
measure
方法就完成了测量过程。 - 如果是一个 ViewGroup 除了完成自己的测量过程还会遍历调用所有子 View 的
measure
方法,而且各个子 View 还会递归执行这个过程。
5.1.1 View 的 measure 过程
View 的 measure 过程由 measure
方法来完成, measure
方法是一个 final 类型,子类不可以重写,而 View 的 measure() 方法中会调用 onMeasure 方法,因此我们只需要分析 onMeasure 方法即可,源码如下:
|
|
上面方法很简单,就是给 View 设置了测量高宽的测量值,而这个测量值是通过 getDefaultSize 方法获取,那么接着分析 getDefaultSize 方法:
|
|
这里来分析一下 UNSPECIFIED 条件下 View 的测量高宽默认值 size 是通过 getSuggestedMinimumWidth() 和 getSuggestedMinimumHeight() 函数获取,这两个方法原理一样,这里我们就看一下 getSuggestedMinimumHeight() 源码:
|
|
上面代码可以看出,如果 View 没有背景,View 的高度就是 mMinHeight,这个 mMinHeight 是由 android:minHeight 这个属性控制,可以为 0,如果有背景,就返回 mMinHeight 和背景的最小高度两者中的最大值。
从 getDefaultSize 方法可以看出,View 的高/宽由 父容器传递进来的 specSize 决定,因此可以得出结论:
直接继承自 View 的自定义控件需要重写 onMeasure 方法来设置 wrap_content 时候的自身大小,而设置的具体值需要根据实际情况自己去计算或者直接给定一个默认固定值,否则在布局中使用 wrap_content 时候就相当于使用 match_parent ,因为在布局中使用 wrap_content 的时候,它的 specMode 是 AT_MOST 最大测量模式,在这种模式下 View 的宽/高等于 speceSize 大小,即父容器中可使用的大小,也就是父容器当前剩余全部空间大小,这种情况,很显然,View 的宽/高就是等于父容器剩余空间的大小,填充父布局,这种效果和布局中使用 match_parent 一样,解决这个问题代码如下:
|
|
上面代码中在 widthSpecMode 或 heightSpecMode 为 MeasureSpec.AT_MOST 我们就给定一个对应的 mWith 和 mHeight 默认固定值宽高,而这个默认值没有固定依据,需要我们根据自定义的 view 的具体情况去计算给定。
5.1.2 ViewGroup 的 measure 过程
ViewGroup 除了完成自己的测量过程还会遍历调用所有子 View 的measure
方法,而且各个子 View 还会递归执行这个过程,我们知道 View Group 继承自 View ,是一个抽象类,因此没有重写 View onMeasure 方法,也就是没有提供具体如何测量自己的方法,但是它提供了一个 measureChildren 方法,定义了如何测量子 View 的规则,代码如下:
|
|
我们再看一下对子 View 进行测量的 measureChild 方法 :
|
|
上面代码中的第二步调用的方法 getChildMeasureSpec 在标题 4 MeasureSpec和LayoutParams的对应关系 中已经分析过。
ViewGroup 并没有定义具体的测量过程,这是因为 ViewGroup 是一个抽象类,其不同子类具有不同的特性,导致他们的测量过程有所不同,不能有一个统一的 onMeasure 方法,所以其测量过程的 onMeasure 方法需要子类去具体实现,比如 LinearLayout 和 RelativeLayout 等,下面通过 LinearLayout 的 onMeasure 方法来分析一下 ViewGroup 的测量过程。
|
|
上面代码可以看出 ViewGroup 内部测量方式分为垂直方向和水平方向,两者原理基本一样,下面看一下垂直方向的 LinearLayout 测量方式,由于这个方法代码比较长,所以贴出重点部分:
|
|
以上代码就是对 LinearLayout onMeasure 分析过程,整个过程原理已经在代码中加以注释说明,这里我们重点分析一下 resolveSizeAndState(heightSize, heightMeasureSpec, 0) 这个方法是如何实现最终确定 LinearLayout 高度值的,方法如下:
|
|
以上代码总结起来就是 LinearLayout 会根据测量子 View 的情况和 MeasureSpec 约束条件来决定自己最终的大小,具体来说就是如果它的布局中高度才用 具体数值,那么它的测量过程和 View 一致,即高度为 specSize 值,如果它的布局中使用 wrap_content 那么它的高度是所有子 View 高度总和,但是不能超过父容器剩余空间。
最后对整个测量过程总结一下就是分为以下几步:
- 对 LinearLayout 中的子 View 进行第一次遍历测量,主要是通过 measureChildBeforeLayout 这个方法,这个方法内部会调用 measureChildWithMargins 方法,而在 measureChildWithMargins 方法内部会去调用 child.measure(childWidthMeasureSpec, childHeightMeasureSpec) 方法进行测量。在这次的测量过程中,如果满足了第1.1步测量条件的子 view 不需要进行测量,会在后面的第5.1步中进行测量。
- 根据测量各个子 View 的高度会得到一个初步的 LinearLayout 总高度 mTotalLength 值。
- 如果 LinearLayout 设置了 android:measureWithLargestChild=”true” 属性并且测量模式为 AT_MOST或者 UNSPECIFIED 重新计算 mTotalLength 总高度。
- 根据 LinearLayout 的 heightMeasureSpec 测量模式 和已经测量得到的总高度 mTotalLength ,来确定得到最终 LinearLayout 高度和状态 。
- 根据已经测量得到的 LinearLayout 高度来重新测量确定各个子 View 的大小。
- 最终执行 setMeasuredDimension 方法设置 LinearLayout 的测量高宽。
6 实际问题解决
View 的 measure 过程和 Activity 的生命周期方法不是同步执行的,因此无法保证 Activity 执行了onCreate、onStart、onResume 时某个 View 已经测量完毕了。如果View还没有测量完毕,那么获得的宽和高都是 0。下面是四种解决该问题的方法:
1、Activity/View#onWindowsChanged 方法
onWindowFocusChanged 方法表示 View 已经初始化完毕了,宽高已经准备好了,这个时候去获取是没问题的。这个方法会被调用多次,当 Activity 继续执行或者暂停执行的时候,这个方法都会被调用,典型代码如下:
|
|
2、View.post(runnable)
通过 post 将一个 Runnable 投递到消息队列的尾部,然后等待 Looper 调用此 runnable 的时候 View 也已经初始化好了。
|
|
3、ViewTreeObsever
使用 ViewTreeObserver 的众多回调方法可以完成这个功能,比如使用 onGlobalLayoutListener 接口,当 View 树的状态发生改变或者 View 树内部的 View 的可见性发生改变时,onGlobalLayout 方法将被回调。伴随着View树的变化,这个方法也会被多次调用。
|
|
4、view.measure(int widthMeasureSpec, int heightMeasureSpec)
通过手动对 View 进行 measure 来得到 View 的宽高,这个要根据 View 的 LayoutParams 来处理:
(1)match_parent:无法 measure 出具体的宽高,原因是根据上面我们分析 View 的measure 过程原理可知,此种 MeasureSpec 需要知道 parentSize ,即父容器剩余空间,而这个时候无法知道 parentSize 大小,所以无法测量。
(2)wrap_content: 可以采用设置最大值方法进 measure :
|
|
注意这里作者为什么使用 (1 << 30) - 1 ) 来构造 MeasureSpec 呢?笔者解释是:”通过分析 MeasureSpec 的实现可以得知 View 的尺寸是使用 30 位的二进制表示,也就是说最大是 30 个 1 即(2^30-1),也就是 (1 << 30) - 1 ),在最大化模式下,使用 View 能支持的最大值去构造 MeasureSpec 是合理的“。为什么这样就合理呢?我们前面分析在子 View 使用 wrap_content 模式的时候,其测量规则是根据自身的情况去测量尺寸,但是不能超过父容器的剩余空间的最大值,换句话说就是父容器给子 View 一个最大值,然后告诉子 View 你自己看着办,但是别超过这个尺寸就行,但是现在我们自己去测量的时候不知道父容器给定的 MeasureSpec 情况, 也就是不知道父容器给多大的限定值,需要自己去构造一个MeasureSpec ,那么这个最大值我们给定多少合适呢?所以这里干脆就给一个 View 所能支持的最大值,然子 View 根据自身情况去测量,怎么也不能超过这个值就行了。
(3)具体数值(dp/px):例如100px,如下 measure :
|
|
以上四种解决方法的代码和原理上面已经详细说明,大家可以根据需要进行选择使用。
结语:
本次笔记主要记录 View 三大流程中的 onMeasure() 测量原理部分,笔者感觉这一部分也是比较重要和难懂的部分,在我们实际中很多问题会涉及到,所以静下心来详细记录,如有理解错误,还望指出,谢谢!!!!!