AppCompatActivity的setContentView

作者: Android天之骄子 | 来源:发表于2017-08-24 10:13 被阅读71次

    前言

    上一篇我们分析了Activity的setContentView()的源码,这篇我们分析一下AppCompatActivity的setContentView()源码,我们都知道Android Studio创建一个Activity后默认都是继承AppCompatActivity的,话不多说直接分析。

    AppCompatActivity的setContentView()方法

        @Override
        public void setContentView(@LayoutRes int layoutResID) {
            getDelegate().setContentView(layoutResID);
        }
    
        @Override
        public void setContentView(View view) {
            getDelegate().setContentView(view);
        }
    
        @Override
        public void setContentView(View view, ViewGroup.LayoutParams params) {
            getDelegate().setContentView(view, params);
        }
    

    这里重载了三个方法,我们通常使用第一个,这三个方法里都有一句getDelegate(),字面意思就是代理,采用了代理模式对自身对象资源的访问,看下是如何实现的

       /**
        * @return The {@link AppCompatDelegate} being used by this Activity.
        */
       @NonNull
       public AppCompatDelegate getDelegate() {
           if (mDelegate == null) {
               mDelegate = AppCompatDelegate.create(this, this);
           }
           return mDelegate;
       }
    

    就是创建一个自身代理并返回,看下具体创建过程

        private static AppCompatDelegate create(Context context, Window window,
                AppCompatCallback callback) {
            final int sdk = Build.VERSION.SDK_INT;
            if (BuildCompat.isAtLeastN()) {
                return new AppCompatDelegateImplN(context, window, callback);
            } else if (sdk >= 23) {
                return new AppCompatDelegateImplV23(context, window, callback);
            } else if (sdk >= 14) {
                return new AppCompatDelegateImplV14(context, window, callback);
            } else if (sdk >= 11) {
                return new AppCompatDelegateImplV11(context, window, callback);
            } else {
                return new AppCompatDelegateImplV9(context, window, callback);
            }
        }
    

    我们发现针对不同版本,AppCompatDelegate 有很多子类,下面我们看下AppCompatDelegateImplV9的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();
        }
    

    createSubDecor()

    我们发现首先调用的是ensureSubDecor(),看到名字我们是不是想到DecorView了呢?我们点进去发现调用createSubDecor(),这里又做了一些什么事情呢!

       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.");
            }    //我们自定义主题必须继承AppTheme,否则会抛出异常
             ...
             ...
            // Now let's make sure that the Window has installed its decor by retrieving it
            mWindow.getDecorView();          
            
            //填充subDecor 
            final LayoutInflater inflater = LayoutInflater.from(mContext);
            ViewGroup subDecor = null;
            ...
            subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
            ...
            final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
                    R.id.action_bar_activity_content);           // 在subDecor 中找到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.setId(View.NO_ID);
                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
            mWindow.setContentView(subDecor);
    
            return subDecor;
        }
    

    这里调用了mWindow.getDecorView(); 通过上一篇文章Activity的setContentView()源码的分析,我们知道就是创建generateDecor和generateLayout的过程,接着再往下看,subDecor也会根据不同的属性加载不同的XML布局,我们看下布局abc_screen_simple的布局

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

    incluse引入一个id为abc_screen_content_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>
    

    发现它就是一个id为action_bar_activity_content的兼容FrameLayout,那有人就问了,既然完全类似,为什么不直接只用Activity呢?别急,下面是重点,首先mWindow会找到一个id为content 的布局,相信大家对这个布局已经很熟悉了,它就是Activity显示的布局加载到这个布局里面, 再往下通过while循环删除FrameLayout里面的所用控件,然后添加到这个兼容的FrameLayout里面,最后将subDecor作为参数传到PhoneWindow的setContentView()方法中;

    windowContentView.setId(View.NO_ID);
    contentView.setId(android.R.id.content);
    

    我们发现用了一个巧妙的技巧把id给偷梁换柱了,DecorView中FrameLayout的id设置为空,将subDecor中兼容的ContentFrameLayout重新设置为content 了,我们再看下AppCompatDelegateImplV9的setContentView()方法中有这样一句

    ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
    

    在这里获得还是id为content的ViewGroup,对外表现的还是获取content,其实内部已经偷偷的替换了,我们在其他地方获取根节点,通过content还是可以获取的到,这就是为了做兼容,我们看一张图片

    AppCompatActivity的View布局结构.png
    这就是AppCompatActivity的布局结构。那了解了这个content后,有什么作用呢,相信大家都用到过每一个Activity都需要弹出一个菜单,我们可以通过这个启发,将我们的菜单布局设置到content上面,说到这里大家有没有想到Snackbar呢,下面我看看下Snackbar的创建过程

    Snackbar源码

        @NonNull
        public static Snackbar make(@NonNull View view, @NonNull CharSequence text,
                @Duration int duration) {
            Snackbar snackbar = new Snackbar(findSuitableParent(view));
            snackbar.setText(text);
            snackbar.setDuration(duration);
            return snackbar;
        }
    
        private static ViewGroup findSuitableParent(View view) {
            ViewGroup fallback = null;
            do {
                if (view instanceof CoordinatorLayout) {
                    // We've found a CoordinatorLayout, use it
                    return (ViewGroup) view;
                } else if (view instanceof FrameLayout) {
                    if (view.getId() == android.R.id.content) {
                        // If we've hit the decor content view, then we didn't find a CoL in the
                        // hierarchy, so use it.
                        return (ViewGroup) view;
                    } else {
                        // It's not the content view but we'll use it as our fallback
                        fallback = (ViewGroup) view;
                    }
                }
    
                if (view != null) {
                    // Else, we will loop and crawl up the view hierarchy and try to find a parent
                    final ViewParent parent = view.getParent();
                    view = parent instanceof View ? (View) parent : null;
                }
            } while (view != null);
    
            // If we reach here then we didn't find a CoL or a suitable content view so we'll fallback
            return fallback;
        }
    

    在findSuitableParent()方法中,会不断地循环去找ViewParent parent = view.getParent();直到是CoordinatorLayout或者id为content的FrameLayout,找到后返回这个ViewGroup,我们接着看下Snackbar的构造方法

        private Snackbar(ViewGroup parent) {
            mParent = parent;
            mContext = parent.getContext();
    
            LayoutInflater inflater = LayoutInflater.from(mContext);
            mView = (SnackbarLayout) inflater.inflate(R.layout.design_layout_snackbar, mParent, false);
        }
    

    在这里将返回id为content或者CoordinatorLayout作为Snackbar的根布局,谷歌工程师封装的Snackbar就是这个思想,这就是我们通过分析源码分析到的!!!下一篇我们分析下系统如何将DecorView添加到Window。


    推荐阅读

    Activity的setContentView()源码分析

    相关文章

      网友评论

        本文标题:AppCompatActivity的setContentView

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