UI 優(yōu)化系列專題颤介,來聊一聊 Android 渲染相關(guān)知識(shí)梳星,主要涉及 UI 渲染背景知識(shí)、如何優(yōu)化 UI 渲染兩部?jī)?nèi)容滚朵。
UI 優(yōu)化系列專題
- UI 渲染背景知識(shí)
《View 繪制流程之 setContentView() 到底做了什么冤灾?》
《View 繪制流程之 DecorView 添加至窗口的過程》
《深入 Activity 三部曲(3)View 繪制流程》
《Android 之 LayoutInflater 全面解析》
《關(guān)于渲染,你需要了解什么辕近?》
《Android 之 Choreographer 詳細(xì)分析》
- 如何優(yōu)化 UI 渲染
《Android 之如何優(yōu)化 UI 渲染(上)》
《Android 之如何優(yōu)化 UI 渲染(下)》
setContentView() 相信大家肯定不會(huì)感到陌生韵吨,幾乎每個(gè) Activity 都會(huì)使用該方法為其添加一個(gè) xml 布局界面。但是你真的了解 setContentView 方法嗎移宅?為什么通過它就可以展示出我們添加的 xml 布局界面呢归粉?
關(guān)于 Activity 的 View 加載過程大家肯定聽說過 Window、PhoneWindow漏峰、DecorView 等內(nèi)容糠悼,它們之間是什么關(guān)系?我們先通過幾個(gè)問題來了解下浅乔。
相關(guān)問題
- setContentView 方法到底做了什么倔喂?為什么調(diào)用后可以顯示我們?cè)O(shè)置的布局?
- PhoneWindow 是什么?它和 Window 是什么關(guān)系席噩?
- DecorView 是干什么用的班缰?和我們添加的布局又有什么關(guān)系?
- requestFeatrue 方法為什么要在 setContentView 方法之前班挖?
- Layoutinflater 到底怎么把 XML 布局文件添加到 DecorView 上鲁捏?
- <include> 標(biāo)簽為什么不能作為布局的根節(jié)點(diǎn)芯砸?
- <merge> 標(biāo)簽為什么要作為布局資源的根節(jié)點(diǎn)萧芙?
- inflate( int resource, ViewGroup root, boolean attachToRoot) 參數(shù) root 和 attachToRoot 的作用和規(guī)則?
- AppComatActivity 實(shí)現(xiàn)原理是怎樣的假丧?它是如何完成布局兼容的双揪?
如果以上問題你都能夠熟練并正確的回答出來,那么恭喜你可以直接跳過該篇文章了包帚。
1. 從 setContentView 開始
打開 Activity 源碼找到 setContentView 方法如下:
public void setContentView(@LayoutRes int layoutResID) {
//調(diào)用getWindow
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
getWindow 方法返回一個(gè) Window 對(duì)象:
public Window getWindow(){
return mWindow;
}
但是 Window 本質(zhì)上是一個(gè)抽象類渔期,在 Window 的源碼中對(duì)其介紹是這樣的:
Window 是一個(gè)顯示頂層窗口的外觀,包括一些基礎(chǔ)行為的封裝(如 findViewById()渴邦、事件分發(fā) dispatch 等)疯趟,而且每一個(gè) Window 實(shí)例必須添加到 WindowManager 里面,它提供了標(biāo)準(zhǔn)的 UI 策略谋梭,比如背景信峻、標(biāo)題區(qū)域等。它的唯一實(shí)現(xiàn)類是 PhoneWindow瓮床。
此時(shí)我們需要去跟蹤下 Window 的創(chuàng)建過程盹舞,翻閱 Activity 源碼發(fā)現(xiàn)在它的 attach 方法:
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) {
//content實(shí)際類型是ContextImpl
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
//可以看到Window的類型是PhoneWindow
//創(chuàng)建當(dāng)前 Activity 的 Window 實(shí)例。
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
//... 省略
}
當(dāng)啟動(dòng)一個(gè) Activity 時(shí)首先創(chuàng)建該 Activity 實(shí)例隘庄,隨后就會(huì)調(diào)用它的 attach 方法(這部分內(nèi)容主要在 ActivityThread performLaunchActivity())踢步。在 attach 方法中我們可以看到 Window 的實(shí)際類型是 PhoneWindow。
也就是當(dāng)我們通過 Activity 的 setContentView 實(shí)際是調(diào)用了 PhoneWindow 的 setContentView 方法:
public void setContentView(int layoutResID) {
//mContentParent是ViewGroup
//我們的setContentView設(shè)置的布局實(shí)際就是被添加到該容器中
//它是我們添加布局文件的直接根視圖
if (mContentParent == null) {
//mContentParent默認(rèn)為null
//安裝當(dāng)前DecorView
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
//透明的
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
//解析布局資源丑掺,添加到mContentParent
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
//回調(diào)Activity的onContentChanged方法
cb.onContentChanged();
}
//表示已經(jīng)設(shè)置過布局
//如果此時(shí)使用requestFeature則會(huì)拋出異常
mContentParentExplicitlySet = true;
}
mContentParent 是一個(gè) ViewGroup获印,它的實(shí)際類型是 FrameLayout,實(shí)際我們通過 setContentView 設(shè)置的 View 就被添加到該容器街州。也就是它是我們布局文件的直接父視圖(下面會(huì)分析到)兼丰。
方法最后 mContentParentExplicitlySet,在 setContentView 方法執(zhí)行完畢后置為 true菇肃,表示當(dāng)前窗口已經(jīng)設(shè)置完成地粪。后面我們會(huì)分析道,如果此后在調(diào)用 requestFeature 方法設(shè)置 Window 窗口的 Feature 將會(huì)拋出異常琐谤。
mContentParent 默認(rèn)為 null蟆技,此時(shí)執(zhí)行 installDecor 方法,為當(dāng)前 Window 創(chuàng)建 DecorView 視圖,DecorView 是我們整個(gè) Activity 的最頂級(jí)視圖质礼,它的實(shí)際類型是 FrameLayout:
private void installDecor() {
mForceDecorInstall = false;
//mDecor是DecorView旺聚,繼承自FrameLayout
if (mDecor == null) {
//為當(dāng)前Window創(chuàng)建DecorView
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
//mContentParent默認(rèn)為null
if (mContentParent == null) {
//找到當(dāng)前主題布局中,內(nèi)容的父容器
mContentParent = generateLayout(mDecor);
// Set up decor part of UI to ignore fitsSystemWindows if appropriate.
mDecor.makeOptionalFitsSystemWindows();
//... 省略
}
每個(gè) Window 有且僅有一個(gè) DecorView眶蕉,DecorView 用來描述窗口的視圖砰粹,看下它的創(chuàng)建過程 generateDecor 方法如下:
protected DecorView generateDecor(int featureId) {
Context context;
//mUseDecorContext構(gòu)造方法中默認(rèn)置為true
//表示使用DecorContext山下文
if (mUseDecorContext) {
Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {
context = getContext();
} else {
//使用DecorContext
context = new DecorContext(applicationContext, getContext().getResources());
if (mTheme != -1) {
context.setTheme(mTheme);
}
}
} else {
//使用應(yīng)用程序上下文
context = getContext();
}
//直接創(chuàng)建DecorView
return new DecorView(context, featureId, this, getAttributes());
}
方法的最后可以看到直接創(chuàng)建 DecorView 并返回(DecorView 繼承自 FrameLayout)≡焱欤回到 installDecor 方法碱璃,看下 setContentView 方法的直接父容器 mContentParent 的創(chuàng)建過程 generateLayout 方法(注意這時(shí)候 mContentParent 與 DecorView 還沒有任何關(guān)聯(lián)):
//我們給Window設(shè)置的相關(guān)屬性就是在generateLayout時(shí)加進(jìn)來的
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
//獲取當(dāng)前Window的Style
//這個(gè)是不是很熟悉,
TypedArray a = getWindowStyle();
//Window是否是Floating
//浮窗類型時(shí) Dialog 就是Floating 類型
mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR)
& (~getForcedWindowFlags());
if (mIsFloating) {
setLayout(WRAP_CONTENT, WRAP_CONTENT);
setFlags(0, flagsToUpdate);
} else {
setFlags(FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
}
//是否需要標(biāo)題欄
if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
//對(duì)Feature狀態(tài)為進(jìn)行設(shè)置
requestFeature(FEATURE_NO_TITLE);
} else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
// Don't allow an action bar if there is no title.
requestFeature(FEATURE_ACTION_BAR);
}
if (a.getBoolean(R.styleable.Window_windowActionBarOverlay, false)) {
requestFeature(FEATURE_ACTION_BAR_OVERLAY);
}
if (a.getBoolean(R.styleable.Window_windowActionModeOverlay, false)) {
requestFeature(FEATURE_ACTION_MODE_OVERLAY);
}
if (a.getBoolean(R.styleable.Window_windowSwipeToDismiss, false)) {
requestFeature(FEATURE_SWIPE_TO_DISMISS);
}
if (a.getBoolean(R.styleable.Window_windowFullscreen, false)) {
setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN & (~getForcedWindowFlags()));
}
if (a.getBoolean(R.styleable.Window_windowTranslucentStatus,
false)) {
setFlags(FLAG_TRANSLUCENT_STATUS, FLAG_TRANSLUCENT_STATUS
& (~getForcedWindowFlags()));
}
if (a.getBoolean(R.styleable.Window_windowTranslucentNavigation,
false)) {
setFlags(FLAG_TRANSLUCENT_NAVIGATION, FLAG_TRANSLUCENT_NAVIGATION
& (~getForcedWindowFlags()));
}
if (a.getBoolean(R.styleable.Window_windowOverscan, false)) {
setFlags(FLAG_LAYOUT_IN_OVERSCAN, FLAG_LAYOUT_IN_OVERSCAN & (~getForcedWindowFlags()));
}
if (a.getBoolean(R.styleable.Window_windowShowWallpaper, false)) {
setFlags(FLAG_SHOW_WALLPAPER, FLAG_SHOW_WALLPAPER & (~getForcedWindowFlags()));
}
if (a.getBoolean(R.styleable.Window_windowEnableSplitTouch,
getContext().getApplicationInfo().targetSdkVersion
>= android.os.Build.VERSION_CODES.HONEYCOMB)) {
setFlags(FLAG_SPLIT_TOUCH, FLAG_SPLIT_TOUCH & (~getForcedWindowFlags()));
}
a.getValue(R.styleable.Window_windowMinWidthMajor, mMinWidthMajor);
a.getValue(R.styleable.Window_windowMinWidthMinor, mMinWidthMinor);
if (a.hasValue(R.styleable.Window_windowFixedWidthMajor)) {
if (mFixedWidthMajor == null) mFixedWidthMajor = new TypedValue();
a.getValue(R.styleable.Window_windowFixedWidthMajor,
mFixedWidthMajor);
}
if (a.hasValue(R.styleable.Window_windowFixedWidthMinor)) {
if (mFixedWidthMinor == null) mFixedWidthMinor = new TypedValue();
a.getValue(R.styleable.Window_windowFixedWidthMinor,
mFixedWidthMinor);
}
if (a.hasValue(R.styleable.Window_windowFixedHeightMajor)) {
if (mFixedHeightMajor == null) mFixedHeightMajor = new TypedValue();
a.getValue(R.styleable.Window_windowFixedHeightMajor,
mFixedHeightMajor);
}
if (a.hasValue(R.styleable.Window_windowFixedHeightMinor)) {
if (mFixedHeightMinor == null) mFixedHeightMinor = new TypedValue();
a.getValue(R.styleable.Window_windowFixedHeightMinor,
mFixedHeightMinor);
}
if (a.getBoolean(R.styleable.Window_windowContentTransitions, false)) {
requestFeature(FEATURE_CONTENT_TRANSITIONS);
}
if (a.getBoolean(R.styleable.Window_windowActivityTransitions, false)) {
requestFeature(FEATURE_ACTIVITY_TRANSITIONS);
}
//這個(gè)地方是不是很熟悉饭入,Window是否是透明的
mIsTranslucent = a.getBoolean(R.styleable.Window_windowIsTranslucent, false);
/**以上都是對(duì)Window一些狀態(tài)進(jìn)行設(shè)置*/
//requestFeature為什么要在setContentView之前嵌器?
final Context context = getContext();
final int targetSdk = context.getApplicationInfo().targetSdkVersion;
final boolean targetPreHoneycomb = targetSdk < android.os.Build.VERSION_CODES.HONEYCOMB;
final boolean targetPreIcs = targetSdk < android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH;
final boolean targetPreL = targetSdk < android.os.Build.VERSION_CODES.LOLLIPOP;
final boolean targetHcNeedsOptions = context.getResources().getBoolean(
R.bool.target_honeycomb_needs_options_menu);
final boolean noActionBar = !hasFeature(FEATURE_ACTION_BAR) || hasFeature(FEATURE_NO_TITLE);
if (targetPreHoneycomb || (targetPreIcs && targetHcNeedsOptions && noActionBar)) {
setNeedsMenuKey(WindowManager.LayoutParams.NEEDS_MENU_SET_TRUE);
} else {
setNeedsMenuKey(WindowManager.LayoutParams.NEEDS_MENU_SET_FALSE);
}
if (!mForcedStatusBarColor) {
mStatusBarColor = a.getColor(R.styleable.Window_statusBarColor, 0xFF000000);
}
if (!mForcedNavigationBarColor) {
mNavigationBarColor = a.getColor(R.styleable.Window_navigationBarColor, 0xFF000000);
}
WindowManager.LayoutParams params = getAttributes();
// Non-floating windows on high end devices must put up decor beneath the system bars and
// therefore must know about visibility changes of those.
if (!mIsFloating && ActivityManager.isHighEndGfx()) {
if (!targetPreL && a.getBoolean(
R.styleable.Window_windowDrawsSystemBarBackgrounds,
false)) {
setFlags(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,
FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS & ~getForcedWindowFlags());
}
if (mDecor.mForceWindowDrawsStatusBarBackground) {
params.privateFlags |= PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND;
}
}
if (a.getBoolean(R.styleable.Window_windowLightStatusBar, false)) {
decor.setSystemUiVisibility(
decor.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
}
if (mAlwaysReadCloseOnTouchAttr || getContext().getApplicationInfo().targetSdkVersion
>= android.os.Build.VERSION_CODES.HONEYCOMB) {
if (a.getBoolean(
R.styleable.Window_windowCloseOnTouchOutside,
false)) {
setCloseOnTouchOutsideIfNotSet(true);
}
}
if (!hasSoftInputMode()) {
params.softInputMode = a.getInt(
R.styleable.Window_windowSoftInputMode,
params.softInputMode);
}
if (a.getBoolean(R.styleable.Window_backgroundDimEnabled,
mIsFloating)) {
/* All dialogs should have the window dimmed */
if ((getForcedWindowFlags() & WindowManager.LayoutParams.FLAG_DIM_BEHIND) == 0) {
params.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND;
}
if (!haveDimAmount()) {
params.dimAmount = a.getFloat(
android.R.styleable.Window_backgroundDimAmount, 0.5f);
}
}
if (params.windowAnimations == 0) {
params.windowAnimations = a.getResourceId(
R.styleable.Window_windowAnimationStyle, 0);
}
// The rest are only done if this window is not embedded; otherwise,
// the values are inherited from our container.
if (getContainer() == null) {
if (mBackgroundDrawable == null) {
if (mBackgroundResource == 0) {
mBackgroundResource = a.getResourceId(
R.styleable.Window_windowBackground, 0);
}
if (mFrameResource == 0) {
mFrameResource = a.getResourceId(R.styleable.Window_windowFrame, 0);
}
mBackgroundFallbackResource = a.getResourceId(
R.styleable.Window_windowBackgroundFallback, 0);
}
if (mLoadElevation) {
mElevation = a.getDimension(R.styleable.Window_windowElevation, 0);
}
mClipToOutline = a.getBoolean(R.styleable.Window_windowClipToOutline, false);
mTextColor = a.getColor(R.styleable.Window_textColor, Color.TRANSPARENT);
}
// Inflate the window decor.
/**生成對(duì)應(yīng)的Window Decor 要根據(jù)當(dāng)前設(shè)置的Features屬性
* 加載不同的 DecorView 的xml布局*/
int layoutResource;
//獲取當(dāng)前Window的Features
int features = getLocalFeatures();
// System.out.println("Features: 0x" + Integer.toHexString(features));
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
setCloseOnSwipeEnabled(true);
} else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleIconsDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_title_icons;
}
// XXX Remove this once action bar supports these features.
removeFeature(FEATURE_ACTION_BAR);
// System.out.println("Title Icons!");
} else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
&& (features & (1 << FEATURE_ACTION_BAR)) == 0) {
// Special case for a window with only a progress bar (and title).
// XXX Need to have a no-title version of embedded windows.
layoutResource = R.layout.screen_progress;
// System.out.println("Progress!");
} else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
// Special case for a window with a custom title.
// If the window is floating, we need a dialog layout
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogCustomTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_custom_title;
}
// XXX Remove this once action bar supports these features.
removeFeature(FEATURE_ACTION_BAR);
} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
// If no other features and not embedded, only need a title.
// If the window is floating, we need a dialog layout
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
layoutResource = a.getResourceId(
R.styleable.Window_windowActionBarFullscreenDecorLayout,
R.layout.screen_action_bar);
} else {
layoutResource = R.layout.screen_title;
}
// System.out.println("Title!");
} else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
layoutResource = R.layout.screen_simple_overlay_action_mode;
} else {
// 這是最簡(jiǎn)單的一個(gè),看下 DecorView 要加載布局文件是怎樣的谐丢?
layoutResource = R.layout.screen_simple;
// System.out.println("Simple!");
}
mDecor.startChanging();
//將DecorView的xml文件添加到DecorView
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
//在DecorView對(duì)應(yīng)的布局中爽航,查找id為content的FrameLayout,該容器便是我們布局直接父容器
ViewGroup contentParent = (ViewGroup) findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) {
ProgressBar progress = getCircularProgressBar(false);
if (progress != null) {
progress.setIndeterminate(true);
}
}
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
registerSwipeCallbacks(contentParent);
}
//... 省略
mDecor.finishChanging();
//返回DecorView對(duì)應(yīng)布局中的id為content的FrameLayout
//它實(shí)際上就是我們setContentView的直接根視圖
return contentParent;
}
generateLayout 方法雖然較長(zhǎng)乾忱,但是工作內(nèi)容并不復(fù)雜讥珍,我們首先看 getWindowStyle 方法:
public final TypedArray getWindowStyle() {
synchronized (this) {
if (mWindowStyle == null) {
//styleable是不是很熟悉,在一些自定義控件時(shí)經(jīng)常用到
mWindowStyle = mContext.obtainStyledAttributes(
com.android.internal.R.styleable.Window);
}
return mWindowStyle;
}
}
styleable 是不是很熟悉窄瘟,在一些自定義控件時(shí)經(jīng)常會(huì)用到衷佃。實(shí)際上我們給 Window 設(shè)置的相關(guān)屬性就是在 generateLayout 方法進(jìn)行設(shè)置的,例如非常熟悉和經(jīng)常使用到的 :
//Window是否是Floating狀態(tài)
//Dialog時(shí)Window就是Floating
mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
//Window是否包含標(biāo)題欄
if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
//Window是否是透明的
mIsTranslucent = a.getBoolean(R.styleable.Window_windowIsTranslucent, false);
然后它們都會(huì)調(diào)用 requestFeature 方法對(duì)當(dāng)前 Window 的 Feature 狀態(tài)位進(jìn)行設(shè)置寞肖。
public boolean requestFeature(int featureId) {
if (mContentParentExplicitlySet) {
//在setContentView方法最后會(huì)將該標(biāo)志位置為true纲酗,如果
//在setCotnentView方法后再執(zhí)行requestFeature將會(huì)拋出異常。
throw new AndroidRuntimeException("requestFeature() must be called before adding content");
}
final int features = getFeatures();
final int newFeatures = features | (1 << featureId);
if ((newFeatures & (1 << FEATURE_CUSTOM_TITLE)) != 0 &&
(newFeatures & ~CUSTOM_TITLE_COMPATIBLE_FEATURES) != 0) {
//不能既有自定義標(biāo)題欄新蟆,又有其他標(biāo)題欄
throw new AndroidRuntimeException(
"You cannot combine custom titles with other title features");
}
if ((features & (1 << FEATURE_NO_TITLE)) != 0 && featureId == FEATURE_ACTION_BAR) {
return false; // Ignore. No title dominates.
}
if ((features & (1 << FEATURE_ACTION_BAR)) != 0 && featureId == FEATURE_NO_TITLE) {
//沒有標(biāo)題欄
removeFeature(FEATURE_ACTION_BAR);
}
//... 省略
}
注意方法中 if (mContentParentExplicitlySet) 如果滿足則直接拋出異常觅赊。該標(biāo)志位在上面也有分析到, setContentView 方法最后會(huì)將其置為 true琼稻。即 requestFeature 方法必須在 setContentView 方法之前吮螺。那為什么要在 setContentView 方法之前呢?下面分析到帕翻。
要根據(jù)當(dāng)前 Feature 加載不同的 DecorView 的 XML 布局文件鸠补。注意查看源碼中 generateLayout 方法的下半部分,我們以最簡(jiǎn)單的 R.layout.screen_simple 布局為例:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
可以看到布局本身就是一個(gè) LinearLayout嘀掸,包含上下兩部:標(biāo)題欄 ViewStub 區(qū)域紫岩,內(nèi)容區(qū)域 id 為 content 的 FrameLayout(這就是 Window 中的 mContentParent,即 setContentView 的直接父容器)睬塌。
然后將DecorView 對(duì)應(yīng)的 xml 布局文件添加到 DecorView 中:
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
DecorView 的 onResourcesLoaded 方法如下:
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
mStackId = getStackId();
//判斷當(dāng)前DecorView是否包含DecorCaptionView
mDecorCaptionView = createDecorCaptionView(inflater);
//通過LayoutInflater完成xml布局加載
final View root = inflater.inflate(layoutResource, null);
if (mDecorCaptionView != null) {
if (mDecorCaptionView.getParent() == null) {
//DecorCaptionView也是DecorView子視圖
addView(mDecorCaptionView,
new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
//此時(shí)直接添加到DecorCaptionView中
mDecorCaptionView.addView(root,
new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
} else {
// 添加到DecorView中
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mContentRoot = (ViewGroup) root;
initializeElevation();
}
也就是說泉蝌,Window 會(huì)根據(jù)當(dāng)前設(shè)置的 Feature 為 DecorView 添加一個(gè)對(duì)應(yīng)的 xml 布局文件歇万,該布局文件主要?jiǎng)澐稚舷聝刹糠郑渲邪粋€(gè) id 為 content 的 FrameLayout勋陪,它會(huì)賦值給 PhoneWindow 中的 mContentParent贪磺,表示 setContentView 的的父容器。
在 DecorView 中找到對(duì)應(yīng)的 mContentParent(就是 id 為 content 的 FrameLayout):
ViewGroup contentParent = (ViewGroup) findViewById(ID_ANDROID_CONTENT);
在 generateLayout 方法最后返回該 contentParent诅愚,此時(shí)賦值給 PhoneWidow 的成員 mContentParent寒锚。
重新回到 PhoneWindow 的 setContentView 方法。將我們這是的布局文件添加 contentParent 過程如下:
// 解析布局資源违孝,添加到mContentParent
mLayoutInflater.inflate(layoutResID, mContentParent);
至此刹前,我們可以回答文章開頭的第一個(gè)問題了,先通過一張圖了解下 Activity 加載 UI - 類圖關(guān)系和視圖結(jié)構(gòu)等浊。
每個(gè) Activity 都有一個(gè)關(guān)聯(lián)的 Window 對(duì)象(該對(duì)象實(shí)際類型為 PhoneWindow腮郊,PhoneWindow 為 Window 的唯一實(shí)現(xiàn)類)用來描述應(yīng)用程序窗口摹蘑。
每個(gè)窗口內(nèi)部又包含了一個(gè) DecorView 對(duì)象筹燕,DecorView 繼承自 FrameLayout;DecorView 用來描述窗口的視圖 — xml 布局(我們通過 setContentView 方法設(shè)置的 xml 布局最終被添加到該 DecorView 對(duì)應(yīng) xml 布局中 id 為 content 的 FrameLayout 中衅鹿,下面分析到)撒踪。
另外 requestFeature 必須要在 setContentView 方法之前,因?yàn)橐鶕?jù)該 Feature 為 DecorView 添加一個(gè)對(duì)應(yīng)的 xml 布局文件大渤;該布局包含上下兩部分制妄,標(biāo)題欄和內(nèi)容區(qū)域(id 為 content 的 FrameLayout)。
2. LayoutInfalter 解析過程
需要回到 setContentView 方法泵三,看下我們?cè)O(shè)置的布局是如何添加到 mContentParent 中:
//layoutResID通過setContentView設(shè)置的布局資源id
//mContentParent就是id為content的FrameLayout
mLayoutInflater.inflate(layoutResID, mContentParent);
關(guān)于 LayoutInflater 大家肯定不會(huì)感到陌生耕捞,它可以將我們傳入的 xml 布局文件解析成對(duì)應(yīng)的 View 對(duì)象。
/**
* resource 表示當(dāng)前布局資源id
* root 表示布局的父容器烫幕,可以為null
* attachToRoot 是否將布局資源添加到父容器root上
* */
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
final XmlResourceParser parser = res.getLayout(resource);
try {
//重點(diǎn)看下inflate方法
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
首先通過 Resources 獲取一個(gè) XML 資源解析器俺抽,我們重點(diǎn)關(guān)注 inflate 方法:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
//獲取在XML設(shè)置的屬性
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
//注意root容器在這里,在我們當(dāng)前分析中該root就是mContentParent
View result = root;
try {
// 查找xml布局的根節(jié)點(diǎn)
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
//找到起始根節(jié)點(diǎn)
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
//獲取到節(jié)點(diǎn)名稱
final String name = parser.getName();
//判斷是否是merge標(biāo)簽
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
//此時(shí)如果ViewGroup==null,與attachToRoot==false將會(huì)拋出異常
//merge必須添加到ViewGroup中较曼,這也是merge為什么要作為布局的根節(jié)點(diǎn)磷斧,它要添加到上層容器中
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// 否則創(chuàng)建該節(jié)點(diǎn)View對(duì)象
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
//如果contentParent不為null,在分析setContentView中捷犹,這里不為null
if (root != null) {
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
//解析Child
rInflateChildren(parser, temp, attrs, true);
if (root != null && attachToRoot) {
//添加到ViewGroup
root.addView(temp, params);
}
if (root == null || !attachToRoot) {
//此時(shí)布局根節(jié)點(diǎn)為temp
result = temp;
}
}
} catch (XmlPullParserException e) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw i.e
} catch (Exception e) {
final InflateException ie = new InflateException(parser.getPositionDescription()
+ ": " + e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw i.e
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
return result;
}
}
while 循環(huán)部分弛饭,找到 xml 布局文件的根節(jié)點(diǎn),如果 if (type != XmlPullParser.START_TAG) 未找到根節(jié)點(diǎn)直接拋異常了萍歉。否則獲取到該節(jié)點(diǎn)名稱侣颂,判斷如果是 merge 標(biāo)簽,此時(shí)需要注意參數(shù) root 和 attachToRoot枪孩,root 必須不為null憔晒,并且 attachToRoot 必須為 true胳蛮,即 merge 內(nèi)容必須要添加到 root 容器中。
如果不是 merge 標(biāo)簽丛晌,此時(shí)根據(jù)標(biāo)簽名 name 直接創(chuàng)建該 View 對(duì)象仅炊,rInflate 和 rInflateChildren 都是去解析子 View,rInflateChildren 方法實(shí)際也是調(diào)用到了 rInflate 方法:
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
//還是調(diào)用rInflate方法
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
區(qū)別在于最后一個(gè)參數(shù) finishInflate澎蛛,它的作用是標(biāo)志當(dāng)前 ViewGroup 樹創(chuàng)建完成后回調(diào)其 onFinishInflate 方法抚垄。
如果根標(biāo)簽是 merge 此時(shí) finishInflate 為 false,這也很容易理解谋逻,此時(shí)的父容器為 inflate 中傳入的 ViewGroup呆馁,它是不需要再次回調(diào) onFinishInflate() 的。該過程如下:
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
boolean pendingRequestFocus = false;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
//獲取到節(jié)點(diǎn)名稱
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
pendingRequestFocus = true;
consumeChildElements(parser);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
//include標(biāo)簽
if (parser.getDepth() == 0) {
//include如果為根節(jié)點(diǎn)則拋出異常了
//include不能作為布局文件的根節(jié)點(diǎn)
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
//如果此時(shí)包含merge標(biāo)簽毁兆,此時(shí)也會(huì)拋出異常
//merge只能作為布局文件的根節(jié)點(diǎn)
throw new InflateException("<merge /> must be the root element");
} else {
//創(chuàng)建該節(jié)點(diǎn)的View對(duì)象
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true);
//添加到父容器
viewGroup.addView(view, params);
}
}
if (pendingRequestFocus) {
parent.restoreDefaultFocus();
}
if (finishInflate) {
//回調(diào)ViewGroup的onFinishInflate方法
parent.onFinishInflate();
}
}
while 循環(huán)部分浙滤,parser.next() 獲取下一個(gè)節(jié)點(diǎn),如果獲取到節(jié)點(diǎn)名為 include气堕,此時(shí) parse.getDepth() == 0 表示根節(jié)點(diǎn)纺腊,直接拋出異常,即 <include /> 不能作為布局的根節(jié)點(diǎn)茎芭。
如果此時(shí)獲取到節(jié)點(diǎn)名稱為 merge揖膜,也是直接拋出異常了,即 <merge /> 只能作為布局的根節(jié)點(diǎn)梅桩。
否則創(chuàng)建該節(jié)點(diǎn)對(duì)應(yīng) View 對(duì)象壹粟,rInflateChildren 遞歸完成以上步驟。并將解析到的 View 添加到其直接父容器 viewGroup.addView()宿百。
注意方法的最后通知調(diào)用每個(gè) ViewGroup 的 onFinishInflate()趁仙,大家是否有注意到這其實(shí)是入棧的操作,即最頂層的 ViewGroup 最后回調(diào) onFinishInflate()垦页。
至此雀费,我們可以回答文章開頭提出的第二個(gè)問題了,再來通過一張流程圖熟悉下整個(gè)解析過程:
在 inflater 解析布局資源過程中外臂,首先找到布局的根節(jié)點(diǎn) START_TAG坐儿,如果未找到直接拋出異常。否則獲取到當(dāng)前節(jié)點(diǎn)的名稱宋光。
如果節(jié)點(diǎn)名稱為 merge 貌矿,會(huì)判斷 inflate 方法參數(shù) if ( root(ViewGroup)!= null && attachToot == true ),表示布局文件要直接添加到 root 中罪佳,否則拋出異常(<merge /> can be used only with a valid ViewGroup root and attachToRoot=true)逛漫;
繼續(xù)解析子節(jié)點(diǎn)的過程中如果再次解析到 merge 標(biāo)簽,則直接拋出異常赘艳,<merge /> 標(biāo)簽必須作為布局的根節(jié)點(diǎn)(<merge /> must be the root element)酌毡。
如果解析到節(jié)點(diǎn)名稱為 include克握,會(huì)判斷當(dāng)前節(jié)點(diǎn)深度是否為 0,0 表示當(dāng)前處于根節(jié)點(diǎn)枷踏,此時(shí)直接拋出異常菩暗,即 <include /> 不能作為布局文件的根節(jié)點(diǎn)(<include /> cannot be the root element)。
3. 偷梁換柱之為兼容而生的 AppCompatActivity
在 Android Level 21 之后旭蠕,Android 引入了 Material Design 的設(shè)計(jì)停团,為了支持 Material Color、調(diào)色版掏熬、Toolbar 等各種新特性佑稠,AppCompatActivity 就應(yīng)用而生。Google 考慮到仍然有很大部分低于 5.0 版本的設(shè)備旗芬,所有將 AppCompatActivity 放在了 support v7 包內(nèi)舌胶。
接下來我們就看下 AppCompatActivity 是如何實(shí)現(xiàn) UI 兼容設(shè)計(jì)的。
//AppCompatActivity的setContentView方法
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
與 Activity 有所不同疮丛,AppCompatActivity 的 setContentView 方法中首先調(diào)用 getDelegate 方法得到一個(gè)代理對(duì)象幔嫂。
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
//創(chuàng)建AppCompatDelegate對(duì)象
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
創(chuàng)建當(dāng)前 AppCompatDelegate 過程如下:
//AppCompatDelegate的create方法
private static AppCompatDelegate create(Context context, Window window,
AppCompatCallback callback) {
final int sdk = Build.VERSION.SDK_INT;
if (BuildCompat.isAtLeastN()) {
return new AppCompatDelegateImplN(context, window, callback);
} else if (sdk >= 23) {
return new AppCompatDelegateImplV23(context, window, callback);
} else if (sdk >= 14) {
return new AppCompatDelegateImplV14(context, window, callback);
} else if (sdk >= 11) {
return new AppCompatDelegateImplV11(context, window, callback);
} else {
return new AppCompatDelegateImplV9(context, window, callback);
}
}
根據(jù)當(dāng)前系統(tǒng)版本創(chuàng)建對(duì)應(yīng)的 AppCompatDelegate 對(duì)象,AppCompatActivity 其實(shí)就是通過引入 AppCompatDelegate 來解決兼容問題这刷。
這里需要說明的是婉烟,各 Delegate 實(shí)際根據(jù)版本由高到低繼承關(guān)系,即 AppCompatDelegateImplN extends AppCompatDelegateImplV23 extends AppCompatDelegateImplV14 extends AppCompatDelegateImplV11 extends AppCompatDelegateImplV9暇屋。
setContentView 實(shí)際調(diào)用到 AppCompatDelegate 的第一個(gè)實(shí)現(xiàn)類 AppCompatDelegateImplV9 中:
public void setContentView(int resId) {
//創(chuàng)建一個(gè)SubDecor
ensureSubDecor();
//獲取SubDecor中content區(qū)域,此時(shí)setContentView的直接父容器為SubDecor中id為content的FrameLayout
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
//將布局添加到SubDecor的Content區(qū)域
LayoutInflater.from(mContext).inflate(resId, contentParent);
//回調(diào)到Activity的onContentChanged洞辣,
mOriginalWindowCallback.onContentChanged();
}
ensureSubDecor 方法是要?jiǎng)?chuàng)建一個(gè) SubDecor咐刨,SubDecor 實(shí)際與 PhoneWindow 中的 DecorView 類似,它的出現(xiàn)就是為了兼容布局扬霜。
private void ensureSubDecor() {
if (!mSubDecorInstalled) {
//創(chuàng)建SubDecor定鸟,
mSubDecor = createSubDecor();
//是否設(shè)置了標(biāo)題
CharSequence title = getTitle();
if (!TextUtils.isEmpty(title)) {
onTitleChanged(title);
}
applyFixedSizeWindow();
onSubDecorInstalled(mSubDecor);
//表示當(dāng)前Window已經(jīng)安裝SubDecor
mSubDecorInstalled = true;
PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);
if (!isDestroyed() && (st == null || st.menu == null)) {
invalidatePanelMenu(FEATURE_SUPPORT_ACTION_BAR);
}
}
}
這里主要看下 SubDecor 的創(chuàng)建過程 createSubDecor 方法,該方法過程與 PhoneWindow 創(chuàng)建 DecorView 類似著瓶,區(qū)別是 Delegate 中找的都是 AppCompat 的屬性联予,也就是做的兼容相關(guān)的事情。
private ViewGroup createSubDecor() {
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
if (!a.hasValue(R.styleable.AppCompatTheme_windowActionBar)) {
a.recycle();
throw new IllegalStateException(
"You need to use a Theme.AppCompat theme (or descendant) with this activity.");
}
if (a.getBoolean(R.styleable.AppCompatTheme_windowNoTitle, false)) {
requestWindowFeature(Window.FEATURE_NO_TITLE);
} else if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBar, false)) {
//調(diào)用到Window中requestFeature
requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR);
}
if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBarOverlay, false)) {
requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY);
}
if (a.getBoolean(R.styleable.AppCompatTheme_windowActionModeOverlay, false)) {
requestWindowFeature(FEATURE_ACTION_MODE_OVERLAY);
}
mIsFloating = a.getBoolean(R.styleable.AppCompatTheme_android_windowIsFloating, false);
a.recycle();
/**
* 以上根據(jù)style設(shè)置Window的Feature
* createSubDecor 方法的上半部分
*/
//確保Window中已經(jīng)安裝DecorView
mWindow.getDecorView();
final LayoutInflater inflater = LayoutInflater.from(mContext);
ViewGroup subDecor = null;
//根據(jù)style加載SubDecor的xml布局
if (!mWindowNoTitle) {
//不需要標(biāo)題欄類型窗口
if (mIsFloating) {
//Floating 類型窗口
subDecor = (ViewGroup) inflater.inflate(
R.layout.abc_dialog_title_material, null);
// Floating windows can never have an action bar, reset the flags
mHasActionBar = mOverlayActionBar = false;
} else if (mHasActionBar) {
//含有 ActionBar
TypedValue outValue = new TypedValue();
mContext.getTheme().resolveAttribute(R.attr.actionBarTheme, outValue, true);
Context themedContext;
if (outValue.resourceId != 0) {
themedContext = new ContextThemeWrapper(mContext, outValue.resourceId);
} else {
themedContext = mContext;
}
//解析SubDecor對(duì)應(yīng)的xml布局文件
subDecor = (ViewGroup) LayoutInflater.from(themedContext)
.inflate(R.layout.abc_screen_toolbar, null);
//同樣包含一個(gè)id為content
mDecorContentParent = (DecorContentParent) subDecor
.findViewById(R.id.decor_content_parent);
mDecorContentParent.setWindowCallback(getWindowCallback());
/**
* Propagate features to DecorContentParent
*/
if (mOverlayActionBar) {
mDecorContentParent.initFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY);
}
if (mFeatureProgress) {
mDecorContentParent.initFeature(Window.FEATURE_PROGRESS);
}
if (mFeatureIndeterminateProgress) {
mDecorContentParent.initFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
}
}
} else {
if (mOverlayActionMode) {
//解析DecorView對(duì)應(yīng)xml布局文件
subDecor = (ViewGroup) inflater.inflate(
R.layout.abc_screen_simple_overlay_action_mode, null);
} else {
//解析DecorView對(duì)應(yīng)xml布局文件
subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
}
//... 省略
}
if (subDecor == null) {
throw new IllegalArgumentException(
"AppCompat does not support the current theme features: { "
+ "windowActionBar: " + mHasActionBar
+ ", windowActionBarOverlay: "+ mOverlayActionBar
+ ", android:windowIsFloating: " + mIsFloating
+ ", windowActionModeOverlay: " + mOverlayActionMode
+ ", windowNoTitle: " + mWindowNoTitle
+ " }");
}
if (mDecorContentParent == null) {
mTitleView = (TextView) subDecor.findViewById(R.id.title);
}
/**
* 以下為 createSubDecor 方法的下半部分
* 偷梁換柱過程材原,將 SubDecor 對(duì)應(yīng)布局中 content 替換原 DecorView 中 id 為 content 的 FrameLayout沸久。
*/
// Make the decor optionally fit system windows, like the window's decor
ViewUtils.makeOptionalFitsSystemWindows(subDecor);
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
R.id.action_bar_activity_content);
final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
if (windowContentView != null) {
// There might be Views already added to the Window's content view so we need to
// migrate them to our content view
while (windowContentView.getChildCount() > 0) {
final View child = windowContentView.getChildAt(0);
windowContentView.removeViewAt(0);
contentView.addView(child);
}
//這里很關(guān)鍵,將原來Window中id為content的FrameLayout(mContentParent)設(shè)置為NO_ID
windowContentView.setId(View.NO_ID);
//最新創(chuàng)建的SubDecor中內(nèi)容區(qū)域FramLayout的id設(shè)置為content
//偷梁換柱
contentView.setId(android.R.id.content);
// The decorContent may have a foreground drawable set (windowContentOverlay).
// Remove this as we handle it ourselves
if (windowContentView instanceof FrameLayout) {
((FrameLayout) windowContentView).setForeground(null);
}
}
// 將新創(chuàng)建的SubDecor添加到DecorView的內(nèi)容區(qū)域(mContentParent容器)
mWindow.setContentView(subDecor);
//... 省略
return subDecor;
}
可以看到方法的上半部分余蟹,獲取一系列 AppCompat 兼容屬性卷胯,設(shè)置 Window 的 Feature 屬性;然后方法的中間部分威酒,注意 mWindow.getDecorView() 作用是創(chuàng)建當(dāng)前 Window 的 DecorView 整個(gè)過程(文章上面已經(jīng)做了分析)窑睁;然后根據(jù) Feature 加載 SubDecor 對(duì)應(yīng)的 xml 布局文件挺峡,這里我們以最簡(jiǎn)單的 abc_screen_simple.xml 布局文件為例:
<android.support.v7.widget.FitWindowsLinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/action_bar_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:fitsSystemWindows="true">
<android.support.v7.widget.ViewStubCompat
android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/abc_action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
//這里包含一個(gè)FrameLayout
<include layout="@layout/abc_screen_content_include" />
</android.support.v7.widget.FitWindowsLinearLayout>
abc_screen_simple.xml 布局文件中 include 一個(gè) abc_screen_content_include.xml 布局文件,如下:
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<android.support.v7.widget.ContentFrameLayout
android:id="@id/action_bar_activity_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</merge>
ContentFrameLayout 繼承自 FrameLayout担钮,注意該 FrameLayout 最后會(huì)替代原 PhoneWindow 中 DecorView 對(duì)應(yīng)布局內(nèi) id 為 content 的 FrameLayout橱赠。這一過程也是 AppCompatActivity 中偷梁換柱的核心內(nèi)容,一起來看下這個(gè)重要過程箫津,注意查看 createSubDecor 方法的下半部分:
獲取到當(dāng)前 SubDecor 中 content 容器病线,也就是上面 include 布局內(nèi) ContentFrameLayout。
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
R.id.action_bar_activity_content);
然后拿到 PhoneWindow 中 DecorView 內(nèi) content 容器(mContentParent)鲤嫡,注意這個(gè)原本是我們 setContentView() 的直接父容器送挑。
final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
重要的偷梁換柱過程:
// 這里很關(guān)鍵,將原來Window中id為content的FrameLayout(mContentParent)設(shè)置為NO_ID
windowContentView.setId(View.NO_ID);
// 最新創(chuàng)建的SubDecor中內(nèi)容區(qū)域ContentFramLayout的id設(shè)置為content
// 偷梁換柱
contentView.setId(android.R.id.content);
即將 DecorView 中原 content 容器 id 置為 NO_ID暖眼,將 SubDecor 中 content 容器 id 置為 content惕耕。經(jīng)過前面的分析我們知道通過 setContentView 添加布局最終會(huì)被添加到一個(gè) id 為 content 的 FrameLayout,此時(shí)該 content 實(shí)際變?yōu)?SubDecor 中 ContentFrameLayout容器。偷梁換柱完成。
然后將 SubDecor 添加到 DecorView 中昼捍,此時(shí)原 DecorView 中 content 容器實(shí)際添加的是 SubDecor胁附。
// 將新創(chuàng)建的SubDecor添加到DecorView的內(nèi)容區(qū)域(mContentParent容器)
mWindow.setContentView(subDecor);
最后我們重新回到 Delegate 的 setContentView 方法,看下我們?cè)O(shè)置的布局如何添加到 SubDecor 中的 content (ContentFrameLayout)容器的:
// 將布局添加到SubDecor的Content區(qū)域
LayoutInflater.from(mContext).inflate(resId, contentParent)
通過一張結(jié)構(gòu)圖了解下 AppCompatActivity 整個(gè) UI 視圖關(guān)系喜最,注意與前面分析的 Activity 做下比較,最主要的差別在 DecorView 的 content 容器。此時(shí)已經(jīng)替換成了 SubDecor蛤铜。
至此,我們可以回答文章開頭的第三個(gè)問題了丛肢。
AppCompatActivity 通過引入 AppCompatDelegate 來兼容不同版本的 Material Design 支持围肥。
在 AppCompatDelegate 中做了一個(gè)巧妙的偷梁換柱操作,即在原 DecorView 的 content 區(qū)域添加一個(gè) SubDecor(兼容布局)蜂怎,我們通過 setContentView 設(shè)置的布局最終被添加到該 SubDecor 的 content 容器中穆刻,這樣完成布局兼容操作。
其實(shí)我們可以看出 Google 工程師在處理兼容時(shí)也很“暴力”杠步,這也是沒有辦法的辦法氢伟,因?yàn)橹巴诘目犹嗔耍@么多版本幽歼,為了做兼容費(fèi)了很多心思朵锣,有些心思設(shè)計(jì)的也非常巧妙。
總結(jié)
每個(gè) Activity 都有一個(gè)關(guān)聯(lián)的 Window 對(duì)象试躏,用來描述應(yīng)用程序窗口猪勇,每個(gè)窗口內(nèi)部又包含一個(gè) DecorView 對(duì)象,DecorView 對(duì)象用來描述窗口的視圖 — xml 布局颠蕴。
AppCompatActivity 在 DecorView 中又添加了一個(gè) SubDecor 視圖 — xml 布局泣刹,解決布局兼容性問題助析。
以上便是個(gè)人在學(xué)習(xí) View 的加載過程心得和體會(huì),文中如有不妥或有更好的分析結(jié)果椅您,歡迎大家指出外冀。
文章如果對(duì)你有幫助,請(qǐng)留個(gè)贊吧掀泳!