內容
- 作用
- 知識儲備
- ViewGroup.LayoutParams
- MeasureSpecs
- 自定義View基礎
- measure過程解析
- 單一view
- ViewGroup
1 作用
測量View的寬 / 高
- 在某些情況下,需要多次測量(measure)才能確定View最終的寬/高;
- 該情況下,measure過程后得到的寬 / 高可能不準確;
- 此處建議:在layout過程中onLayout()去獲取最終的寬 / 高
2 儲備知識
了解measure過程前,需要先了解傳遞尺寸(寬 / 高測量值)的2個類:
- ViewGroup.LayoutParams類()
- MeasureSpecs 類(父視圖對子視圖的測量要求)
2.1 ViewGroup.LayoutParams
- 簡介
布局參數類
- ViewGroup 的子類(RelativeLayout、LinearLayout)有其對應的 ViewGroup.LayoutParams 子類
- 如:RelativeLayout的 ViewGroup.LayoutParams子類
= RelativeLayoutParams
- 作用
指定視圖View 的高度(height) 和 寬度(width)等布局參數。可 - 具體使用
通過以下參數指定
參數 | 解釋 |
---|---|
具體值 | dp / px |
fill_parent | 強制性使子視圖的大小擴展至與父視圖大小相等(不含 padding ) |
match_parent | 與fill_parent相同,用于Android 2.3 & 之后版本 |
wrap_content | 自適應大小,強制性地使視圖擴展以便顯示其全部內容(含 padding ) |
android:layout_height="wrap_content" //自適應大小
android:layout_height="match_parent" //與父視圖等高
android:layout_height="fill_parent" //與父視圖等高
android:layout_height="100dip" //精確設置高度值為 100dip
2.2 MeasureSpec
2.2.1 簡介
2.2.2 組成
測量規格(MeasureSpec) = 測量模式(mode) + 測量大小(size)
其中,測量模式(Mode)的類型有3種:UNSPECIFIED、EXACTLY 和
AT_MOST。具體如下:
2.2.3 具體使用
- MeasureSpec 被封裝在View類中的一個內部類里:MeasureSpec類
- MeasureSpec類 用1個變量封裝了2個數據(size,mode):通過使用二進制,將測量模式(mode) & 測量大小(size)打包成一個int值來,并提供了打包 & 解包的方法
該措施的目的 = 減少對象內存分配
- 實際使用
/**
* MeasureSpec類的具體使用
**/
// 1. 獲取測量模式(Mode)
int specMode = MeasureSpec.getMode(measureSpec)
// 2. 獲取測量大小(Size)
int specSize = MeasureSpec.getSize(measureSpec)
// 3. 通過Mode 和 Size 生成新的SpecMode
int measureSpec=MeasureSpec.makeMeasureSpec(size, mode);
- 源碼分析
/**
* MeasureSpec類的源碼分析
**/
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
}
}
2.2.6 MeasureSpec值的計算
-
結論:子View的MeasureSpec值根據子View的布局參數(LayoutParams)和父容器的MeasureSpec值計算得來的,具體計算邏輯封裝在getChildMeasureSpec()里。
image
即:子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為標準,橫向觀察)
由于UNSPECIFIED模式適用于系統內部多次measure情況,很少用到,故此處不討論
-
注
區別于頂級View(即DecorView)的測量規格MeasureSpec計算邏輯:取決于 自身布局參數 & 窗口尺寸
image
measure過程詳解
measure過程 根據View的類型分為2種情況:
3.1 單一View的measure過程
- 應用場景
在無現成的控件View滿足需求、需自己實現時,則使用自定義單一View
- 如:制作一個支持加載網絡圖片的ImageView控件
- 注:自定義View在多數情況下都有替代方案:圖片 / 組合動畫,但二者可能會導致內存耗費過大,從而引起內存溢出等問題。
- 具體使用
繼承自View、SurfaceView 或 其他View;不包含子View -
具體流程
image
下面我將一個個方法進行詳細分析:入口 = measure()
/**
* 源碼分析: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;
}
- 上面提到,當模式是UNSPECIFIED時,使用的是提供的默認大小(即第一個參數size);那么,提供的默認大小具體是多少呢?
- 答:在onMeasure()方法中,getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)中傳入的默認大小是getSuggestedMinimumWidth()。
接下來我們繼續看getSuggestedMinimumWidth()的源碼分析
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth,mBackground.getMinimumWidth());
}
從代碼可以看出:
- 若 View 無設置背景,那么View的寬度 = mMinWidth
- mMinWidth· = android:minWidth屬性所指定的值;
- 若android:minWidth沒指定,則默認為0
- 若 View設置了背景,View的寬度為mMinWidth和mBackground.getMinimumWidth()中的最大值
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
//返回背景圖Drawable的原始寬度
return intrinsicWidth > 0 ? intrinsicWidth :0 ;
}
// 由源碼可知:mBackground.getMinimumWidth()的大小 = 背景圖Drawable的原始寬度
// 若無原始寬度,則為0;
// 注:BitmapDrawable有原始寬度,而ShapeDrawable沒有
總結:getDefaultSize()計算View的寬/高值的邏輯
至此,單一View的寬/高值已經測量完成,即對于單一View的measure過程已經完成。
總結
對于單一View的measure過程,如下:
實際作用的方法:getDefaultSize() = 計算View的寬/高值、setMeasuredDimension() = 存儲測量后的View寬 / 高
3.2 ViewGroup的measure過程
- 應用場景
利用現有的組件根據特定的布局方式來組成新的組件 - 具體使用
繼承自ViewGroup 或 各種Layout;含有子 View - 原理
- 遍歷 測量所有子View的尺寸
- 合并將所有子View的尺寸進行,最終得到ViewGroup父視圖的測量值
-
流程
image - 入口 = measure()
若需進行自定義ViewGroup,則需重寫onMeasure(),下文會提到
/**
* 源碼分析: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 {
...
}
/**
* 分析1:onMeasure()
* 作用:遍歷子View & 測量
* 注:ViewGroup = 一個抽象類 = 無重寫View的onMeasure(),需自身復寫
**/
- 因為不同的ViewGroup子類(LinearLayout、RelativeLayout / 自定義ViewGroup子類等)具備不同的布局特性,這導致他們子View的測量方法各有不同
- 因此,ViewGroup無法對onMeasure()作統一實現。這個也是單一View的measure過程與ViewGroup過程最大的不同
- 即 單一View measure過程的onMeasure()具有統一實現,而ViewGroup則沒有
- 注:其實,在單一View measure過程中,getDefaultSize()只是簡單的測量了寬高值,在實際使用時有時需更精細的測量。所以有時候也需重寫onMeasure()
- 在自定義ViewGroup中,關鍵在于:根據需求復寫onMeasure()從而實現你的子View測量邏輯。復寫onMeasure()的套路如下:
/**
* 根據自身的測量邏輯復寫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);
}
// 回到調用原處
至此,ViewGroup的measure過程分析完畢
-
總結
ViewGroup的measure過程如下:
image - 為了讓大家更好地理解ViewGroup的measure過程(特別是復寫onMeasure()),下面,我將用ViewGroup的子類LinearLayout來分析下ViewGroup的measure過程
3.3 ViewGroup的measure過程實例解析(LinearLayout)
此處直接進入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))
...
}
至此,自定義View的中最重要、最復雜的measure過程講解完畢。