PS:本文系转载文章,阅读原文可读性会更好些,原文链接:https://mp.weixin.qq.com/s/uTBv_evqvpetO0F8pWXY1Q
ps:源码是基于 android api 27 来分析的
前面写了一篇Android中Activity的setContentView方法分析,这一篇打算写对 AppCompatActivity 的setContentView 方法进行分析,AppCompatActivity 的 setContentView 方法和 Activity的 setContentView 方法是有区别的,区别在哪呢?我们就从AppCompatActivity 的 setContentView 方法进行分析,从而去寻找答案;
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
这里用 getDelegate 方法来调用 setContentView 方法的,那么 getDelegate 方法肯定是返回一个对象,我们来看 AppCompatActivity 的 getDelegate 方法;
@NonNull
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
getDelegate 方法返回的是一个 AppCompatDelegate 类型的对象,从字面意思可以看出,它是一个做兼容的代理并且位于 v7 包里,v7 是为了做兼容而存在的;AppCompatDelegate 是一个抽象类,我们来看 AppCompatDelegate 的 create(Activity activity, AppCompatCallback callback) 方法,看看 AppCompatDelegate 具体的实现类是什么;
public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
return create(activity, activity.getWindow(), callback);
}
private static AppCompatDelegate create(Context context, Window window,
AppCompatCallback callback) {
if (BuildCompat.isAtLeastO()) {
return new AppCompatDelegateImplO(context, window, callback);
} else if (Build.VERSION.SDK_INT >= 24) {
return new AppCompatDelegateImplN(context, window, callback);
} else if (Build.VERSION.SDK_INT >= 23) {
return new AppCompatDelegateImplV23(context, window, callback);
} else if (Build.VERSION.SDK_INT >= 14) {
return new AppCompatDelegateImplV14(context, window, callback);
} else if (Build.VERSION.SDK_INT >= 11) {
return new AppCompatDelegateImplV11(context, window, callback);
} else {
return new AppCompatDelegateImplV9(context, window, callback);
}
}
从 create(Activity activity, AppCompatCallback callback) 方法可以看出,它又调用了 create(Context context, Window window,AppCompatCallback callback) 方法,create(Context context, Window window,AppCompatCallback callback) 方法根据 SDK 的版本实例化不同的 AppCompatDelegate 的子类,我们这次就拿 AppCompatDelegateImplV9 类进行分析,其他的子类就读者自行去看了,所以我们来看 AppCompatDelegateImplV9 类的 setContentView 方法;
@Override
public void setContentView(int resId) {
//1、
ensureSubDecor();
//2、
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
//3、
contentParent.removeAllViews();
//4、
LayoutInflater.from(mContext).inflate(resId, contentParent);
//5、
mOriginalWindowCallback.onContentChanged();
}
注释2 中的 contentParent 还是 DecorView 布局中 id 为 content 的 FrameLayout 吗?答案却不是,mSubDecor 是什么呢?从字面来看可以理解为 代替的 DecorView 或者兼容的 DecorView,又或者第二个 DecorView;注释3 表示将 contentParent 所有的子视图都删除掉;注释4 表示将 id 为 resId 的布局文件填充到 contentParent,也就是将我们 AppCompatActivity 中 setContentView 设置的内容布局文件填充到 contentParent;注释5 表示回调 AppCompatActivity 的 onContentChanged 方法;好,我们往下看 注释1 中 AppCompatDelegateImplV9 的 ensureSubDecor 方法;
private void ensureSubDecor() {
if (!mSubDecorInstalled) {
//6、
mSubDecor = createSubDecor();
......
//7、
mSubDecorInstalled = true;
......
}
}
注释7 表示视图是否已经创建,如果创建过了就说明已经调用过 AppCompatDelegateImplV9 的 ensureSubDecor 方法了;注释6的代码包含创建 mSubDecor 的方法,我们看一下 AppCompatDelegateImplV9 的 createSubDecor 方法;
private ViewGroup createSubDecor() {
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
…
//8、
if (a.getBoolean(R.styleable.AppCompatTheme_windowNoTitle, false)) {
requestWindowFeature(Window.FEATURE_NO_TITLE);
} else if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBar, false)) {
// Don’t allow an action bar if there is no title.
//9、
requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR);
}
if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBarOverlay, false)) {
requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY);
}
if (a.getBoolean(R.styleable.AppCompatTheme_windowActionModeOverlay, false)) {
requestWindowFeature(FEATURE_ACTION_MODE_OVERLAY);
}
…
ViewGroup subDecor = null;
//10、
if (!mWindowNoTitle) {
if (mIsFloating) {
// If we're floating, inflate the dialog title decor
//11、
subDecor = (ViewGroup) inflater.inflate(
R.layout.abc_dialog_title_material, null);
......
} else if (mHasActionBar) {
......
// Now inflate the view using the themed context and set it as the content view
//12、
subDecor = (ViewGroup) LayoutInflater.from(themedContext)
.inflate(R.layout.abc_screen_toolbar, null);
......
}
} else {
//13、
if (mOverlayActionMode) {
subDecor = (ViewGroup) inflater.inflate(
R.layout.abc_screen_simple_overlay_action_mode, null);
} else {
subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
}
//14、
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
R.id.action_bar_activity_content);
//15、
final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
if (windowContentView != null) {
// There might be Views already added to the Window's content view so we need to
// migrate them to our content view
//16、
while (windowContentView.getChildCount() > 0) {
final View child = windowContentView.getChildAt(0);
windowContentView.removeViewAt(0);
contentView.addView(child);
}
// Change our content FrameLayout to use the android.R.id.content id.
// Useful for fragments.
//17、
windowContentView.setId(View.NO_ID);
contentView.setId(android.R.id.content);
......
}
// Now set the Window's content view with the decor
//18、
mWindow.setContentView(subDecor);
......
return subDecor;
}
类似与注释8 相似的 a.getBoolean(R.styleable.AppCompatTheme_windowXXX, false)) 语句其实是获取 Window 的 style 属性;类似注释9 的 requestWindowFeature(XXX) 其实是调用 Window 中的requestFeature 方法;注释10 表示如果没有标题;注释11 表示 Floating 类型的窗口;注释12 表示通过 themedContext 加载视图;注释13 表示 Overlay 模式下加载 Overlay 模式的视图;我们假设 subDecor 要加载的布局文件是 abc_screen_simple.xml,我们且看 abc_screen_simple.xml 的结构;
<android.support.v7.widget.FitWindowsLinearLayout
xmlns:android=“http://schemas.android.com/apk/res/android”
android:id="@+id/action_bar_root"
android:layout_width=“match_parent”
android:layout_height=“match_parent”
android:orientation=“vertical”
android:fitsSystemWindows=“true”>
<android.support.v7.widget.ViewStubCompat
android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/abc_action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<include layout="@layout/abc_screen_content_include" />
</android.support.v7.widget.FitWindowsLinearLayout>
abc_screen_content_include.xml 布局如下所示:
<?xml version="1.0" encoding="utf-8"?><android.support.v7.widget.ContentFrameLayout
android:id="@id/action_bar_activity_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
所以注释14 中的 contentView 其实是 abc_screen_content_include.xml 文件中 id 为 action_bar_activity_content 的 ContentFrameLayout;注释15 的 mWindow 其实是 PhoneWindow,所以本质是调用 PhoneWindow 的 findViewById 方法,PhoneWindow 的 findViewById 方法又调用 DecorView 的 findViewById 方法,DecorView 的 findViewById 方法调用之前先判断 DecorView 是否为空,如果为空就调用 PhoneWindow 的 installDecor 方法,installDecor 方法的相关解析可以看Android中Activity的setContentView方法分析这篇文章,我们假设 DecorView 要加载的布局文件是 screen_simple.xml,screen_simple.xml 的结构如下所示:
<?xml version="1.0" encoding="utf-8"?>这时候注释15 的 windowContentView 本质上就是 screen_simple.xml 文件中 id 为 content 的 FrameLayout;注释16 的 while 整体代码是这样的,先判断 DecorView 中的 FrameLayout 中子元素个数是否大于0,如果大于0,就取出第一个子元素并删除,删除后的子元素就添加到 subDecor 的子元素 ContentFrameLayout 中,也就是说 把DecorView 中的 FrameLayout 中第一个子元素移动到 subDecor 的子元素 ContentFrameLayout 中;注释17 表示将 DecorView 中 FrameLayout 的 id 设置成-1,可以理解成没有 id,然后再将 subDecor 中 ContentFrameLayout 的 id 设置为 content;注释18 表示将我们的 subDecor 添加到 PhoneWindow 中,我们且看 PhoneWindow 的 setContentView(View view) 方法;
@Override
public void setContentView(View view) {
setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
// 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)) {
......
} else {
//19、
mContentParent.addView(view, params);
}
......
}
PhoneWindow 的 setContentView(View view) 方法又调用了 PhoneWindow 的 setContentView(View view, ViewGroup.LayoutParams params) 方法,注释19 中的 mContentParent 是什么?它是 DecorView 中的 FrameLayout,之前它的 id 是 content,现在它的 id 是-1,因为它的 id 在上面分析的代码中执行替换了;因为在第一次执行 PhoneWindow 的 installDecor 方法的时候就已经实例化 mContentParent 并且已经知道 mContentParent 对象具体的实现类是 FrameLayout,不明白的读者可以看看Android中Activity的setContentView方法分析这篇文章,mContentParent.addView(view, params) 其实就是将 subDecor 添加到 DecorView 的子元素 FrameLayout 里面;我们回到 AppCompatDelegateImplV9 类的 setContentView 方法中,注释2 的代码,注释2 中的 contentParent 其实是 subDecor 中的 ContentFrameLayout;假设我的 AppCompatActivity 中要设置的内容布局文件为 activity_main.xml,那么注释4 的 resId 就等于 R.layout.activity_main,那么 activity_main.xml 就会填充到 subDecor 中的 ContentFrameLayout。
下面我们来画一下 AppCompatActivity 中的 DecorView 的布局结构图:
图片
Activity 的 DecorView 布局结构图,我就不在这里画出来了,可以看一下认识Android中的ViewRootImpl和DecorView这篇文章的末尾,会有 Activity 的 DecorView 布局结构图,读者可以拿 AppCompatActivity 中的 DecorView 的布局结构图和 Activity 的 DecorView 布局结构图对比一下有什么不同。
在 Android Level 21之后,Android 引入了 Material Design 的设计,为了支持 Material,Color、调色版、Toolbar等各种新特性,AppCompatActivity 就应用而生;AppCompatActivity 为了兼容以前的东西,就做了偷梁换柱,其中一个就是替换了以前 Activity 加载内容布局的父容器,即在原 DecorView 的 content 区域添加一个 SubDecor,我们通过 setContentView 设置的布局最终被添加到该 SubDecor 的 content 容器中,这样完成布局兼容操作。