概述
本篇文章會(huì)從源碼(基于Android 6.0)角度分析Android中View的繪制流程,側(cè)重于對(duì)整體流程的分析拂募,對(duì)一些難以理解的點(diǎn)加以重點(diǎn)闡述羽莺,目的是把View繪制的整個(gè)流程把握好弧蝇,而對(duì)于特定實(shí)現(xiàn)細(xì)節(jié)則可以日后再對(duì)相應(yīng)源碼進(jìn)行研讀欣喧。
在進(jìn)行實(shí)際的分析之前,我們先來看下面這張圖:
我們來對(duì)上圖做出簡(jiǎn)單解釋:DecorView是一個(gè)應(yīng)用窗口的根容器应役,它本質(zhì)上是一個(gè)FrameLayout情组。DecorView有唯一一個(gè)子View燥筷,它是一個(gè)垂直LinearLayout,包含兩個(gè)子元素院崇,一個(gè)是TitleView(ActionBar的容器)肆氓,另一個(gè)是ContentView(窗口內(nèi)容的容器)。關(guān)于ContentView底瓣,它是一個(gè)FrameLayout(android.R.id.content)谢揪,我們平常用的setContentView就是設(shè)置它的子View。上圖還表達(dá)了每個(gè)Activity都與一個(gè)Window(具體來說是PhoneWindow)相關(guān)聯(lián)捐凭,用戶界面則由Window所承載拨扶。
Window
Window即窗口,這個(gè)概念在Android Framework中的實(shí)現(xiàn)為android.view.Window這個(gè)抽象類茁肠,這個(gè)抽象類是對(duì)Android系統(tǒng)中的窗口的抽象患民。在介紹這個(gè)類之前,我們先來看看究竟什么是窗口呢垦梆?
實(shí)際上匹颤,窗口是一個(gè)宏觀的思想,它是屏幕上用于繪制各種UI元素及響應(yīng)用戶輸入事件的一個(gè)矩形區(qū)域托猩。通常具備以下兩個(gè)特點(diǎn):
- 獨(dú)立繪制惋嚎,不與其它界面相互影響;
- 不會(huì)觸發(fā)其它界面的輸入事件站刑;
在Android系統(tǒng)中,窗口是獨(dú)占一個(gè)Surface實(shí)例的顯示區(qū)域鼻百,每個(gè)窗口的Surface由WindowManagerService分配绞旅。我們可以把Surface看作一塊畫布,應(yīng)用可以通過Canvas或OpenGL在其上面作畫温艇。畫好之后因悲,通過SurfaceFlinger將多塊Surface按照特定的順序(即Z-order)進(jìn)行混合,而后輸出到FrameBuffer中勺爱,這樣用戶界面就得以顯示晃琳。
android.view.Window這個(gè)抽象類可以看做Android中對(duì)窗口這一宏觀概念所做的約定,而PhoneWindow這個(gè)類是Framework為我們提供的Android窗口概念的具體實(shí)現(xiàn)琐鲁。接下來我們先來介紹一下android.view.Window這個(gè)抽象類卫旱。
這個(gè)抽象類包含了三個(gè)核心組件:
- WindowManager.LayoutParams: 窗口的布局參數(shù);
- Callback: 窗口的回調(diào)接口围段,通常由Activity實(shí)現(xiàn)顾翼;
- ViewTree: 窗口所承載的控件樹。
下面我們來看一下Android中Window的具體實(shí)現(xiàn)(也是唯一實(shí)現(xiàn))——PhoneWindow奈泪。
PhoneWindow
前面我們提到了适贸,PhoneWindow這個(gè)類是Framework為我們提供的Android窗口的具體實(shí)現(xiàn)灸芳。我們平時(shí)調(diào)用setContentView()方法設(shè)置Activity的用戶界面時(shí),實(shí)際上就完成了對(duì)所關(guān)聯(lián)的PhoneWindow的ViewTree的設(shè)置拜姿。我們還可以通過Activity類的requestWindowFeature()方法來定制Activity關(guān)聯(lián)PhoneWindow的外觀烙样,這個(gè)方法實(shí)際上做的是把我們所請(qǐng)求的窗口外觀特性存儲(chǔ)到了PhoneWindow的mFeatures成員中,在窗口繪制階段生成外觀模板時(shí)蕊肥,會(huì)根據(jù)mFeatures的值繪制特定外觀谒获。
從setContentView()說開去
在分析setContentView()方法前,我們需要明確:這個(gè)方法只是完成了Activity的ContentView的創(chuàng)建晴埂,而并沒有執(zhí)行View的繪制流程究反。
當(dāng)我們自定義Activity繼承自android.app.Activity時(shí)候,調(diào)用的setContentView()方法是Activity類的儒洛,源碼如下:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
. . .
}
getWindow()方法會(huì)返回Activity所關(guān)聯(lián)的PhoneWindow精耐,也就是說,實(shí)際上調(diào)用到了PhoneWindow的setContentView()方法琅锻,源碼如下:
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
// mContentParent即為上面提到的ContentView的父容器卦停,若為空則調(diào)用installDecor()生成
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
// 具有FEATURE_CONTENT_TRANSITIONS特性表示開啟了Transition
// mContentParent不為null,則移除decorView的所有子View
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
// 開啟了Transition恼蓬,做相應(yīng)的處理惊完,我們不討論這種情況
// 感興趣的同學(xué)可以參考源碼
. . .
} else {
// 一般情況會(huì)來到這里,調(diào)用mLayoutInflater.inflate()方法來填充布局
// 填充布局也就是把我們?cè)O(shè)置的ContentView加入到mContentParent中
mLayoutInflater.inflate(layoutResID, mContentParent);
}
. . .
// cb即為該Window所關(guān)聯(lián)的Activity
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
// 調(diào)用onContentChanged()回調(diào)方法通知Activity窗口內(nèi)容發(fā)生了改變
cb.onContentChanged();
}
. . .
}
LayoutInflater.inflate()
在上面我們看到了处硬,PhoneWindow的setContentView()方法中調(diào)用了LayoutInflater的inflate()方法來填充布局小槐,這個(gè)方法的源碼如下:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
. . .
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
在PhoneWindow的setContentView()方法中傳入了decorView作為L(zhǎng)ayoutInflater.inflate()的root參數(shù),我們可以看到荷辕,通過層層調(diào)用凿跳,最終調(diào)用的是inflate(XmlPullParser, ViewGroup, boolean)方法來填充布局。這個(gè)方法的源碼如下:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
. . .
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
// Look for the root node.
int type;
// 一直讀取xml文件疮方,直到遇到開始標(biāo)記
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
// 最先遇到的不是開始標(biāo)記控嗜,報(bào)錯(cuò)
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
final String name = parser.getName();
. . .
// 單獨(dú)處理<merge>標(biāo)簽,不熟悉的同學(xué)請(qǐng)參考官方文檔的說明
if (TAG_MERGE.equals(name)) {
// 若包含<merge>標(biāo)簽骡显,父容器(即root參數(shù))不可為空且attachRoot須為true疆栏,否則報(bào)錯(cuò)
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
// 遞歸地填充布局
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// temp為xml布局文件的根View
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
. . .
// 獲取父容器的布局參數(shù)(LayoutParams)
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// 若attachToRoot參數(shù)為false,則我們只會(huì)將父容器的布局參數(shù)設(shè)置給根View
temp.setLayoutParams(params);
}
}
// 遞歸加載根View的所有子View
rInflateChildren(parser, temp, attrs, true);
. . .
if (root != null && attachToRoot) {
// 若父容器不為空且attachToRoot為true惫谤,則將父容器作為根View的父View包裹上來
root.addView(temp, params);
}
// 若root為空或是attachToRoot為false壁顶,則以根View作為返回值
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
. . .
} catch (Exception e) {
. . .
} finally {
. . .
}
return result;
}
}
在上面的源碼中,首先對(duì)于布局文件中的<merge>標(biāo)簽進(jìn)行單獨(dú)處理溜歪,調(diào)用rInflate()方法來遞歸填充布局博助。這個(gè)方法的源碼如下:
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
// 獲取當(dāng)前標(biāo)記的深度,根標(biāo)記的深度為0
final int depth = parser.getDepth();
int type;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
// 不是開始標(biāo)記則繼續(xù)下一次迭代
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
// 對(duì)一些特殊標(biāo)記做單獨(dú)處理
if (TAG_REQUEST_FOCUS.equals(name)) {
parseRequestFocus(parser, parent);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
// 對(duì)<include>做處理
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
// 對(duì)一般標(biāo)記的處理
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params=viewGroup.generateLayoutParams(attrs);
// 遞歸地加載子View
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
if (finishInflate) {
parent.onFinishInflate();
}
}
我們可以看到痹愚,上面的inflate()和rInflate()方法中都調(diào)用了rInflateChildren()方法富岳,這個(gè)方法的源碼如下:
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
從源碼中我們可以知道蛔糯,rInflateChildren()方法實(shí)際上調(diào)用了rInflate()方法。
到這里窖式,setContentView()的整體執(zhí)行流程我們就分析完了蚁飒,至此我們已經(jīng)完成了Activity的ContentView的創(chuàng)建與設(shè)置工作。接下來萝喘,我們開始進(jìn)入正題淮逻,分析View的繪制流程。
ViewRoot
在介紹View的繪制前阁簸,首先我們需要知道是誰(shuí)負(fù)責(zé)執(zhí)行View繪制的整個(gè)流程爬早。實(shí)際上,View的繪制是由ViewRoot來負(fù)責(zé)的启妹。每個(gè)應(yīng)用程序窗口的decorView都有一個(gè)與之關(guān)聯(lián)的ViewRoot對(duì)象筛严,這種關(guān)聯(lián)關(guān)系是由WindowManager來維護(hù)的。
那么decorView與ViewRoot的關(guān)聯(lián)關(guān)系是在什么時(shí)候建立的呢饶米?答案是Activity啟動(dòng)時(shí)桨啃,ActivityThread.handleResumeActivity()方法中建立了它們兩者的關(guān)聯(lián)關(guān)系。這里我們不具體分析它們建立關(guān)聯(lián)的時(shí)機(jī)與方式檬输,感興趣的同學(xué)可以參考相關(guān)源碼照瘾。下面我們直入主題,分析一下ViewRoot是如何完成View的繪制的丧慈。
View繪制的起點(diǎn)
當(dāng)建立好了decorView與ViewRoot的關(guān)聯(lián)后析命,ViewRoot類的requestLayout()方法會(huì)被調(diào)用,以完成應(yīng)用程序用戶界面的初次布局逃默。實(shí)際被調(diào)用的是ViewRootImpl類的requestLayout()方法鹃愤,這個(gè)方法的源碼如下:
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
// 檢查發(fā)起布局請(qǐng)求的線程是否為主線程
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
上面的方法中調(diào)用了scheduleTraversals()方法來調(diào)度一次完成的繪制流程,該方法會(huì)向主線程發(fā)送一個(gè)“遍歷”消息笑旺,最終會(huì)導(dǎo)致ViewRootImpl的performTraversals()方法被調(diào)用。下面馍资,我們以performTraversals()為起點(diǎn)筒主,來分析View的整個(gè)繪制流程。
三個(gè)階段
View的整個(gè)繪制流程可以分為以下三個(gè)階段:
- measure: 判斷是否需要重新計(jì)算View的大小鸟蟹,需要的話則計(jì)算乌妙;
- layout: 判斷是否需要重新計(jì)算View的位置,需要的話則計(jì)算建钥;
- draw: 判斷是否需要重新繪制View藤韵,需要的話則重繪制。
這三個(gè)子階段可以用下圖來描述:
measure階段
此階段的目的是計(jì)算出控件樹中的各個(gè)控件要顯示其內(nèi)容的話熊经,需要多大尺寸泽艘。起點(diǎn)是ViewRootImpl的measureHierarchy()方法欲险,這個(gè)方法的源碼如下:
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp, final Resources res,
final int desiredWindowWidth, final int desiredWindowHeight) {
// 傳入的desiredWindowXxx為窗口尺寸
int childWidthMeasureSpec;
int childHeightMeasureSpec;
boolean windowSizeMayChange = false;
. . .
boolean goodMeasure = false;
if (!goodMeasure) {
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
windowSizeMayChange = true;
}
}
return windowSizeMayChange;
}
上面的代碼中調(diào)用getRootMeasureSpec()方法來獲取根MeasureSpec,這個(gè)根MeasureSpec代表了對(duì)decorView的寬高的約束信息匹涮。繼續(xù)分析之前天试,我們先來簡(jiǎn)單地介紹下MeasureSpec的概念。
MeasureSpec是一個(gè)32位整數(shù)然低,由SpecMode和SpecSize兩部分組成喜每,其中,高2位為SpecMode雳攘,低30位為SpecSize带兜。SpecMode為測(cè)量模式,SpecSize為相應(yīng)測(cè)量模式下的測(cè)量尺寸吨灭。View(包括普通View和ViewGroup)的SpecMode由本View的LayoutParams結(jié)合父View的MeasureSpec生成刚照。
SpecMode的取值可為以下三種:
- EXACTLY: 對(duì)子View提出了一個(gè)確切的建議尺寸(SpecSize);
- AT_MOST: 子View的大小不得超過SpecSize沃于;
- UNSPECIFIED: 對(duì)子View的尺寸不作限制涩咖,通常用于系統(tǒng)內(nèi)部。
傳入performMeasure()方法的MeasureSpec的SpecMode為EXACTLY繁莹,SpecSize為窗口尺寸檩互。
performMeasure()方法的源碼如下:
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
. . .
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
. . .
}
}
上面代碼中的mView即為decorView,也就是說會(huì)轉(zhuǎn)向?qū)iew.measure()方法的調(diào)用咨演,這個(gè)方法的源碼如下:
/**
* 調(diào)用這個(gè)方法來算出一個(gè)View應(yīng)該為多大闸昨。參數(shù)為父View對(duì)其寬高的約束信息。
* 實(shí)際的測(cè)量工作在onMeasure()方法中進(jìn)行
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
. . .
// 判斷是否需要重新布局
// 若mPrivateFlags中包含PFLAG_FORCE_LAYOUT標(biāo)記薄风,則強(qiáng)制重新布局
// 比如調(diào)用View.requestLayout()會(huì)在mPrivateFlags中加入此標(biāo)記
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
|| heightMeasureSpec != mOldHeightMeasureSpec;
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
// 需要重新布局
if (forceLayout || needsLayout) {
. . .
// 先嘗試從緩從中獲取饵较,若forceLayout為true或是緩存中不存在或是
// 忽略緩存,則調(diào)用onMeasure()重新進(jìn)行測(cè)量工作
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
. . .
} else {
// 緩存命中遭赂,直接從緩存中取值即可循诉,不必再測(cè)量
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
. . .
}
. . .
}
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
從measure()方法的源碼中我們可以知道,只有以下兩種情況之一撇他,才會(huì)進(jìn)行實(shí)際的測(cè)量工作:
- forceLayout為true:這表示強(qiáng)制重新布局茄猫,可以通過View.requestLayout()來實(shí)現(xiàn);
- needsLayout為true困肩,這需要specChanged為true(表示本次傳入的MeasureSpec與上次傳入的不同)划纽,并且以下三個(gè)條件之一成立:
- sAlwaysRemeasureExactly為true: 該變量默認(rèn)為false;
- isSpecExactly為false: 若父View對(duì)子View提出了精確的寬高約束锌畸,則該變量為true勇劣,否則為false
- matchesSpecSize為false: 表示父View的寬高尺寸要求與上次測(cè)量的結(jié)果不同
對(duì)于decorView來說,實(shí)際執(zhí)行測(cè)量工作的是FrameLayout的onMeasure()方法潭枣,該方法的源碼如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
. . .
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
. . .
}
}
// Account for padding too
maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
// Check against our minimum height and width
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
// Check against our foreground's minimum height and width
final Drawable drawable = getForeground();
if (drawable != null) {
maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
}
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
. . .
}
FrameLayout是ViewGroup的子類比默,后者有一個(gè)View[]類型的成員變量mChildren幻捏,代表了其子View集合。通過getChildAt(i)能獲取指定索引處的子View退敦,通過getChildCount()可以獲得子View的總數(shù)粘咖。
在上面的源碼中,首先調(diào)用measureChildWithMargins()方法對(duì)所有子View進(jìn)行了一遍測(cè)量侈百,并計(jì)算出所有子View的最大寬度和最大高度瓮下。而后將得到的最大高度和寬度加上padding,這里的padding包括了父View的padding和前景區(qū)域的padding钝域。然后會(huì)檢查是否設(shè)置了最小寬高讽坏,并與其比較,將兩者中較大的設(shè)為最終的最大寬高例证。最后路呜,若設(shè)置了前景圖像,我們還要檢查前景圖像的最小寬高织咧。
經(jīng)過了以上一系列步驟后胀葱,我們就得到了maxHeight和maxWidth的最終值,表示當(dāng)前容器View用這個(gè)尺寸就能夠正常顯示其所有子View(同時(shí)考慮了padding和margin)笙蒙。而后我們需要調(diào)用resolveSizeAndState()方法來結(jié)合傳來的MeasureSpec來獲取最終的測(cè)量寬高抵屿,并保存到mMeasuredWidth與mMeasuredHeight成員變量中。
從以上代碼的執(zhí)行流程中捅位,我們可以看到轧葛,容器View通過measureChildWithMargins()方法對(duì)所有子View進(jìn)行測(cè)量后,才能得到自身的測(cè)量結(jié)果艇搀。也就是說尿扯,對(duì)于ViewGroup及其子類來說,要先完成子View的測(cè)量焰雕,再進(jìn)行自身的測(cè)量(考慮進(jìn)padding等)衷笋。
接下來我們來看下ViewGroup的measureChildWithMargins()方法的實(shí)現(xiàn):
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);
}
由以上代碼我們可以知道,對(duì)于ViewGroup來說矩屁,它會(huì)調(diào)用child.measure()來完成子View的測(cè)量辟宗。傳入ViewGroup的MeasureSpec是它的父View用于約束其測(cè)量的,那么ViewGroup本身也需要生成一個(gè)childMeasureSpec來限制它的子View的測(cè)量工作档插。這個(gè)childMeasureSpec就由getChildMeasureSpec()方法生成慢蜓。接下來我們來分析這個(gè)方法:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
// spec為父View的MeasureSpec
// padding為父View在相應(yīng)方向的已用尺寸加上父View的padding和子View的margin
// childDimension為子View的LayoutParams的值
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
// 現(xiàn)在size的值為父View相應(yīng)方向上的可用大小
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) {
// 表示子View的LayoutParams指定了具體大小值(xx dp)
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 子View想和父View一樣大
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 子View想自己決定其尺寸亚再,但不能比父View大
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// 子View指定了具體大小
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 子View想跟父View一樣大郭膛,但是父View的大小未固定下來
// 所以指定約束子View不能比父View大
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 子View想要自己決定尺寸,但不能比父View大
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
. . .
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
上面的方法展現(xiàn)了根據(jù)父View的MeasureSpec和子View的LayoutParams生成子View的MeasureSpec的過程氛悬,** 子View的LayoutParams表示了子View的期待大小**则剃。這個(gè)產(chǎn)生的MeasureSpec用于指導(dǎo)子View自身的測(cè)量結(jié)果的確定耘柱。
在上面的代碼中,我們可以看到當(dāng)ParentMeasureSpec的SpecMode為EXACTLY時(shí)棍现,表示父View對(duì)子View指定了確切的寬高限制调煎。此時(shí)根據(jù)子View的LayoutParams的不同,分以下三種情況:
- 具體大屑喊埂(childDimension):這種情況下令子View的SpecSize為childDimension士袄,即子View在LayoutParams指定的具體大小值;令子View的SpecMode為EXACTLY谎僻,即這種情況下若該子View為容器View娄柳,它也有能力給其子View指定確切的寬高限制(子View只能在這個(gè)寬高范圍內(nèi)),若為普通View艘绍,它的最終測(cè)量大小就為childDimension赤拒。
- match_parent:此時(shí)表示子View想和父View一樣大。這種情況下得到的子View的SpecMode與上種情況相同诱鞠,只不過SpecSize為size挎挖,即父View的剩余可用大小。
- wrap_content: 這表示了子View想自己決定自己的尺寸(根據(jù)其內(nèi)容的大小動(dòng)態(tài)決定)航夺。這種情況下子View的確切測(cè)量大小只能在其本身的onMeasure()方法中計(jì)算得出蕉朵,父View此時(shí)無從知曉。所以暫時(shí)將子View的SpecSize設(shè)為size(父View的剩余大蟹蟠妗)墓造;令子View的SpecMode為AT_MOST,表示了若子View為ViewGroup锚烦,它沒有能力給其子View指定確切的寬高限制觅闽,畢竟它本身的測(cè)量寬高還懸而未定。
當(dāng)ParentMeasureSpec的SpecMode為AT_MOST時(shí)涮俄,我們也可以根據(jù)子View的LayoutParams的不同來分三種情況討論:
- 具體大序茸尽:這時(shí)令子View的SpecSize為childDimension,SpecMode為EXACTLY彻亲。
- match_parent:表示子View想和父View一樣大孕锄,故令子View的SpecSize為size,但是由于父View本身的測(cè)量寬高還無從確定,所以只是暫時(shí)令子View的測(cè)量結(jié)果為父View目前的可用大小鲫售。這時(shí)令子View的SpecMode為AT_MOST溢陪。
- wrap_content:表示子View想自己決定大小(根據(jù)其內(nèi)容動(dòng)態(tài)確定)轴脐。然而這時(shí)父View還無法確定其自身的測(cè)量寬高,所以暫時(shí)令子View的SpecSize為size,SpecMode為AT_MOST大咱。
從上面的分析我們可以得到一個(gè)通用的結(jié)論恬涧,當(dāng)子View的測(cè)量結(jié)果能夠確定時(shí),子View的SpecMode就為EXACTLY碴巾;當(dāng)子View的測(cè)量結(jié)果還不能確定(只是暫時(shí)設(shè)為某個(gè)值)時(shí)溯捆,子View的SpecMode為AT_MOST。
在measureChildWithMargins()方法中厦瓢,獲取了知道子View測(cè)量的MeasureSpec后提揍,接下來就要調(diào)用child.measure()方法,并把獲取到的childMeasureSpec傳入煮仇。這時(shí)便又會(huì)調(diào)用onMeasure()方法碳锈,若此時(shí)的子View為ViewGroup的子類,便會(huì)調(diào)用相應(yīng)容器類的onMeasure()方法欺抗,其他容器View的onMeasure()方法與FrameLayout的onMeasure()方法執(zhí)行過程相似售碳。
下面會(huì)我們回到FrameLayout的onMeasure()方法,當(dāng)遞歸地執(zhí)行完所有子View的測(cè)量工作后绞呈,會(huì)調(diào)用resolveSizeAndState()方法來根據(jù)之前的測(cè)量結(jié)果確定最終對(duì)FrameLayout的測(cè)量結(jié)果并存儲(chǔ)起來贸人。View類的resolveSizeAndState()方法的源碼如下:
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
// 父View給定的最大尺寸小于完全顯示內(nèi)容所需尺寸
// 則在測(cè)量結(jié)果上加上MEASURED_STATE_TOO_SMALL
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
// 若specMode為EXACTLY,則不考慮size佃声,result直接賦值為specSize
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
對(duì)于普通View艺智,會(huì)調(diào)用View類的onMeasure()方法來進(jìn)行實(shí)際的測(cè)量工作,該方法的源碼如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
對(duì)于普通View(非ViewgGroup)來說圾亏,只需完成自身的測(cè)量工作即可十拣。以上代碼中通過setMeasuredDimension()方法設(shè)置測(cè)量的結(jié)果,具體來說是以getDefaultSize()方法的返回值來作為測(cè)量結(jié)果志鹃。getDefaultSize()方法的源碼如下:
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;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
由以上代碼我們可以看到夭问,View的getDefaultSize()方法對(duì)于AT_MOST和EXACTLY這兩種情況都返回了SpecSize作為result。所以若我們的自定義View直接繼承了View類曹铃,我們就要自己對(duì)wrap_content (對(duì)應(yīng)了AT_MOST)這種情況進(jìn)行處理缰趋,否則對(duì)自定義View指定wrap_content就和match_parent效果一樣了。
layout階段
layout階段的基本思想也是由根View開始陕见,遞歸地完成整個(gè)控件樹的布局(layout)工作秘血。
View.layout()
我們把對(duì)decorView的layout()方法的調(diào)用作為布局整個(gè)控件樹的起點(diǎn),實(shí)際上調(diào)用的是View類的layout()方法评甜,源碼如下:
public void layout(int l, int t, int r, int b) {
// l為本View左邊緣與父View左邊緣的距離
// t為本View上邊緣與父View上邊緣的距離
// r為本View右邊緣與父View左邊緣的距離
// b為本View下邊緣與父View上邊緣的距離
. . .
boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
. . .
}
. . .
}
這個(gè)方法會(huì)調(diào)用setFrame()方法來設(shè)置View的mLeft灰粮、mTop、mRight和mBottom四個(gè)參數(shù)忍坷,這四個(gè)參數(shù)描述了View相對(duì)其父View的位置(分別賦值為l, t, r, b)粘舟,在setFrame()方法中會(huì)判斷View的位置是否發(fā)生了改變红柱,若發(fā)生了改變,則需要對(duì)子View進(jìn)行重新布局蓖乘,對(duì)子View的局部是通過onLayout()方法實(shí)現(xiàn)了。由于普通View( 非ViewGroup)不含子View韧骗,所以View類的onLayout()方法為空嘉抒。因此接下來,我們看看ViewGroup類的onLayout()方法的實(shí)現(xiàn)袍暴。
ViewGroup.onLayout()
實(shí)際上ViewGroup類的onLayout()方法是abstract些侍,這是因?yàn)椴煌牟季止芾砥饔兄煌牟季址绞健?br> 這里我們以decorView,也就是FrameLayout的onLayout()方法為例政模,分析ViewGroup的布局過程:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
final int count = getChildCount();
final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft;
int childTop;
int gravity = lp.gravity;
if (gravity == -1) {
gravity = DEFAULT_CHILD_GRAVITY;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}
case Gravity.LEFT:
default:
childLeft = parentLeft + lp.leftMargin;
}
switch (verticalGravity) {
case Gravity.TOP:
childTop = parentTop + lp.topMargin;
break;
case Gravity.CENTER_VERTICAL:
childTop = parentTop + (parentBottom - parentTop - height) / 2 +
lp.topMargin - lp.bottomMargin;
break;
case Gravity.BOTTOM:
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
childTop = parentTop + lp.topMargin;
}
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
在上面的方法中岗宣,parentLeft表示當(dāng)前View為其子View顯示區(qū)域指定的一個(gè)左邊界,也就是子View顯示區(qū)域的左邊緣到父View的左邊緣的距離淋样,parentRight耗式、parentTop、parentBottom的含義同理趁猴。確定了子View的顯示區(qū)域后刊咳,接下來,用一個(gè)for循環(huán)來完成子View的布局儡司。
在確保子View的可見性不為GONE的情況下才會(huì)對(duì)其進(jìn)行布局娱挨。首先會(huì)獲取子View的LayoutParams、layoutDirection等一系列參數(shù)捕犬。上面代碼中的childLeft代表了最終子View的左邊緣距父View左邊緣的距離跷坝,childTop代表了子View的上邊緣距父View的上邊緣的距離。會(huì)根據(jù)子View的layout_gravity的取值對(duì)childLeft和childTop做出不同的調(diào)整碉碉。最后會(huì)調(diào)用child.layout()方法對(duì)子View的位置參數(shù)進(jìn)行設(shè)置柴钻,這時(shí)便轉(zhuǎn)到了View.layout()方法的調(diào)用,若子View是容器View垢粮,則會(huì)遞歸地對(duì)其子View進(jìn)行布局顿颅。
到這里,layout階段的大致流程我們就分析完了足丢,這個(gè)階段主要就是根據(jù)上一階段得到的View的測(cè)量寬高來確定View的最終顯示位置粱腻。顯然,經(jīng)過了measure階段和layout階段斩跌,我們已經(jīng)確定好了View的大小和位置绍些,那么接下來就可以開始繪制View了。
draw階段
對(duì)于本階段的分析耀鸦,我們以decorView.draw()作為分析的起點(diǎn)柬批,也就是View.draw()方法啸澡,它的源碼如下:
public void draw(Canvas canvas) {
. . .
// 繪制背景,只有dirtyOpaque為false時(shí)才進(jìn)行繪制氮帐,下同
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
. . .
// 繪制自身內(nèi)容
if (!dirtyOpaque) onDraw(canvas);
// 繪制子View
dispatchDraw(canvas);
. . .
// 繪制滾動(dòng)條等
onDrawForeground(canvas);
}
簡(jiǎn)單起見嗅虏,在上面的代碼中我們省略了實(shí)現(xiàn)滑動(dòng)時(shí)漸變邊框效果相關(guān)的邏輯。實(shí)際上上沐,View類的onDraw()方法為空皮服,因?yàn)槊總€(gè)View繪制自身的方式都不盡相同,對(duì)于decorView來說参咙,由于它是容器View龄广,所以它本身并沒有什么要繪制的。dispatchDraw()方法用于繪制子View蕴侧,顯然普通View(非ViewGroup)并不能包含子View择同,所以View類中這個(gè)方法的實(shí)現(xiàn)為空。
ViewGroup類的dispatchDraw()方法中會(huì)依次調(diào)用drawChild()方法來繪制子View净宵,drawChild()方法的源碼如下:
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
這個(gè)方法調(diào)用了View.draw(Canvas, ViewGroup敲才,long)方法來對(duì)子View進(jìn)行繪制。在draw(Canvas, ViewGroup, long)方法中择葡,首先對(duì)canvas進(jìn)行了一系列變換归斤,以變換到將要被繪制的View的坐標(biāo)系下。完成對(duì)canvas的變換后刁岸,便會(huì)調(diào)用View.draw(Canvas)方法進(jìn)行實(shí)際的繪制工作脏里,此時(shí)傳入的canvas為經(jīng)過變換的,在將被繪制View的坐標(biāo)系下的canvas虹曙。
進(jìn)入到View.draw(Canvas)方法后迫横,會(huì)向之前介紹的一樣,執(zhí)行以下幾步:
- 繪制背景;
- 通過onDraw()繪制自身內(nèi)容;
- 通過dispatchDraw()繪制子View;
- 繪制滾動(dòng)條
至此酝碳,整個(gè)View的繪制流程我們就分析完了矾踱。若文中有敘述不清晰或是不準(zhǔn)確的地方,希望大家能夠指出疏哗,謝謝大家:)
參考資料
《深入理解Android(卷三)》
《Android開發(fā)藝術(shù)探索》
公共技術(shù)點(diǎn)之View的繪制流程
長(zhǎng)按或掃描二維碼關(guān)注我們呛讲,讓您利用每天等地鐵的時(shí)間就能學(xué)會(huì)怎樣寫出優(yōu)質(zhì)app。