Open qingmei2 opened 5 years ago
大佬,getChildMeasureSpec()中第一个case中有个疑问
//如果父容器的模式是Exactly即确定的大小
case MeasureSpec.EXACTLY:
//子View的高度或宽度>0说明其实一个确切的值,因为match_parent和wrap_content的值是<0的
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
//子View的高度或宽度为match_parent
} else if (childDimension == LayoutParams.MATCH_PARENT) {
...
}
这里父view有确定的大小(size),子view自己也设置了大小(childDimension),为啥最后resultSize =childDimension,难道不应该先判断一下childDimension是否会大于父view的大小,childDimension很有可能会大于size,果断的取childDimension岂不是存在子view长度或者宽度溢出的情况:thinking:,而且上面也说到了:
EXACTLY:父元素决定子元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;
望大佬解惑:pray:
EXACTLY:父元素决定子元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;
望大佬解惑🙏
看源码发现父容器不管在什么测量模式下,只要子元素有确切的尺寸值(非match_parent和wrap_content),都会把这个值设置为最终测量值,我理解是Framework层为了尊重开发者意见,可以用demo试一下,感觉这里的tip确实有点歧义
概述
Android
本身的View
体系非常宏大,源码中值得思考和借鉴之处众多,以View
本身的绘制流程为例,其经过measure
测量、layout
布局、draw
绘制三个过程,最终才能够将其绘制出来并展示在用户面前。本文将针对绘制过程中的 测量流程 的设计思想进行系统地归纳总结,读者需要对
View
的measure()
相关知识有初步的了解:整体思路
View
的测量机制本质非常简单,顾名思义,其目的便是 测量控件的宽高值,围绕该目的,View
的设计者通过代码编织了一整套复杂的逻辑:1、对于子
View
而言,其本身宽高直接受限于父View
的 布局要求,举例来说,父View
被限制宽度为40px
,子View
的最大宽度同样也需受限于这个数值。因此,在测量子View
之时,子View
必须已知父View
的布局要求,这个 布局要求,Android
中通过使用MeasureSpec
类来进行描述。2、对于完整的测量流程而言,父控件必然依赖子控件宽高的测量;若子控件本身未测量完毕,父控件自身的测量亦无从谈起。
Android
中View
的测量流程中使用了非常经典的 递归思想:对于一个完整的界面而言,每个页面都映射了一个View
树,其最顶端的父控件测量开始时,会通过 遍历 将其 布局要求 传递给子控件,以开始子控件的测量,子控件在测量过程中也会通过 遍历 将其 布局要求 传递给它自己的子控件,如此往复一直到最底层的控件...这种通过遍历自顶向下传递数据的方式我们称为 测量过程中的“递”流程。而当最底层位置的子控件自身测量完毕后,其父控件会将所有子控件的宽高数据进行聚合,然后通过对应的 测量策略 计算出父控件本身的宽高,测量完毕后,父控件的父控件也会根据其所有子控件的测量结果对自身进行测量,这种从底部向上传递各自的测量结果,最终完成最顶层父控件的测量方式我们称为测量过程中的“归”流程,至此界面整个View
树测量完毕。对于绘制流程不甚熟悉的开发者而言,上述文字似乎晦涩难懂,但这些文字的概括其本质却是绘制流程整体的设计思想,读者不应该将本文视为源码分析,而应该将自己代入到设计的过程中 ,当深刻理解整个流程的设计思路之后,测量流程代码地设计和编写自然行云流水一气呵成。
布局要求
在整个 测量流程 中, 布局要求 都是一个非常重要的核心名词,
Android
中通过使用MeasureSpec
类来对其进行描述。为什么说 布局要求 非常重要呢,其又是如何定义的呢?这要先从结果说起,对于单个
View
来说,测量流程的结果无非是获取控件自身宽和高的值,Android
提供了setMeasureDimension()
函数,开发者仅需要将测量结果作为参数并调用该函数,便可以视为View
完成了自身的测量:需要注意的是,子控件的测量过程本身还应该依赖于父控件的一些布局约束,比如:
${x}px
,子控件设置为layout_height="${y}px"
;wrap_content
(包裹内容),子控件设置为layout_height="match_parent"
;match_parent
(填充),子控件设置为layout_height="match_parent"
;这些情况下,因为无法计算出准确控件本身的宽高值,简单的通过
setMeasuredDimension()
函数似乎不可能达到测量控件的目的,因为 子控件的测量结果是由父控件和其本身共同决定的 (这个下文会解释),而父控件对子控件的布局约束,便是前文提到的 布局要求,即MeasureSpec
类。MeasureSpec类
从面向对象的角度来看,我们将
MeasureSpec
类设计成这样:在设计的过程中,我们将布局要求分成了2个属性。测量大小 意味着控件需要对应大小的宽高,测量模式 则表示控件对应的宽高模式:
巧妙的是,
Android
并非通过上述定义MeasureSpec
对象的方式对 布局要求 进行描述,而是使用了更简单的二进制的方式,用一个32位的int
值进行替代:这个
int
值中,前2位代表了测量模式,后30位则表示了测量的大小,对于模式和大小值的获取,只需要通过位运算即可。以宽度举例来说,若我们设置宽度=5px(二进制对应了101),那么
mode
对应EXACTLY
,在创建测量要求的时候,只需要通过二进制的相加,便可得到存储了相关信息的int
值:而当需要获得
Mode
的时候只需要用measureSpec
与MODE_TASK
相与即可,如下图:同理,想获得
size
的话只需要只需要measureSpec
与~MODE_TASK
相与即可,如下图:现在读者对
MeasureSpec
类有了初步地认识,在Android
绘制过程中,View
宽或者高的 布局要求 实际上是通过32位的int
值进行的描述, 而MeasureSpec
类本身只是一个静态方法的容器而已。至此
MeasureSpec
类所代表的 布局要求 已经介绍完毕,这里我们浅尝辄止,其在后文的 整体测量流程 中占有至关重要的作用,届时我们再进行对应的引申。测量单个控件
只考虑单个控件的测量,整个过程需要定义三个重要的函数,分别为:
final void measure(int widthMeasureSpec, int heightMeasureSpec)
:执行测量的函数;void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
:真正执行测量的函数,开发者需要自己实现自定义的测量逻辑;final void setMeasuredDimension(int measuredWidth, int measuredHeight)
:完成测量的函数;为什么说需要定义这样三个函数?
1.measure()入口函数:标记测量的开始
首先父控件需要通过调用子控件的
measure()
函数,并同时将宽和高的 布局要求 作为参数传入,标志子控件本身测量的开始:对于
View
的测量流程,其必然包含了2部分:公共逻辑部分 和 开发者自定义测量的逻辑部分,为了保证公共逻辑部分代码的安全性,设计者将measure()
方法配置了final
修饰符:开发者不能重写
measure()
函数,并将View自定义测量的策略通过定义一个新的onMeasure()
接口暴露出来供开发者重写。2.onMeasure()函数:自定义View的测量策略
onMeasure()
函数中,View
自身也提供了一个默认的测量策略:以宽度为例,通过这样获取
View
默认的宽度:minWidth
或者background
属性),View
需要通过getSuggestedMinimumWidth()
函数作为默认的宽度值:getDefaultSize(minWidth, widthMeasureSpec)
函数中,根据 布局要求 计算出View
最后测量的宽度值:3.setMeasuredDimension()函数:标志测量的完成
setMeasuredDimension(width,height)
函数的存在意义非常重要,在onMeasure()
执行自定义测量策略的过程中,调用该函数标志着View
的测量得出了结果:该函数被设计为由
protected final
修饰,这意味着只能由子类进行调用而不能重写。函数调用完毕,开发者可以通过
getMeasuredWidth()
或者getMeasuredHeight()
来获取View
测量的宽高,代码设计大概是这样:经过
measure()
->onMeasure()
->setMeasuredDimension()
函数的调用,最终View
自身测量流程执行完毕。完整测量流程
对于一个完整的界面而言,每个页面都映射了一个
View
树,见微知著,了解了单个View
的测量过程,从宏观的角度思考,View
树整体的测量流程将如何实现?1、设计思路
首先需要理解的是,每种
ViewGroup
的子类的测量策略(也就是onMeasure()
函数内的逻辑)不尽相同,比如RelativeLayout
或者LinearLayout
宽高的测量策略自然不同,但整体思路都大同小异,即 遍历 测量所有子控件,根据父控件自身测量策略进行宽高的计算并得出测量结果。以 竖直方向布局 的
LinearLayout
为例,如何完成LinearLayout
高度的测量?本文抛去不重要的细节,化繁为简,将LinearLayout
高度的测量策略简单定义为 遍历获取所有子控件,将高度累加 ,所得值即自身高度的测量结果——如果不知道每个子控件的高度,LinearLayout
自然无法测量出本身的高度。因此对于
View
树整体的测量而言,控件的测量实际上是 自底向上 的,正如文章开篇 整体思路 一节所描述的:此外,因为子控件的测量逻辑受限于父控件传过来的 布局要求(MeasureSpec), 因此整体逻辑应该是:
setMeasuredDimension()
函数,其父控件根据自己的测量策略,将所有child
的宽高和布局属性进行对应的计算(比如上文中LinearLayout
就是计算所有子控件高度的和),得到自己本身的测量宽高;setMeasuredDimension()
函数完成测量,这之后,它的父控件再根据其自身测量策略完成测量,如此往复,直至完成顶层级View
的测量,自此,整个页面测量完毕。这里的设计体现出了经典的 递归思想,1、2步骤,开始测量的通知自顶至下,我们称之为测量步骤的 递流程,3、4步骤,测量完毕的顺序却是自底至顶,我们称之为测量步骤的 归流程。
2、递流程的实现
现在根据上一小节的设计思路,开始对 递流程 进行编码实现。
在整个递流程中,
MeasureSpec
所代表的 布局要求 占有至关重要的作用,了解了它在这个过程中的意义,也就理解了为什么我们常说 子控件的测量结果是由父控件和其本身共同决定的。依然以 竖直方向布局 的
LinearLayout
为例,我们需要遍历测量其所有的子控件,因此,在onMeasure()
函数中,第一次我们编码如下:这里关注
int heightMeasureSpec
参数,我们知道,这个32位int类型的值,包含了父布局传过来高度的 布局要求:测量的大小和模式。现在我们思考,若父布局传过来大小的是屏幕的高度,那么将其作为参数直接执行child.measure(widthMeasureSpec, heightMeasureSpec)
,让子控件直接开始测量,是合理的吗?答案当然是否定的,试想这样一个简单的场景,若
LinearLayout
本身设置了padding
值,那么子控件的最大高度便不能再达到heightMeasureSpec
中size的大小了,但是如果像上述代码中的步骤2一样,直接对子控件进行测量,子控件就可以从heightMeasureSpec
参数中取得屏幕的高度,通过setMeasuredDimension()
将自己的高度设置和父控件高度一致——这导致了padding
值配置的失效,并不符合预期。因此,我们需要额外设计一个可重写的函数,用于自定义对
child
的测量:我们定义了
measureChild()
函数,其作用是计算子控件的布局要求,并把新的布局要求传给子控件,再让子控件根据新的布局要求进行测量,这样就解决了上述的问题,由此也说明了为什么 子控件的测量结果是由父控件和其本身共同决定的。这里我们注意到我们设计了一个
getChildMeasureSpec()
函数,那么这个函数是做什么的呢?getChildMeasureSpec()函数
getChildMeasureSpec()
函数的作用是根据父布局的MeasureSpec
和padding
值,计算出对应子控件的MeasureSpec
,因为这个函数的逻辑是可以复用的,因此将其定义为一个静态函数:逻辑分支相对较多,注释中已经将子控件 布局要求 的计算逻辑写清楚了,总结如下图,原图链接:
回到前文,现在我们对
onMeasure()
的方法定义如下:3、归流程的实现
现在,所有子控件测量完毕,接下来 归流程 的实现就很简单了,将所有
child
的height
进行累加,并调用setMeasuredDimension()
结束测量即可:乍一看,似乎很难体现出整个流程的 递归 性,实际上当我们宏观从
View
树的树顶顺着往下整理思路,代码逻辑的执行顺序一目了然:至此,测量流程整体实现完毕。
参考
关于我
Hello,我是 却把清梅嗅 ,如果您觉得文章对您有价值,欢迎 ❤️,也欢迎关注我的 博客 或者 Github。
如果您觉得文章还差了那么点东西,也请通过关注督促我写出更好的文章——万一哪天我进步了呢?