AppCompatActivity#setContentView源码分析

在我的上一篇文章Android开发Activity的setContentView源码分析中介绍了Activity的setContentView源码,但是我们在开发过程中用的基本都是AppCompatActivity,那么让我们看看AppCompatActivity的setContentView方法跟Activity的setContentView方法有什么不同。
首先来回忆以下Activity的setContentView方法中有两个很重要的东西:mDecor和mContentParent。前者是整个界面的根部局,后者是加载我们自定义布局的容器。其实AppCompatActivity的布局的层级跟Activity基本是一样的,只是在mContentParent中又多了一层布局而已。
话不多说,点开AppCompatActivity的setContentView方法:

@Override
public void setContentView(@LayoutRes int layoutResID) {
    getDelegate().setContentView(layoutResID);
}
    
@NonNull
public AppCompatDelegate getDelegate() {
   	if (mDelegate == null) {
        mDelegate = AppCompatDelegate.create(this, this);
    }
    return mDelegate;
}

public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
        return new AppCompatDelegateImpl(activity, activity.getWindow(), callback);
}

可以看到,里面是调用了一个委托对象的setContentView方法,这个对象就是AppCompatDelegateImpl,那么我们找到AppCompatDelegateImpl的setContentView方法:

@Override
public void setContentView(int resId) {
    ensureSubDecor();
    ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    mOriginalWindowCallback.onContentChanged();
}

明白了Activity布局加载的过程我们可以猜到,方法中第二行获取到的contentParent的功能就相当于Activity中mContentParent,我们自定义的布局就是加载到这个contentParent中的,接下来的代码LayoutInflater.from(mContext).inflate(resId, contentParent)马上就证实了我们的判断。

那么第一行的ensureSubDecor()是干啥的呢?在开篇的时候说过,AppCompatActivity的布局的层级跟Activity基本是一样的,只是在mContentParent中又多了一层布局。这里提前给出结论:mContentParent中又多的那一层布局就是mSubDecor,而contentParent又是在mSubDecor下面的一个子布局。

点进ensureSubDecor方法:

private void ensureSubDecor() {
    if (!mSubDecorInstalled) {
        mSubDecor = createSubDecor();
        ……
    }

可以看到通过createSubDecor方法创建mSubDecor。

private ViewGroup createSubDecor() {
        TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);

        if (!a.hasValue(R.styleable.AppCompatTheme_windowActionBar)) {
            a.recycle();
            throw new IllegalStateException(
                    "You need to use a Theme.AppCompat theme (or descendant) with this activity.");
        }

        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.
            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);
        }
        mIsFloating = a.getBoolean(R.styleable.AppCompatTheme_android_windowIsFloating, false);
        a.recycle();

        // Now let's make sure that the Window has installed its decor by retrieving it
        ensureWindow();
        
        //注释1
        mWindow.getDecorView();

        final LayoutInflater inflater = LayoutInflater.from(mContext);
        ViewGroup subDecor = null;
        
        //根据设置给subDecor加载不同的布局
        if (!mWindowNoTitle) {
            if (mIsFloating) {
                // If we're floating, inflate the dialog title decor
                subDecor = (ViewGroup) inflater.inflate(
                        R.layout.abc_dialog_title_material, null);

                // Floating windows can never have an action bar, reset the flags
                mHasActionBar = mOverlayActionBar = false;
            } else if (mHasActionBar) {
                /**
                 * This needs some explanation. As we can not use the android:theme attribute
                 * pre-L, we emulate it by manually creating a LayoutInflater using a
                 * ContextThemeWrapper pointing to actionBarTheme.
                 */
                TypedValue outValue = new TypedValue();
                mContext.getTheme().resolveAttribute(R.attr.actionBarTheme, outValue, true);

                Context themedContext;
                if (outValue.resourceId != 0) {
                    themedContext = new ContextThemeWrapper(mContext, outValue.resourceId);
                } else {
                    themedContext = mContext;
                }

                // Now inflate the view using the themed context and set it as the content view
                subDecor = (ViewGroup) LayoutInflater.from(themedContext)
                        .inflate(R.layout.abc_screen_toolbar, null);

                mDecorContentParent = (DecorContentParent) subDecor
                        .findViewById(R.id.decor_content_parent);
                mDecorContentParent.setWindowCallback(getWindowCallback());

                /**
                 * Propagate features to DecorContentParent
                 */
                if (mOverlayActionBar) {
                    mDecorContentParent.initFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY);
                }
                if (mFeatureProgress) {
                    mDecorContentParent.initFeature(Window.FEATURE_PROGRESS);
                }
                if (mFeatureIndeterminateProgress) {
                    mDecorContentParent.initFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
                }
            }
        } else {
            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);
            }

            if (Build.VERSION.SDK_INT >= 21) {
                // If we're running on L or above, we can rely on ViewCompat's
                // setOnApplyWindowInsetsListener
                ViewCompat.setOnApplyWindowInsetsListener(subDecor,
                        new OnApplyWindowInsetsListener() {
                            @Override
                            public WindowInsetsCompat onApplyWindowInsets(View v,
                                    WindowInsetsCompat insets) {
                                final int top = insets.getSystemWindowInsetTop();
                                final int newTop = updateStatusGuard(top);

                                if (top != newTop) {
                                    insets = insets.replaceSystemWindowInsets(
                                            insets.getSystemWindowInsetLeft(),
                                            newTop,
                                            insets.getSystemWindowInsetRight(),
                                            insets.getSystemWindowInsetBottom());
                                }

                                // Now apply the insets on our view
                                return ViewCompat.onApplyWindowInsets(v, insets);
                            }
                        });
            } else {
                // Else, we need to use our own FitWindowsViewGroup handling
                ((FitWindowsViewGroup) subDecor).setOnFitSystemWindowsListener(
                        new FitWindowsViewGroup.OnFitSystemWindowsListener() {
                            @Override
                            public void onFitSystemWindows(Rect insets) {
                                insets.top = updateStatusGuard(insets.top);
                            }
                        });
            }
        }

        if (subDecor == null) {
            throw new IllegalArgumentException(
                    "AppCompat does not support the current theme features: { "
                            + "windowActionBar: " + mHasActionBar
                            + ", windowActionBarOverlay: "+ mOverlayActionBar
                            + ", android:windowIsFloating: " + mIsFloating
                            + ", windowActionModeOverlay: " + mOverlayActionMode
                            + ", windowNoTitle: " + mWindowNoTitle
                            + " }");
        }

        if (mDecorContentParent == null) {
            mTitleView = (TextView) subDecor.findViewById(R.id.title);
        }

        // Make the decor optionally fit system windows, like the window's decor
        ViewUtils.makeOptionalFitsSystemWindows(subDecor);

        //注释2
        final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
                R.id.action_bar_activity_content);
        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
            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.
            //清除windowContentView的id
            windowContentView.setId(View.NO_ID);
            //将contentView的id设置成android.R.id.content
            contentView.setId(android.R.id.content);

            // The decorContent may have a foreground drawable set (windowContentOverlay).
            // Remove this as we handle it ourselves
            if (windowContentView instanceof FrameLayout) {
                ((FrameLayout) windowContentView).setForeground(null);
            }
        }

        // Now set the Window's content view with the decor
        //注释3
        mWindow.setContentView(subDecor);

        contentView.setAttachListener(new ContentFrameLayout.OnAttachListener() {
            @Override
            public void onAttachedFromWindow() {}

            @Override
            public void onDetachedFromWindow() {
                dismissPopups();
            }
        });

        return subDecor;
    }

在注释1处调用了mWindow的getDecorView方法,这里的mWindow就是PhoneWindow了,在PhoneWinow中找到getDecorView方法:

    @Override
    public final View getDecorView() {
        if (mDecor == null || mForceDecorInstall) {
            installDecor();
        }
        return mDecor;
    }

可以看到里面调用了installDecor方法,在前面的文章里提过,做的事情就是初始化mDecor和mContentParent。接下来是根据设置给subDecor加载不同的布局。再接着,在注释2处,通过subDecor的findViewById(R.id.action_bar_activity_content)方法获取到了id为R.id.action_bar_activity_content的contentView,然后再通过mWindow的findViewById(android.R.id.content)方法获取到了windowContentView(对应着Activity中的mContentParent),接着通过windowContentView.setId(View.NO_ID)方法将windowContentView的id清除,之后再用contentView.setId(android.R.id.content)将contentView的id设为android.R.id.content。这样一来,contentView 就成为了Activity中的mContentParent,我们编写的的布局加载到contentView中。

最后在注释3处,调用了PhoneWindow的setContentView(subDecor)方法将创建的subDecor放到mContentParent中,但是此时的mContentParent已经不是加载我们自己编写布局的那个容器了,加载我们编写的布局的容器已经变成了subDecor中的contentView。

让我们再回到AppCompatDelegateImpl的setContentView方法:

@Override
public void setContentView(int resId) {
    ensureSubDecor();
    ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    mOriginalWindowCallback.onContentChanged();
}

经过ensureSubDecor方法后,下面获取到的contentParent已经替换成了刚刚提到的contentView了,接下来通过LayoutInflater.from(mContext).inflate(resId, contentParent)将自己编写的布局加载到这个contentParent中来。

来看看整个流程:
在这里插入图片描述
AppCompatActivity布局层级如下:

在这里插入图片描述
最后总结一下Activity和AppCompatActivity的setContView方法区别:
Activity的setContView直接将我们的布局渲染到mContentParent容器里面
AppCompactActivity的setContView会根据不同的主题特性在mContentParent容器里面添加一个不同主题的subDecor容器,在subDecor容器里面有一个id为action_bar_activity_content的ContentFrameLayout容器(后来被替换成了R.id.content),并将我们的布局渲染到ContentFrameLayout里面(ContentFrameLayout也继承自FrameLayout)。

以上就是我对AppCompatActivity的setContentView加载布局的理解,欢迎大家在评论区留言指正。

文章参考自:
(1)AppCompatActivity和Activity的setContentView方法的区别
(2)从源码角度分析AppCompactActivity#setContentView
(3)Android 之 setContentView 源码阅读
(4)深入解析Android中的setContentView加载布局原理

猜你喜欢

转载自blog.csdn.net/weixin_44965650/article/details/106882366