概述
對於Android開發人員來說,想必對setContentView方法不會陌生,每當我們創建一個Activity時,都會重寫該Activity的onCreate方法,在該方法中我們必須要調用setContentView方法來顯示我們指定的佈局或者View。那麼setContentView方法又是如何將我們指定的佈局或者View放入到指定的Activity中顯示出來的呢?
今天這篇博客主要就是講解Activity的UI繪製流程,而setContentView就是Activity的UI繪製起始過程。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
這裏先貼一張圖,便於大家更好的理解這個流程
PhoneWindow對象
當我們創建一個Activity時,該類都是默認繼承自Activity方法,所以首先我們需要進入到Activity方法中,然後找到該類的setContentView方法,該方法還有幾個重載的方法,這裏我們主要討論的是定義資源id參數layoutResID的方法,代碼如下:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
通過這個方法我們可以看出,在該方法中調用了 getWindow().setContentView(layoutResID),那麼這個getWindow又是什麼呢?
mWindow = new PhoneWindow(this, window);
public Window getWindow() {
return mWindow;
}
setContentView方法
通過源碼源碼可知,getWindow方法返回的時一個Window類對象,而Window是系統定義一個抽象類,在Activity中我們實例化的是Window的一個實現類PhoneWindow。所以我們需要進入到PhoneWindow類中,查看setContentView方法具體實現。
但是通過Eclipse或者Android studio是不能直接查看到PhoneWindow的源代碼的,要想查看該類的源碼需要去sdk的源碼中查找,具體路徑爲: /sdk/sources/android-21/com/android/internal/policy/impl/PhoneWindow.java。
進入該類之後找到setContentView方法,
@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);
}
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
installDecor方法
通過源碼可知,當第一次初始化的時候會執行installDecor方法,該方法源碼如下:
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor();
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
}
}
protected DecorView generateDecor() {
return new DecorView(getContext(), -1);
}
在installDecor方法中首先會判斷mDecor對象是否爲空,如果爲空則會調用generateDecor方法,generateDecor方法很簡單就是生成一個DecorView對象。
在mDecor對象在執行完成之後又會判斷mContentParent對象是否爲空,如果爲空則會通過generateLayout方法生成mContentParent,大家注意這個時候mDecor是作爲參數傳入到generateLayout方法中,不用質疑在該方法中會對mDecor做一些操作,那麼或許有人會疑問這個DecorView又是什麼呢?
DecorView
DecroView,其實就是PhoneWindow對象的一個內部類,該類繼承自FrameLayout,對於DecorView,這裏先不做過多的詳細介紹,大家只需要知道DecroView其實就是作爲Activity的頂級佈局顯示出來的就可以了。
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
....
}
generateLayout方法
generateLayout方法如下:
protected ViewGroup generateLayout(DecorView decor) {
//1,獲取<Application android:theme=""/>, <Activity/>節點指定的themes或者代碼requestWindowFeature()中指定的Features, 並設置
TypedArray a = getWindowStyle();
//...
//2,獲取窗口Features, 設置相應的修飾佈局文件,這些xml文件位於frameworks/base/core/res/res/layout下
int layoutResource;
int features = getLocalFeatures();
if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(com.android.internal.R.attr.dialogTitleIconsDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = com.android.internal.R.layout.screen_title_icons;
}
removeFeature(FEATURE_ACTION_BAR);
} else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0 && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
layoutResource = com.android.internal.R.layout.screen_progress;
//...
mDecor.startChanging();
//3, 將上面選定的佈局文件inflate爲View樹,添加到decorView中
View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
//將窗口修飾佈局文件中id="@android:id/content"的View賦值給mContentParent, 後續自定義的view/layout都將是其子View
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
//...
}
在該方法中,首先會通過getWindowStyle方法獲取window的樣式閒逛屬性並對Window進行一系列的初始化,這裏大家可以看看代碼,方法開始時這裏通過一系列的判斷調用requestFeature方法,對於requestFeature方法想必大家也不陌生,在開發中我們會經常在activity中調用該方法來設置FEATURE_NO_TITLE等屬性。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().requestFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
}
而且該方法的調用必須是在setContentView方法之前,或許在一開始大家並不明白爲什麼要在setContentView方法之前調用requestFeature方法,但是現在應該就明白了。因爲在setContentView方法中會間接的初始化Window的屬性,這裏會調用requestFeature等方法,如果在開發中我們在setContentView方法之後調用requestFeature方法改變一些屬性值,那麼此時window的初始化已經完成,在調用requestFeature方法就沒有作用了。
接下來,在generateLayout方法中,mLayoutInflater會根據layoutResource創建一個View對象,這個View對象in會被放入到decor中,也就是之前創建的DecroView中。
而layoutResource的獲取則和之前requestFeature方法初始化有關,這裏我們分析下默認情況下layoutResource的值,也就是 layoutResource = R.layout.screen_simple;,這裏我們可以看看screen_simple的佈局,該佈局的文件位置在sdk/platforms/android-21/data/res/layout/screen_simple.xml。當然,如果大家有興趣也可以看看其他情況下如actionbar下的佈局。
<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>
在該佈局中的FrameLayout的id爲content,想必大家就不陌生了,我們通過setContentView方法指定的佈局最終就是給這個FrameLayout添加一個子佈局。
最後,對DecorView添加完view對象之後,會通過該一下代碼獲取一個ViewGroup對象,這個對象就是generateLayout方法按最終返回的ViewGroup對象。
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
最終我們在回到setContentView方法中,在該方法中我們會填充指定給activity的佈局,並且將mContentParent作爲view的跟佈局,而這個mContentParent就是通過generateLayout方法創建的。
@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);
}
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
總結
對於setContentView方法的分析就到此爲止,這裏我們總結一下:
當我們創建一個Activity時,會有一個PhoneWindow的對象被創建,PhoneWindow是抽象類Window的具體實現
在PhoneWindow中有一個內部類DecorView,DecroView是繼承自FrameLayout,該類是所有應用窗口的根View
在DecorView中會添加一個具體的View,該View會根據不同的theme和feature而不同,但是有一個共同點就是在該View中會有一個id爲”@android:id/content”的FrameLayout存在,該類將作爲activity中顯示指定佈局的父佈局存在,也就是activity顯示的佈局江北添加到這個FrameLayout中
最後貼上一張圖,通過hierarchyviewer工具獲取的activity的view的結構,也可以一一印證上面的結論。