自定义View系列教程02--onMeasure源码详尽分析
Android多分辨率适配框架(1)— 核心基础
Android多分辨率适配框架(2)— 原理剖析
Android多分辨率适配框架(3)— 使用指南
自定义View系列教程00–推翻自己和过往,重学自定义View
自定义View系列教程01–常用工具介绍
自定义View系列教程02–onMeasure源码详尽分析
自定义View系列教程03–onLayout源码详尽分析
自定义View系列教程04–Draw源码分析及其实践
自定义View系列教程05–示例分析
自定义View系列教程06–详解View的Touch事件处理
自定义View系列教程07–详解ViewGroup分发Touch事件
自定义View系列教程08–滑动冲突的产生及其处理
PS:如果觉得文章太长,那就直接看视频吧
大家知道,自定义View有三个重要的步骤:measure,layout,draw。而measure处于该链条的首端,占据着极其重要的地位;然而对于measure的理解却不是那么容易,许多问题都是一知半解,比如:为什么父View影响到了子View的MeasureSpec的生成?为什么我们自定义一个View在布局时将其宽或者高指定为wrap_content但是其实际是match_parent的效果?子View的specMode和specSize的生成依据又是什么?这些问题以前一直困扰着我,我就去找资料看,搜了一大筐,沮丧的发现这些文章大同小异:只举个简单的例子,很少研究为什么;人云亦云,文章里的内容没有去验证和深究就发出来了;或者避重就轻直接把难点给绕过去了…….每次,看完这些文章就没有勇气去看layout和draw了,就算了;这可能就是《自定义View——从入门到放弃》的剧本吧。看了那么多文章依旧不能解答原来的疑惑;就像听过了许多大道理依旧不过好这一生。连measure都没有很好的理解又何谈真正的理解layout和draw呢?要是能找到一篇文章能解开这些疑惑该有多好呀!
咦,等等。要是一直没有找到这么一篇文章那又怎么办呢?就真的不学习了?妙龄少妇郭大婶不是说过么:每当你在感叹,如果有这样一个东西就好了的时候,请注意,其实这是你的机会。是啊,机会来了。嗯,我们来一起搞清楚这些问题,我们从源码里来寻找答案。
MeasureSpec基础知识
系统显示一个View,首先需要通过测量(measure)该View来知晓其长和宽从而确定显示该View时需要多大的空间。在测量的过程中MeasureSpec贯穿全程,发挥着不可或缺的作用。
所以,了解View的测量过程,最合适的切入点就是MeasureSpec。
我们先来瞅瞅官方文档对于MeasureSpec 的介绍:
A MeasureSpec encapsulates the layout requirements passed from parent to child.Each MeasureSpec represents a requirement for either the width or the height.A MeasureSpec is comprised of a size and a mode.
请注意这段话所包含的重要信息点:
1 MeasureSpec封装了父布局传递给子View的布局要求。
2 MeasureSpec可以表示宽和高
3 MeasureSpec由size和mode组成
MeasureSpec通常翻译为”测量规格”,它是一个32位的int数据.
其中高2位代表SpecMode即某种测量模式,低30位为SpecSize代表在该模式下的规格大小.
可以通过如下方式分别获取这两个值:
int specSize = MeasureSpec.getSize(measureSpec)
获取SpecSize
int specMode = MeasureSpec.getMode(measureSpec)
获取specMode
当然,也可以通过这两个值生成新的MeasureSpec
int measureSpec=MeasureSpec.makeMeasureSpec(size, mode);
SpecMode一共有三种:
MeasureSpec.EXACTLY , MeasureSpec.AT_MOST , MeasureSpec.UNSPECIFIED
嗯哼,它们已经躺在这里了,我们来挨个瞅瞅,每个SpecMode是什么意思
MeasureSpec.EXACTLY
官方文档的描述:
The parent has determined an exact size for the child. The child is going to be given those bounds regardless of how big it wants to be.
MeasureSpec.EXACTLY模式表示:父容器已经检测出子View所需要的精确大小。
在该模式下,View的测量大小即为SpecSize。
MeasureSpec.AT_MOST
官方文档的描述:
The child can be as large as it wants up to the specified size.
MeasureSpec.AT_MOST模式表示:父容器未能检测出子View所需要的精确大小,但是指定了一个可用大小即specSize
在该模式下,View的测量大小不能超过SpecSize。
MeasureSpec.UNSPECIFIED
官方文档的描述:
The parent has not imposed any constraint on the child. It can be whatever size it wants.
父容器不对子View的大小做限制.
MeasureSpec.UNSPECIFIED这种模式一般用作Android系统内部,或者ListView和ScrollView等滑动控件,在此不做讨论。
看完了这三个SpecMode的含义,我们再从源码里看看它们是怎么形成的。
在ViewGroup中测量子View时会调用到measureChildWithMargins()方法,或者与之类似的方法。源码如下:
/**
* @param child
* 子View
* @param parentWidthMeasureSpec
* 父容器(比如LinearLayout)的宽的MeasureSpec
* @param widthUsed
* 父容器(比如LinearLayout)在水平方向已经占用的空间大小
* @param parentHeightMeasureSpec
* 父容器(比如LinearLayout)的高的MeasureSpec
* @param heightUsed
* 父容器(比如LinearLayout)在垂直方向已经占用的空间大小
*/
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec =
getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight +
lp.leftMargin + lp.rightMargin + widthUsed, lp.width);
final int childHeightMeasureSpec =
getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom +
lp.topMargin + lp.bottomMargin + heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
请务必注意该方法的参数;明白这几个参数的含义才能准确理解方法的实现。
通过这些参数看出来一些端倪,该方法要测量子View传进来的参数却包含了父容器的宽的MeasureSpec,父容器在水平方向已经占用的空间大小,父容器的高的MeasureSpec,父容器在垂直方向已经占用的空间大小等父View相关的信息。这在一定程度体现了:父View影响着子View的MeasureSpec的生成。
该方法主要有四步操作:
第一步:
得到子View的LayoutParams,请参见第15行代码。
第二步:
得到子View的宽的MeasureSpec,请参见第16-18行代码。
第三步:
得到子View的高的MeasureSpec,请参见第19-21行代码。
第四步:
测量子View,请参见第22行代码。
第一步,没啥好说的;第二步和第三步都调用到了getChildMeasureSpec( ),在该方法内部又做了哪些操作呢?
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = View.MeasureSpec.getMode(spec);
int specSize = View.MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
case View.MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = View.MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = View.MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = View.MeasureSpec.AT_MOST;
}
break;
case View.MeasureSpec.AT_MOST:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = View.MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = View.MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = View.MeasureSpec.AT_MOST;
}
break;
case View.MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = View.MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = View.MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = View.MeasureSpec.UNSPECIFIED;
}
break;
}
return View.MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
该方法就是确定子View的MeasureSpec的具体实现。
请注意该方法的参数:
spec
父容器(比如LinearLayout)的宽或高的MeasureSpec
padding
父容器(比如LinearLayout)在垂直方向或者水平方向已被占用的空间。
为什么这么说,它的依据在哪里?
请看在measureChildWithMargins()方法里调用getChildMeasureSpec()的地方,传递给getChildMeasureSpec()的第二个参数是如下构成:
比如:mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
其中:
mPaddingLeft和mPaddingRight表示父容器左右两内侧的padding
lp.leftMargin和lp.rightMargin表示子View左右两外侧的margin
这四部分都不可以再利用起来布局子View.所以说这些值的和表示:父容器在水平方向已经被占用的空间
同理:
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
表示:
父容器(比如LinearLayout)在垂直方向已被占用的空间.
childDimension
通过子View的LayoutParams获取到的子View的宽或高
所以,从getChildMeasureSpec()方法的第一个参数spec和第二个参数padding也可以看出:
父容器(如LinearLayout)的MeasureSpec和子View的LayoutParams共同决定了子View的MeasureSpec!
明白了该方法的参数,我们再来看方法的具体实现步骤。
第一步:
得到父容器的specMode和specSize,请参见第2-3行代码。
第二步:
得到父容器在水平方向或垂直方向可用的最大空间值,请参见第5行代码。
第三步:
确定子View的specMode和specSize,请参见第10-50行代码。
在这里出现了一个很关键的switch语句,该语句的判断条件就是父View的specMode;在此根据父View的specMode的不同来决定子View的specMode和specSize.
情况1:
父容器的specMode为MeasureSpec.EXACTLY,请参见第11-22行代码。
也请记住该先决条件,因为以下的讨论都是基于此展开的。
我们首先看到一个if判断if (childDimension >= 0),或许看到这有点懵了:childDimension>=0是啥意思?难道还有小于0的情况?是的,请注意两个系统常量:
LayoutParams.MATCH_PARENT=-1和LayoutParams.WRAP_CONTENT=-2
所以在此处的代码:
if (childDimension >= 0)
表示子View的宽或高不是match_parent,也不是wrap_content而是一个具体的数值,比如100px。
那么:子View的size就是childDimension,子View的mode也为MeasureSpec.EXACTLY,即:
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
看完这个if,我们来看第一个else if
else if (childDimension == LayoutParams.MATCH_PARENT)
表示子View的宽或高是LayoutParams.MATCH_PARENT。
那么:子View的size就是父容器在水平方向或垂直方向可用的最大空间值即size,子View的mode也为MeasureSpec.EXACTLY,即:
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
我们来看第二个else if
else if (childDimension == LayoutParams.WRAP_CONTENT)
表示子View的宽或高是LayoutParams.WRAP_CONTENT。
那么:子View的size就是父容器在水平方向或垂直方向可用的最大空间值即size,子View的mode为MeasureSpec.AT_MOST,即:
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
情况2:
父容器的specMode为MeasureSpec.AT_MOST,请参见第24-35行代码。
也请记住该先决条件,因为以下的讨论都是基于此展开的。
还是先看这个if判断
if (childDimension >= 0)
表示子View的宽或高不是match_parent,也不是wrap_content而是一个具体的数值,比如100px。
那么:子View的size就是childDimension,子View的mode也为MeasureSpec.EXACTLY,即:
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
继续看第一个else if
else if (childDimension == LayoutParams.MATCH_PARENT)
表示子View的宽或高是LayoutParams.MATCH_PARENT。
那么:子View的size就是父容器在水平方向或垂直方向可用的最大空间值即size,子View的mode也为MeasureSpec.AT_MOST,即:
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
接着看第二个else if
else if (childDimension == LayoutParams.WRAP_CONTENT)
表示子View的宽或高是LayoutParams.WRAP_CONTENT。
那么:子View的size就是父容器在水平方向或垂直方向可用的最大空间值即size,子View的mode也为MeasureSpec.AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
情况3:
父容器的specMode为MeasureSpec.UNSPECIFIED,请参见第37-48行代码。
也请记住该先决条件,因为以下的讨论都是基于此展开的。
还是先看这个if判断
if (childDimension >= 0)
表示子View的宽或高不是match_parent,也不是wrap_content而是一个具体的数值,比如100px。
那么:子View的size就是childDimension,子View的mode也为MeasureSpec.EXACTLY,即:
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
继续看第一个else if
else if (childDimension == LayoutParams.MATCH_PARENT)
表示子View的宽或高是LayoutParams.MATCH_PARENT。
那么:子View的size为0,子View的mode为MeasureSpec.UNSPECIFIED,即:
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
接着看第二个else if
else if (childDimension == LayoutParams.WRAP_CONTENT)
表示子View的宽或高是LayoutParams.WRAP_CONTENT。
那么:子View的size为0,子View的mode为MeasureSpec.UNSPECIFIED,即:
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
至此,我们可以清楚地看到:
子View的MeasureSpec由其父容器的MeasureSpec和该子View本身的布局参数LayoutParams共同决定。
在此经过测量得出的子View的MeasureSpec是系统给出的一个期望值(参考值),我们也可摒弃系统的这个测量流程,直接调用setMeasuredDimension( )设置子View的宽和高的测量值。
对于以上的分析可用表格来规整各一下MeasureSpec的生成
好了,看到这个图,感觉清晰多了。为了便于理解和记忆,我在此再用大白话再对该图进行详细的描述:
在哪些具体的情况下子View的SpecMode为MeasureSpec.EXACTLY?
在此,对各情况一一讨论和分析:
第一种情况:
当子View的LayoutParams的宽(高)采用具体的值(如100px)时且父容器的MeasureSpec为MeasureSpec.EXACTLY或者MeasureSpec.AT_MOST或者MeasureSpec.UNSPECIFIED时:系统返回给该子View的specMode就为MeasureSpec.EXACTLY,系统返回给该子View的specSize就为子View自己指定的大小(childSize)。
通俗地理解:
子View的LayoutParams的宽(高)采用具体的值(如100px)时,那么说明该子View的大小是非常明确的,明确到了令人发指的地址,都已经到了用具体px值指定的地步了。那么此时不管父容器的specMode是什么,系统返回给该子View的specMode总是MeasureSpec.EXACTLY,并且系统返回给该子View的specSize就是子View自己指定的大小(childSize)。
第二种情况:
当子View的LayoutParams的宽(高)采用match_parent时并且父容器的MeasureSpec为MeasureSpec.EXACTLY时:系统返回给该子View的specMode就为 MeasureSpec.EXACTLY,系统返回给该子View的specSize就为该父容器剩余空间的大小(parentLeftSize)。
通俗地理解:
子View的LayoutParams的宽(高)采用match_parent并且父容器的MeasureSpec为MeasureSpec.EXACTLY。这时候说明子View的大小还是挺明确的:就是要和父容器一样大,更加直白地说就是父容器要怎样子View就要怎样。所以,如果父容器MeasureSpec为MeasureSpec.EXACTLY,那么系统返回给该子View的specMode就为 MeasureSpec.EXACTLY和父容器一样;系统返回给该子View的specSize就为该父容器剩余空间的大小(parentLeftSize),即为父容器的剩余大小.
在哪些具体的情况下子View的SpecMode为MeasureSpec.AT_MOST?
在此,对各情况一一讨论和分析:
第一种情况:
当子View的LayoutParams的宽(高)采用match_parent并且父容器的MeasureSpec为MeasureSpec.AT_MOST时:系统返回给该子View的specMode就为MeasureSpec.AT_MOST,系统返回给该子View的specSize就为该父容器剩余空间的大小(parentLeftSize)
通俗地理解:
子View的LayoutParams的宽(高)采用match_parent并且父容器的MeasureSpec为MeasureSpec.AT_MOST。这时候说明子View的大小还是挺明确的:就是要和父容器一样大,直白地说就是父容器要怎样子View就要怎样。但是此时父容器的大小不是很明确其MeasureSpec为MeasureSpec.AT_MOST,那么系统返回给该子View的specMode就为MeasureSpec.AT_MOST和父容器一样;系统返回给该子View的specSize就为该父容器剩余空间的大小(parentLeftSize),即为父容器的剩余大小.
第二种情况:
当子View的LayoutParams的宽(高)采用wrap_content时并且父容器的MeasureSpec为MeasureSpec.EXACTLY时:系统返回给该子View的specMode就为 MeasureSpec.AT_MOST,系统返回给该子View的specSize就为该父容器剩余空间的大小(parentLeftSize)
通俗地理解:
子View的LayoutParams的宽(高)采用wrap_content时说明这个子View的宽高不明确,要视content而定。这时如果父容器的MeasureSpec为MeasureSpec.EXACTLY即父容器是一个精确模式。这种情况概况起来简单地说就是:子View大小是不确定的,但父容器大小是确定的,那么系统返回给该子View的specMode也就是不确定的即为MeasureSpec.AT_MOST,系统返回给该子View的specSize就为该父容器剩余空间的大小(parentLeftSize)
第三种情况:
当子View的LayoutParams的宽(高)采用wrap_content时并且父容器的MeasureSpec为MeasureSpec.AT_MOST时:系统返回给该子View的specMode就为MeasureSpec.AT_MOST,系统返回给该子View的specSize就为该父容器剩余空间的大小(parentLeftSize)
通俗地理解:
子View的LayoutParams的宽(高)采用wrap_content,即说明这个子View的宽高不明确,要视content而定。这个时候父容器的MeasureSpec为MeasureSpec.AT_MOST。这种情况概况起来简单地说就是:子View的宽高是不确定的,父容器的宽高也是不确定的,那么系统返回给该子View的specMode也就是不确定的即为MeasureSpec.AT_MOST,系统返回给该子View的specSize就为该父容器剩余空间的大小(parentLeftSize)
在哪些具体的情况下子View的SpecMode为MeasureSpec.UNSPECIFIED?
前面也说了该模式在实际开发中极少用到,故在此不做讨论。
刚才我们讨论的是:
measureChildWithMargins( )调用getChildMeasureSpec( )
除此以外还有一种常见的操作:
measureChild( )调用getChildMeasureSpec( )
那么,measureChildWithMargins( )和measureChild( )有什么相同点和区别呢?
- measureChildWithMargins( )和measureChild( )均用来测量子View的大小
- 两者在调用getChildMeasureSpec( )均要计算父View已占空间
- 在measureChild( )计算父View所占空间为mPaddingLeft + mPaddingRight,即父容器左右两侧的padding值之和
- measureChildWithMargins( )计算父View所占空间为mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin+ widthUsed。此处,除了父容器左右两侧的padding值之和还包括了子View左右的margin值之和( lp.leftMargin + lp.rightMargin),因为这两部分也是不能用来摆放子View的应算作父View已经占用的空间。这点从方法名measureChildWithMargins也可以看出来它是考虑了子View的margin所占空间的。其实,在源码注释中也说了:
Ask one of the children of this view to measure itself, taking into account both the MeasureSpec requirements for this view and its padding and margins.
好了,搞懂了MeasureSpec我们才正真地进入到View的onMeasure()源码分析。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
onMeasure( )源码流程如下:
(1) 在onMeasure调用setMeasuredDimension( )设置View的宽和高.
(2) 在setMeasuredDimension()中调用getDefaultSize()获取View的宽和高.
(3) 在getDefaultSize()方法中又会调用到getSuggestedMinimumWidth()或者getSuggestedMinimumHeight()获取到View宽和高的最小值.
即这一系列的方法调用顺序为:
为了理清这几个方法间的调用及其作用,在此按照倒序分析每个方法的源码。
先来看getSuggestedMinimumWidth( )
//Returns the suggested minimum width that the view should use
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
该方法返回View的宽度的最小值MinimumWidth.
在此需要注意该View是否有背景.
(1) 若该View没有背景。
那么该MinimumWidth为View本身的最小宽度即mMinWidth。
有两种方法可以设置该mMinWidth值:
第一种:XML布局文件中定义minWidth
第二种:调用View的setMinimumWidth()方法为该值赋值
(2) 若该View有背景。
那么该MinimumWidth为View本身最小宽度mMinWidth和View背景的最小宽度的最大值
getSuggestedMinimumHeight()方法与此处分析很类似,故不再赘述.
接下来看看getDefaultSize( )的源码
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;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
该方法用于获取View的宽或者高的大小。
该方法的第一个输入参数size就是调用getSuggestedMinimumWidth()方法获得的View的宽或高的最小值。
从getDefaultSize()的源码里的switch可看出该方法的返回值有两种情况:
(1) measureSpec的specMode为MeasureSpec.UNSPECIFIED
在此情况下该方法的返回值就是View的宽或者高最小值.
该情况很少见,基本上可忽略
(2) measureSpec的specMode为MeasureSpec.AT_MOST或MeasureSpec.EXACTLY:
在此情况下getDefaultSize()的返回值就是该子View的measureSpec中的specSize。
除去第一种情况不考虑以外,可知:
在measure阶段View的宽和高由其measureSpec中的specSize决定!!
看了这么久的源码,我们终于搞清楚了这个问题;但是刚刚舒展开的眉头又皱起来了。结合刚才的图发现一个问题:在该图的最后一行,如果子View在XML布局文件中对于大小的设置采用wrap_content,那么不管父View的specMode是MeasureSpec.AT_MOST还是MeasureSpec.EXACTLY对于子View而言系统给它设置的specMode都是MeasureSpec.AT_MOST,并且其大小都是parentLeftSize即父View目前剩余的可用空间。这时wrap_content就失去了原本的意义,变成了match_parent一样了.
所以自定义View在重写onMeasure()的过程中应该手动处理View的宽或高为wrap_content的情况。
至此,已经看完了getSuggestedMinimumWidth()和getDefaultSize()
最后再来看setMeasuredDimension( )的源码
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= MEASURED_DIMENSION_SET;
}
经过了前面的一系列操作,终于得到了View的宽高。
在此调用setMeasuredDimension( )设置View的宽和高的测量值。
好了,关于View的onMeasure( )的源码及其调用流程都已经分析完了。
但是,刚才还遗留了一个问题:
自定义View在重写onMeasure()的过程中要处理View的宽或高为wrap_content的情况(请参见下图中的绿色标记部分)
我们该怎么处理呢?
第一种情况:
如果在xml布局中View的宽和高均用wrap_content.那么需要设置
View的宽和高为mWidth和mHeight.
第二种情况:
如果在xml布局中View的宽或高其中一个为wrap_content,那么就将该值设置为默认的宽或高,另外的一个值采用系统测量的specSize即可.
具体的实现可以这样做:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec , heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpceSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode=MeasureSpec.getMode(heightMeasureSpec);
int heightSpceSize=MeasureSpec.getSize(heightMeasureSpec);
if(widthSpecMode==MeasureSpec.AT_MOST&&heightSpecMode==MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth, mHeight);
}else if(widthSpecMode==MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth, heightSpceSize);
}else if(heightSpecMode==MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpceSize, mHeight);
}
}
该部分的处理主要有两步
第一步:
调用super.onMeasure(),请参见第2行代码
第二步:
处理子View的大小为wrap_content的情况,请参见第3-14行代码。
此处涉及到的mWidth和mHeight均为一个默认值;应根据具体情况而设值。
其实,Andorid系统的控件比如TextView等也在onMeasure()中对其大小为wrap_content这一情况作了特殊的处理。
请注意在第二步的代码中用的判断条件:
widthSpecMode==MeasureSpec.AT_MOST或者heightSpecMode==MeasureSpec.AT_MOST或者兼而有之;总之,这里的着眼点是模式MeasureSpec.AT_MOST。
看到这里再联想到下图就有一个疑问了(请参见下图中的红色标记部分):
如果子View在布局文件中采用match_parent,并且父容器的SpecMode为MeasureSpec.AT_MOST, 那么此时该子View的SpecMode也为MeasureSpec.AT_MOST且其View的大小为parentLeftSize。
既然此时该子View的SpecMode也为MeasureSpec.AT_MOST那么当执行到onMeasure()时按照我们的判断逻辑,它的宽或者高至少有一个会被设置成默认值(mWidth和mHeight)。说白了,本来是个match_parent,却被设置成了具体指的默认值。
看到这里好像觉得也没啥不对,但是不符合常理!!!到底是哪里错了??? 我们这么想:
在什么情况下父容器的SpecMode为MeasureSpec.AT_MOST?
这个问题不难回答,共有两种情况:
情况1:
当父容器的大小为wrap_content时系统给父容器的SpecMode为MeasureSpec.AT_MOST.
情况2:
当父容器的大小为match_parent时系统给父容器的SpecMode为MeasureSpec.AT_MOST.
回答了这个问题就以此答案为基础继续讨论。
刚才的问题就可以描述为以下两种情况:
情况1:
当父容器大小为wrap_content且其specMode为MeasureSpec.AT_MOST,子View大小为match_parent。
也就是说:子View想和父容器一样大但父容器的大小又设定为包裹内容大小即wrap_content。那么,到底谁决定谁呢?谁也不能决定谁!父View和子View这父子俩就这么耗上了。
所以,该情况是理论上存在的但在实际情况中是很不合理甚至错误的,当然也是不可取的。
情况2:
当父容器大小为match_parent且其SpecMode为MeasureSpec.AT_MOST,子View大小为match_parent。
既然父容器大小为match_parent且其SpecMode为MeasureSpec.AT_MOST,那么父容器的父容器(以下简称“爷容器”)又该是什么情况呢?
1 爷容器的大小不可能是wrap_content(原理同情况1)
2 爷容器的大小不可能是某个具体的值。
因为若其大小为某具体值,那么其specMode应该为MeasureSpec.EXACTLY;父容器的specMode也该为MeasureSpec.EXACTLY。但是这里父容器的SpecMode为MeasureSpec.AT_MOST,相互矛盾了。
3 爷容器的大小是match_parent;那么其SpecMode有两种情况:MeasureSpec.EXACTLY或者MeasureSpec.AT_MOST。
在此,为了便于理清思路,继续分情况来讨论
第一种情况:
爷容器的大小是match_parent,SpecMode为MeasureSpec.EXACTLY,且父容器此时大小为match_parent;那么父容器的SpecMode应该为MeasureSpec.EXACTLY;但是这里父容器的SpecMode为MeasureSpec.AT_MOST。两者是矛盾的。所以不会出现这个情况。
第二种情况:
爷容器的大小是match_parent,SpecMode为MeasureSpec.AT_MOST,且父容器此时大小为match_parent,那么父容器的SpecMode可以为MeasureSpec.AT_MOST。这是唯一可能存在的情况。
试着将这种情况抽取出来,就陷入到一个循环:子View大小是match_parent其SpecMode为MeasureSpec.AT_MOST;父View的大小也是match_parent其SpecMode也为MeasureSpec.AT_MOST,爷容器亦如此……..直到根View根为止。但是根View的大小如果为match_parent,那么其SpecMode必为MeasureSpec.EXACTLY。所以这种情况也是矛盾的,也不会出现。
至此,综上所述,我们发现:子View在布局文件中采用match_parent,并且父容器的SpecMode为MeasureSpec.AT_MOST,那么此时该子View的SpecMode也为MeasureSpec.AT_MOST且其View的大小为parentLeftSize这种情况本身(图中红色标记部分)就是不合理的,不可取的。将这个问题再次抽取就可以简化为情况1,殊途同归。
以此类推:
(1) 不可能出现根View的大小为wrap_content但它的一个子View大小为match_parent。
(2) 从根到这个子View的父容器都是wrap_content,而子View的大小为match_parent。这个极端情况也是不会的,可见情况1的分析.
(3)从根到这个子View的父容器都是wrap_content,而子View大小也为wrap_content。这是个正常情况也正是我们改良后的onMeasure()来专门处理的子View大小为wrap_content的情况。
刚分析完了View的onMeasure()源码,现在接着看ViewGroup的measure阶段的实现
public abstract class ViewGroup extends View implements ViewParent,ViewManager{ }
首先,请注意ViewGroup是一个抽象类,它没有重写View的onMeasure( )但它提供了measureChildren( )测量所有的子View。在measureChildren()方法中调用measureChild( )测量每个子View,在该方法内又会调用到child.measure( )方法,这又回到了前面熟悉的流程。
即ViewGroup中测量子View的流程:
measureChildren( )—>measureChild( )—>child.measure( )
既然ViewGroup继承自View而且它是一个抽象类,那么ViewGroup的子类就应该根据自身的要求和特点重写onMeasure( )方法。
那么这些ViewGroup的子类会怎么测量子View和确定自身的大小呢?
假若ViewGroup知道了每个子View的大小,将它们累加起来是不是就知道了自身的大小呢?
顺着这个猜想,我们选择ViewGroup的子类LinearLayout瞅瞅,就从它的onMeasure( )开始看吧
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
嗯哼,代码里分了水平线性和垂直线性这两种情况进行测量,类似的逻辑在其onLayout( )阶段也会看到的。我们就选measureVertical( )继续往下看
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
mTotalLength = 0;
int maxWidth = 0;
int childState = 0;
int alternativeMaxWidth = 0;
int weightedMaxWidth = 0;
boolean allFillParent = true;
float totalWeight = 0;
final int count = getVirtualChildCount();
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
boolean matchWidth = false;
boolean skippedMeasure = false;
final int baselineChildIndex = mBaselineAlignedChildIndex;
final boolean useLargestChild = mUseLargestChild;
int largestChildHeight = Integer.MIN_VALUE;
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else {
int oldHeight = Integer.MIN_VALUE;
if (lp.height == 0 && lp.weight > 0) {
oldHeight = 0;
lp.height = LayoutParams.WRAP_CONTENT;
}
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);
if (oldHeight != Integer.MIN_VALUE) {
lp.height = oldHeight;
}
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}
if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
mBaselineChildTop = mTotalLength;
}
if (i < baselineChildIndex && lp.weight > 0) {
throw new RuntimeException("Exception");
}
boolean matchWidthLocally = false;
if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
matchWidth = true;
matchWidthLocally = true;
}
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
childState = combineMeasuredStates(childState, child.getMeasuredState());
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
if (lp.weight > 0) {
weightedMaxWidth = Math.max(weightedMaxWidth,
matchWidthLocally ? margin : measuredWidth);
} else {
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
}
i += getChildrenSkipCount(child, i);
}
if (mTotalLength > 0 && hasDividerBeforeChildAt(count)) {
mTotalLength += mDividerHeight;
}
if (useLargestChild &&
(heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {
mTotalLength = 0;
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
child.getLayoutParams();
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
}
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
int delta = heightSize - mTotalLength;
if (skippedMeasure || delta != 0 && totalWeight > 0.0f) {
float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
mTotalLength = 0;
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
float childExtra = lp.weight;
if (childExtra > 0) {
int share = (int) (childExtra * delta / weightSum);
weightSum -= childExtra;
delta -= share;
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
mPaddingLeft + mPaddingRight +
lp.leftMargin + lp.rightMargin, lp.width);
if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) {
int childHeight = child.getMeasuredHeight() + share;
if (childHeight < 0) {
childHeight = 0;
}
child.measure(childWidthMeasureSpec,
MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
} else {
child.measure(childWidthMeasureSpec,
MeasureSpec.makeMeasureSpec(share > 0 ? share : 0,
MeasureSpec.EXACTLY));
}
childState = combineMeasuredStates(childState, child.getMeasuredState()
& (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
}
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
lp.width == LayoutParams.MATCH_PARENT;
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
mTotalLength += mPaddingTop + mPaddingBottom;
} else {
alternativeMaxWidth = Math.max(alternativeMaxWidth,
weightedMaxWidth);
if (useLargestChild && heightMode != MeasureSpec.EXACTLY) {
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null || child.getVisibility() == View.GONE) {
continue;
}
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
float childExtra = lp.weight;
if (childExtra > 0) {
child.measure(
MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(),
MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(largestChildHeight,
MeasureSpec.EXACTLY));
}
}
}
}
if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
maxWidth = alternativeMaxWidth;
}
maxWidth += mPaddingLeft + mPaddingRight;
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
if (matchWidth) {
forceUniformWidth(count, heightMeasureSpec);
}
}
这段代码的主要操作:
- 遍历每个子View,并对每个子View调用measureChildBeforeLayout(),请参见代码第115-133行
在measureChildBeforeLayout()方法内又会调用measureChildWithMargins()从而测量每个子View的大小。在该过程中mTotalLength保存了LinearLayout的高度,所以每当测量完一个子View该值都会发生变化。 - 设置LinearLayout的大小,请参见代码第241
以上就是LinearLayout的onMeasure( )的简要分析。
其余的ViewGroup的子类均有自己各不相同的measure操作,具体实现请参考对应的源码。
终于,分析完了和Measure有关的主要代码。
代码稍微有点多,所以就有点小小的犯晕了。
再回过头看刚才讨论过的两个方法:
以下为measureChildWithMargins
/**
* @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view
* @param widthUsed Extra space that has been used up by the parent
* horizontally (possibly by other children of the parent)
* @param parentHeightMeasureSpec The height requirements for this view
* @param heightUsed Extra space that has been used up by the parent
* vertically (possibly by other children of the parent)
*/
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
}
对于参数parentWidthMeasureSpec和parentHeightMeasureSpec注释的描述是:
The width(height) requirements for this view
父容器对子View宽(高)的要求(或者说是期望值)。
以下为getChildMeasureSpec
/**
*
* @param spec The requirements for this view
* @param padding The padding of this view for the current dimension and margins, if applicable
* @param childDimension How big the child wants to be in the current dimension
* @return a MeasureSpec integer for the child
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
}
注释对于第一个参数spec的描述也是类似的:
The requirements for this view
咦,为什么我们在解读这两个方法的时候说:
parentWidthMeasureSpec和parentHeightMeasureSpec和spec这三个值表示宽或者高的MeasureSpec呢?
我们的表述和注释的说明怎么不一样呢?
难道我们理解错了?非也,非也。
- 子View的MeasureSpec是由父View的MeasureSpec及子View自身的LayoutParam共同决定的。
- 关于“子View自身的LayoutParam”好理解,就是子View的宽或高等条件
- 那“父View的MeasureSpec”又体现在哪里呢?
当然就是这里出现的parentWidthMeasureSpec和parentHeightMeasureSpec以及spec。只不过文档说得委婉些“对于子View的要求(期望)”。
其实,我个人觉得理解成为“要求”更好一些,就是父View对子View的要求。比如,在绝大多数情况下父View要求子View的大小不能超过其大小,这就是一种要求。而父View的大小就封装在父View的MeasureSpec里的。
所以,可以从这个角度来体会文档的描述。
嗯哼,终于看完了measure的过程。
当理解了MeasureSpec和measure的原理,我们才能更好的理解layout和draw从而掌握自定义View的流程。
who is the next one? ——> layout.
PS:如果觉得文章太长,那就直接看视频吧
自定义View系列教程02--onMeasure源码详尽分析的更多相关文章
- 自定义View系列教程04--Draw源码分析及其实践
深入探讨Android异步精髓Handler 站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)- 核心基础 Android多分辨率适配框架(2)- 原理剖析 Andr ...
- 自定义View系列教程03--onLayout源码详尽分析
深入探讨Android异步精髓Handler 站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)- 核心基础 Android多分辨率适配框架(2)- 原理剖析 Andr ...
- 自定义View系列教程01--常用工具介绍
站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)- 核心基础 Android多分辨率适配框架(2)- 原理剖析 Android多分辨率适配框架(3)- 使用指南 自定 ...
- 自定义View系列教程08--滑动冲突的产生及其处理
深入探讨Android异步精髓Handler 站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)- 核心基础 Android多分辨率适配框架(2)- 原理剖析 Andr ...
- 自定义View系列教程07--详解ViewGroup分发Touch事件
深入探讨Android异步精髓Handler 站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)- 核心基础 Android多分辨率适配框架(2)- 原理剖析 Andr ...
- 自定义View系列教程06--详解View的Touch事件处理
深入探讨Android异步精髓Handler 站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)- 核心基础 Android多分辨率适配框架(2)- 原理剖析 Andr ...
- 自定义View系列教程05--示例分析
站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)- 核心基础 Android多分辨率适配框架(2)- 原理剖析 Android多分辨率适配框架(3)- 使用指南 自定 ...
- 框架源码系列六:Spring源码学习之Spring IOC源码学习
Spring 源码学习过程: 一.搞明白IOC能做什么,是怎么做的 1. 搞明白IOC能做什么? IOC是用为用户创建.管理实例对象的.用户需要实例对象时只需要向IOC容器获取就行了,不用自己去创建 ...
- 一步步实现windows版ijkplayer系列文章之六——SDL2源码分析之OpenGL ES在windows上的渲染过程
一步步实现windows版ijkplayer系列文章之一--Windows10平台编译ffmpeg 4.0.2,生成ffplay 一步步实现windows版ijkplayer系列文章之二--Ijkpl ...
随机推荐
- [转]js的垃圾回收机制
javascript具有自动垃圾收集机制,执行环境会负责管理代码执行过程中使用的内存.在编写javascript程序时,开发人员不用再关心内存使用问题,所需内存的分配以及无用内存的回收完全实现了自动管 ...
- JavaScript的坑,缺陷
JavaScript的缺陷 1.在做判断的时候用=======而不是== 2.浮点预算有精度问题 通过差值去把这个精度锁定到一个范围 Math. Abs(A-B)<0.0001** 3.null ...
- Jeecms网站直接访问html静态页面
jeecms网站维护,遇到了直接通过链接的方式访问静态页面,jeecms官网也做了详细的解答,但是没有得到满意的结果.但是通过自己的深入研究以及别人的帮助,发现了一个很好的解决方法. 首先说明一下je ...
- git命令移动文件夹到另一文件夹
- 一个宽度不确定的DIV里放三个水平对齐的DIV,左右两个DIV宽度固定为100px,中间那个DIV自适应宽度
方法一:浮动 注意三个div的位置 <html><head> <meta charset="utf-8"> <style type=&q ...
- hdu 4722 Good Numbers( 数位dp入门)
Good Numbers Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Tota ...
- YouTube上最受欢迎的十大机器学习视频(最新)
2017-05-04 机器之心 选自KDnuggets 作者:Thuy T. Pham 机器之心编译 参与:微胖.黄小天 虽然 YouTube 有很多不错的机器学习视频,但是很难搞清楚是否值得一看,何 ...
- 2017年浙工大迎新赛热身赛 A 毕业设计选题 【结构体排序】
时间限制:C/C++ 1秒,其他语言2秒空间限制:C/C++ 65536K,其他语言131072K64bit IO Format: %lld 题目描述 又到了一年一度,大四老学长们毕业设计选题的时候, ...
- Spring MVC使用ModelAndView进行重定向(转)
1.Servlet重定向forward与redirect: 使用servlet重定向有两种方式,一种是forward,另一种就是redirect.forward是服务器内部重定向,客户端并不知道服务器 ...
- JS对HTML实体字符转义和反转义
一.名词解释 HTML实体字符: 由于在HTML中有些符号是预留的,比如在html中不能直接使用尖括号(‘<’或‘>’),会被误认为标签符号.所以需要通过HTML实体字符去进行替换: HT ...