我们每天都在写 onCreate,然后在 onCreate 中绑定布局,方法就是 setContentView,但是你有没有想过为什么要这么写呢?为什么 setContentView 绑定布局之后,Activity 就能显示出对应的界面呢?这正是我们今天要说的。
对于有几年工作经验的都知道在 Android 中 有一个叫做 DecorView 的布局,它的父类是 FrameLayout,它里边包含了一个 title 布局,然后包含了一个 content 布局,而这个 content 布局中又有什么呢?进行了什么操作呢?让我们一起来揭晓它。
话不多说,我们直接从 Activity 的 setContentView 入手,为什么从它入手呢?就是因为 setContentView 绑定的布局就是添加在我们的 content 布局中的,来看下它的源码:
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
emmmm,我用的是 25 的 SDK,而继承的 AppCompatActivity,这或许是我们现在最常用的了吧,反正我现在是继承它,不会直接继承 Activity 了,继续看它调用的 setContentView 方法:
/**
* Should be called instead of {@link Activity#setContentView(int)}}
*/
public abstract void setContentView(@LayoutRes int resId);
它调用了 AppCompatDelegate 的 setContentView 方法,而 AppCompatDelegate 是一个抽象类,setContentView 是一个抽象方法,根据注释我们知道我们该去找 setContentView 真正的实现了,我们来看 Activity 的 setContentView 方法:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
来看下 getWindow 方法做了什么事情:
public Window getWindow() {
return mWindow;
}
直接返回了一个 Window,而这个 Window 在哪里声明的呢?这不是本篇的重点,有兴趣的可以去看下源码,它是在 attach 中声明的,而在调用 onCreate 之前,会先调用 Activity 的 attach 方法。好了,我们继续往下看,既然 getWindow 返回了一个 Window,那么就说明我们调用的就是 Window 的 setContentView 方法咯,而 Window 又是一个抽象类,它里边并没有写 setContentView 的方法体,那该怎么办呢?其实我在以前的博客中有提过,Window 的实现类是 PhoneWindow,所以,我们来看下 PhoneWindow 中的 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);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
好了,我们已经找到问题的根源了,让我们来对症下药吧!!
1. installDecor 方法
我们来看下上边那个方法,先看第六行,对 mContentParent 进行了一次判断,mContentParent 是什么呢?mContentParent 其实是个 ViewGroup 并且包裹我们整个布局文件,当我们的布局没有添加到 mContentParent 中的时候,mContentParent 为 null,所以我们需要调用 installDecor 去把我们的布局添加到 mContentParent 中,来看下 installDecor 的源码:
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
// Set up decor part of UI to ignore fitsSystemWindows if appropriate.
mDecor.makeOptionalFitsSystemWindows();
//各种样式处理
...
}
}
代码比较长,省略了一部分,有兴趣的可以去查看下。这部分代码已经够我们用的了,我们来看下 3 - 12 行代码,不用我多解释了,相信大家都看得出来,就是初始化了一个 DecorView,在第十三行我们知道 mContentParent 肯定是 null, 所以进入第十四行,这也是整个文章的重点,把 DecorView 添加到 mContentParent 中。来看下 generateLayout 方法:
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
//根据当前设置的主题来加载默认布局
TypedArray a = getWindowStyle();
...
//首先通过WindowStyle中设置的各种属性,对Window进行requestFeature或者setFlags
//如果你在theme中设置了window_windowNoTitle,则这里会调用到,其他方法同理,
//这里是根据你在theme中的设置去设置的
if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
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_windowFullscreen, false)) {
setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN & (~getForcedWindowFlags()));
}
...
//根据当前sdk的版本确定是否需要menukey
WindowManager.LayoutParams params = getAttributes();
//通过a中设置的属性,设置 params.softInputMode 软键盘的模式;
//如果当前是浮动Activity,在params中设置FLAG_DIM_BEHIND并记录dimAmount的值。
//以及在params.windowAnimations记录WindowAnimationStyle
...
//添加布局到DecorView,前面说到,DecorView是继承与FrameLayout,它本身也是一个ViewGroup,而我们前面创建它的时候,只是调用了new DecorView,
// 此时里面并无什么东西。而下面的步奏则是根据用户设置的Feature来创建相应的默认布局主题。
// 举个例子,如果我在setContentView之前调用了requestWindowFeature(Window.FEATURE_NO_TITLE),这里则会通过getLocalFeatures来获取你设置的feature,
// 进而选择加载对应的布局,此时则是加载没有标题栏的主题,对应的就是R.layout.screen_simple
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;
setCloseOnSwipeEnabled(true);
} else if (...} else {
// Embedded, so no decoration is needed.
layoutResource = R.layout.screen_simple;
// System.out.println("Simple!");
}
mDecor.startChanging();
//将找到的不同的布局文件,添加给decorView
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
...
mDecor.finishChanging();
return contentParent;
}
这段代码比较长,但是理解起来还是很好理解的,一句话概括,找到对应布局,然后添加到 DecorView 中,在添加到 mContentParent 中。
我们再详细的分析下,先通过 TypedArray 得到我们设置的属性,也就是 \frameworks\base\core\res\res\values\attrs.xml 下边的:
<!-- The set of attributes that describe a Windows's theme. -->
<declare-styleable name="Window">
<attr name="windowBackground" />
<attr name="windowContentOverlay" />
<attr name="windowFrame" />
<attr name="windowNoTitle" />
<attr name="windowFullscreen" />
<attr name="windowOverscan" />
<attr name="windowIsFloating" />
<attr name="windowIsTranslucent" />
<attr name="windowShowWallpaper" />
<attr name="windowAnimationStyle" />
<attr name="windowSoftInputMode" />
<attr name="windowDisablePreview" />
<attr name="windowNoDisplay" />
<attr name="textColor" />
<attr name="backgroundDimEnabled" />
<attr name="backgroundDimAmount" />
然后根据 TypedArray 的属性对 Window 进行 requestFeature 或者 setFlags,当然这些都不是这个方法的重点
接下来我们来看重点,从 34 行开始到 44 行,我们通过 getLocalFeatures 方法得到我们设置的 Feature,然后根据 Feature 得到我们对应的 layoutResource。到这里你们是否有个印象,在设置全屏的时候,需要把 requestFeature 写在 setContentView 上边,原因就在这里了。
当我们得到对应的布局之后,通过 onResourcesLoaded 把我们从 layoutResource 得到的布局添加到 DecorView 中,我们来看下在设置无标题的情况下使用的布局文件 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>
然后我们再来看下这句话:
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
我们来找到这个常亮:
/**
* The ID that the main layout in the XML layout file should have.
*/
public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
这时候你应该理解了,它是根据 layoutResource 通过 inflater 得到一个布局,然后把这个布局添加到 DecorView,根据这个得到的布局存在的 id,得到我们需要的 mContentParent。
好了,到了这里,setContentView 方法也就基本分析完了,还剩下最后一步,我们来回到 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);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
既然我们得到了 mContentParent,那我们就把写的布局文件通过 inflater 加入到 mContentParent 中。
至此,setContentView 已经完全分析完,有不对的地方,请大家指出,谢谢!!