MeasureSpec 与 LayoutParams 不得不说的二三事
MesureSpec,测量规格
前言
MeasureSpec 这个话题往大了说,关系到整个 Android 测量体系的设定,往小了说,只是一个封装了位运算的内部静态类而已。当然我们不会仅仅满足于后者,所以我们就借 MeasureSpec 来简单窥探一下 Android 的测量体系。
正文
MeasureSpec,Android 给出的解释是封装了从父视图传递给子视图的布局要求。
A MeasureSpec encapsulates the layout requirements passed from parent to child.
这句话其实已经讲得足够清晰了,但对于从未接触过 MeasureSpec 的初学者来说,还是有点太过简单。
在 Android 的视图系统中,屏幕资源是被整个 View 树一层层消费的,这个 View 树中,往上是父 View,往下是子 View,子 View 的资源都是直接来自父 View,所以在测量的过程中,子 View 并不能不受限制的获取屏幕资源,也要受限于父 View 所给的布局要求,这个要求就是通过 MeasureSpec 这个类来传递的。
先来讲一讲 LayoutParams
为什么在这里会讲到 LayoutParams,因为要确定子 View 自身的测量状态,除了知道父 View 的限制之外,还需要知道子 View 自身对尺寸的期望,而 View 自身的期望在代码中就是通过 LayoutParams 来封装的。
所以,可以这么说, LayoutParams 就是影响测量过程另一大因素。
LayoutParams are used by views to tell their parents how they want to be laid out.
LayoutParams 是 ViewGroup 的一个内部类,被用于来告诉父视图,子视图想怎么样被布局。另外,每个父视图都有继承 ViewGroup.LayoutParams 自己的内部实现,所以对于不同的布局,我们可使用的 LayoutParams 参数也不一样,这个我们后边会详细说明
我们知道,一般的创建一个 View 有两种方式,一种是通过 Java 的方式 new 一个 View 对象,另一种就是通过 Android 的方式 Inflate 一个 View 对象。这两种方式的差别之处在于,new 一个对象是需要手动设置大量的参数,而 Inflate 一个对象,只需要在 xml 布局文件中写好就OK,系统帮我们做了大量的准备工作而已,效率高了不少。除此之外,其实是没有区别的。
那么在写 xml 布局的时候,会发现每个 View 有两个参数是必不可少的,layout_width 和 layout_height ,对应于 View 的宽和高,这两个 xml 属性,其实对应的就是 LayoutParams 中的 width 和 height
相对应的:
- margin 相关的参数,可以在 android.view.ViewGroup.MarginLayoutParams 中找到
- RelativeLayout 相关的参数,可以在 android.widget.RelativeLayout.LayoutParams 中找到
- weight 相关的参数,可以在 android.widget.LinearLayout.LayoutParams 中找到
- ……
这里附上一个 Android 中 LayoutParams 的类图,感兴趣的可以详细查询
以上这些内容理解之后就可以解释很多初学者的一些疑惑:
- 我们将获取到的 LayoutParams 強转为 ViewGroup.LayoutParams 会导致 margin 丢失,是因为 margin 是 ViewGroup.MarginLayoutParams 才有的属性
- RelativeLayout 不支持 layout_gravity 的属性,LinearLayout 不支持 alignInParent 的属性,是因为他们互相不关联,相同之处在于只是都是继承自 MarginLayoutParams
- 我们获取的控件的 LayoutParams 是父视图的 LayoutParams 的内部实现类,而不是子视图自己的,因为设置给子视图的 LayoutParams 是被父视图拿来测量用的,而不是自己使用
注意:因为 padding 是占用 View 内部空间的属性,会干预自身空间内 content 中的显示,并不会影响子 View 在父 view 中所占用的尺寸,所以并没有放在 LayoutParmas 中,而是在给子 View 分配空间,或者绘制自己的 content 的时候才会用到,在后边解析 FrameLayout 的时候也会提到这一点
MeasureSpec 类的解析
理解了 LayoutParams 之后,接下来看一下 MeasureSpec 类的具体内容,因为对于后边涉及到的 MeasureSpec 的转换,这里的内容是必不可少的。
按照惯例,我们先来看类的注释
A MeasureSpec encapsulates the layout requirements passed from parent to child.
1 | /** |
A MeasureSpec is comprised of a size and a mode.
MeasureSpec 是通过 int 值的位运算来实现的,目的是减少对象的内存分配。将32位 int 值的前两位作为 mode(将 int 值左位运算30位),将后30位作为 size,如下
1 | private static final int MODE_SHIFT = 30; |
MeasureSpec 中还带了一些方法,提供 mode 和 size 的打包和解包,方便 MeasureSpec 到 size 和 mode 的互相转换,这里我们不再细说,可以参看 MeasureSpec 的具体内容。
MeasureSpec 模式
每个 MeasureSpec 代表一个宽或者高的要求。一个 MeasureSpec 由尺寸 size 和模式 mode 组成。
其中 size 表示当前 View 已经确定的尺寸,模式表示要对子 View 限定的模式。
可以这么说,MeasureSpec 主要是对子 View 起作用,如果当前 View 已经是最后一层 View,那么 最后确定的 MeasureSpec 是没有多大意义的
接下来,简单讲一下 MeasureSpec 表示的几种模式
1 | /** There are three possible modes: |
我们来看一下有哪些模式
- UNSPECIFIED
父 View 没有施加任何限制给子 View,可以是子 View 想要的任何尺寸
- EXACTLY
父 View 已经限定了子 View 的精确尺寸,子 View 必须是这个尺寸,无论他自己想要多大的尺寸
- AT_MOST
子 View 可以是不超过某个特定的值任意大小
直接看到这些模式可能有点懵,各种要求和被要求,没有关系,我们先对这些模式有个大概的认识,在后续转换的过程中,我们再回来参考就可以
注意:Android 的类的注释往往言简意赅,理解一个类之前,读注释会带来很大的帮助,要养成读类注释的习惯很多人遇到一个函数不理解时,下意识的跑去 Google, Baidu,但其实对于 Android 来说,函数的使用说明都已经放到了源码注释中,直接读注释会比搜索工具来的更快,也更准确。
MeasureSpec 的使用解析
能找到这篇文文章并看到这里的童鞋,一定是已经了解过 Android 的整个绘制流程,这里我们简单的提及就可。没有看过的同学,可以通过之前的 invalidate 系列文章 来了解这部分内容
MeasureSpec 的具体含义,我们在前边已经讲过了,接下来进行的 MeasureSpec 的解析,我们需要弄明白两个问题:
- MeasureSpec 是起始自哪里?
- MeasureSpec 是怎么从父 View 传递给子 View 的,这其中做了哪些处理?
我们先来看第一个问题
MeasureSpec 的起始通过前面的说明我们已经知道,MeasureSpec 是辅助父 View 测量子 View 的一个工具,所以测量过程中子 View 的 MeasureSpec 都是来自于父 View ,父 View 的 MeasureSpec 又来自于父 View 的父 View,那么最开始的 MeasureSpec 是来自哪里的呢,我们接下来就跟着 View 测量的起源来分析一下最开始的 MeasureSpec
1 | # android.view.ViewRootImpl#performTraversals() |
performTraversals 是每一次屏幕进行测量布局绘制的入口,performMeasure 是真正测量的开始,而在performMeasure 之前的 getRootMeasureSpec 显然就是 MeasureSpec 的起始
1 | # android.view.ViewRootImpl#getRootMeasureSpec() |
传入的参数有两个:
第一个是 Window 的尺寸,每个 View 都会依附一个 Window 存在,所以 Window 大小直接会影响 Window 内的 View 测量。
第二个是 View期望的大小,也就是我们设置的 LayoutParams 中的 width 或 height,在 Window 的 addView 中会传入,如果不传入会获得一个默认值
我们来看一下转换规则
- MATCH_PARENT,如果子 View 期望和父 View 一样大,当前 Window 的尺寸已经确定了,那么也就可以说子 View 的尺寸精确了,所以构建一个 MeasureSpec 传给子 View,标识自己的尺寸和模式,尺寸就是 Window 的尺寸,模式就是精确模式
- WRAP_CONTENT,如果子 View 期望自己的大小包裹内容就OK,那么父 View 表示没啥问题,只要这个大小不超过我的大小就OK,所以构建一个 MeasureSpec,大小为 Window 尺寸,模式表示最大模式,也就是最大不超过 Window 尺寸
- 其他,这个其他其实也就是固定的尺寸,也就是子 View 期望自己决定自己的尺寸,父 View 表示OK,给你期望的精确的尺寸,构建一个 MeasureSpec,大小为子 View 想要的尺寸,模式为精确
经过以上解析,相信大家对 MeasureSpec 是经由父 View 和子 View 共同决定然后传递到子 View 有了更深的认识
这个最初构建的 MeasureSpec 会从 View 树的顶端,经过每一个子节点的共同结果,继续向下传递,直到到达叶子节点,这样一轮遍历过后,每个 View 都拿到了属于自己的最终的 MeasureSpec,也就相当于确定了自己的最后的尺寸
因为 MeasureSpec 是由父 View 和子 View 共同决定的,所以是在 ViewGroup,而不是 View 中存在 Measurespec的转换过程
所以我们就以一个 ViewGroup 的视角来看 ViewGoup 与 View 之间的转换过程
但是所有的 View 包括 ViewGroup 都是继承自 View 这个类,需要覆写 onMeasureSpec 来实现自己的测量方法,而 ViewGroup 没有实现自己的 onMeasure 方法,所以无法看到 ViewGroup 这一类父控件在测量过程中对子 View 施加的测量工作,所以我们选一个常见的 FrameLayout 来解析
FrameLayout 的测量转换
我们就以 FrameLayout 作为示例,来分析一下 ViewGroup 和 View 之间的转换规则
FrameLayout 的测量
1 | # android.widget.FrameLayout#onMeasure() |
我们来看 FrameLayout 的主流程的代码
其中分三个部分:
第一部分
按照正常流程,遍历测量所有子 View 的大小第二部分
设定 FrameLayout 的宽高,这里需要区分两点,如果 FrameLayout 要 MATCH_PARENT,其实高度在父 View 中计算 MeasureSpec 就已经可以确定下来了,如果 FrameLayout 要 WRAP_CONTENT,那么顾名思义,需要根据测量的子 View 的宽高来计算第三部分
第三部分的测量有个条件1
2
3final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;这个其实表示,父 View 限定给当前 View 的测量模式是 AT_MOST,(至于为什么是 AT_MOST,在文章最后的表中可以推算出来),那么表示当前父 View 的高度是没有确定的,需要先测量子 View 的宽高之后才能知道自己的宽高,那么对于那些想要 MATCH_PARENT 的子View来说,只有父 View 宽高确定,才能知道自己的宽高,所以在第二部分设定完成 FrameLayou t的宽高之后,这里需要对这些 MATCH_PARENT 的子 View 再来测量一遍
三个部分中,其实第一部分是我们所关心的,所以我们着重来看一下第一部分
测量模式转换
递归测量子view,需要考虑 Margin 的影响,因为 Margin 是 View 之间的间隔,不包含在 View 的宽高内
1 | # android.view.ViewGroup#measureChildWithMargins() |
这里有个 widthUsed 和 heightUsed,分别表示宽度和高度上已经被消费的大小。因为父 View 可能存在多个子 View,所以分析当前 View 可使用的父 View 空间时,要去掉已经分配给其他子 View 的空间。
比如 LinearLayout 水平布局时,从左到右测量时,这个已经被使用的宽度会越来越小,而对于 FrameLayout 的覆盖式布局传入的都是0
这个函数会先根据父View的 MeasureSpec 和 LayoutParams 转换为子 View 的 LayoutParams,然后交给子View 的 measure 方法继续往下测量。
我们来看具体的转换细节
1 | # android.view.ViewGroup#getChildMeasureSpec() |
以上就是转换规则,每个分支都有详细注释,我们不再赘述,将以上的代码转换为表格的形式给出
子LayoutParams\父MeasureSpec | UNSPECIFIED#0 | AT_MOST#parentSize | EXACTLY#parentSize |
---|---|---|---|
MATCH_PARENT | UNSPECIFIED#0(子View想要和父View一样大,那么找出来具体多大) | AT_MOST#parentSize(子view表示想和父View一样大,父View不固定,只能限制子View不要超过这个尺寸) | EXACTLY#parentSize(子view想要和父View一样大,父view已经确定了,那么子view就是父view大小了) |
WRAP_CONTENT | UNSPECIFIED#0(子view想要自己决定,那么找出来具体多大 ) | AT_MOST#parentSize(子View表示自己决定大小,父View表示不超过我就OK) | AT_MOST#parentSize(子View表示要自己决定,父view表示,只要不超过我的自身值就OK) |
固定值大小 | EXACTLY#childSize(子view表明自己的大小,父view表示你开心就好) | EXACTLY#childSize(子view表明自己的大小,父View表示你开心就好) | EXACTLY#childSize(子view明确表示了自己的期望值,那么子View你开心就好) |
注:
#
用来分割 mode 和 size- 其中 parentSize 就是 specSize,也就是父 View 的尺寸大小,或者准确来说,是限定父 View 的最大尺寸。因为对于 EXACTLY 来说,父 View 大小是可以确定的,而对于 AT_MOST 来说,还需要测量子 View 的大小,所以这里是最大尺寸
- 其中 childSize 就是 childDimens
对于上述的表格,其实也不是很直观,只是将代码的转换算法用表格的方式整理出来了。
我们来尝试用另一个角度的方式理解这个表格
UNSPECIFIED 、AT_MOST、EXACTLY 其实代表着测量的三个状态
- UNSPECIFIED表示是未执行测量,一切都是初始化的状态
- AT_MOST表示当前View的尺寸不确定,需要经过测量子View才能决定
- EXACTLY表示当前View的尺寸不需要经过测量子View就已经决定好了
基于以上的理论,我们来重新解析刚刚的表格(横向为ABC,竖向为123)
- 子View 如果是固定尺寸(A3,B3,C3):这个很好理解,直接就可以确定大小
- 父View 是UNSPECIFIED(A1,A2):这一列也很好理解,都是未初始化
- 父View 是 AT_MOST(B1,B2):也即父View没有确定高度,那么子View都无法确认高度,也即必须要先行测量
- 父View是EXACTLY(C1,C2):子View想要适应自己大小,那么其实就必须先经过测量
后记
我将这篇文章中讲到的一些关键知识点罗列如下,可以查看是否真的理解了这些内容
- 子 View 提供给父 View 的 LayoutParams 是用来表示子 View 自身的期望大小
- 每个 View 都有自己的 MeasureSpec 来代表自己测量后的状态
- 每个 View 的 MeasureSpec 都是由父 View 的 Measure 和自身 LayoutParams 共同确定的
- View 树的测量是深度优先
- 每个 View 都是先确定测量模式,然后再去根据测量模式实际测量
搞明白了MeasureSpec和LayoutParams相关的内容,对于理解 Android 的测量体系,以及自定义View的测量过程,或者灵活使用View的测量结果都有很大的帮助,另外在对于布局过程中出现的一些异常布局,也能通过快速查看源码定位问题,是作为高级工程师必不可少的一项技能。
MeasureSpec 与 LayoutParams 不得不说的二三事
http://www.0xforee.top/2019/07/01/android-measurespec-layoutparams/