Android中AppCompatActivity的setContentView方法分析

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 容器中,这样完成布局兼容操作。

上一篇:2021-2022-1 20211408 《信息安全专业导论》第三周学习总结


下一篇:面试官亲讲Activity显示界面背后的故事:一文让你理清View的那些复杂关系