AppCompatActivity的setContentView()源码分析

前言

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,816评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,729评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,300评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,780评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,890评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,084评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,151评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,912评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,355评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,666评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,809评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,504评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,150评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,121评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,628评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,724评论 2 351

推荐阅读更多精彩内容