Android 技术开发Android开发经验谈Android技术知识

AppCompatActivity的setContentView

2017-08-24  本文已影响71人  Android天之骄子

前言

上一篇我们分析了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()源码分析

上一篇 下一篇

猜你喜欢

热点阅读