前言
对于MeasureSpec,你了解多少呢?它的作用是什么?它的mode和size具体指的是什么?MeasureSpec是如何计算的,受哪些因素影响?父View测量好子View的MeasureSpec之后,子View会如何处理?View/ViewGroup、DecorView的MeasureSpec有什么区别?UNSPECIFIED这个特殊模式又有什么用途呢?
介绍
首先,我们来看一下这个类:
```java
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
//00后面跟30个0
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
//01后面跟30个0
public static final int EXACTLY = 1 << MODE_SHIFT;
//10后面跟30个0
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
//获取mode
public static int getMode(int measureSpec) {
//保留高2位,剩下30个0
return (measureSpec & MODE_MASK);
}
//获取size
public static int getSize(int measureSpec) {
//替换高两位00,保留低30位
return (measureSpec & ~MODE_MASK);
}
}
```
接下来,我们来详细解释一下这段代码中的每个部分。
我留下了比较重要的三个方法:makeMeasureSpec、获取mode和获取size。这些方法用于生成一个MeasureSpec,生成的方式就是size+mode,得到一个32位的int值。这样做的目的主要是避免过多的对象内存分配。
我们可以大致猜测,这个MeasureSpec就是用来标记View的测量参数,其中测量模式可能和View具体怎么显示有关,而测量大小就是值的View实际大小。当然,这只是我们的初步猜测。要搞清楚具体信息,就要从View树的绘制测量开始说起。
首先,测量代码是从ViewRootImpl的measureHierarchy开始的,然后会执行到performMeasure方法:
```java
private void measureHierarchy(){
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
```
很明显,在这里就会进行第一次MeasureSpec的计算,并且传给了下层的mView,也就是DecorView。那我们就来看看DecorView的MeasureSpec测量规格计算方式:
您好!View的测量规格mode的含义是:如果View的值是确定大小,比如MATCH_PARENT或者固定值,那么它的测量模式就是MeasureSpec.EXACTLY。如果View的值是自适应,比如WRAP_CONTENT,那么它的测量模式就是 MeasureSpec.AT_MOST。
在给定的代码中,getRootMeasureSpec()方法根据rootDimension参数返回一个MeasureSpec对象。如果rootDimension等于ViewGroup.LayoutParams.MATCH_PARENT,则表示View的大小不能改变,因此测量模式为 MeasureSpec.EXACTLY;如果rootDimension等于ViewGroup.LayoutParams.WRAP_CONTENT,则表示View的大小可以改变,因此测量模式为 MeasureSpec.AT_MOST;否则,表示View的大小是固定的,因此测量模式也为 MeasureSpec.EXACTLY。
对于具体的View/ViewGroup测量,我们可以使用另一个方法`measureChildWithMargins`,这个方法在许多布局中都会看到,例如LinearLayout。下面是该方法的代码:
```java
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (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的LayoutParams。然后,根据padding、margin、width以及parentWidthMeasureSpec计算出宽的测量模式——childWidthMeasureSpec。高度测量模式也是类似的。至此,我们对子View的测量模式MeasureSpec有了更深入的认识:它与两个元素有关:子View的LayoutParams(包括margin和width),以及父View的MeasureSpec(再加上padding)。
接下来,我们可以继续研究`getChildMeasureSpec`方法。
以下是重构后的代码:
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) {
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;
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. 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;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
以下是重构后的内容:
```java
} 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 = size;
resultMode = MeasureSpec.AT_MOST;
}
```
这段代码的主要目的是计算子View的MeasureSpec。首先,根据父View的specMode和specSize以及子View的LayoutParams,计算出子View的测量大小(resultSize)和测量模式(resultMode)。然后,根据这些信息,使用MeasureSpec.makeMeasureSpec方法创建一个新的MeasureSpec对象并返回。
代码如下:
```java
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
```
举一个例子,当父view的测量模式为MeasureSpec.EXACTLY,子View宽的LayoutParams为MATCH_PARENT。想象一下,这种情况,子View的宽肯定就会占满父View的大小,所以子View的测量模式中的mode肯定就是确定值,为MeasureSpec.EXACTLY,而大小就是父View的大小了。对应的代码就是:
```java
case MeasureSpec.AT_MOST:
if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
}
```
综合所有的情况,一张经典的表格就来了:
| 父View的测量模式 | 子View的测量模式 | 含义 |
| --- | --- | --- |
| MeasureSpec.EXACTLY | MeasureSpec.EXACTLY | 父View可以确定子View的精确大小,比如子View大小是固定的值,在所有的情况下都会是EXACTLY模式。 |
| MeasureSpec.AT_MOST | MeasureSpec.EXACTLY | 父View给定一个最大的值,意思是子View大小可以不确定,但是肯定不能超过某个最大的值,例如窗口的大小。 |
| MeasureSpec.UNSPECIFIED | MeasureSpec.UNSPECIFIED | 父View对子View完全没限制,要多大给多大。这个模式似乎听起来有点奇怪?待会我们再细谈。 |
到此,似乎就结束了?当然没啦,获取子View的MeasureSpec之后,子View又会怎么处理呢?这部分代码没有给出,但通常情况下,子View会根据其自身的需求和布局要求来设置其测量大小和测量模式。
在上文中,当测量子View的尺寸规格后,会调用`child.measure`方法。这个方法是View的`measure`方法,它会继续执行`onMeasure`方法。
以下是重构后的代码:
```java
protected void measureChildWithMargins() {
final MarginLayoutParams lp = (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);
}
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
onMeasure(widthMeasureSpec, heightMeasureSpec);
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int measuredWidth = getDefaultSize(Math.min(widthMode == MeasureSpec.EXACTLY ? widthSize : getSuggestedMinimumWidth(), widthMeasureSpec), widthMode, widthSize);
int measuredHeight = getDefaultSize(Math.min(heightMode == MeasureSpec.EXACTLY ? heightSize : getSuggestedMinimumHeight(), heightMeasureSpec), heightMode, heightSize);
setMeasuredDimension(measuredWidth, measuredHeight);
}
```
这段代码的作用是设置子View的测量宽度和高度。首先,它会检查当前布局模式是否为光学模式(optical),如果不是,则根据光学模式的值调整测量宽度和高度。然后,调用setMeasuredDimensionRaw方法,将计算出的测量宽度和高度传递给父View。
在这段代码中,measuredWidth用于获取子View的宽度大小。getMeasuredWidth()方法返回的值是通过将mMeasuredWidth与MEASURED_SIZE_MASK进行按位与操作得到的。这个操作限制了返回的大小,使得其不会超过父View的建议最小宽度和最大宽度之间的值。
接下来,我们来看看getDefaultSize()方法的作用。这个方法接收两个参数:一个是建议的最小宽度,另一个是测量规格(measureSpec)。它的主要作用是验证测量规格中的size是否就是我们要获取的视图的宽度或高度。如果是,则返回建议的最小宽度;否则,返回0。这意味着如果测量规格中的size不符合我们的期望,那么我们可以认为视图的实际宽度或高度可能不正确。
以下是重构后的内容:
```
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;
}
```
可以看到,在 AT_MOST 和 EXACTLY 这两种常用的情况下,确实是等于测量大小 specSize 的。只是在一个特殊情况,也就是 UNSPECIFIED 的时候,这个大小会等于 getSuggestedMinimumWidth() 方法的大小。问题来了,UNSPECIFIED 模式到底是什么,getSuggestedMinimumWidth() 方法又做了什么?
```
UNSPECIFIED
```
很多文章会忽略这个模式,其实它也是很重要的,在前两天的讨论群中,我们还讨论了这个问题,一起看看吧~首先,我们看看什么时候会存在 UNSPECIFIED 模式呢?它的概念是父 View 对子 View 的大小没有限制,很容易想到的一个控件就是 ScrollView,那么在 ScrollView 中肯定有对这个模式的设置:
```java
@Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
}
```
在ScrollView中重写了measureChildWithMargins方法后,childHeightMeasureSpec的计算发生了变化。原来的代码中,直接调用了makeSafeMeasureSpec方法生成了MeasureSpec,并将SpecMode设置成了MeasureSpec.UNSPECIFIED,表示子View的高度是无限制的。
这也是符合ScrollView的理念的。当ScrollView嵌套一个普通View时,就会触发getDefaultSize中UNSPECIFIED的逻辑,也就是View的实际大小为getSuggestedMinimumWidth的大小。
接下来看看getSuggestedMinimumWidth到底获取的是什么大小:它返回的是一个整数值,如果view的背景为null,则等于最小宽度mMinWidth;如果view的背景不为null,则等于最小宽度和背景的最小宽度中取较大值。
您好,MeasureSpec是Android中用于测量View大小的一个32位整数。其中,SpecMode为高两位,共有三种模式:精确模式(MeasureSpec.EXACTLY),最大模式(MeasureSpec.AT_MOST)和未指定模式(MeasureSpec.UNSPECIFIED) 。
在自定义View和ViewGroup的时候,我们经常会遇到int型的MeasureSpec来表示一个组件的大小,这个变量里面不仅有组件的尺寸大小,还有大小的模式。在系统中组件的大小模式有三种:精确模式(MeasureSpec.EXACTLY),最大模式(MeasureSpec.AT_MOST)和未指定模式(MeasureSpec.UNSPECIFIED)。
如果是UNSPECIFIED模式,实际大小为0。否则实际大小就等于计算好的specSize。