總體流程
Android控件的測(cè)量從根布局開(kāi)始梯码,根布局即DecorView 宝泵;
測(cè)量開(kāi)始的地方,由ViewRootImol類的performMeasure方法開(kāi)啟測(cè)量轩娶,調(diào)用了DecorView的onMeasure方法儿奶,
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec);
其中參數(shù)widthMeasureSpec和heightMeasureSpec里包含的信息有兩個(gè),寬高尺寸和尺寸模式鳄抒,其中寬高為屏幕的尺寸闯捎,尺寸模式為MeasureSpec.EXACTLY椰弊。
在DecorView的onMeasure方法中,根據(jù)傳入的兩個(gè)參數(shù)瓤鼻、子View的尺寸信息以及自身的布局邏輯秉版,來(lái)判斷需要給子View設(shè)定的寬高尺寸和尺寸模式,然后繼續(xù)調(diào)用子View的measure方法茬祷;
子view在measure方法中會(huì)處理一系列邏輯清焕,調(diào)用到自身的onMeasure方法,然后根據(jù)父布局傳入的寬高尺寸祭犯、尺寸模式以及自身的具體需求來(lái)確定自己最終的尺寸大薪胀住;
在上一步中如果子View也是一個(gè)ViewGroup子類沃粗,則根據(jù)傳入的參數(shù)和自身的邏輯繼續(xù)測(cè)量自己的子View粥惧,直到最后一層。
以上就是整個(gè)流程的簡(jiǎn)要?dú)w納最盅,如果上面那段話中讓你覺(jué)得云里霧里影晓,或者你感覺(jué)有很多東西不知道從哪冒出來(lái)的,不要緊檩禾,我們一個(gè)一個(gè)點(diǎn)來(lái)說(shuō)挂签,捋清其中的細(xì)節(jié)。這個(gè)結(jié)論可以留到最后再來(lái)看盼产,到時(shí)會(huì)更加清晰饵婆。
根布局
在Android應(yīng)用中,你所寫(xiě)的每一個(gè)頁(yè)面戏售,都有一個(gè)根布局侨核,這個(gè)根布局不是你調(diào)用setContentView()時(shí)設(shè)置的那個(gè),而是DecorView灌灾。
我們來(lái)捋一捋Activity搓译,DecorView,你填入的布局锋喜,還有一個(gè):Window些己,這幾個(gè)東西之間的聯(lián)系。
Window是一個(gè)頂級(jí)窗口嘿般,它定義了窗體樣式和行為段标,提供標(biāo)準(zhǔn)的UI規(guī)則,例如背景炉奴、標(biāo)題逼庞、默認(rèn)關(guān)鍵過(guò)程等。實(shí)際上瞻赶,每當(dāng)你寫(xiě)一個(gè)新的Activity赛糟,在Activity的 attach方法中派任,都會(huì)初始化一個(gè)PhoneWindow實(shí)例。
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback) {
//省略部分代碼...
mWindow = new PhoneWindow(this, window, activityConfigCallback);
//省略部分代碼...
}
在你調(diào)用setContentView方法的時(shí)候璧南,實(shí)際上調(diào)用的是Window的setContentView方法:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
然后PhoneWindow的setContentView方法中調(diào)用了installDecor方法:
@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
//省略部分代碼...
}
installDecor()再調(diào)用generateLayout方法掌逛,這個(gè)方法中做了很多事情,會(huì)根據(jù)設(shè)置的主題樣式來(lái)設(shè)置DecorView的風(fēng)格穆咐,比如有沒(méi)有TitleBar颤诀,有沒(méi)有ActionBar等等;
這個(gè)方法中也為DecorView添加了子View对湃,即你通過(guò)setContentView設(shè)置進(jìn)來(lái)的布局崖叫。
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
TypedArray a = getWindowStyle();
//省略好多好多代碼...
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
//省略好多好多代碼...
return contentParent;
}
onResourcesLoaded代碼如下:
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
mStackId = getStackId();
//省略部分代碼...
//根據(jù)id實(shí)例化你填入的布局
final View root = inflater.inflate(layoutResource, null);
if (mDecorCaptionView != null) {
if (mDecorCaptionView.getParent() == null) {
addView(mDecorCaptionView,
new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mDecorCaptionView.addView(root,
new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
} else {
// Put it below the color views.
//將你的布局加入到DecorView
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mContentRoot = (ViewGroup) root;
//省略部分代碼...
}
所以,到這里拍柒,我們應(yīng)該明白了心傀,一個(gè)頁(yè)面中的幾層關(guān)系:
- 每個(gè)Activity會(huì)持有一個(gè)Window實(shí)例
- Window下有DecorView
- DecorView下是你設(shè)置的頁(yè)面內(nèi)容布局
開(kāi)啟測(cè)量的起始點(diǎn)
現(xiàn)在我們知道根布局在哪里,然后我們來(lái)看從哪里開(kāi)始測(cè)量拆讯。
View的繪制過(guò)程脂男,是由ViewRootImpl這個(gè)類來(lái)完成的,測(cè)量工作當(dāng)然也包含在其中种呐。ViewRootImpl是連接WindowManager和DecorView的紐帶宰翅,負(fù)責(zé)向DecorView分發(fā)收到的用戶發(fā)起的event事件(如按鍵,觸屏等)爽室,也負(fù)責(zé)完成View的繪制汁讼。
具體處理繪制流程是在一個(gè)performTraversals方法中,這個(gè)方法被調(diào)用的時(shí)候很多:控件焦點(diǎn)變化被調(diào)用阔墩、顯示狀態(tài)變化被調(diào)用嘿架、繪制刷新被調(diào)用、等等等等...
performTraversals方法內(nèi)部邏輯相當(dāng)相當(dāng)多&復(fù)雜啸箫,截取相關(guān)部分代碼:
private void performTraversals() {
// 省略巨多的代碼…
if (!mStopped) {
// ……省略一些代碼
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// ……省省省
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
// 省略巨多的代碼…
}
可以看到在performTraversals方法中通過(guò)getRootMeasureSpec獲取原始的測(cè)量規(guī)格并將其作為參數(shù)傳遞給performMeasure方法處理耸彪,這里我們重點(diǎn)來(lái)看getRootMeasureSpec方法是如何確定測(cè)量規(guī)格的,首先我們要知道m(xù)Width, lp.width和mHeight, lp.height這兩組參數(shù)的意義忘苛,其中l(wèi)p.width和lp.height均為MATCH_PARENT蝉娜,其在mWindowAttributes(WindowManager.LayoutParams類型)將值賦予給lp時(shí)就已被確定,mWidth和mHeight表示當(dāng)前窗口的大小柑土,其值由performTraversals中一系列邏輯計(jì)算確定蜀肘,這里跳過(guò),而在getRootMeasureSpec中作了如下判斷:
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window不能調(diào)整其大小稽屏,強(qiáng)制使根視圖大小與Window一致
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window可以調(diào)整其大小,為根視圖設(shè)置一個(gè)最大值
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window想要一個(gè)確定的尺寸西乖,強(qiáng)制將根視圖的尺寸作為其尺寸
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
所以最終的測(cè)量規(guī)格的確定走的是這一步:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
這里解釋一下傳入onMeasure()的這個(gè)參數(shù)狐榔,measureSpec 這個(gè)整型值坛增,包含了兩個(gè)信息,一個(gè)是具體尺寸薄腻,一個(gè)是尺寸模式收捣。具體尺寸很好理解,尺寸模式包括三種:
- MeasureSpec.EXACTLY 代表父View已經(jīng)為子View規(guī)定好了具體的尺寸庵楷,是否遵循看子view的意愿罢艾;
- MeasureSpec.AT_MOST 代表父View為子View圈定了一片地方,子View最大不能超過(guò)這片地方尽纽;
- MeasureSpec.UNSPECIFIED 代表父view對(duì)子view沒(méi)有任何約束咐蚯,子view想多大就多大;
(具體這個(gè)整形值如何包含著兩個(gè)信息的自行查找資料學(xué)習(xí)弄贿,這里略過(guò))
所以回到計(jì)算中春锋,這里的結(jié)果很明顯:尺寸是窗口大小,模式是MeasureSpec.EXACTLY 差凹,頂層DecorView接收到的參數(shù)就是這兩個(gè)期奔。也就是說(shuō)不管如何,我們的根視圖大小必定都是全屏的…
好了危尿,到這一步呐萌,我們測(cè)量的起點(diǎn)也找到了,頂層View接收到的尺寸參數(shù)也明確了谊娇,接下來(lái)我們看看它如何把測(cè)量流程繼續(xù)往下走肺孤。
ViewGroup測(cè)量子View
頂層的ViewGroup從onMeasure方法中接收到了尺寸參數(shù),大小確定為屏幕寬高邮绿,模式是MeasureSpec.EXACTLY 渠旁。
好了,現(xiàn)在頂層的DecorView要開(kāi)始自己的工作了:我有這么大塊地盤(pán)船逮,我要好好安置我的兒子們顾腊,每人劃一片地兒...安置的妥妥的......
為了理清脈絡(luò),DecorView自己的布局邏輯我們摒開(kāi)不看挖胃,它在onMeasure方法中調(diào)用了父類的onMeasure方法杂靶,也就是FrameLayout的onMeasure方法。現(xiàn)在我們把常用的幾個(gè)Layout拎出來(lái)酱鸭,看看他們的onMeasure方法吗垮,比對(duì)一下,可以發(fā)現(xiàn)凹髓,大致都有這么一段邏輯:
1.對(duì)子View進(jìn)行遍歷烁登;
2.調(diào)用measureChildWithMargins()方法獲取對(duì)子view的建議尺寸規(guī)格(這個(gè)是ViewGroup本身的方法);
3.用獲取到的尺寸規(guī)格蔚舀,調(diào)用子view的measure方法饵沧;
由于各種布局自身的排列邏輯不同锨络,相關(guān)的實(shí)現(xiàn)細(xì)節(jié)必定差異極大,但是測(cè)量的流程卻都是幾乎相同的狼牺,measureChildWithMargins()方法就是其中的共同點(diǎn)之一羡儿,來(lái)看看這個(gè)方法:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
很簡(jiǎn)單,獲取子View的測(cè)量規(guī)格是钥,然后調(diào)用子View的measure方法掠归。接著看getChildMeasureSpec():
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
首先判斷自己作為父View的尺寸模式,再判斷子view的寬高值:具體值悄泥、WRAP_CONTENT虏冻、MATCH_PARENT,根據(jù)這兩個(gè)值的具體情況來(lái)返回相應(yīng)的MeasureSpec信息码泞。
然后用這個(gè)確定好的MeasureSpec信息傳給子View兄旬,調(diào)用其measure方法,讓它確定自身的尺寸余寥。
到這里已經(jīng)很清晰了...
接下來(lái)领铐,就是View拿到父View建議的尺寸規(guī)格,結(jié)合自身情況宋舷,設(shè)置自身的具體尺寸大小陕靠。
View設(shè)定具體寬高
終于到了View這一層了驳遵。
View的measure方法邏輯中翅敌,會(huì)調(diào)用到onMeasure方法绣张,其默認(rèn)的實(shí)現(xiàn)是:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
//onMeasure內(nèi)用到的的相關(guān)方法
protected int getSuggestedMinimumWidth() {
//mMinWidth和mMinHeight 好像都是100px
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
public static int getDefaultSize(int size, int measureSpec) {
// 將我們獲得的最小值賦給result
int result = size;
// 從measureSpec中解算出測(cè)量規(guī)格的模式和尺寸
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
/*
* 根據(jù)測(cè)量規(guī)格模式確定最終的測(cè)量尺寸
*/
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
如果父view不限制,就按自己的背景大小或者最小值來(lái)顯示绎狭,如果父view有限制细溅,就按父view給的尺寸來(lái)顯示。
按照這個(gè)邏輯儡嘶,如果要自己寫(xiě)一個(gè)自定義View喇聊,大小可以在布局中確定的話,一般不用再重新onMeasure 再做什么工作了蹦狂。
但是如果自己的自定義View在布局中使用WRAP_CONTENT誓篱,并且內(nèi)容大小并不確定的話,還是要根據(jù)自己的顯示邏輯做一些工作的凯楔。
比如窜骄,自己寫(xiě)一個(gè)顯示圖片的控件,布局中使用WRAP_CONTENT摆屯,那么根據(jù)以上的邏輯梳理邻遏,父view很可能就扔給你一個(gè)尺寸模式:大小是父view本身的大小,模式是MeasureSpec.AT_MOST;這樣的話即使你布局里寫(xiě)的是WRAP_CONTENT党远,你也會(huì)使用父view建議給你的尺寸削解,占滿父view全部的空間了富弦,即使你的圖片并沒(méi)有那么大~是不是會(huì)很奇怪沟娱?
所以,一般情況下腕柜,展示內(nèi)容尺寸不確定的自定義View济似,onMeasure可以作如下類似的邏輯:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 聲明一個(gè)臨時(shí)變量來(lái)存儲(chǔ)計(jì)算出的測(cè)量值
int resultWidth = 0;
// 獲取寬度測(cè)量規(guī)格中的mode
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
// 獲取寬度測(cè)量規(guī)格中的size
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
/*
* 如果爹心里有數(shù)
*/
if (modeWidth == MeasureSpec.EXACTLY) {
// 那么兒子也不要讓爹難做就取爹給的大小吧
resultWidth = sizeWidth;
}
/*
* 如果爹心里沒(méi)數(shù)
*/
else {
// 那么兒子可要自己看看自己需要多大了
resultWidth = getSelfContentWidth();//自己實(shí)現(xiàn)...
/*
* 如果爹給兒子的是一個(gè)限制值
*/
if (modeWidth == MeasureSpec.AT_MOST) {
// 那么兒子自己的需求就要跟爹的限制比比看誰(shuí)小要誰(shuí)
resultWidth = Math.min(resultWidth, sizeWidth);
}
}
int resultHeight = 0;
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
if (modeHeight == MeasureSpec.EXACTLY) {
resultHeight = sizeHeight;
} else {
resultHeight = getSelfContentHeight();//自己實(shí)現(xiàn)...
if (modeHeight == MeasureSpec.AT_MOST) {
resultHeight = Math.min(resultHeight, sizeHeight);
}
}
// 設(shè)置測(cè)量尺寸
setMeasuredDimension(resultWidth, resultHeight);
}
這樣,既考慮了自身內(nèi)容的尺寸盏缤,也適應(yīng)了View的測(cè)量流程砰蠢,就可以正確的顯示大小了。當(dāng)然唉铜,具體情況還是要看自定義view 的具體邏輯台舱,這里只是一個(gè)示例,不一定適合各種場(chǎng)合潭流。
那么竞惋,到這里,關(guān)于Android中控件尺寸測(cè)量流程的梳理灰嫉,差不多就都結(jié)束了〔鹜穑現(xiàn)在你再去看開(kāi)頭的結(jié)論,會(huì)不會(huì)清晰一些了讼撒?
最后浑厚,如發(fā)現(xiàn)有問(wèn)題,請(qǐng)斧正根盒,不勝感激钳幅!
最后的最后,感謝AIGE的博客炎滞, 學(xué)習(xí)了很多敢艰,本篇文章很多的參考了AIGE的博客,甚至摘抄了部分他的代碼和描述片段厂榛,當(dāng)做筆記盖矫,往后以方便查閱。原文地址击奶,請(qǐng)點(diǎn)擊這里辈双。