爲了更好地理解View地測量過程,我們還需要MeasureSpec。從名字上來看,MeasureSpec看起來像“測量規格”或者“測量說明書”,不管怎麼翻譯,它看起來都好像是或多或少地決定了View的測量過程。通過源碼可以發現,MeasureSpec的確參與了View的measure過程。那麼,MeasureSpec是幹什麼的呢?確切來說,MeasureSpec在很大程度上決定了一個View的尺寸規格,之所以說是很大程度上是因爲這個過程還受父容器的影響,因爲父容器影響View的MeasureSpec的創建過程。在測量過程中,系統會將View的LayoutParams根據父容器所施加的規則轉換成對應的MeasureSpec,然後在根據這個MeasureSpec來測量出View的寬/高。上面提到過,這裏的寬/高是測量寬/高,不一定等於View的最終寬/高。MeasureSpec看起來有點複雜,其實它的實現過程很簡單的,下面會詳細地分析MeasureSpec。
1.MeasureSpec
MeasureSpec代表一個32位int值,高2位代表SpecMode,低30位代碼SpecSize,SpecMode是指測量模式,而SpecSize是指在某種測量模式下地規格地大小。下面先看一下MeasureSpec內部地一些常量地定義,通過下面地代碼,應該不難理解MeasureSpec的工作過程。
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/** @hide */
@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
@Retention(RetentionPolicy.SOURCE)
public @interface MeasureSpecMode {}
/**
* 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;
/**
* Creates a measure specification based on the supplied size and mode.
*
* The mode must always be one of the following:
* <ul>
* <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li>
* <li>{@link android.view.View.MeasureSpec#EXACTLY}</li>
* <li>{@link android.view.View.MeasureSpec#AT_MOST}</li>
* </ul>
*
* <p><strong>Note:</strong> On API level 17 and lower, makeMeasureSpec's
* implementation was such that the order of arguments did not matter
* and overflow in either value could impact the resulting MeasureSpec.
* {@link android.widget.RelativeLayout} was affected by this bug.
* Apps targeting API levels greater than 17 will get the fixed, more strict
* behavior.</p>
*
* @param size the size of the measure specification
* @param mode the mode of the measure specification
* @return the measure specification based on size and mode
*/
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);
}
}
/**
* Like {@link #makeMeasureSpec(int, int)}, but any spec with a mode of UNSPECIFIED
* will automatically get a size of 0. Older apps expect this.
*
* @hide internal use only for compatibility with system widgets and older apps
*/
public static int makeSafeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}
/**
* Extracts the mode from the supplied measure specification.
*
* @param measureSpec the measure specification to extract the mode from
* @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
* {@link android.view.View.MeasureSpec#AT_MOST} or
* {@link android.view.View.MeasureSpec#EXACTLY}
*/
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
/**
* Extracts the size from the supplied measure specification.
*
* @param measureSpec the measure specification to extract the size from
* @return the size in pixels defined in the supplied measure specification
*/
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
MeasureSpec通過SpecMode和SpecSize打包成一個int值來避免過多的對象內存分配,爲了方便操作,其提供了打包和解包方法。SpecMode和SpecSize也是一個int值,一組SpecMode和SpecSize可以打包位一個MeasureSpec,而一個MeasureSpec可以通過解包的形式來得出其原始的SpecMode和SpecSize,需要注意的是這裏提到的MeasureSpec是指MeasureSpec所代表的int值,而非MeasureSpec本身。
SpecMode有三類,每一類都表示特殊的含義,如下所示。
UNSPECIFIED
父容器不對View有任何限制,要多大給多大,這種情況一般用於系統內部,表示一種測量狀態。
EXACTLY
父容器以及檢測出View所需要的大小,這個時候View的最終大小就是SpecSize所指定的值。它對應於LayoutParams中的match_parent和具體數值這兩種模式。
AT_MOST
父容器指定了一個可用大小即SpecSize,View的大小不能大於這個值,具體是什麼要看不同View的具體實現。它對應於LayoutParams中的wrap_content。
2.MeasureSpec和LayoutParams的對應關係
上面提到,系統內部是通過MeasureSpec來進行View的測量,但是正常情況下我們使用View指定MeasureSpec,儘管如此,但是我們可以給View設置LayoutParams。在View測量的時候,系統會將LayoutParams在父容器的約束下轉換成對應的MeasureSpec,然後再根據這個MeasureSpec來確定View測量後的寬高。需要注意的是,MeasureSpec不是唯一由LayoutParams決定的,LayoutParams需要和父容器一起才能決定View的MeasureSpec,從而進一步決定View的寬高。另外,對於頂級View(即DecorView)和普通View來說,MeasureSpec的轉換過程略有不同。對於DecorView,其Measure由窗口的尺寸和其自身的LayoutParams來共同決定;對於普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams來共同決定,MeasureSpec一旦確定後,onMeasure中就可以確定View測量的寬/高。
對於DecorView來說,在ViewRootImpl中的measureHierarchy方法中由如下一段代碼,它展示了DecorView的MeasureSpec的創建過程,其中desiredWindowWidth和desiredWindowHeight是屏幕的尺寸:
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
接着再看一下getRootMeasureSpec方法的實現:
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
通過上述代碼,Decor的MeasureSpec的產生過程就很明確了,具體來說遵守如下規則,根據它的LayoutParams中的寬高的參數來劃分。
- LayoutParams.MATCH_PARENT:精確模式,大小就是窗口的大小
- LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超過窗口的大小
- 固定大小(比如100dp):精確模式,大小位LayoutParams中指定的大小。
對於普通View來說,這裏我們指佈局中的View,View的measure過程由ViewGroup傳遞而來,先看一下ViewGroup的measureChildWithMargins方法:
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);
}
上述方法回對子元素進行measure,在調用子元素的measure方法之前會先通過getChildMeasureSpec方法來得到子元素的MeasureSpec。從代碼來看,很顯然,子元素的Measure的創建與父容器的Measure和子元素本身的LayoutParams有關,此外還和View的margin以及padding有關,具體情況可以看下ViewGroup的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) {
// 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);
}
上述方法不難理解,它的主要作用是根據父容器的MeasureSpec同時結合View本身的LayoutParams來確定子元素的MeasureSpec,參數中的padding是指父容器中已佔用的空間大小,因此子元素可用的大小位父容器的尺寸減去padding,具體代碼如下所示。
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
getChildMeasureSpec清楚展示了普通View的MeasureSpec的創建規則,爲了更加清晰地理解getChildMeasureSpec的邏輯,這裏提供一個表,表中對getChildMeasureSpec的工作原理進行了梳理,請看下錶。注意,表中的parentSize是指父容器中目前可使用的大小。
針對表,這裏再做一下說明。簽名已經提到,對於普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams來共同決定,那麼針對不同的父容器和View本身不同的LayoutParams,View就可以有多種MeasureSpec。這裏簡單說下,當View採用固定寬高的時候,不管父容器的MeasureSpec是什麼,View的MeasureSpec都是精確模式且其大小遵循LayoutParams中的大小。當View的寬高是match_parent時,如果父容器的模式是精準模式,那麼View也是最大模式並且其大小不過超過父容器的剩餘空間;如果父容器是最大模式,那麼View也是最大模式並且其大小不會超過父容器的剩餘空間。當View的寬高是wrap_content時,不管父容器的模式時精準還是最大化,View的模式總是最大化並且大小不能超過父容器的剩餘空間。可能讀者會發現,在我們的分析中漏掉了UNSPEIFIED模式,那是因爲這個模式主要用於系統內部多次Measure的情形,一般來說,我們也不需要關注此模式。
通過上表可以看出,只要提供父容器的MeasureSpec和子元素的LayoutParams,就可以快速地確定出子元素地MeasureSpec了,有了MeasureSpec就可以進一步確定出子元素測量後的大小了。需要說明的是,表中並非是什麼經驗總結,它只是getChildMeasureSpec這個方法以表格的方式呈現出來而已。