反思 系列博客是個人一種新學習方式的嘗試,該系列起源和目錄請參考 這裏 。html
Android
自己的View
體系很是宏大,源碼中值得思考和借鑑之處衆多,以View
自己的繪製流程爲例,其通過measure
測量、layout
佈局、draw
繪製三個過程,最終纔可以將其繪製出來並展現在用戶面前。android
本文將針對繪製過程當中的 測量流程 的設計思想進行系統地概括總結,讀者須要對View
的measure()
相關知識有初步的瞭解:git
View
的測量機制本質很是簡單,顧名思義,其目的即是 測量控件的寬高值,圍繞該目的,View
的設計者經過代碼編織了一整套複雜的邏輯:github
一、對於子View
而言,其自己寬高直接受限於父View
的 佈局要求,舉例來講,父View
被限制寬度爲40px
,子View
的最大寬度一樣也需受限於這個數值。所以,在測量子View
之時,子View
必須已知父View
的佈局要求,這個 佈局要求, Android
中經過使用 MeasureSpec
類來進行描述。安全
二、對於完整的測量流程而言,父控件必然依賴子控件寬高的測量;若子控件自己未測量完畢,父控件自身的測量亦無從談起。Android
中View
的測量流程中使用了很是經典的 遞歸思想:對於一個完整的界面而言,每一個頁面都映射了一個View
樹,其最頂端的父控件測量開始時,會經過 遍歷 將其 佈局要求 傳遞給子控件,以開始子控件的測量,子控件在測量過程當中也會經過 遍歷 將其 佈局要求 傳遞給它本身的子控件,如此往復一直到最底層的控件...這種經過遍歷自頂向下傳遞數據的方式咱們稱爲 測量過程當中的「遞」流程。而當最底層位置的子控件自身測量完畢後,其父控件會將全部子控件的寬高數據進行聚合,而後經過對應的 測量策略 計算出父控件自己的寬高,測量完畢後,父控件的父控件也會根據其全部子控件的測量結果對自身進行測量,這種從底部向上傳遞各自的測量結果,最終完成最頂層父控件的測量方式咱們稱爲測量過程當中的「歸」流程,至此界面整個View
樹測量完畢。app
對於繪製流程不甚熟悉的開發者而言,上述文字彷佛晦澀難懂,但這些文字的歸納其本質倒是繪製流程總體的設計思想,讀者不該該將本文視爲源碼分析,而應該將本身代入到設計的過程當中 ,當深入理解整個流程的設計思路以後,測量流程代碼地設計和編寫天然行雲流水一鼓作氣。函數
在整個 測量流程 中, 佈局要求 都是一個很是重要的核心名詞,Android
中經過使用 MeasureSpec
類來對其進行描述。源碼分析
爲何說 佈局要求 很是重要呢,其又是如何定義的呢?這要先從結果提及,對於單個View
來講,測量流程的結果無非是獲取控件自身寬和高的值,Android
提供了setMeasureDimension()
函數,開發者僅須要將測量結果做爲參數並調用該函數,即可以視爲View
完成了自身的測量:佈局
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
// measuredWidth 測量結果,View的寬度
// measuredHeight 測量結果,View的高度
// 省略其它代碼...
// 該方法的本質就是將測量結果存起來,以便後續的layout和draw流程中獲取控件的寬高
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
}
複製代碼
須要注意的是,子控件的測量過程自己還應該依賴於父控件的一些佈局約束,好比:post
${x}px
,子控件設置爲layout_height="${y}px"
;wrap_content
(包裹內容),子控件設置爲layout_height="match_parent"
;match_parent
(填充),子控件設置爲layout_height="match_parent"
;這些狀況下,由於沒法計算出準確控件自己的寬高值,簡單的經過setMeasuredDimension()
函數彷佛不可能達到測量控件的目的,由於 子控件的測量結果是由父控件和其自己共同決定的 (這個下文會解釋),而父控件對子控件的佈局約束,即是前文提到的 佈局要求,即MeasureSpec
類。
從面向對象的角度來看,咱們將MeasureSpec
類設計成這樣:
public final class MeasureSpec {
int size; // 測量大小
Mode mode; // 測量模式
enum Mode { UNSPECIFIED, EXACTLY, AT_MOST }
MeasureSpec(Mode mode, int size){
this.mode = Mode;
this.size = size;
}
public int getSize() { return size; }
public Mode getMode() { return mode; }
}
複製代碼
在設計的過程當中,咱們將佈局要求分紅了2個屬性。測量大小 意味着控件須要對應大小的寬高,測量模式 則表示控件對應的寬高模式:
- UNSPECIFIED:父元素不對子元素施加任何束縛,子元素能夠獲得任意想要的大小;平常開發中自定義View不考慮這種模式,可暫時先忽略;
* EXACTLY:父元素決定子元素的確切大小,子元素將被限定在給定的邊界裏而忽略它自己大小;這裏咱們理解爲控件的寬或者高被設置爲match_parent
或者指定大小,好比20dp
;- AT_MOST:子元素至多達到指定大小的值;這裏咱們理解爲控件的寬或者高被設置爲
wrap_content
。
巧妙的是,Android
並不是經過上述定義MeasureSpec
對象的方式對 佈局要求 進行描述,而是使用了更簡單的二進制的方式,用一個32位的int
值進行替代:
public static class MeasureSpec {
private static final int MODE_SHIFT = 30; //移位位數爲30
//int類型佔32位,向右移位30位,該屬性表示掩碼值,用來與size和mode進行"&"運算,獲取對應值。
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
//00左移30位,其值爲00 + (30位0)
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
//01左移30位,其值爲01 + (30位0)
public static final int EXACTLY = 1 << MODE_SHIFT;
//10左移30位,其值爲10 + (30位0)
public static final int AT_MOST = 2 << MODE_SHIFT;
// 根據size和mode,建立一個測量要求
public static int makeMeasureSpec(int size, int mode) {
return size + mode;
}
// 根據規格提取出mode,
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
// 根據規格提取出size
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
複製代碼
這個int
值中,前2位表明了測量模式,後30位則表示了測量的大小,對於模式和大小值的獲取,只須要經過位運算便可。
以寬度舉例來講,若咱們設置寬度=5px(二進制對應了101),那麼mode
對應EXACTLY
,在建立測量要求的時候,只須要經過二進制的相加,即可獲得存儲了相關信息的int
值:
而當須要得到Mode
的時候只須要用measureSpec
與MODE_TASK
相與便可,以下圖:
同理,想得到size
的話只須要只須要measureSpec
與~MODE_TASK
相與便可,以下圖:
如今讀者對MeasureSpec
類有了初步地認識,在Android
繪製過程當中,View
寬或者高的 佈局要求 其實是經過32位的int
值進行的描述, 而MeasureSpec
類自己只是一個靜態方法的容器而已。
至此MeasureSpec
類所表明的 佈局要求 已經介紹完畢,這裏咱們淺嘗輒止,其在後文的 總體測量流程 中佔有相當重要的做用,屆時咱們再進行對應的引伸。
只考慮單個控件的測量,整個過程須要定義三個重要的函數,分別爲:
final void measure(int widthMeasureSpec, int heightMeasureSpec)
:執行測量的函數;void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
:真正執行測量的函數,開發者須要本身實現自定義的測量邏輯;final void setMeasuredDimension(int measuredWidth, int measuredHeight)
:完成測量的函數;爲何說須要定義這樣三個函數?
首先父控件須要經過調用子控件的measure()
函數,並同時將寬和高的 佈局要求 做爲參數傳入,標誌子控件自己測量的開始:
// 這個是父控件的代碼,讓子控件開始測量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
複製代碼
對於View
的測量流程,其必然包含了2部分:公共邏輯部分 和 開發者自定義測量的邏輯部分,爲了保證公共邏輯部分代碼的安全性,設計者將measure()
方法配置了final
修飾符:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// ... 公共邏輯
// 開發者須要本身重寫onMeasure函數,以自定義測量邏輯
onMeasure(widthMeasureSpec, heightMeasureSpec);
}
複製代碼
開發者不能重寫measure()
函數,並將View自定義測量的策略經過定義一個新的onMeasure()
接口暴露出來供開發者重寫。
onMeasure()
函數中,View
自身也提供了一個默認的測量策略:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
複製代碼
以寬度爲例,經過這樣獲取View
默認的寬度:
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)
minWidth
或者background
屬性),View
須要經過getSuggestedMinimumWidth()
函數做爲默認的寬度值:protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
複製代碼
getDefaultSize(minWidth, widthMeasureSpec)
函數中,根據 佈局要求 計算出View
最後測量的寬度值:public static int getDefaultSize(int size, int measureSpec) {
// 寬度的默認值
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
// 根據不一樣的測量模式,返回的測量結果不一樣
switch (specMode) {
// 任意模式,寬度爲默認值
case MeasureSpec.UNSPECIFIED:
result = size;
break;
// match_parent、wrap_content則返回佈局要求中的size值
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
複製代碼
上述代碼中,View的默認測量策略也印證了,即便View設置的是
layout_width="wrap_content"
,其寬度也會填充父佈局(效果同match_parent
),高度依然。
setMeasuredDimension(width,height)
函數的存在乎義很是重要,在onMeasure()
執行自定義測量策略的過程當中,調用該函數標誌着View
的測量得出告終果:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 廣泛意義上,setMeasuredDimension()標誌着測量結束
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
// measuredWidth 測量結果,View的寬度
// measuredHeight 測量結果,View的高度
// 省略其它代碼...
// 該方法的本質就是將測量結果存起來,以便後續的layout和draw流程中獲取控件的寬高
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
}
複製代碼
該函數被設計爲由protected final
修飾,這意味着只能由子類進行調用而不能重寫。
函數調用完畢,開發者能夠經過getMeasuredWidth()
或者getMeasuredHeight()
來獲取View
測量的寬高,代碼設計大概是這樣:
public final int getMeasuredWidth() {
return mMeasuredWidth;
}
public final int getMeasuredHeight() {
return mMeasuredHeight;
}
// 如何使用
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight()
複製代碼
通過measure()
-> onMeasure()
-> setMeasuredDimension()
函數的調用,最終View
自身測量流程執行完畢。
對於一個完整的界面而言,每一個頁面都映射了一個View
樹,見微知著,瞭解了單個View
的測量過程,從宏觀的角度思考,View
樹總體的測量流程將如何實現?
首先須要理解的是,每種ViewGroup
的子類的測量策略(也就是onMeasure()
函數內的邏輯)不盡相同,好比RelativeLayout
或者LinearLayout
寬高的測量策略天然不一樣,但總體思路都大同小異,即 遍歷 測量全部子控件,根據父控件自身測量策略進行寬高的計算並得出測量結果。
以 豎直方向佈局 的LinearLayout
爲例,如何完成LinearLayout
高度的測量?本文拋去不重要的細節,化繁爲簡,將LinearLayout
高度的測量策略簡單定義爲 遍歷獲取全部子控件,將高度累加 ,所得值即自身高度的測量結果——若是不知道每一個子控件的高度,LinearLayout
天然沒法測量出自己的高度。
所以對於View
樹總體的測量而言,控件的測量其實是 自底向上 的,正如文章開篇 總體思路 一節所描述的:
對於完整的測量流程而言,父控件必然依賴子控件寬高的測量;若子控件自己未測量完畢,父控件自身的測量亦無從談起。
此外,由於子控件的測量邏輯受限於父控件傳過來的 佈局要求(MeasureSpec), 所以總體邏輯應該是:
setMeasuredDimension()
函數,其父控件根據本身的測量策略,將全部child
的寬高和佈局屬性進行對應的計算(好比上文中LinearLayout
就是計算全部子控件高度的和),獲得本身自己的測量寬高;setMeasuredDimension()
函數完成測量,這以後,它的父控件再根據其自身測量策略完成測量,如此往復,直至完成頂層級View
的測量,自此,整個頁面測量完畢。這裏的設計體現出了經典的 遞歸思想,一、2步驟,開始測量的通知自頂至下,咱們稱之爲測量步驟的 遞流程,三、4步驟,測量完畢的順序倒是自底至頂,咱們稱之爲測量步驟的 歸流程。
如今根據上一小節的設計思路,開始對 遞流程 進行編碼實現。
在整個遞流程中,MeasureSpec
所表明的 佈局要求 佔有相當重要的做用,瞭解了它在這個過程當中的意義,也就理解了爲何咱們常說 子控件的測量結果是由父控件和其自己共同決定的。
依然以 豎直方向佈局 的LinearLayout
爲例,咱們須要遍歷測量其全部的子控件,所以,在onMeasure()
函數中,第一次咱們編碼以下:
// 1.0版本的LinearLayout
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 1.經過遍歷,對每一個child進行測量
for(int i = 0 ; i < getChildCount() ; i++){
View child = getChildAt(i);
// 2.直接測量子控件
child.measure(widthMeasureSpec, heightMeasureSpec);
}
// ...
// 3.全部子控件測量完畢...
// ...
}
複製代碼
這裏關注int heightMeasureSpec
參數,咱們知道,這個32位int類型的值,包含了父佈局傳過來高度的 佈局要求:測量的大小和模式。如今咱們思考,若父佈局傳過來大小的是屏幕的高度,那麼將其做爲參數直接執行child.measure(widthMeasureSpec, heightMeasureSpec)
,讓子控件直接開始測量,是合理的嗎?
答案固然是否認的,試想這樣一個簡單的場景,若LinearLayout
自己設置了padding
值,那麼子控件的最大高度便不能再達到heightMeasureSpec
中size的大小了,可是若是像上述代碼中的步驟2同樣,直接對子控件進行測量,子控件就能夠從heightMeasureSpec
參數中取得屏幕的高度,經過setMeasuredDimension()
將本身的高度設置和父控件高度一致——這致使了padding
值配置的失效,並不符合預期。
所以,咱們須要額外設計一個可重寫的函數,用於自定義對child
的測量:
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
// 獲取子元素的佈局參數
final LayoutParams lp = child.getLayoutParams();
// 經過padding值,計算出子控件的佈局要求
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
// 將新的佈局要求傳入measure方法,完成子控件的測量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
複製代碼
咱們定義了measureChild()
函數,其做用是計算子控件的佈局要求,並把新的佈局要求傳給子控件,再讓子控件根據新的佈局要求進行測量,這樣就解決了上述的問題,由此也說明了爲何 子控件的測量結果是由父控件和其自己共同決定的。
這裏咱們注意到咱們設計了一個getChildMeasureSpec()
函數,那麼這個函數是作什麼的呢?
getChildMeasureSpec()
函數的做用是根據父佈局的MeasureSpec
和padding
值,計算出對應子控件的MeasureSpec
,由於這個函數的邏輯是能夠複用的,所以將其定義爲一個靜態函數:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//獲取父View的測量模式
int specMode = MeasureSpec.getMode(spec);
//獲取父View的測量大小
int specSize = MeasureSpec.getSize(spec);
//父View計算出的子View的大小,子View不必定用這個值
int size = Math.max(0, specSize - padding);
//聲明變量用來保存實際計算的到的子View的size和mode即大小和模式
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
//若是父容器的模式是Exactly即肯定的大小
case MeasureSpec.EXACTLY:
//子View的高度或寬度>0說明其實一個確切的值,由於match_parent和wrap_content的值是<0的
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
//子View的高度或寬度爲match_parent
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;//將size即父View的大小減去邊距值所獲得的值賦值給resultSize
resultMode = MeasureSpec.EXACTLY;//指定子View的測量模式爲EXACTLY
//子View的高度或寬度爲wrap_content
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;//將size賦值給result
resultMode = MeasureSpec.AT_MOST;//指定子View的測量模式爲AT_MOST
}
break;
//若是父容器的測量模式是AT_MOST
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
// 由於父View的大小是受到限制值的限制,因此子View的大小也應該受到父容器的限制而且不能超過父View
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
//若是父容器的測量模式是UNSPECIFIED即父容器的大小未受限制
case MeasureSpec.UNSPECIFIED:
//若是自View的寬和高是一個精確的值
if (childDimension >= 0) {
//子View的大小爲精確值
resultSize = childDimension;
//測量的模式爲EXACTLY
resultMode = MeasureSpec.EXACTLY;
//子View的寬或高爲match_parent
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//由於父View的大小是未定的,因此子View的大小也是未定的
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//根據resultSize和resultMode調用makeMeasureSpec方法獲得測量要求,並將其做爲返回值
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
複製代碼
邏輯分支相對較多,註釋中已經將子控件 佈局要求 的計算邏輯寫清楚了,總結以下圖,原圖連接:
爲何說這個函數很是重要?由於這個函數纔是 子控件的測量結果是由父控件和其自己共同決定的 最直接的體現,同時,在不一樣的佈局模式下(
match_parent
、wrap_content
、指定dp/px
),其對應子控件的佈局要求的返回值亦不一樣,建議讀者認真理解這段代碼。
回到前文,如今咱們對onMeasure()
的方法定義以下:
// 2.0版本的LinearLayout
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 1.經過遍歷,對每一個child進行測量
for(int i = 0 ; i < getChildCount() ; i++){
View child = getChildAt(i);
// 2.計算新的佈局要求,並對子控件進行測量
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
// ...
// 3.全部子控件測量完畢...
// ...
}
複製代碼
如今,全部子控件測量完畢,接下來 歸流程 的實現就很簡單了,將全部child
的height
進行累加,並調用 setMeasuredDimension()
結束測量便可:
// 3.0版本的LinearLayout
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 1.經過遍歷,對每一個child進行測量
for(int i = 0 ; i < getChildCount() ; i++){
View child = getChildAt(i);
// 2.計算新的佈局要求,並對子控件進行測量
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
// 3.完成子控件的測量,對高度進行累加
int height = 0;
for(int i = 0 ; i < getChildCount() ; i++){
height += child.getMeasuredHeight();
}
// 4.完成LinearLayout的測量
setMeasuredDimension(width, height);
}
複製代碼
乍一看,彷佛很難體現出整個流程的 遞歸 性,實際上當咱們宏觀從View
樹的樹頂順着往下整理思路,代碼邏輯的執行順序一目瞭然:
如圖所示,實線表明了測量流程中總體自頂向下的 遞流程, 而虛線表明了自底向上的 歸流程。
至此,測量流程總體實現完畢。
Hello,我是 卻把清梅嗅 ,若是您以爲文章對您有價值,歡迎 ❤️,也歡迎關注個人 博客 或者 Github。
若是您以爲文章還差了那麼點東西,也請經過關注督促我寫出更好的文章——萬一哪天我進步了呢?