
前言
- 自定義
View是Android開發(fā)者必須了解的基礎(chǔ) - 網(wǎng)上有大量關(guān)于自定義
View原理的文章,但存在一些問(wèn)題:內(nèi)容不全、思路不清晰、無(wú)源碼分析、簡(jiǎn)單問(wèn)題復(fù)雜化 等 - 今天,我將全面總結(jié)自定義View原理中的
measure過(guò)程,我能保證這是市面上的最全面、最清晰、最易懂的
Carson帶你學(xué)Android自定義View文章系列:
Carson帶你學(xué)Android:自定義View基礎(chǔ)
Carson帶你學(xué)Android:一文梳理自定義View工作流程
Carson帶你學(xué)Android:自定義View繪制準(zhǔn)備-DecorView創(chuàng)建
Carson帶你學(xué)Android:自定義View Measure過(guò)程
Carson帶你學(xué)Android:自定義View Layout過(guò)程
Carson帶你學(xué)Android:自定義View Draw過(guò)程
Carson帶你學(xué)Android:手把手教你寫一個(gè)完整的自定義View
Carson帶你學(xué)Android:Canvas類全面解析
Carson帶你學(xué)Android:Path類全面解析
目錄

1. 作用
測(cè)量View的寬 / 高
- 在某些情況下,需要多次測(cè)量
(measure)才能確定View最終的寬/高;- 該情況下,
measure過(guò)程后得到的寬 / 高可能不準(zhǔn)確;- 此處建議:在
layout過(guò)程中onLayout()去獲取最終的寬 / 高
2. 儲(chǔ)備知識(shí)
了解measure過(guò)程前,需要3個(gè)儲(chǔ)備知識(shí):
- 自定義
View基礎(chǔ)知識(shí) -
ViewGroup.LayoutParams類() -
MeasureSpecs類
2.1 最基本的知識(shí)儲(chǔ)備
具體請(qǐng)看文章:自定義View基礎(chǔ) - 最易懂的自定義View原理系列
2.2 ViewGroup.LayoutParams
- 簡(jiǎn)介
布局參數(shù)類
ViewGroup的子類(RelativeLayout、LinearLayout)有其對(duì)應(yīng)的ViewGroup.LayoutParams子類- 如:
RelativeLayout的ViewGroup.LayoutParams子類
=RelativeLayoutParams
- 作用
指定視圖View的高度(height)和 寬度(width)等布局參數(shù)。
- 具體使用
通過(guò)以下參數(shù)指定
| 參數(shù) | 解釋 |
|---|---|
| 具體值 | dp / px |
| fill_parent | 強(qiáng)制性使子視圖的大小擴(kuò)展至與父視圖大小相等(不含 padding ) |
| match_parent | 與fill_parent相同,用于Android 2.3 & 之后版本 |
| wrap_content | 自適應(yīng)大小,強(qiáng)制性地使視圖擴(kuò)展以便顯示其全部?jī)?nèi)容(含 padding ) |
android:layout_height="wrap_content" //自適應(yīng)大小
android:layout_height="match_parent" //與父視圖等高
android:layout_height="fill_parent" //與父視圖等高
android:layout_height="100dip" //精確設(shè)置高度值為 100dip
- 構(gòu)造函數(shù)
構(gòu)造函數(shù) =View的入口,可用于初始化 & 獲取自定義屬性
// View的構(gòu)造函數(shù)有四種重載
public DIY_View(Context context){
super(context);
}
public DIY_View(Context context,AttributeSet attrs){
super(context, attrs);
}
public DIY_View(Context context,AttributeSet attrs,int defStyleAttr ){
super(context, attrs,defStyleAttr);
// 第三個(gè)參數(shù):默認(rèn)Style
// 默認(rèn)Style:指在當(dāng)前Application或Activity所用的Theme中的默認(rèn)Style
// 且只有在明確調(diào)用的時(shí)候才會(huì)生效,
}
public DIY_View(Context context,AttributeSet attrs,int defStyleAttr ,int defStyleRes){
super(context, attrs,defStyleAttr,defStyleRes);
}
// 最常用的是1和2
}
2.3 MeasureSpec

具體請(qǐng)看文章:Android自定義View基礎(chǔ):MeasureSpec類到底是什么?
3. measure過(guò)程詳解
measure過(guò)程 根據(jù)View的類型分為2種情況:

接下來(lái),我將詳細(xì)分析這兩種measure過(guò)程
3.1 單一View的measure過(guò)程
應(yīng)用場(chǎng)景
在無(wú)現(xiàn)成的控件View滿足需求、需自定義單一View時(shí)。
- 如:制作一個(gè)支持加載網(wǎng)絡(luò)圖片的
ImageView控件- 注:自定義
View在多數(shù)情況下都有替代方案:圖片 / 組合動(dòng)畫,但二者可能會(huì)導(dǎo)致內(nèi)存耗費(fèi)過(guò)大,從而引起內(nèi)存溢出等問(wèn)題。
具體流程

源碼分析
/**
* 源碼分析:measure()
* 定義:Measure過(guò)程的入口;屬于View.java類 & final類型,即子類不能重寫此方法
* 作用:基本測(cè)量邏輯的判斷
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// 參數(shù)說(shuō)明:View的寬 / 高測(cè)量規(guī)格
...
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
onMeasure(widthMeasureSpec, heightMeasureSpec);
// 計(jì)算視圖大小 ->>分析1
} else {
...
}
/**
* 分析1:onMeasure()
* 作用:a. 根據(jù)View寬/高的測(cè)量規(guī)格計(jì)算View的寬/高值:getDefaultSize()
* b. 存儲(chǔ)測(cè)量后的View寬 / 高:setMeasuredDimension()
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 參數(shù)說(shuō)明:View的寬 / 高測(cè)量規(guī)格
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
// setMeasuredDimension() :獲得View寬/高的測(cè)量值 ->>分析2
// 傳入的參數(shù)通過(guò)getDefaultSize()獲得 ->>分析3
}
/**
* 分析2:setMeasuredDimension()
* 作用:存儲(chǔ)測(cè)量后的View寬 / 高
* 注:該方法即為我們重寫onMeasure()所要實(shí)現(xiàn)的最終目的
*/
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
//參數(shù)說(shuō)明:測(cè)量后子View的寬 / 高值
// 將測(cè)量后子View的寬 / 高值進(jìn)行傳遞
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
// 由于setMeasuredDimension()的參數(shù)是從getDefaultSize()獲得的
// 下面繼續(xù)看getDefaultSize()的介紹
/**
* 分析3:getDefaultSize()
* 作用:根據(jù)View寬/高的測(cè)量規(guī)格計(jì)算View的寬/高值
*/
public static int getDefaultSize(int size, int measureSpec) {
// 參數(shù)說(shuō)明:
// size:提供的默認(rèn)大小
// measureSpec:寬/高的測(cè)量規(guī)格(含模式 & 測(cè)量大?。?
// 設(shè)置默認(rèn)大小
int result = size;
// 獲取寬/高測(cè)量規(guī)格的模式 & 測(cè)量大小
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
// 模式為UNSPECIFIED時(shí),使用提供的默認(rèn)大小 = 參數(shù)Size
case MeasureSpec.UNSPECIFIED:
result = size;
break;
// 模式為AT_MOST,EXACTLY時(shí),使用View測(cè)量后的寬/高值 = measureSpec中的Size
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
// 返回View的寬/高值
return result;
}
上面提到,當(dāng)測(cè)試規(guī)格的模式(mode)是UNSPECIFIED時(shí),使用的是提供的默認(rèn)大小(即getDefaultSize()的第一個(gè)參數(shù)size)。那么,提供的默認(rèn)大小具體是多少呢?
答:getSuggestedMinimumWidth() / getSuggestedMinimumHeight()。具體請(qǐng)看下面源碼分析。
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth,mBackground.getMinimumWidth());
}
// 邏輯說(shuō)明
// 1. 若View無(wú)設(shè)置背景,那么View的寬度 = mMinWidth
// 即android:minWidth屬性所指定的值,若無(wú)指定則為0.
// 2. 若View設(shè)置了背景,View的寬度為mMinWidth和mBackground.getMinimumWidth()中的最大值
// 下面繼續(xù)看mBackground.getMinimumWidth()的源碼分析
/**
* mBackground.getMinimumWidth()源碼分析
*/
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
// 即mBackground.getMinimumWidth()的大小 = 背景圖Drawable的原始寬度
return intrinsicWidth > 0 ? intrinsicWidth :0 ;
// 若無(wú)原始寬度,則為0;
}
至此,單一View的寬/高值已經(jīng)測(cè)量完成,即對(duì)于單一View的measure過(guò)程已經(jīng)完成。
源碼總結(jié)
對(duì)于單一View的測(cè)量流程(Measure)各個(gè)方法說(shuō)明如下所示。

測(cè)量寬高的關(guān)鍵在于getDefaultSize(),該方法的測(cè)量邏輯如下圖所示。

3.2 ViewGroup的measure過(guò)程
應(yīng)用場(chǎng)景
利用現(xiàn)有的多個(gè)組件根據(jù)特定的布局方式組成一個(gè)新的組件(即包含多個(gè)子View)。
如:底部導(dǎo)航條中的條目,一般都是上圖標(biāo)(ImageView)、下文字(TextView),那么這兩個(gè)就可以用自定義ViewGroup組合成為一個(gè)Veiw,提供兩個(gè)屬性分別用來(lái)設(shè)置文字和圖片,使用起來(lái)會(huì)更加方便。

測(cè)量原理
從ViewGroup至子View、自上而下遍歷進(jìn)行(即樹形遞歸),通過(guò)計(jì)算整個(gè)ViewGroup中各個(gè)View的屬性,從而最終確定整個(gè)ViewGroup的屬性。即:
- 遍歷測(cè)量所有子View的尺寸(寬/高);
- 合并所有子View的尺寸(寬/高),最終得到ViewGroup父視圖的測(cè)量值。

具體流程

需要特別注意的是:若需進(jìn)行自定義ViewGroup,則需重寫onMeasure(),在下面的章節(jié)會(huì)詳細(xì)講解。
源碼分析
/**
* 源碼分析:measure()
* 作用:
* 1. 基本測(cè)量邏輯的判斷;
* 2. 調(diào)用onMeasure()
* 注:與單一View measure過(guò)程中講的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) {
// 調(diào)用onMeasure()計(jì)算視圖大小 -> 分析1
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
// ...
}
/**
* 分析1:onMeasure()
* 作用:遍歷子View &測(cè)量
* 注:ViewGroup = 一個(gè)抽象類 = 無(wú)重寫View的onMeasure(),需自身復(fù)寫
**/
根據(jù)上一小節(jié)可知,單一View的measure過(guò)程對(duì)onMeasure()有統(tǒng)一的實(shí)現(xiàn)(如下代碼),但為什么ViewGroup的measure過(guò)程沒有呢?
/**
* onMeasure()
* 作用:a. 根據(jù)View寬/高的測(cè)量規(guī)格計(jì)算View的寬/高值:getDefaultSize()
* b. 存儲(chǔ)測(cè)量后的View寬 / 高:setMeasuredDimension()
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 參數(shù)說(shuō)明:View的寬 / 高測(cè)量規(guī)格
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
// setMeasuredDimension() :獲得View寬/高的測(cè)量值 ->>分析2
// 傳入的參數(shù)通過(guò)getDefaultSize()獲得 ->>分析3
}
原因是:onMeasure()方法的作用是測(cè)量View的寬/高值,而不同的ViewGroup(如LinearLayout、RelativeLayout、自定義ViewGroup子類等)具備不同的布局特性,這導(dǎo)致它們的子View測(cè)量方法各有不同,所以onMeasure()的實(shí)現(xiàn)也會(huì)有所不同。
因此,ViewGroup無(wú)法對(duì)onMeasure()作統(tǒng)一實(shí)現(xiàn)。這個(gè)也是單一View的measure過(guò)程與ViewGroup的measure過(guò)程最大的不同。
復(fù)寫onMeasure()
針對(duì)Measure流程,自定義ViewGroup的關(guān)鍵在于:根據(jù)需求復(fù)寫onMeasure(),從而實(shí)現(xiàn)子View的測(cè)量邏輯。復(fù)寫onMeasure()的步驟主要分為三步:
- 遍歷所有子View及測(cè)量:measureChildren()
- 合并所有子View的尺寸大小,最終得到ViewGroup父視圖的測(cè)量值:需自定義實(shí)現(xiàn)
- 存儲(chǔ)測(cè)量后View寬/高的值:setMeasuredDimension()
具體如下所示。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//僅展示關(guān)鍵代碼
...
// 步驟1:遍歷所有子View & 測(cè)量 -> 分析1
measureChildren(widthMeasureSpec, heightMeasureSpec);
// 步驟2:合并所有子View的尺寸大小,最終得到ViewGroup父視圖的測(cè)量值
void measureCarson{
... // 需自定義實(shí)現(xiàn)
}
// 步驟3:存儲(chǔ)測(cè)量后View寬/高的值
setMeasuredDimension(widthMeasure, heightMeasure);
// 類似單一View的過(guò)程,此處不作過(guò)多描述
}
/**
* 分析1:measureChildren()
* 作用:遍歷子View & 調(diào)用measureChild()進(jìn)行下一步測(cè)量
*/
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
// 參數(shù)說(shuō)明:父視圖的測(cè)量規(guī)格(MeasureSpec)
final int size = mChildrenCount;
final View[] children = mChildren;
// 遍歷所有子view
for (int i = 0; i < size; ++i) {
final View child = children[i];
// 調(diào)用measureChild()進(jìn)行下一步的測(cè)量 ->分析2
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
/**
* 分析2:measureChild()
* 作用:1. 計(jì)算單個(gè)子View的MeasureSpec
* 2. 測(cè)量每個(gè)子View最后的寬 / 高:調(diào)用子View的measure()
*/
protected void measureChild(View child, int parentWidthMeasureSpec,int parentHeightMeasureSpec) {
// 1. 獲取子視圖的布局參數(shù)
final LayoutParams lp = child.getLayoutParams();
// 2. 根據(jù)父視圖的MeasureSpec & 布局參數(shù)LayoutParams,計(jì)算單個(gè)子View的MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,mPaddingTop + mPaddingBottom, lp.height);
// 3. 將計(jì)算好的子View的MeasureSpec值傳入measure(),進(jìn)行最后的測(cè)量
// 下面的流程即類似單一View的過(guò)程,此處不作過(guò)多描述
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
至此,ViewGroup的measure過(guò)程分析完畢
流程總結(jié)
對(duì)于視圖組ViewGroup的測(cè)量流程(Measure)各個(gè)方法說(shuō)明總結(jié)如下所示。

為了讓大家更好地理解ViewGroup的measure過(guò)程(特別是復(fù)寫onMeasure()),下面,我將用ViewGroup的子類LinearLayout來(lái)分析下ViewGroup的measure過(guò)程
實(shí)例解析
為了更好理解ViewGroup的measure過(guò)程(特別是復(fù)寫onMeasure()),本小節(jié)將用ViewGroup的子類LinearLayout來(lái)分析ViewGroup的measure過(guò)程。
此處主要分析的是LinearLayout的onMeasure(),具體如下所示。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 根據(jù)不同的布局屬性進(jìn)行不同的計(jì)算
// 此處只選垂直方向的測(cè)量過(guò)程,即measureVertical() ->分析1
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
/**
* 分析1:measureVertical()
* 作用:測(cè)量LinearLayout垂直方向的測(cè)量尺寸
*/
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
// 獲取垂直方向上的子View個(gè)數(shù)
final int count = getVirtualChildCount();
// 遍歷子View獲取其高度,并記錄下子View中最高的高度數(shù)值
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
// 子View不可見,直接跳過(guò)該View的measure過(guò)程,getChildrenSkipCount()返回值恒為0
// 注:若view的可見屬性設(shè)置為VIEW.INVISIBLE,還是會(huì)計(jì)算該view大小
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
// 記錄子View是否有weight屬性設(shè)置,用于后面判斷是否需要二次measure
totalWeight += lp.weight;
if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
// 如果LinearLayout的specMode為EXACTLY且子View設(shè)置了weight屬性,在這里會(huì)跳過(guò)子View的measure過(guò)程
// 同時(shí)標(biāo)記skippedMeasure屬性為true,后面會(huì)根據(jù)該屬性決定是否進(jìn)行第二次measure
// 若LinearLayout的子View設(shè)置了weight,會(huì)進(jìn)行兩次measure計(jì)算,比較耗時(shí)
// 這就是為什么LinearLayout的子View需要使用weight屬性時(shí)候,最好替換成RelativeLayout布局
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else {
int oldHeight = Integer.MIN_VALUE;
// 步驟1:該方法內(nèi)部最終會(huì)調(diào)用measureChildren(),從而 遍歷所有子View & 測(cè)量
measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec,totalWeight == 0 ? mTotalLength : 0);
...
}
// 步驟2:合并所有子View的尺寸大小,最終得到ViewGroup父視圖的測(cè)量值(需自定義實(shí)現(xiàn))
final int childHeight = child.getMeasuredHeight();
// 1. mTotalLength用于存儲(chǔ)LinearLayout在豎直方向的高度
final int totalLength = mTotalLength;
// 2. 每測(cè)量一個(gè)子View的高度, mTotalLength就會(huì)增加
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
// 3. 記錄LinearLayout占用的總高度
// 即除了子View的高度,還有本身的padding屬性值
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// 步驟3:存儲(chǔ)測(cè)量后View寬/高的值
setMeasureDimension(resolveSizeAndState(maxWidth,width))
...
}
至此,對(duì)于自定義View流程中最重要、最復(fù)雜的測(cè)量流程(measure)分析完畢。
4. 總結(jié)
- 測(cè)量流程(Measure)根據(jù)視圖(View)的類型分為兩種情況:?jiǎn)我籚iew和視圖組ViewGroup;
- 二者最大的區(qū)別在于:?jiǎn)我籚iew的measure過(guò)程對(duì)onMeasure()有作統(tǒng)一實(shí)現(xiàn),而ViewGroup的Measuer過(guò)程沒有;
- 具體測(cè)量流程總結(jié)如下所示


- Carson帶你學(xué)Android自定義View文章系列:
Carson帶你學(xué)Android:自定義View基礎(chǔ)
Carson帶你學(xué)Android:一文梳理自定義View工作流程
Carson帶你學(xué)Android:自定義View繪制準(zhǔn)備-DecorView創(chuàng)建
Carson帶你學(xué)Android:自定義View Measure過(guò)程
Carson帶你學(xué)Android:自定義View Layout過(guò)程
Carson帶你學(xué)Android:自定義View Draw過(guò)程
Carson帶你學(xué)Android:手把手教你寫一個(gè)完整的自定義View
Carson帶你學(xué)Android:Canvas類全面解析
Carson帶你學(xué)Android:Path類全面解析
歡迎關(guān)注Carson_Ho的簡(jiǎn)書
不定期分享關(guān)于安卓開發(fā)的干貨,追求短、平、快,但卻不缺深度。
