Android自定義View-Measure原理篇
在自定義View中有時需要測量View的尺寸,因此,瞭解View的Measure過成有助於我們開發自定義View。
一、目的:測量View的寬與高
在有些情況下,需要多次測量(measure)才能夠最終確定View的寬高(比如父檢視MeasureSpec使用UNSPECIFIED模式等),在這種情況下,通過onMeasure方法獲得的寬高很可能是不準確的,因此,《Android開發藝術探索》建議在onLayout方法中去獲取View的最終寬高。
二、基礎:在開始瞭解measure過程之前,我們需要對兩個傳遞尺寸的類做個瞭解
1.ViewGroup.LayoutParams類:佈局引數類
作用:設定檢視的寬度和高度等佈局引數
引數 | 解釋 |
---|---|
fill_parent | 與父檢視等高(2.3之前使用) |
match_parent | 同fill_parent,2.3及之後版本使用 |
wrap_parent | 自適應大小 |
具體值 |
dp/px |
2.MeasureSpec類:測量規格類,測量View大小的依據
(1)作用:決定一個檢視View的尺寸
(2)型別:①寬測量規格widthMeasureSpec、②高測量規格heightMeasureSpec
(3)組成:測量規格(MeasureSpec,32位,int型別) = 測量模式(mode,高2位) + 測量大小(size,低30位)
相關原始碼如下:
public class MeasureSpec { // 進位大小 = 2的30次方 // int的大小為32位,所以進位30位 = 使用int的32和31位做標誌位 private static final int MODE_SHIFT = 30; // 運算遮罩:0x3為16進位制,10進製為3,二進位制為11 // 3向左進位30 = 11 00000000000(11後跟30個0) // 作用:用1標註需要的值,0標註不要的值。因1與任何數做與運算都得任何數、0與任何數做與運算都得0 private static final int MODE_MASK = 0x3 << MODE_SHIFT; // UNSPECIFIED的模式設定:0向左進位30 = 00後跟30個0,即00 00000000000 // 通過高2位 public static final int UNSPECIFIED = 0 << MODE_SHIFT; // EXACTLY的模式設定:1向左進位30 = 01後跟30個0 ,即01 00000000000 public static final int EXACTLY = 1 << MODE_SHIFT; // AT_MOST的模式設定:2向左進位30 = 10後跟30個0,即10 00000000000 public static final int AT_MOST = 2 << MODE_SHIFT; /** * makeMeasureSpec()方法 * 作用:根據提供的size和mode得到一個詳細的測量結果嗎,即measureSpec **/ public static int makeMeasureSpec(int size, int mode) { return size + mode; // measureSpec = size + mode;此為二進位制的加法 而不是十進位制 // 設計目的:使用一個32位的二進位制數,其中:32和31位代表測量模式(mode)、後30位代表測量大小(size) // 例如size=100(4),mode=AT_MOST,則measureSpec=100+10000...00=10000..00100 } /** * getMode()方法 * 作用:通過measureSpec獲得測量模式(mode) **/ public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); // 即:測量模式(mode) = measureSpec & MODE_MASK; // MODE_MASK = 運算遮罩 = 11 00000000000(11後跟30個0) //原理:保留measureSpec的高2位(即測量模式)、使用0替換後30位 // 例如10 00..00100 & 11 00..00(11後跟30個0) = 10 00..00(AT_MOST),這樣就得到了mode的值 } /** * getSize方法 * 作用:通過measureSpec獲得測量大小size **/ public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); // size = measureSpec & ~MODE_MASK; // 原理類似上面,即 將MODE_MASK取反,也就是變成了00 111111(00後跟30個1),將32,31替換成0也就是去掉mode,保留後30位的size } }
(5)計算:子View的具體大小由父View的MeasureSpec值和子View的LayoutParams屬性共同決定,即:
具體的計算封裝在getChildMeasureSpec裡,原始碼如下:
/**
* 原始碼分析:getChildMeasureSpec()
* 作用:根據父檢視的MeasureSpec & 佈局引數LayoutParams,計算單個子View的MeasureSpec
* 注:子view的大小由父view的MeasureSpec值 和 子view的LayoutParams屬性 共同決定
**/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//引數說明
* @param spec 父view的詳細測量值(MeasureSpec)
* @param padding view當前尺寸的的內邊距和外邊距(padding,margin)
* @param childDimension 子檢視的佈局引數(寬/高)
//父view的測量模式
int specMode = MeasureSpec.getMode(spec);
//父view的大小
int specSize = MeasureSpec.getSize(spec);
//通過父view計算出的子view = 父大小-邊距(父要求的大小,但子view不一定用這個值)
int size = Math.max(0, specSize - padding);
//子view想要的實際大小和模式(需要計算)
int resultSize = 0;
int resultMode = 0;
//通過父view的MeasureSpec和子view的LayoutParams確定子view的大小
// 當父view的模式為EXACITY時,父view強加給子view確切的值
//一般是父view設定為match_parent或者固定值的ViewGroup
switch (specMode) {
case MeasureSpec.EXACTLY:
// 當子view的LayoutParams>0,即有確切的值
if (childDimension >= 0) {
//子view大小為子自身所賦的值,模式大小為EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
// 當子view的LayoutParams為MATCH_PARENT時(-1)
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//子view大小為父view大小,模式為EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
// 當子view的LayoutParams為WRAP_CONTENT時(-2)
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//子view決定自己的大小,但最大不能超過父view,模式為AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 當父view的模式為AT_MOST時,父view強加給子view一個最大的值。(一般是父view設定為wrap_content)
case MeasureSpec.AT_MOST:
// 道理同上
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 當父view的模式為UNSPECIFIED時,父容器不對view有任何限制,要多大給多大
// 多見於ListView、GridView
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// 子view大小為子自身所賦的值
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 因為父view為UNSPECIFIED,所以MATCH_PARENT的話子類大小為0
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 因為父view為UNSPECIFIED,所以WRAP_CONTENT的話子類大小為0
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
對此,我們可以總結出如下規律:
這裡需要注意的是,頂級View,即DecorView的測量規格=自身佈局引數+視窗尺寸
三、measure過程
measure會根據View的型別分成兩種情況
View型別 | measure過程 |
單一View | 只測量自身一個View |
ViewGroup | 對ViewGroup檢視中所有的子View都進行測量 |
(1)我們首先來看下單一View的measure過程
場景分析:在現有View無法滿足需求,需要自己實現時使用自定義單一View
具體使用:繼承View、SurfaceView或其他View
具體流程:measure()→onMeasure()→setMeasureDimension()→getDefaultSize()
先來看一下這幾個方法:
/**
* 原始碼分析:measure()
* 定義:Measure過程的入口;屬於View.java類 & final型別,即子類不能重寫此方法
* 作用:基本測量邏輯的判斷
**/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// 引數說明:View的寬 / 高測量規格
...
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
onMeasure(widthMeasureSpec, heightMeasureSpec);
// 計算檢視大小 ->>分析1
} else {
...
}
/**
* 分析1:onMeasure()
* 作用:a. 根據View寬/高的測量規格計算View的寬/高值:getDefaultSize()
* b. 儲存測量後的View寬 / 高:setMeasuredDimension()
**/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 引數說明:View的寬 / 高測量規格
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
// setMeasuredDimension() :獲得View寬/高的測量值 ->>分析2
// 傳入的引數通過getDefaultSize()獲得 ->>分析3
}
/**
* 分析2:setMeasuredDimension()
* 作用:儲存測量後的View寬 / 高
* 注:該方法即為我們重寫onMeasure()所要實現的最終目的
**/
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
//引數說明:測量後子View的寬 / 高值
// 將測量後子View的寬 / 高值進行傳遞
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
// 由於setMeasuredDimension()的引數是從getDefaultSize()獲得的
// 下面我們繼續看getDefaultSize()的介紹
/**
* 分析3:getDefaultSize()
* 作用:根據View寬/高的測量規格計算View的寬/高值
**/
public static int getDefaultSize(int size, int measureSpec) {
// 引數說明:
// size:提供的預設大小
// measureSpec:寬/高的測量規格(含模式 & 測量大小)
// 設定預設大小
int result = size;
// 獲取寬/高測量規格的模式 & 測量大小
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
// 模式為UNSPECIFIED時,使用提供的預設大小 = 引數Size
case MeasureSpec.UNSPECIFIED:
result = size;
break;
// 模式為AT_MOST,EXACTLY時,使用View測量後的寬/高值 = measureSpec中的Size
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
// 返回View的寬/高值
return result;
}
在onMeasure方法中,我們可以知道函式getSuggestedMinimumWidth()是獲取預設大小,那麼,我們可以看一下它具體的實現:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth,mBackground.getMinimumWidth());
}
從這裡我們可以知道,當View沒有設定背景時,View寬度=mMinWidth(如果android:minWidth沒有指定,則為0,否則為該屬性所設定的值),如果View設定了背景,則View寬度為mMinWidth和mBackground.getMinimumWidth()中的最大值。
對於以上過程,我們可以使用下圖理一下邏輯:
至此,單一View measure過程完成,總結一下:
(2)ViewGroup的measure過程
場景分析:利用現有元件來組成新的元件
具體使用:繼承自ViewGroup或各種Layout,可以含有子View
具體流程:measure()→onMeasure()(需要複寫)→measureChildren()→measureChild()→getChildMeasureSpec()→遍歷子View測量併合並→setMeasureDimension()
具體原始碼分析如下:
/**
* 原始碼分析:measure()
* 作用:基本測量邏輯的判斷;呼叫onMeasure()
* 注:與單一View measure過程中講的measure()一致
**/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// 呼叫onMeasure()計算檢視大小
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
...
}
/**
* 根據自身的測量邏輯複寫onMeasure(),分為3步
* 1. 遍歷所有子View & 測量:measureChildren()
* 2. 合併所有子View的尺寸大小,最終得到ViewGroup父檢視的測量值(自身實現)
* 3. 儲存測量後View寬/高的值:呼叫setMeasuredDimension()
**/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 定義存放測量後的View寬/高的變數
int widthMeasure ;
int heightMeasure ;
// 1. 遍歷所有子View & 測量(measureChildren())
// ->> 分析1
measureChildren(widthMeasureSpec, heightMeasureSpec);
// 2. 合併所有子View的尺寸大小,最終得到ViewGroup父檢視的測量值
void measureCarson{
... // 自身實現
}
// 3. 儲存測量後View寬/高的值:呼叫setMeasuredDimension()
// 類似單一View的過程,此處不作過多描述
setMeasuredDimension(widthMeasure, heightMeasure);
}
// 從上可看出:
// 複寫onMeasure()有三步,其中2步直接呼叫系統方法
// 需自身實現的功能實際僅為步驟2:合併所有子View的尺寸大小
/**
* 分析1:measureChildren()
* 作用:遍歷子View & 呼叫measureChild()進行下一步測量
**/
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
// 引數說明:父檢視的測量規格(MeasureSpec)
final int size = mChildrenCount;
final View[] children = mChildren;
// 遍歷所有子view
for (int i = 0; i < size; ++i) {
final View child = children[i];
// 呼叫measureChild()進行下一步的測量 ->>分析1
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
/**
* 分析2:measureChild()
* 作用:a. 計算單個子View的MeasureSpec
* b. 測量每個子View最後的寬 / 高:呼叫子View的measure()
**/
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
// 1. 獲取子檢視的佈局引數
final LayoutParams lp = child.getLayoutParams();
// 2. 根據父檢視的MeasureSpec & 佈局引數LayoutParams,計算單個子View的MeasureSpec
// getChildMeasureSpec() 請看上面第2節儲備知識處
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,// 獲取 ChildView 的 widthMeasureSpec
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,// 獲取 ChildView 的 heightMeasureSpec
mPaddingTop + mPaddingBottom, lp.height);
// 3. 將計算好的子View的MeasureSpec值傳入measure(),進行最後的測量
// 下面的流程即類似單一View的過程,此處不作過多描述
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
// 回到呼叫原處
為了更好理解,我們可以看一下LinearLayout複寫的onMeasure程式碼分析:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 根據不同的佈局屬性進行不同的計算
// 此處只選垂直方向的測量過程,即measureVertical()->>分析1
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
/**
* 分析1:measureVertical()
* 作用:測量LinearLayout垂直方向的測量尺寸
**/
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
/**
* 其餘測量邏輯
**/
// 獲取垂直方向上的子View個數
final int count = getVirtualChildCount();
// 遍歷子View獲取其高度,並記錄下子View中最高的高度數值
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
// 子View不可見,直接跳過該View的measure過程,getChildrenSkipCount()返回值恆為0
// 注:若view的可見屬性設定為VIEW.INVISIBLE,還是會計算該view大小
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
// 記錄子View是否有weight屬性設定,用於後面判斷是否需要二次measure
totalWeight += lp.weight;
if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
// 如果LinearLayout的specMode為EXACTLY且子View設定了weight屬性,在這裡會跳過子View的measure過程
// 同時標記skippedMeasure屬性為true,後面會根據該屬性決定是否進行第二次measure
// 若LinearLayout的子View設定了weight,會進行兩次measure計算,比較耗時
// 這就是為什麼LinearLayout的子View需要使用weight屬性時候,最好替換成RelativeLayout佈局
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else {
int oldHeight = Integer.MIN_VALUE;
/**
* 步驟1:遍歷所有子View & 測量:measureChildren()
* 注:該方法內部,最終會呼叫measureChildren(),從而 遍歷所有子View & 測量
**/
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);
...
}
/**
* 步驟2:合併所有子View的尺寸大小,最終得到ViewGroup父檢視的測量值(自身實現)
**/
final int childHeight = child.getMeasuredHeight();
// 1. mTotalLength用於儲存LinearLayout在豎直方向的高度
final int totalLength = mTotalLength;
// 2. 每測量一個子View的高度, mTotalLength就會增加
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
// 3. 記錄LinearLayout佔用的總高度
// 即除了子View的高度,還有本身的padding屬性值
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
/**
* 步驟3:儲存測量後View寬/高的值:呼叫setMeasuredDimension()
**/
setMeasureDimension(resolveSizeAndState(maxWidth,width))
...
}
到這裡,ViewGroup的measure講完了,我們來總結一下: