在我們開發過程中,我們有的時候會碰到一些眼熟的單詞,如Window、PhoneWindow、DecorView、ViewGroup等等名詞,雖然不知道它們都包含什麼意思,但是經常會碰到,作為一個準備進階的Android程式設計師,我們有必要了解一下前因後果,接下來我們便一一瞭解這些名詞。首先我們從最常用的部分-setContentView學起。
學習工具
//開發工具
1、Android Studio
Android Studio 2.2.3
Build #AI-145.3537739, built on December 2, 2016
JRE: 1.8.0_112-release-b05 x86_64
JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o
//原始碼環境
2、Android API -25
compileSdkVersion 25
buildToolsVersion "25.0.2"複製程式碼
開始學習
首先我們從最常見的setContentView開始分析原始碼。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//最常見的開始分析,進入詳細程式碼
setContentView(R.layout.activity_set_content_view_learn);
}複製程式碼
進入到了詳細程式碼中我們會發現其實我們進入到了activity類程式碼中了。
//程式碼清單Activity.java
/**
* Set the activity content from a layout resource. The resource will be
* inflated, adding all top-level views to the activity.
*
* @param layoutResID Resource ID to be inflated.
*
* @see #setContentView(android.view.View)
* @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
*/
public void setContentView(@LayoutRes int layoutResID) {
//呼叫Window類中的方法setContentView。
getWindow().setContentView(layoutResID);
//無關程式碼,初始化actionBar相關東東...
initWindowDecorActionBar();
}複製程式碼
首先我們會發現,我們先呼叫getWindow獲取window物件,然後再呼叫Window類中的setContentView方法,我們可以發現Window類其實是一個抽象類,所以肯定會有它的實現類,在這裡,它的實現類就是PhoneWindow類。在這裡,我們終於碰到了一個經常會提到的類PhoneWindow。
//程式碼清單 抽象類Window
public abstract class Window {
//省略相關程式碼...
}
//程式碼清單 Window實現類,PhoneWindow
public class PhoneWindow extends Window implements MenuBuilder.Callback {
//省略相關程式碼...
}複製程式碼
從上面的類申明當中我們就可以發現,PhoneWindow類其實是抽象類Window的實現類,所以對於setContentView方法,其實我們就得到它的實現類PhoneWindow中去檢視。
//程式碼清單 PhoneWindow.java
// This is the view in which the window contents are placed. It is either mDecor itself, or a child of mDecor where the contents go.
//變數申明
ViewGroup mContentParent;
@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();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}複製程式碼
在PhoneWindow這個具體的實現類中,我們可以看到的流程是首先判斷mContentParent是不是等於null,是的話就加在installDecor()方法,不是的話再判斷是否使用了這個FEATURE_CONTENT_TRANSITIONS的flag,如果沒有的話則mContentParent刪除其中所有的view。接下來再判斷是否需要通過LayoutInflater.inflate將我們傳入的layout放置到mContentParent中。
其實我們可以稍微預測下installDecor()的功能,大概就是初始化mContentParent這個變數。而mContentParent這個變數就是包裹我們設定的整個xml佈局內容的ViewGroup。
最後就是呼叫了一個介面Callback裡面的方法。我們可以發現其實Activity實現了這個介面,但是onContentChanged這個介面方法在Activity中是一個空實現,這不是重點。
接下來,我們繼續研究installDecor()中的原始碼~
//程式碼清單 PhoneWindow中的installDecor方法
//申明變數
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
//省略無關程式碼...
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
//省略無關程式碼...
} else {
mTitleView = (TextView) findViewById(R.id.title);
//設定是否需要標題
if (mTitleView != null) {
if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
final View titleContainer = findViewById(R.id.title_container);
if (titleContainer != null) {
titleContainer.setVisibility(View.GONE);
} else {
mTitleView.setVisibility(View.GONE);
}
mContentParent.setForeground(null);
} else {
mTitleView.setText(mTitle);
}
}
}
//省略無關程式碼...
}
}複製程式碼
從上面程式碼我們可以看出大概的流程。首先通過generateDecor(-1)來初始化一下mDecor,這個mDecor是DecorView的物件,看吧,這裡面已經出現了這個常見名詞,等等我們來分析一下DecorView這個類的作用。
接下來,我們用mDecor這個物件通過generateLayout(mDecor)來初始化mContentParent這個物件。然後我們便可以通過findViewById這個方法來獲取相關的控制元件了。
//程式碼清單 window.java類獲取相關控制元件
@Nullable
public View findViewById(@IdRes int id) {
return getDecorView().findViewById(id);
}複製程式碼
在這裡的getDecorView()方法其實就是獲取mDecor這個物件。接下來我們開始分析DecorView這個類。入口點就是我們剛剛在installDecor方法中初始化mDecor這個物件的地方mDecor = generateDecor(-1)。
//程式碼清單 DecorView.java類
protected DecorView generateDecor(int featureId) {
Context context;
//沒什麼鳥用的無關程式碼,省略...
return new DecorView(context, featureId, this, getAttributes());
}複製程式碼
從generateDecor中我們沒發現什麼有用的東西,我們繼續分析相關程式碼。接下來我們分析mContentParent = generateLayout(mDecor);
從這個方法名便能看出個大概了,它是生成佈局,然後賦值給mContentParent,我們進入到方法中詳細瞭解一下整個佈局生成過程。
//程式碼清單 generateLayout方法流程
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
//設定當前activity的theme
TypedArray a = getWindowStyle();
//省略無關程式碼...
//首先通過WindowStyle中設定的各種屬性,對Window進行requestFeat
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);
}
//省略無關程式碼...
//根據feature來載入對應的xml佈局檔案
int layoutResource;
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;
} 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 {
// Embedded, so no decoration is needed.
layoutResource = R.layout.screen_simple;
// System.out.println("Simple!");
}
//將上面獲取到的xml佈局載入到mDecor物件中
mDecor.startChanging();
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
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();
}
// Remaining setup -- of background and title -- that only applies
// to top-level windows.
if (getContainer() == null) {
final Drawable background;
if (mBackgroundResource != 0) {
background = getContext().getDrawable(mBackgroundResource);
} else {
background = mBackgroundDrawable;
}
mDecor.setWindowBackground(background);
final Drawable frame;
if (mFrameResource != 0) {
frame = getContext().getDrawable(mFrameResource);
} else {
frame = null;
}
mDecor.setWindowFrame(frame);
mDecor.setElevation(mElevation);
mDecor.setClipToOutline(mClipToOutline);
if (mTitle != null) {
setTitle(mTitle);
}
if (mTitleColor == 0) {
mTitleColor = mTextColor;
}
setTitleColor(mTitleColor);
}
mDecor.finishChanging();
return contentParent;
}複製程式碼
上面這個方法流程我們能看出個大概,首先getWindowStyle在當前的Window的theme中獲取我們的Window中定義的屬性。然後就根據這些屬性的值,對我們的Window進行各種requestFeature,setFlags等等。
還記得我們平時寫應用Activity時設定的theme或者feature嗎(全屏啥的,NoTitle等)?我們一般是不是通過XML的android:theme屬性或者java的requestFeature()方法來設定的呢?譬如:
通過java檔案設定:
requestWindowFeature(Window.FEATURE_NO_TITLE);
通過xml檔案設定:
android:theme="@android:style/Theme.NoTitleBar"複製程式碼
對的,其實我們平時requestWindowFeature()設定的值就是在這裡通過getLocalFeature()獲取的;而android:theme屬性也是通過這裡的getWindowStyle()獲取的。
所以這裡就是解析我們為Activity設定theme的地方,至於theme一般可以在AndroidManifest裡面進行設定。接下來,通過對features和mIsFloating的判斷,為layoutResource進行賦值,至於值可以為R.layout.screen_custom_title;R.layout.screen_action_bar;等等。至於features,除了theme中設定的,我們也可以在Activity的onCreate的setContentView之前進行requestFeature,也解釋了,為什麼需要在setContentView前呼叫requestFeature設定全屏什麼的。
最後通過我們得到了layoutResource,然後將它載入給mDecor物件,這個mDecor物件其實是一個FrameLayout,它的中心思想就是根據theme或者我們在setContentView之前設定的Feature來獲取對應的xml佈局,然後通過mLayoutInflater轉化為view,賦值給mDecor物件,這些佈局檔案中都包含一個id為content的FrameLayout,最後將其引用返回給mContentParent。
//程式碼清單 xml佈局檔案,包含id為content的FrameLayout佈局
<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>複製程式碼
我們用word來畫一個簡單的示意圖
到這裡我們分析了生成佈局後的大概流程,這樣生成了佈局後我們接著幹嗎呢?請回看當初的程式碼:
//程式碼清單 PhoneWindow.java
@Override
public void setContentView(int layoutResID) {
//我們上面分析的生成系統xml佈局的流程
if (mContentParent == null) {
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 {
//將我們自己寫的xml載入到我們上面獲取到的裡面包含id為content的xml佈局中去,並賦值給mContentParent
mLayoutInflater.inflate(layoutResID, mContentParent);
}
//省略無關程式碼...
}複製程式碼
我們剛剛寫了一個系統生成的xml佈局就是包含id為content的佈局:
<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" />複製程式碼
其實這就是我們將自己設定的xml佈局裝載進這個佈局中。這就是整個setContentView所做的工作。
最後我們來總結一下全部流程,用一個圖來表示,更方面直接:
Android中有一個成員叫Window,Window是一個抽象類,提供了繪製視窗的一組通用API,PhoneWindow繼承自Window,是Window的具體實現類,PhoneWindow中有一個私有成員DecorView,這個DecorView物件是所有應用Activity頁面的根View,DecorView繼承自FrameLayout,在內部根據使用者設定的Activity的主題(theme)對FrameLayout進行修飾,為使用者提供給定Theme下的佈局樣式。一般情況下,DecorView中包含一個用於顯示Activity標題的TitleView和一個用於顯示內容的ContentView。
可以看出,DecorView中包含一個Vertical的LinearLayout佈局檔案,檔案中有兩個FrameLayout,上面一個FrameLayout用於顯示Activity的標題,下面一個FrameLayout用於顯示Activity的具體內容,也就是說,我們通過setContentView方法載入的佈局檔案View將顯示在下面這個FrameLayout中。
首先初始化mDecor,即DecorView為FrameLayout的子類。就是我們整個視窗的根檢視了。然後,根據theme中的屬性值,選擇合適的佈局,通過infalter.inflater放入到我們的mDecor中。在這些佈局中,一般會包含ActionBar,Title,和一個id為content的FrameLayout。最後,我們在Activity中設定的佈局,會通過infalter.inflater壓入到我們的id為content的FrameLayout中去。
參考
1、http://blog.csdn.net/lmj623565791/article/details/41894125
2、http://blog.csdn.net/yanbober/article/details/45970721/
3、http://www.2cto.com/kf/201409/331824.html
關於作者
github: github.com/crazyandcod…
部落格: crazyandcoder.github.io