原創(chuàng)-轉(zhuǎn)載請(qǐng)注明出處。
當(dāng)我們給Activity設(shè)置布局時(shí)骏庸,都是直接調(diào)用setContentView來完成的戚啥,但具體Android是怎么把布局加載到window,又是怎么通過findViewById獲取view對(duì)象的宛畦,我們可能并沒有太關(guān)心瘸洛,下面就結(jié)合源碼來分析下這個(gè)過程。
Android setContentView
打開Activity的源碼發(fā)現(xiàn)次和,setContentView有三個(gè)重載方法反肋,
- public void setContentView(int layoutResID);
- public void setContentView(View view);
- public void setContentView(View view, ViewGroup.LayoutParams params)
我們就來看下最常用的第一個(gè)方法:
public void setContentView(int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
這個(gè)方法調(diào)用了,Window類中的setContentView()方法,其他方法也是調(diào)用了Window類中的setContentView()踏施,但是Window是一個(gè)抽象類石蔗,在Activity的attach方法中被初始化,其實(shí)是一個(gè)PhoneWindow實(shí)例畅形,所以這個(gè)setContentView方法在PhoneWindow中實(shí)現(xiàn)养距。
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();
}
}
首先判斷mContentParent是否為空,如果為空的話則調(diào)用installDecor()方法日熬,其次判斷是否設(shè)置了FEATURE_CONTENT_TRANSITIONS屬性棍厌,如果沒有的話則移除所有view(從這里我們可以得出setContentView可以調(diào)用多次,反正會(huì)removeAllViews)竖席,然后調(diào)用LayoutInflater.inflate(),將我們?cè)O(shè)置的布局文件添加到mContentParent中耘纱。接著獲取了一個(gè)Callback對(duì)象,那這個(gè)是在Activity的attach方法中設(shè)置的一個(gè)回調(diào)
mWindow.setCallback(this);
所以可以得出在Activity中一定有一個(gè)onContentChanged回調(diào)毕荐,我們來看下這個(gè)回調(diào)
public void onContentChanged() {}
額揣炕,空空如也。但是我們可以在自己的Activity中重寫這個(gè)回調(diào)东跪,用于在setContentView之后做一些事情畸陡,比如findViewById,但貌似實(shí)際場景也不需要鹰溜。。丁恭。
好了曹动,現(xiàn)在我們回到上面提到的installDecor()方法,好長牲览,我們撿重要的看吧墓陈。
private void installDecor() {
//初始化decorView
if (mDecor == null) {
mDecor = generateDecor();
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
}
//初始化mContentParent
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
......
//設(shè)置一堆屬性值
}
}
看下PhoneWindow中的generateDecor()方法
protected DecorView generateDecor() {
return new DecorView(getContext(), -1);
}
只是單純的new了一個(gè)DecorView實(shí)例。這個(gè)DecorView是什么鬼第献。其實(shí)它是PhoneWindow的一個(gè)內(nèi)部類贡必,是整個(gè)window界面最頂層的view。包含ActionBar,內(nèi)容塊等庸毫。好了仔拟,現(xiàn)在我們縷一下Window,PhoneWindow,decorView的關(guān)系
1.Window類是一個(gè)抽象類飒赃,提供了繪制窗口的一組通用API利花。可以將之理解為一個(gè)載體载佳,各種View在這個(gè)載體上顯示炒事。
2.PhoneWindow是Window的一個(gè)子類,是Window的具體實(shí)現(xiàn)蔫慧,包含一個(gè)內(nèi)部類DecorView,PhoneWindow是將decorView進(jìn)行了一定包裝挠乳,并提供一些方法用于操作窗口。
3姑躲。DecorView繼承自FrameLayout,是窗口的根view睡扬。
好了,接著看mContentParent的初始化肋联,generateLayout(mDecor).這里傳入了上一部初始化好的DecorView. 又是一個(gè)長方法,我們還是挑出重要的部分刁俭。
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
TypedArray a = getWindowStyle();
//......
//根據(jù)定義的style設(shè)置一些值橄仍,比如是否顯示ActionBar,
// Inflate the window decor.
int layoutResource;
int features = getLocalFeatures();
//......
//根據(jù)設(shè)定好的features值選擇不同的窗口修飾布局文件,
//得到layoutResource值,系統(tǒng)定義了不同的layout牍戚,比如
//R.layout.screen_custom_title,R.layout.screen_simple
//把選中的窗口修飾布局文件添加到DecorView對(duì)象里侮繁,并且指定contentParent值
View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mContentRoot = (ViewGroup) in;
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
//......
//繼續(xù)一堆屬性設(shè)置,返回contentParent
return contentParent;
}
根據(jù)不同的features值,設(shè)定layoutResource如孝,最終添加到decorView中宪哩,所以我們通過在xml中設(shè)置的theme,還有在代碼中設(shè)置的requestWindowFeature,都是用來設(shè)置features值第晰,這也是為什么requestWindowFeature方法必須在setContentView之前的原因锁孟。
這樣看來彬祖,如果我們?cè)O(shè)置我們的Theme為NoTitleBar,最終layoutResource的值為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>
來看下去處標(biāo)題欄后的視圖樹
所以installDecor主要是初始化了PhoneWindow中的DecorView.和contentParent,之后在setContentView()中通過mLayoutInflater.inflate(layoutResID, mContentParent);將layoutResId,add到初始化好的contentParent中。
大家是否好奇狀態(tài)欄怎么被加載進(jìn)DecorView的品抽,我們來看下DecorView中的updateColorViewInt方法
private View updateColorViewInt(View view, int sysUiVis, int systemUiHideFlag,
int translucentFlag, int color, int height, int verticalGravity,
String transitionName, int id, boolean hiddenByWindowFlag) {
......
if (view == null) {
if (show) {
view = new View(mContext);
view.setBackgroundColor(color);
view.setTransitionName(transitionName);
view.setId(id);
addView(view, new LayoutParams(LayoutParams.MATCH_PARENT, height,
Gravity.START | verticalGravity));
}
} else {
......
}
return view;
}
可以看到直接new了一個(gè)view储笑,這個(gè)view就是狀態(tài)欄,然后將狀態(tài)欄添加到了DecorView,其實(shí)這個(gè)狀態(tài)欄只是一個(gè)單純的占位view圆恤。被updateColorViews方法調(diào)用突倍,比如當(dāng)我們調(diào)用setStatusBarColor時(shí)就是調(diào)用了updateColorViews這個(gè)方法。這里先不做過多介紹盆昙。
findViewById
那么將layout添加進(jìn)decorView中后羽历,我們是怎么通過findViewById找到View的呢?
看下Activity的findViewById方法
/**
* Finds a view that was identified by the id attribute from the XML that
* was processed in {@link #onCreate}.
*
* @return The view if found or null otherwise.
*/
public View findViewById(int id) {
return getWindow().findViewById(id);
}
又是到了window中淡喜,看下window中的方法
public View findViewById(int id) {
return getDecorView().findViewById(id);
}
是調(diào)用了getDecorView的findViewById秕磷,也就是調(diào)用了view的findViewById,我們來看下view類中
public final View findViewById(int id) {
if (id < 0) {
return null;
}
return findViewTraversal(id);
}
protected View findViewTraversal(int id) {
if (id == mID) {
return this;
}
return null;
}
到這我們就疑惑了,直接判斷了id是否為view的id拆火,是的話就返回跳夭。怎么也應(yīng)該有一個(gè)循環(huán)或者遞歸查找啊,什么都沒有们镜。
這時(shí)我們來看下币叹,mID是怎么初始化的
....
case com.android.internal.R.styleable.View_id:
mID = a.getResourceId(attr, NO_ID);
break;
...
喔,這個(gè)id就是我們?cè)趚ml中設(shè)置的id模狭。那會(huì)不會(huì)在ViewGroup中進(jìn)行查找的呢颈抚?來看下
protected View findViewTraversal(int id) {
if (id == mID) {
return this;
}
final View[] where = mChildren;
final int len = mChildrenCount;
for (int i = 0; i < len; i++) {
View v = where[i];
if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) {
v = v.findViewById(id);
if (v != null) {
return v;
}
}
}
return null;
}
果然, ViewGroup重寫了View的findViewTraversal()方法嚼鹉,遍歷了自己的child的findViewById方法贩汉,如果找到了返回View自身。
ok,到現(xiàn)在我們就理解了view是怎么findViewById的了锚赤。
總結(jié)
上面我們介紹了匹舞,Activity setContentView和findViewById的流程,是不是又多了一層理解呢线脚,喜歡的話就點(diǎn)個(gè)贊吧~