美文网首页Android技术知识
03 - [甜汤] - AppCompatActivity.se

03 - [甜汤] - AppCompatActivity.se

作者: __Y_Q | 来源:发表于2020-02-03 19:18 被阅读0次

    01 - [开胃菜] - Activity.setContentView 涉及到的类及相关概念
    02 - [正菜] - Activity.setContentView流程
    03 - [甜汤] - AppCompatActivity.setContentView 流程
    04 - [完结] - setContentView 流程总结

    前面两个章节学习了 Activity 的 setContentView 流程, 复习一下大概流程.

    1. Activity 调用 setContentView .
    2. 执行PhonwWindow.setContentView
    3. PhonwWindow.setContentView 中调用 installDecor 初始化DecorView ( installDecor中调用generateDecor() )
    4. 初始化 mContentParent ( installDecor 调用generateLayout())
      • 在初始化 mContentParent的同时, 又根据Window 不同属性加载不同的布局, 然后添加到 DecorView 中.
      • 最后获取DecorView 中的一个 ViewGroup 作为 mContentParent 并返回.
    5. PhoneWindow.setContentView 中, 调用 mLayoutInflater.inflate(layoutResID, mContentParent); 把我们要设置的布局文件ID, 加载到 mContentParent 中.
    6. 流程结束

    可是目前我们所使用的 Activity , 都是直接继承自 AppCompatActivity, 而 AppCompatActivity 最终也是继承自 Activity, 那么今天来看一下 AppCompatActivity.setContentView 的流程

    ( 以下分析基于 Android 10.0 )

    首先在 MainActivity 中点击 setContentView 进入内部

     public class MainActivity extends AppCompatActivity {
         @Override
         protected void onCreate(Bundle savedInstanceState) {
             super.onCreate(savedInstanceState);
             setContentView(R.layout.activity_main);
         }
     }
    

    跳转到了 AppCompatActivity 的 setContentView

    @Override
    public void setContentView(@LayoutRes int layoutResID) {
        getDelegate().setContentView(layoutResID);
    }
    

    看到这里获取了一个代理, 然后调用代理的 setContentView 方法, 继续跟进, 看一下获取的是什么代理, 点击 getDelegate 进入.

    
    private AppCompatDelegate mDelegate;
    
    @NonNull
    public AppCompatDelegate getDelegate() {
        if (mDelegate == null) {
            mDelegate = AppCompatDelegate.create(this, this);
        }
        return mDelegate;
    }
    

    看看究竟 create 创建了什么.

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

    进入 create内. 发现创建的是 AppCompatDelegateImpl ,
    原来 AppCompatDelegate 是一个抽象类, 而 AppCompatDelegateImpl 继承自 AppCompatDelegate .

    ( 注: AppCompatDelegate 是一个可以放在任意Activity中,并且回调相应生命周期的类,在不使用 AppCompatActivity 的情况下, 也可以得到一致的主题与颜色 )

    那么现在就清楚了. AppCompatActivity.setContentView 真正调用了 AppCompatDelegateImpl 中的 setContentView 方法.

    进入 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, 进入

    // true if we have installed a window sub-decor layout.
    private boolean mSubDecorInstalled;
    private ViewGroup mSubDecor;
    
    private void ensureSubDecor() {
        if (!mSubDecorInstalled) {
            mSubDecor = createSubDecor();
            ...
        }
    }
    

    上面省去了一些暂时不需要关注的代码, 重点就是这个 mSubDecor 的初始化.
    DecorView 知道, 因为上一章说到了, 可是这个 mSubDecor 是什么鬼. 就是一个简单的 ViewGroup ?

    进入 AppCompatDelegateImpl.createSubDecor()

    private ViewGroup createSubDecor() {
    //---------------------------------第一部分----------------------------------
        TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
        ...
        ...
        a.recycle();
    
    //---------------------------------第二部分----------------------------------
        // Now let's make sure that the Window has installed its decor by retrieving it
        mWindow.getDecorView();
    
    //---------------------------------第三部分----------------------------------
        final LayoutInflater inflater = LayoutInflater.from(mContext);
        ViewGroup subDecor = null;
    
        if (!mWindowNoTitle) {
            if (mIsFloating) {
                // If we're floating, inflate the dialog title decor
                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
                subDecor = (ViewGroup) LayoutInflater.from(themedContext).inflate(R.layout.abc_screen_toolbar, null);
                ...
                ...
                ...
            }
        } 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) {
                ...
            } else {
                ...
            }
        }
        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
                                + " }");
          }
       ...
       ...
       ...
    //---------------------------------第四部分----------------------------------
        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) {
            while (windowContentView.getChildCount() > 0) {
                final View child = windowContentView.getChildAt(0);
                windowContentView.removeViewAt(0);
                contentView.addView(child);
            }
            windowContentView.setId(View.NO_ID);
            contentView.setId(android.R.id.content);
            ...
            ...
        }
    
        // Now set the Window's content view with the decor
        mWindow.setContentView(subDecor);
    
        ...
        ...
    
        return subDecor;
    }
    

    代码量有点多, 我已省去了, 目前我们不需要关注的代码. 只留下和流程相关的关键性代码.
    如上代码所示, 我将 createSubDecor 方法整体分为了 4个部分.
     
     
    createSubDecor() 第一部分
    又见到了 TypedArray, 上一章也有这个, 还记得吗? 不记得的回去翻一下加深下印象.
    上章的 TypedArray 是取的是 Window的, 然后赋值给 PhoneWindow 的.
    这里的 TypedArray 取的是 AppCompatTheme 的, 然后赋值给 AppCompatDelegateImpl 的.
    两者之间不要弄混淆了.

    所以说, 第一部分的主要是从 AppCompatTheme 获取 style, 然后逐个进行判断, 然后赋值给 AppCompatDelegateImpl .

     
     

    createSubDecor() 第二部分
    mWindow.getDecorView();
    看到这句, 熟悉吗? 我猜测这里是初始化了DecorView 和 mContentParnt,
    为了验证. 我们直接进入 PhoneWindow 的 getDecorView() 方法.
    PhoneWindow.getDecorView()

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

    果不其然, 确实是去初始化了 DecorView 和 mContentParent. ( installDecor() 调用. installDecor方法具体流程, 这里不再说明, 上一章 [02-正菜] 中已经详细解释 )

    第二部分就是初始化 PhoneWindow 中的 DecorView 和 mContentParent

     
     
    createSubDecor() 第三部分
    看到第三部分是不是还是有点熟悉?
    是不是和在 PhoneWindow 初始化 mContentParent 并且加载一个 LinearLayout 的逻辑一样? 只是这里是给 subDecor 赋值.
    那既然这样, 我们还是先找到一个最简单的布局文件看一下.
    subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
    在我电脑上的路径如下:
    D:\SDK\extras\android\support\v7\appcompat\res\layout\R.layout.abc_screen_simple (家里的是 windows 系统)

    <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>
    

    继续寻找 include 的, abc_screen_content_include, 还是在上面的那个路径下.

    <merge xmlns:android="http://schemas.android.com/apk/res/android">
        <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" />
    </merge>
    

    思考一下, PhoneWindow 中给 DecorView 加载了一个 LinearLayout, 里面有两部分, 其中一部分是一个 FrameLayout.
    而这里呢, 给 subDecor 赋值了一个 FitWindowsLinearLayout 布局 , 里面也有两部分, 其中一部分是一个 ContentFrameLayout .
    这两者看起来是不是很相似? 有没有联想到什么? 别着急继续往下看.

    第三部分根据条件给 subDecor 赋值. (加载不同的布局文件)

     
     
    createSubDecor() 第四部分. 重点
    为了方便, 我把第四部分的代码拿下来放到这里.

    //---------------------------------第四部分----------------------------------
        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) {
            while (windowContentView.getChildCount() > 0) {
                final View child = windowContentView.getChildAt(0);
                windowContentView.removeViewAt(0);
                contentView.addView(child);
            }
            windowContentView.setId(View.NO_ID);
            contentView.setId(android.R.id.content);
            ...
            ...
        }
    
        // Now set the Window's content view with the decor
        mWindow.setContentView(subDecor);
    
        ...
        ...
    
        return subDecor;
    
    1. 先是从 subDecor 中取出一个 ID 为 action_bar_activity_content 的控件赋值给 contentView 变量.
      这个 ID 就是上面布局文件中的 ContentFrameLayout

    2. 接着从 PhoneWindow 中的 DecorView 中获取一个 ID 为 content 的 ViewGroup. 赋值给 windowContentView .
      回想一下, 这个ID 不就是 DecorView 中 LinearLayout 里面的 FrameLayout 嘛.

    3. 循环遍历 windowContentView, 取出 windowContentView 中每一个子 View, 然后 windowContentView 先删除这个View, 再添加到 ContentFrameLayout 中. 直至windowContentView 为空.

    4. 把 DecorView 中 LinearLayout 里面的 FrameLayout ID 改为 NO_ID.

    5. 把 subDecor 中 ContentFrameLayout ID 改为 R.id.content.

    6. 调用 PhoneWindow 的 setContentView 把 subDecor 加载到 PhoneWindow 中的 mContentParent 中.

    7. 返回 subDecor.

    第四部分总的来说就是, 将 PhoneWindow --> DecorView 布局中 id 为 content 的 FrameLayout 所有子 View 遍历添加到 subDecor 中 id 为 action_bar_activity_content 的 ContentFrameLayout 中去. 添加一个删除一个. 遍历完成后, 改变两个 ViewGroup 的 id, 最后将 subDecor 添加到 PhoneWindow 的 mContentParent 中.

     
     
    现在代码继续回到 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(); 已经执行完成, 初始化了 mSubDecor .
    接着从 mSubDecor 中获取 id 为 content 的控件, 并转为 ViewGroup.
    然后把我们传入的资源文件 添加到 这个 ViewGroup 中去.
    至此, 这整个流程分析完成.

    在下一章, 会有一个总结, 从网上找了几张图来总结和对比.

    相关文章

      网友评论

        本文标题:03 - [甜汤] - AppCompatActivity.se

        本文链接:https://www.haomeiwen.com/subject/pdchxhtx.html