反思 系列博客是个人一种新学习方式的尝试,该系列起源和目录请参考 这里 。html
Android
自己的View
体系很是宏大,源码中值得思考和借鉴之处众多,以View
自己的绘制流程为例,其通过measure
测量、layout
布局、draw
绘制三个过程,最终才可以将其绘制出来并展现在用户面前。android
本文将针对绘制过程当中的 测量流程 的设计思想进行系统地概括总结,读者须要对View
的measure()
相关知识有初步的了解:git
View
的测量机制本质很是简单,顾名思义,其目的即是 测量控件的宽高值,围绕该目的,View
的设计者经过代码编织了一整套复杂的逻辑:github
一、对于子View
而言,其自己宽高直接受限于父View
的 布局要求,举例来讲,父View
被限制宽度为40px
,子View
的最大宽度一样也需受限于这个数值。所以,在测量子View
之时,子View
必须已知父View
的布局要求,这个 布局要求, Android
中经过使用 MeasureSpec
类来进行描述。安全
二、对于完整的测量流程而言,父控件必然依赖子控件宽高的测量;若子控件自己未测量完毕,父控件自身的测量亦无从谈起。Android
中View
的测量流程中使用了很是经典的 递归思想:对于一个完整的界面而言,每一个页面都映射了一个View
树,其最顶端的父控件测量开始时,会经过 遍历 将其 布局要求 传递给子控件,以开始子控件的测量,子控件在测量过程当中也会经过 遍历 将其 布局要求 传递给它本身的子控件,如此往复一直到最底层的控件...这种经过遍历自顶向下传递数据的方式咱们称为 测量过程当中的“递”流程。而当最底层位置的子控件自身测量完毕后,其父控件会将全部子控件的宽高数据进行聚合,而后经过对应的 测量策略 计算出父控件自己的宽高,测量完毕后,父控件的父控件也会根据其全部子控件的测量结果对自身进行测量,这种从底部向上传递各自的测量结果,最终完成最顶层父控件的测量方式咱们称为测量过程当中的“归”流程,至此界面整个View
树测量完毕。app
对于绘制流程不甚熟悉的开发者而言,上述文字彷佛晦涩难懂,但这些文字的归纳其本质倒是绘制流程总体的设计思想,读者不该该将本文视为源码分析,而应该将本身代入到设计的过程当中 ,当深入理解整个流程的设计思路以后,测量流程代码地设计和编写天然行云流水一鼓作气。函数
在整个 测量流程 中, 布局要求 都是一个很是重要的核心名词,Android
中经过使用 MeasureSpec
类来对其进行描述。源码分析
为何说 布局要求 很是重要呢,其又是如何定义的呢?这要先从结果提及,对于单个View
来讲,测量流程的结果无非是获取控件自身宽和高的值,Android
提供了setMeasureDimension()
函数,开发者仅须要将测量结果做为参数并调用该函数,即可以视为View
完成了自身的测量:布局
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
// measuredWidth 测量结果,View的宽度
// measuredHeight 测量结果,View的高度
// 省略其它代码...
// 该方法的本质就是将测量结果存起来,以便后续的layout和draw流程中获取控件的宽高
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
}
复制代码
须要注意的是,子控件的测量过程自己还应该依赖于父控件的一些布局约束,好比:post
${x}px
,子控件设置为layout_height="${y}px"
;wrap_content
(包裹内容),子控件设置为layout_height="match_parent"
;match_parent
(填充),子控件设置为layout_height="match_parent"
;这些状况下,由于没法计算出准确控件自己的宽高值,简单的经过setMeasuredDimension()
函数彷佛不可能达到测量控件的目的,由于 子控件的测量结果是由父控件和其自己共同决定的 (这个下文会解释),而父控件对子控件的布局约束,即是前文提到的 布局要求,即MeasureSpec
类。
从面向对象的角度来看,咱们将MeasureSpec
类设计成这样:
public final class MeasureSpec {
int size; // 测量大小
Mode mode; // 测量模式
enum Mode { UNSPECIFIED, EXACTLY, AT_MOST }
MeasureSpec(Mode mode, int size){
this.mode = Mode;
this.size = size;
}
public int getSize() { return size; }
public Mode getMode() { return mode; }
}
复制代码
在设计的过程当中,咱们将布局要求分红了2个属性。测量大小 意味着控件须要对应大小的宽高,测量模式 则表示控件对应的宽高模式:
- UNSPECIFIED:父元素不对子元素施加任何束缚,子元素能够获得任意想要的大小;平常开发中自定义View不考虑这种模式,可暂时先忽略;
* EXACTLY:父元素决定子元素的确切大小,子元素将被限定在给定的边界里而忽略它自己大小;这里咱们理解为控件的宽或者高被设置为match_parent
或者指定大小,好比20dp
;- AT_MOST:子元素至多达到指定大小的值;这里咱们理解为控件的宽或者高被设置为
wrap_content
。
巧妙的是,Android
并不是经过上述定义MeasureSpec
对象的方式对 布局要求 进行描述,而是使用了更简单的二进制的方式,用一个32位的int
值进行替代:
public static class MeasureSpec {
private static final int MODE_SHIFT = 30; //移位位数为30
//int类型占32位,向右移位30位,该属性表示掩码值,用来与size和mode进行"&"运算,获取对应值。
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
//00左移30位,其值为00 + (30位0)
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
//01左移30位,其值为01 + (30位0)
public static final int EXACTLY = 1 << MODE_SHIFT;
//10左移30位,其值为10 + (30位0)
public static final int AT_MOST = 2 << MODE_SHIFT;
// 根据size和mode,建立一个测量要求
public static int makeMeasureSpec(int size, int mode) {
return size + mode;
}
// 根据规格提取出mode,
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
// 根据规格提取出size
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
复制代码
这个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)
:完成测量的函数;为何说须要定义这样三个函数?
首先父控件须要经过调用子控件的measure()
函数,并同时将宽和高的 布局要求 做为参数传入,标志子控件自己测量的开始:
// 这个是父控件的代码,让子控件开始测量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
复制代码
对于View
的测量流程,其必然包含了2部分:公共逻辑部分 和 开发者自定义测量的逻辑部分,为了保证公共逻辑部分代码的安全性,设计者将measure()
方法配置了final
修饰符:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// ... 公共逻辑
// 开发者须要本身重写onMeasure函数,以自定义测量逻辑
onMeasure(widthMeasureSpec, heightMeasureSpec);
}
复制代码
开发者不能重写measure()
函数,并将View自定义测量的策略经过定义一个新的onMeasure()
接口暴露出来供开发者重写。
onMeasure()
函数中,View
自身也提供了一个默认的测量策略:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
复制代码
以宽度为例,经过这样获取View
默认的宽度:
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)
minWidth
或者background
属性),View
须要经过getSuggestedMinimumWidth()
函数做为默认的宽度值:protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
复制代码
getDefaultSize(minWidth, widthMeasureSpec)
函数中,根据 布局要求 计算出View
最后测量的宽度值:public static int getDefaultSize(int size, int measureSpec) {
// 宽度的默认值
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
// 根据不一样的测量模式,返回的测量结果不一样
switch (specMode) {
// 任意模式,宽度为默认值
case MeasureSpec.UNSPECIFIED:
result = size;
break;
// match_parent、wrap_content则返回布局要求中的size值
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
复制代码
上述代码中,View的默认测量策略也印证了,即便View设置的是
layout_width="wrap_content"
,其宽度也会填充父布局(效果同match_parent
),高度依然。
setMeasuredDimension(width,height)
函数的存在乎义很是重要,在onMeasure()
执行自定义测量策略的过程当中,调用该函数标志着View
的测量得出告终果:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 广泛意义上,setMeasuredDimension()标志着测量结束
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
// measuredWidth 测量结果,View的宽度
// measuredHeight 测量结果,View的高度
// 省略其它代码...
// 该方法的本质就是将测量结果存起来,以便后续的layout和draw流程中获取控件的宽高
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
}
复制代码
该函数被设计为由protected final
修饰,这意味着只能由子类进行调用而不能重写。
函数调用完毕,开发者能够经过getMeasuredWidth()
或者getMeasuredHeight()
来获取View
测量的宽高,代码设计大概是这样:
public final int getMeasuredWidth() {
return mMeasuredWidth;
}
public final int getMeasuredHeight() {
return mMeasuredHeight;
}
// 如何使用
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight()
复制代码
通过measure()
-> onMeasure()
-> setMeasuredDimension()
函数的调用,最终View
自身测量流程执行完毕。
对于一个完整的界面而言,每一个页面都映射了一个View
树,见微知著,了解了单个View
的测量过程,从宏观的角度思考,View
树总体的测量流程将如何实现?
首先须要理解的是,每种ViewGroup
的子类的测量策略(也就是onMeasure()
函数内的逻辑)不尽相同,好比RelativeLayout
或者LinearLayout
宽高的测量策略天然不一样,但总体思路都大同小异,即 遍历 测量全部子控件,根据父控件自身测量策略进行宽高的计算并得出测量结果。
以 竖直方向布局 的LinearLayout
为例,如何完成LinearLayout
高度的测量?本文抛去不重要的细节,化繁为简,将LinearLayout
高度的测量策略简单定义为 遍历获取全部子控件,将高度累加 ,所得值即自身高度的测量结果——若是不知道每一个子控件的高度,LinearLayout
天然没法测量出自己的高度。
所以对于View
树总体的测量而言,控件的测量其实是 自底向上 的,正如文章开篇 总体思路 一节所描述的:
对于完整的测量流程而言,父控件必然依赖子控件宽高的测量;若子控件自己未测量完毕,父控件自身的测量亦无从谈起。
此外,由于子控件的测量逻辑受限于父控件传过来的 布局要求(MeasureSpec), 所以总体逻辑应该是:
setMeasuredDimension()
函数,其父控件根据本身的测量策略,将全部child
的宽高和布局属性进行对应的计算(好比上文中LinearLayout
就是计算全部子控件高度的和),获得本身自己的测量宽高;setMeasuredDimension()
函数完成测量,这以后,它的父控件再根据其自身测量策略完成测量,如此往复,直至完成顶层级View
的测量,自此,整个页面测量完毕。这里的设计体现出了经典的 递归思想,一、2步骤,开始测量的通知自顶至下,咱们称之为测量步骤的 递流程,三、4步骤,测量完毕的顺序倒是自底至顶,咱们称之为测量步骤的 归流程。
如今根据上一小节的设计思路,开始对 递流程 进行编码实现。
在整个递流程中,MeasureSpec
所表明的 布局要求 占有相当重要的做用,了解了它在这个过程当中的意义,也就理解了为何咱们常说 子控件的测量结果是由父控件和其自己共同决定的。
依然以 竖直方向布局 的LinearLayout
为例,咱们须要遍历测量其全部的子控件,所以,在onMeasure()
函数中,第一次咱们编码以下:
// 1.0版本的LinearLayout
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 1.经过遍历,对每一个child进行测量
for(int i = 0 ; i < getChildCount() ; i++){
View child = getChildAt(i);
// 2.直接测量子控件
child.measure(widthMeasureSpec, heightMeasureSpec);
}
// ...
// 3.全部子控件测量完毕...
// ...
}
复制代码
这里关注int heightMeasureSpec
参数,咱们知道,这个32位int类型的值,包含了父布局传过来高度的 布局要求:测量的大小和模式。如今咱们思考,若父布局传过来大小的是屏幕的高度,那么将其做为参数直接执行child.measure(widthMeasureSpec, heightMeasureSpec)
,让子控件直接开始测量,是合理的吗?
答案固然是否认的,试想这样一个简单的场景,若LinearLayout
自己设置了padding
值,那么子控件的最大高度便不能再达到heightMeasureSpec
中size的大小了,可是若是像上述代码中的步骤2同样,直接对子控件进行测量,子控件就能够从heightMeasureSpec
参数中取得屏幕的高度,经过setMeasuredDimension()
将本身的高度设置和父控件高度一致——这致使了padding
值配置的失效,并不符合预期。
所以,咱们须要额外设计一个可重写的函数,用于自定义对child
的测量:
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
// 获取子元素的布局参数
final LayoutParams lp = child.getLayoutParams();
// 经过padding值,计算出子控件的布局要求
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
// 将新的布局要求传入measure方法,完成子控件的测量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
复制代码
咱们定义了measureChild()
函数,其做用是计算子控件的布局要求,并把新的布局要求传给子控件,再让子控件根据新的布局要求进行测量,这样就解决了上述的问题,由此也说明了为何 子控件的测量结果是由父控件和其自己共同决定的。
这里咱们注意到咱们设计了一个getChildMeasureSpec()
函数,那么这个函数是作什么的呢?
getChildMeasureSpec()
函数的做用是根据父布局的MeasureSpec
和padding
值,计算出对应子控件的MeasureSpec
,由于这个函数的逻辑是能够复用的,所以将其定义为一个静态函数:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//获取父View的测量模式
int specMode = MeasureSpec.getMode(spec);
//获取父View的测量大小
int specSize = MeasureSpec.getSize(spec);
//父View计算出的子View的大小,子View不必定用这个值
int size = Math.max(0, specSize - padding);
//声明变量用来保存实际计算的到的子View的size和mode即大小和模式
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
//若是父容器的模式是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) {
resultSize = size;//将size即父View的大小减去边距值所获得的值赋值给resultSize
resultMode = MeasureSpec.EXACTLY;//指定子View的测量模式为EXACTLY
//子View的高度或宽度为wrap_content
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;//将size赋值给result
resultMode = MeasureSpec.AT_MOST;//指定子View的测量模式为AT_MOST
}
break;
//若是父容器的测量模式是AT_MOST
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
// 由于父View的大小是受到限制值的限制,因此子View的大小也应该受到父容器的限制而且不能超过父View
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
//若是父容器的测量模式是UNSPECIFIED即父容器的大小未受限制
case MeasureSpec.UNSPECIFIED:
//若是自View的宽和高是一个精确的值
if (childDimension >= 0) {
//子View的大小为精确值
resultSize = childDimension;
//测量的模式为EXACTLY
resultMode = MeasureSpec.EXACTLY;
//子View的宽或高为match_parent
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//由于父View的大小是未定的,因此子View的大小也是未定的
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//根据resultSize和resultMode调用makeMeasureSpec方法获得测量要求,并将其做为返回值
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
复制代码
逻辑分支相对较多,注释中已经将子控件 布局要求 的计算逻辑写清楚了,总结以下图,原图连接:
为何说这个函数很是重要?由于这个函数才是 子控件的测量结果是由父控件和其自己共同决定的 最直接的体现,同时,在不一样的布局模式下(
match_parent
、wrap_content
、指定dp/px
),其对应子控件的布局要求的返回值亦不一样,建议读者认真理解这段代码。
回到前文,如今咱们对onMeasure()
的方法定义以下:
// 2.0版本的LinearLayout
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 1.经过遍历,对每一个child进行测量
for(int i = 0 ; i < getChildCount() ; i++){
View child = getChildAt(i);
// 2.计算新的布局要求,并对子控件进行测量
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
// ...
// 3.全部子控件测量完毕...
// ...
}
复制代码
如今,全部子控件测量完毕,接下来 归流程 的实现就很简单了,将全部child
的height
进行累加,并调用 setMeasuredDimension()
结束测量便可:
// 3.0版本的LinearLayout
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 1.经过遍历,对每一个child进行测量
for(int i = 0 ; i < getChildCount() ; i++){
View child = getChildAt(i);
// 2.计算新的布局要求,并对子控件进行测量
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
// 3.完成子控件的测量,对高度进行累加
int height = 0;
for(int i = 0 ; i < getChildCount() ; i++){
height += child.getMeasuredHeight();
}
// 4.完成LinearLayout的测量
setMeasuredDimension(width, height);
}
复制代码
乍一看,彷佛很难体现出整个流程的 递归 性,实际上当咱们宏观从View
树的树顶顺着往下整理思路,代码逻辑的执行顺序一目了然:
如图所示,实线表明了测量流程中总体自顶向下的 递流程, 而虚线表明了自底向上的 归流程。
至此,测量流程总体实现完毕。
Hello,我是 却把清梅嗅 ,若是您以为文章对您有价值,欢迎 ❤️,也欢迎关注个人 博客 或者 Github。
若是您以为文章还差了那么点东西,也请经过关注督促我写出更好的文章——万一哪天我进步了呢?