??onMeasure()方法從字面意思理解就是測量茸歧,還有個方法名很相似的方法measure()罗标。其中measure()是View測量的入口,它是一個被final修飾的方法获枝,這意味著無法被子類重寫蠢正,實質性的工作就是測量出一個View的實際大小,而真正實現(xiàn)測量工作的方法是onMeasure()來完成的省店,所以具體實現(xiàn)自定義View的測量工作我們是要去實現(xiàn)onMeasure()方法來完成的嚣崭。
先看下測量的基本流程
關于view的繪制流程,這就要從ViewRoot和DecorView的概念說起懦傍。
??《Android藝術開發(fā)探索》中介紹雹舀,ViewRoot對應于ViewRootImpl類,是連接WindowManager和DecorView的紐帶粗俱,View的三大繪制流程都是通過ViewRoot來完成的说榆。在ActivityThread中,當Activity被創(chuàng)建時寸认,會將DecorView添加到Window中签财,同時創(chuàng)建一個ViewRootImpl對象,病假ViewRootImpl對象和DecorView對象建立關聯(lián)偏塞。
??在開發(fā)中不能再子線程進行更新UI的操作荠卷,其實質性原因是ViewRootImpl是在UI線程中創(chuàng)建的,而ViewRootImpl在View中對應的是mParent變量烛愧,即ViewParent接口油宜。在調用其requestFitSystemWindows, requestLayout, invalidateChildInParent時候,都會調用measure方法怜姿。
那么measure到底是用來做什么的呢慎冤?
??從View的源碼中可以知道View的實際大小寬對應的值是mMeasuredWidth,高對應的值是mMeasuredHeight沧卢。而measure就是用來計算著兩個值用的蚁堤。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
boolean optical = isLayoutModeOptical(this);
//先判斷是否有設置為opticalBounds的屬性
if (optical != isLayoutModeOptical(mParent)) {
//如果當前的opticalBounds的屬性和父類的不一致,就重新計算wdithMeasureSpec和heightMeasureSpec但狭。
Insets insets = getOpticalInsets();
int oWidth = insets.left + insets.right;
int oHeight = insets.top + insets.bottom;
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
}
// Suppress sign extension for the low bytes
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
// Optimize layout by avoiding an extra EXACTLY pass when the view is
// already measured as the correct size. In API 23 and below, this
// extra pass is required to make LinearLayout re-distribute weight.
final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
|| heightMeasureSpec != mOldHeightMeasureSpec;
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
//窗體需要強制刷新的時候披诗,如果是不用measure cache撬即,那么需要調用onMeasure方法進行進行view的width和height。如果有measure cache呈队,那么只需要拿到measure cache里的值進行更新就可以了剥槐。
if (forceLayout || needsLayout) {
// 首先清除測量的尺寸標記。
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
//測量我們自己宪摧,這應該設置測量尺寸標志回來(重新調onMeasure)
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
// flag not set, setMeasuredDimension() was not invoked, we raise
// an exception to warn the developer
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("View with id " + getId() + ": "
+ getClass().getName() + "#onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
//保存當前的值到measure cache里和重新記錄old值粒竖,key值是用 widthMeasureSpec和heightMeasureSpec拼湊的64位數
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
當然最主要的還是關注onMeasure方法
??onMeasure方法很簡單,只有兩個流程几于。先調用getDefaultSize方法獲取測View的寬高蕊苗,在調用setMeasuredDimension指定View的寬高。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
//傳遞過來的默認大小值
int result = size;
//獲取測量模式
int specMode = View.MeasureSpec.getMode(measureSpec);
//獲取測量大小
int specSize = View.MeasureSpec.getSize(measureSpec);
switch (specMode) {
//使用默認大小
case View.MeasureSpec.UNSPECIFIED:
result = size;
break;
//使用測量后的大小
case View.MeasureSpec.AT_MOST:
case View.MeasureSpec.EXACTLY:
result = specSize;
break;
}
//返回View的寬或高
return result;
}
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
//為View設置寬高
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
再看一下傳入的參數widthMeasureSpec和heightMeasureSpec
??這兩個參數實際上是由測量規(guī)則和大小組的沿彭,它們是一個32位的int值朽砰,其中高2位是測量規(guī)則,低30位才是測量大小喉刘。在ScrollerView雨ListView或者GridView沖突的時候我們通常是重新計算ScrollerView和ListView的寬高瞧柔。
Integer.MAX_VALUE >> 2 (即int值左移動2位得到最大的測量值,指定模式為AT_MOST)
int height = View.MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, View.MeasureSpec.AT_MOST);
SpecMode分為三類
模式 | 規(guī)則 |
---|---|
EXACTLY | 父容器已經檢測出 View 所需要的精確大小,這個時候 View 的最終大小就是 SpecSize 所指定的值饱搏,它對應于LayoutParams 中的 match_parent 和具體的數值這兩種模式 |
AT_MOST | 父容器指定了一個可用大小即 SpecSize非剃,View 的大小不能大于這個值置逻,具體是什么值要看不同 View 的具體實現(xiàn)推沸。它對應于 LayoutParams 中的 wrap_content |
UNSPECIFIED | 父容器不對 View 有任何的限制,要多大給多大券坞,這種情況下一般用于系統(tǒng)內部鬓催,表示一種測量的狀態(tài) 。 |
??一般可以重寫onMeasure對自定義控件指定其大小即可恨锚。但是在布局中使用wrap_content屬性的時候會失效宇驾,只要參考自定義View的wrap_content屬性失效即可兰迫。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int with = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(with, height);
}
在 生命周期中稽莉,無法直接得到 View 的寬高信息。
??view的寬高尺寸谐宙,只有在測量之后才能得到他挎,也就是measure方法執(zhí)行之后筝尾。通常使用View.getWidth()和View.getHeight()方法可以返回view的寬和高對應的值,在Activity的生命周期中不是一開始就能得到這兩個值的办桨,因為這此時measure方法還沒有被執(zhí)行筹淫,測量還沒有完成。
??有以下幾種常見的解決辦法:
- 在 Activity的onWindowFocusChanged 回調中獲取寬高呢撞。
- view.post(runnable)损姜,在 runnable 中獲取寬高饰剥。
- ViewTreeObserver 添加 OnGlobalLayoutListener,在 onGlobalLayout 回調中獲取寬高摧阅。
- 先調用 view.measure(0,0)汰蓉,再通過 getMeasuredWidth 和 getMeasuredHeight 獲取寬高。傳入0,0本質上是用系統(tǒng)框架幫我們測量逸尖。