原创-转载请注明出处。
当我们给Activity设置布局时,都是直接调用setContentView来完成的,但具体Android是怎么把布局加载到window,又是怎么通过findViewById获取view对象的,我们可能并没有太关心,下面就结合源码来分析下这个过程。
Android setContentView
打开Activity的源码发现,setContentView有三个重载方法,
- public void setContentView(int layoutResID);
- public void setContentView(View view);
- public void setContentView(View view, ViewGroup.LayoutParams params)
我们就来看下最常用的第一个方法:
public void setContentView(int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
这个方法调用了,Window类中的setContentView()方法,其他方法也是调用了Window类中的setContentView(),但是Window是一个抽象类,在Activity的attach方法中被初始化,其实是一个PhoneWindow实例,所以这个setContentView方法在PhoneWindow中实现。
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是否为空,如果为空的话则调用installDecor()方法,其次判断是否设置了FEATURE_CONTENT_TRANSITIONS属性,如果没有的话则移除所有view(从这里我们可以得出setContentView可以调用多次,反正会removeAllViews),然后调用LayoutInflater.inflate(),将我们设置的布局文件添加到mContentParent中。接着获取了一个Callback对象,那这个是在Activity的attach方法中设置的一个回调
mWindow.setCallback(this);
所以可以得出在Activity中一定有一个onContentChanged回调,我们来看下这个回调
public void onContentChanged() {}
额,空空如也。但是我们可以在自己的Activity中重写这个回调,用于在setContentView之后做一些事情,比如findViewById,但貌似实际场景也不需要。。。
好了,现在我们回到上面提到的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);
......
//设置一堆属性值
}
}
看下PhoneWindow中的generateDecor()方法
protected DecorView generateDecor() {
return new DecorView(getContext(), -1);
}
只是单纯的new了一个DecorView实例。这个DecorView是什么鬼。其实它是PhoneWindow的一个内部类,是整个window界面最顶层的view。包含ActionBar,内容块等。好了,现在我们缕一下Window,PhoneWindow,decorView的关系
1.Window类是一个抽象类,提供了绘制窗口的一组通用API。可以将之理解为一个载体,各种View在这个载体上显示。
2.PhoneWindow是Window的一个子类,是Window的具体实现,包含一个内部类DecorView,PhoneWindow是将decorView进行了一定包装,并提供一些方法用于操作窗口。
3。DecorView继承自FrameLayout,是窗口的根view。
好了,接着看mContentParent的初始化,generateLayout(mDecor).这里传入了上一部初始化好的DecorView. 又是一个长方法,我们还是挑出重要的部分。
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
TypedArray a = getWindowStyle();
//......
//根据定义的style设置一些值,比如是否显示ActionBar,
// Inflate the window decor.
int layoutResource;
int features = getLocalFeatures();
//......
//根据设定好的features值选择不同的窗口修饰布局文件,
//得到layoutResource值,系统定义了不同的layout,比如
//R.layout.screen_custom_title,R.layout.screen_simple
//把选中的窗口修饰布局文件添加到DecorView对象里,并且指定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");
}
//......
//继续一堆属性设置,返回contentParent
return contentParent;
}
根据不同的features值,设定layoutResource,最终添加到decorView中,所以我们通过在xml中设置的theme,还有在代码中设置的requestWindowFeature,都是用来设置features值,这也是为什么requestWindowFeature方法必须在setContentView之前的原因。
这样看来,如果我们设置我们的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>
来看下去处标题栏后的视图树
所以installDecor主要是初始化了PhoneWindow中的DecorView.和contentParent,之后在setContentView()中通过mLayoutInflater.inflate(layoutResID, mContentParent);将layoutResId,add到初始化好的contentParent中。
大家是否好奇状态栏怎么被加载进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了一个view,这个view就是状态栏,然后将状态栏添加到了DecorView,其实这个状态栏只是一个单纯的占位view。被updateColorViews方法调用,比如当我们调用setStatusBarColor时就是调用了updateColorViews这个方法。这里先不做过多介绍。
findViewById
那么将layout添加进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);
}
是调用了getDecorView的findViewById,也就是调用了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,是的话就返回。怎么也应该有一个循环或者递归查找啊,什么都没有。
这时我们来看下,mID是怎么初始化的
....
case com.android.internal.R.styleable.View_id:
mID = a.getResourceId(attr, NO_ID);
break;
...
喔,这个id就是我们在xml中设置的id。那会不会在ViewGroup中进行查找的呢?来看下
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,到现在我们就理解了view是怎么findViewById的了。
总结
上面我们介绍了,Activity setContentView和findViewById的流程,是不是又多了一层理解呢,喜欢的话就点个赞吧~