目录
- MeasureSpec
- LayoutParam
- 度量过程
之前一直知道一个界面的测量过程是由Android系统便利ViewTree,递归调用各个View或者说ViewGroup的
onMeasure()
方法完成的,但是对于每一个组件具体的测量过程还是不求甚解,于是阅读以下源码,稍微学习了一下,特此记录。
MeasureSpec
MeasureSpec
是View
的一个静态内部类,是用于辅助完成View的测量的工具类之一,从字面上来看MeasureSpec->测量规格,也就是说Measure会告诉Android系统该如何做,即如何测量。
MeasureSpecs被实现为一个32位的int
值,高两位用于表示SpecMode,低30位表示SpecSize
再看一下源码对这个类的注释说明是这样的:
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.
其中有两个关键点
- MeasureSpec封装了从父级传递到子级的布局要求
- MeasureSpec由大小和模式组成,对应刚才说到的SpecMode和SpecSize
然后再看一下MeasureSpec
的内部构成
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* Measure specification mode: 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.
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
- 可以看到类内一共定义了五个常量,首先是 MODE_SHIFT 和 MODE_MASK。当我们拿到一个MeasureSpec的具体值时,就可以通过
Measure.getMode(int measureSpec)
和Measure.getSize(int measureSpec)
这两个方法对MeasureSpec具体值进行解包得到具体的SpecMode和SpecSize了。这两个工具方法就是通过将 MODE_SHIFT , MODE_MASK 和具体MeasureSpec的进行简单的位运算实现的。当然Measure
内部也提供了makeMeasureSpec( int size, int mode)
方法,可将SpecSize和SpecMode再次打包为一个MeasureSpec
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
- 剩下的三个常量UNSPECIFIED,EXACTLY,AT_MOST对应上文SpecSize的具体实现
- UNSPECIFIED
表示:父容器的测量模式为未指定模式。在这种情况下,父容器布局没有对子View施加任何约束。 可以是任何大小 - EXACTLY
表示:父容器的测量模式为精确模式。在这种情况下,表明父容器已经有了一个确切值,子View的测量过程将受到这个值的限定 - AT_MOST
表示: 父容器的测量模式为最大限定模式。在这种情况下,子View可以根据自己的具体需求分配尺寸大小,但是不可以超过父容器的尺寸
LayoutParams
LayoutParams
是ViewGroup
的一个静态内部类,其中存储了View在XML定义的具体尺寸属性。用于告诉父容器,当前View想要如何被布局。
在看一下LayoutParams的内部构造
/**
* Special value for the height or width requested by a View.
* MATCH_PARENT means that the view wants to be as big as its parent,
* minus the parent's padding, if any. Introduced in API Level 8.
*/
public static final int MATCH_PARENT = -1;
/**
* Special value for the height or width requested by a View.
* WRAP_CONTENT means that the view wants to be just large enough to fit
* its own internal content, taking its own padding into account.
*/
public static final int WRAP_CONTENT = -2;
/**
* Information about how wide the view wants to be. Can be one of the
* constants FILL_PARENT (replaced by MATCH_PARENT
* in API Level 8) or WRAP_CONTENT, or an exact size.
*/
@ViewDebug.ExportedProperty(category = "layout", mapping = {
@ViewDebug.IntToString(from = MATCH_PARENT, to = "MATCH_PARENT"),
@ViewDebug.IntToString(from = WRAP_CONTENT, to = "WRAP_CONTENT")
})
public int width;
/**
* Information about how tall the view wants to be. Can be one of the
* constants FILL_PARENT (replaced by MATCH_PARENT
* in API Level 8) or WRAP_CONTENT, or an exact size.
*/
@ViewDebug.ExportedProperty(category = "layout", mapping = {
@ViewDebug.IntToString(from = MATCH_PARENT, to = "MATCH_PARENT"),
@ViewDebug.IntToString(from = WRAP_CONTENT, to = "WRAP_CONTENT")
})
public int height;
这里的width
和height
分别对应我们在XML中使用android:layout_width=""
和 android:layout_height=""
标签分配给View的宽高的期望值。
当width
或height
为大于0的具体值时,对应我们在XML给View指定的具体期望值,例如android:layout_width="50dp"
。
当width
或height
的值小于0,例,width = -1
, width = -2
分别对应XML中的 android:layout_width="match_parent"
和 android:layout_height="wrap_content"
度量过程
首先看一下ViewGroup.java中的measureChildren
方法
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
显而易见,这里通过遍历所有子View,然后调用measureChild()
方法,对ViewGroup内的所有子View进行了度量。
这里需要说明一下的是,这个方法其实只被NumPadKey
和AbsoluteLayout
在其各自的onMeasure
方法中调用了,我们所熟悉的一些ViewGroup通常都会在自己的onMeasure
方法中根据业务需求遍历所有子View,进而调用measureChild()
方法完成ViewGroup中的子View度量。
那么我们跟进去看一下measureChild()
方法
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
这个方法也很简单,一共做了三件事
- 首先拿到子View的
LayoutParams
属性。 - 然后分别结合ViewGroup的
parentWidthMeasureSpec
和parentHeightMeasureSpec
属性调用getChildMeasureSpec
得到子View在宽维度和高维度上的MeasureSpec
属性。 - 最后调用子View的
measure
方法,这个方法最终又会调用到子View的onMeasure()
方法,进而完成子View的内部度量。如此循环,最终完成ViewTree的递归度量。
这里需要再看一下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) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
首先看一下这个方法的三个入参,从上文的介绍中可以知道:
- int spec :
当前测量维度(宽或者高)上ViewVroup的MeasureSpec
属性。 - int padding:
这里可以理解为父ViewGroup在当前测量维度上已经用掉的空间所占大小。这里需要特别特别特别说明一下,padding直译就是填充的意思。我第一次看这里的时候,总把这里的padding和XML中所定义的padding属性想当然的联系起来,导致我一度认为如果每次传进来都是固定的padding,那么每次度量子View时可用的剩余空间都是一致的,如果ViewGroup里边有很多子View的情况下,如何实现每次度量,ViewGroup可用空间就要减去相应的子View所占去的空间的逻辑,就没办法合理的限制超出ViewGroup可使用空间的子View了呀? 进而导致在这里困惑了好久,直到看LinearLayout的测量过程才突然惊醒。。。。惯性思维真的害死人。即,当当前ViewGroup里边有N个子View控件时,当我们完成了第N-1个子View的度量后,再去度量第N个View的时候,这个padding完全可以用来表示之前N-1个子View加上父容器的padding属性后所占掉的空间,即你可以理解为这N-1个View已经被不重叠的(如果重叠,只会用去最大的一个View的空间嘛)填充进了这个ViewGroup内(当然在Layout之后,各个View才会被以此摆放在ViewGroup内)。 - int childDimension:
被测量子View在当前测量维度上的LayoutParam属性,即LayoutParams.width
或LayoutParams.height
这个方法一共分为三个关键步骤
- 将当前ViewGroup的MeasureSpec进行解包,分别得到当前测量维度上的
specMode
和specSize
- 根据当前ViewGroup的
specMode
和specSize
属性结合当前被测量View的LayoutParam
属性确定当前被测量View的MeasureSpec属性- 最终将
specMode
和specSize
打包成完整的MeasureSpec返回
关于第一步,稍微说一下就是,得到ViewGroup的specSize
之后,再通过
int size = Math.max(0, specSize - padding);
就得到了当前ViewGroup还剩多少空间可以使用。也就是我当初绊倒的地方。。。
OK, 然后重点看一下剩下的switch块内的逻辑,首先看一下第一种情况,即ViewGroup的specMode
为MeasureSpec.EXACTLY
为精确模式时,这里又分为三种情况:
- 如果子View已经定义了明确的尺寸,那么View的大小就是这个值。这时子View的
SpecMode
自然也就被确定为了MeasureSpec.EXACTLY
模式。- 如果子View在当前被测量维度的属性为
MATCH_PARENT
时,这时候子View在当前测量维度的SpecSize
就为ViewGroup剩下的可支配大小。因为大小已经确定,所以子View的SpecMode
自然而然的也就被确定为了MeasureSpec.EXACTLY
。- 如果子View在当前被测量维度的属性为
WRAP_CONTENT
时,这时候,子View在当前测量维度的最大尺寸也只能为ViewGroup所剩下的空间的最大值,故此时子View的SpecSize
被确定为了ViewGroup所剩的SpecSize
的值,SpecMode
也相应被确定为了MATCH_PARENT
模式
剩下的两种情况和第一种情况类似,无非是根据ViewGroup的SpecMode
得到子View相应的SpecMode
和SpecSize
而已,也就不再赘述。
需要特殊说明的是,当ViewGroup在当前测量维度的MeasureSpec的模式为MeasureSpec.UNSPECIFIED
模式这种情况,大多发生在系统内部测量以及可滚动ViewGroup的度量过程中,这里不再展开。但是这里有个地方特殊说明一下这里:
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
可以看到当ViewGroup的MeasureSpec为MeasureSpec.UNSPECIFIED
时,且子View在当前测量维度未定义明确的尺寸时,子View最终的MeasureSpec的size属性是由View.sUseZeroUnspecifiedMeasureSpec
这个属性决定的。不过这个地方稍微跟一下就可以确定啦。sUseZeroUnspecifiedMeasureSpec
是View.java类中一个静态变量初始值为flase
,且这个变量只在View的构造方法被赋值过,赋值逻辑如下:
// In M and newer, our widgets can pass a "hint" value in the size
// for UNSPECIFIED MeasureSpecs. This lets child views of scrolling containers
// know what the expected parent size is going to be, so e.g. list items can size
// themselves at 1/3 the size of their container. It breaks older apps though,
// specifically apps that use some popular open source libraries.
sUseZeroUnspecifiedMeasureSpec = targetSdkVersion < Build.VERSION_CODES.M;
所以这种情况下子View的MeasureSpec的size属性为
- ViewGroup在当前测量维度剩余值,对应-> targetSdkVersion >= Build.VERSION_CODES.M
- 0, 对应 targetSdkVersion < Build.VERSION_CODES.M
最终根据这个switch块,假定:
- 通过getChildMeasureSpec()中传入的第一个参数解包得到ViewGroup的SpecSize和SpecMode分别为
specMode
和specSize
- 通过getChildMeasureSpec()中第二个参数padding得到的ViewGroup在当前测量维度上的剩余空间为
parentSize
( parentSize = Math.max(0, specSize - padding) )- 通过第三个参数得到子View在当前测量维度上的Layoutparams属性为
childDimension
- 此时若将子View的SpecSize和SpecMode分别参照代码命名为
resultSize
和resultMode
我们将到的下面这张在网上以及任玉刚大佬书里边可以看到的表格啦
说到这里ViewGroup的度量过程也就基本完成了,为什么说基本呢?毕竟我们现在只拿到了两样东西
- ViewGroup的MeasureSpec属性
- ViewGroup内各个View的MeasureSpec属性
MeasureSpec说到底也只是参考规范,而不是具体值,也就是我们还是不知道这个ViewGroup具体应该是占用多少空间,具体宽高是多少呢。其实我们看一下View的onMeasure()
方法就知道答案了
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
可以看出,这个方法只调用了setMeasuredDimension()
一个方法。其实也就是View最终通过自己的(注意这里是自己的了)widthMeasureSpec
和heightMeasureSpec
稍微再结合一下自身其他方面对尺寸进行约束的属性,如getSuggestedMinimumWidth()
,就得到了这个View的最终的mMeasuredWidth
和mMeasuredHeight
了。也就是为什么在onMeasure()
方法之后,我们就可以通过getMeasuredWidth()
和getMeasuredHeight()
得到对应View的具体尺寸值了。
所以,在ViewGroup对所有子View完成遍历度量后,得到的不仅仅有所有子View的MeasureSpec,还有各个子View的具体尺寸值。这时候再根据
- 自身的MeasureSpec
- 各个子View的具体尺寸值
- 业务逻辑
进行最后的约束处理,就可以确定ViewGroup自身实际大小了,最终调用setMeasuredDimension(int measuredWidth, int measuredHeight)
将最终尺寸进行保存就可以了。同样,这之后在对应的ViewGroup上调用getMeasuredWidth()
和getMeasuredHeight()
就可以了得到这个ViewGroup的实际大小了。
总结:也就是说,当需要自定义个ViewGroup,可以分以下几步进行(度量方面)就可以了
- 继承ViewGroup
- 重写
onMeasure()
方法- 遍历所有子View,对子View进行度量
- 调用
setMeasuredDimension(int measuredWidth, int measuredHeight)
进行自身尺寸的保存