美文网首页Android技术知识
02 - [正菜] - Activity.setContentV

02 - [正菜] - Activity.setContentV

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

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

    2. 正菜 - Activity.setContentView

    目前我们使用的所有 Activity 默认都继承自 AppCompatActivity, AppCompatActivity 中做了什么, 我们先不用去了解, 因为后面会说到.

    AppCompatActivity 基类 仍然是 Activity, 所以我们先从 Activity 下手.

    下面开始跟着我一步一步的分析.

    2.1 Activity

    我们直接进入 Activity.java 中, 搜索 setContentView 找到参数为 int 类型的.

    /**
      * Set the activity content from a layout resource.  The resource will be
      * inflated, adding all top-level views to the activity.
    */
    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }
    

    注释翻译: 通过资源文件设置 Activity 内容, 把所有顶级 View 添加到 Activity 中

    ( 顶级 View 的概念, 在 01 - 开胃菜中, 已经说过 )

    在第6行调用了 getWindow().setContentView(layoutResID); 并传入了资源 ID.

    先跳转到 getWindow() 看一下.

    
    @UnsupportedAppUsage
    private Window mWindow;
    
    /**
     * Retrieve the current {@link android.view.Window} for the activity.
     * This can be used to directly access parts of the Window API that
     * are not available through Activity/Screen.
     *
     * @return Window The current window, or null if the activity is not
     *         visual.
     */
    public Window getWindow() {
        return mWindow;
    }
    

    注释翻译: 得到当前的 Activity , 可以用来直接访问部分无法通过 Activiy/Screen 访问的 Window API

    在 01 中已经说过, Window 是一个抽象基类, 提供了顶级窗口的外观和行为策略. 有一个唯一的实现类就是 PhoneWindow. 那么上面的调用 getWindow().setContentView(layoutResID); 其实就是实现类调用的. 我们进入继续跳转到 PhneWindowsetContentView(layoutResID)

    2.2 PhonwWindow

    phoneWindow的 setContentView. PhoneWindow: 423行

    ViewGroup mContentParent;
    
    @Override
    public void setContentView(int layoutResID) {
        //1
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }
            //2
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }
    

    mContentParent 是一个放置窗口内容的视图, 它不是 mDecor 本身, 就是一个mDecor 的子View

    这个方法其实我们需要关注的地方就两个部分,

    1. 判断mContentParent是否为 null

      • 为 null 就执行初始化逻辑,

      • 不为 null 就移除所有容器内的View.

    2. 是否使用了过度动画

      • 没有就直接把我们传入的资源 ID 加载到 父容器 mContentParent

      • 有就调用 transitionTo(Scene scene)

        • transitionTo(Scene scene) 最后也是将我们传入的资源ID, 加载到 mContentParent中, 此处不在表述.

    先来看初始化逻辑 installDecor()

    PhoneWindow.java 2681 行

    private void installDecor() {
        ...
        if (mDecor == null) {
            //初始化 DecorView
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            //传入 PhoneWindow 本身
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            //初始化  mContentParent
            mContentParent = generateLayout(mDecor);
            ...
            ...
        }
    }
    

    有同学会问了, 这不是初始化 mContentParent 的吗, 为什么要先初始化了一个叫 mDecor 的 ? 别着急, 慢慢往下看.

    2.2.1 PhoneWindow 初始化 DecorView.

    DecorView 是Window的顶层View,包含了Window的装饰。它继承了 FrameLayout.

    ( 顶层View 与 顶层Window 的关系在 01 中已经说过. )

    PhoneWindow.java 2315行

    进入到 generateDecor(int featureId) 中.

    protected DecorView generateDecor(int featureId) {
            ...
        return new DecorView(context, featureId, this, getAttributes());
    }
    

    我们发现, 这个方法就是创了一个 DecorView对象并返回. ( 注意第三个参数, 传入的也是 PhoneWindow 本身. ) , 在 DecorView 的构造方法内, 也调用了setWindow(window);

    进入到 DecorView 中会发现很多从我们传入的Window 中来获取属性来初始化 DecorView 的属性. 01 - 开胃菜中所说的两者之间的关系, 在这里就能充分的说明了. (DecorView 包含Window的装饰,例如,大小, 是否透明等属性).

    到这里 mDecor就初始化完毕了, 继续回到installDecor() 中开始看 generateLayout(DecorView decor) 方法怎么初始化 mContentParent

    2.2.2 PhoneWindow 初始化 mContentParent.

    PhoneWindow.java 2336 行

    这个方法内有太多太多内容, 但是有很多是我们现在不需要关注的, 我都以 ... 代替了, 保留了一些我们现在需要关注的代码.

    修改后如下所示

    protected ViewGroup generateLayout(DecorView decor) { 
        //-------------------------------1----------------------------
        //获取window属性
        TypedArray a = getWindowStyle();
            ...
        //是不是浮动窗口.
        mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
        ...
        if (mIsFloating) {
            ...
        } else {
            ...
        }
          //是不是设置了notitle
        if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
           ...
        } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
           ...
        }
            ...
        //是不是设置了透明
        mIsTranslucent = a.getBoolean(R.styleable.Window_windowIsTranslucent, false);
            ...
        //-------------------------------2----------------------------
        // Inflate the window decor.
        int layoutResource;
        int features = getLocalFeatures();
     
        if (...) {
            layoutResource = R.layout.screen_swipe_dismiss;
            ...
        } else if (...) {
            if (mIsFloating) {
                    ...
                layoutResource = res.resourceId;
            } else {
                layoutResource = R.layout.screen_title_icons;
            }
            ...
        } else if (...) {
            layoutResource = R.layout.screen_progress;     
        } else if (...) {
            if (mIsFloating) {
                ...
                layoutResource = res.resourceId;
            } else {
                layoutResource = R.layout.screen_custom_title;
            }
            ...
        } else if (...) {
            if (mIsFloating) {
                ...
                layoutResource = res.resourceId;
            } else if (...) {
                layoutResource = a.getResourceId(
                        R.styleable.Window_windowActionBarFullscreenDecorLayout,
                        R.layout.screen_action_bar);
            } else {
                layoutResource = R.layout.screen_title;
            }
        } else if (...) {
            layoutResource = R.layout.screen_simple_overlay_action_mode;
        } else {
            //2.1
            layoutResource = R.layout.screen_simple;
        }
            
        //-------------------------------3----------------------------
        ...
        //3.1将布局文件加载到 DecorView 中.
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
            //3.2
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
     
            ...
    
        return contentParent;
    }
    

    我将这个方法大致分为三个步骤

    1. 获取Window 的属性, 然后赋值给 PhoneWindow的成员变量.
    2. 根据 features 的值去加载不同的布局文件, (features也是根据Window设置的属性获取到的值) , 然后把布局文件ID 赋值给变量 layoutResource
    3. layoutResource 加载到 mDecor中. 最后根据指定的 ViewID获取 mDecor中的View. 并返回.

    重点代码也是在第三步,

    我们先看 3.1 onResourcesLoaded(mLayoutInflater, layoutResource)

    DecorView.java 2074行

    void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
        ...
        final View root = inflater.inflate(layoutResource, null);
        if (mDecorCaptionView != null) {
           ...
        } else {
            addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        }
        mContentRoot = (ViewGroup) root;
        initializeElevation();
    }
    

    省去无用代码, 在第3行, 看到了熟悉的 inflater.inflate, 加载传入的 layoutResource 布局. 在第7行, 把这个 View 添加到 DecorView 中去.

    那么加载的这个View 到底是什么样的呢, 我们在generateLayout() 中随便找一个最简单的布局文件来看. 2.1

    //2.1
    layoutResource = R.layout.screen_simple;
    
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true"
        android:orientation="vertical">
        <ViewStub android:id="@+id/action_mode_bar_stub"
                  android:inflatedId="@+id/action_mode_bar"
                  android:layout="@layout/action_mode_bar"
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content"
                  android:theme="?attr/actionBarTheme" />
        <FrameLayout
             android:id="@android:id/content"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:foregroundInsidePadding="false"
             android:foregroundGravity="fill_horizontal|top"
             android:foreground="?android:attr/windowContentOverlay" />
    </LinearLayout>
    

    FrameLayout 的这个ID , content需要记住, 因为下面就会说到这个ID.

    接着看generateLayout() 中的3.2.

    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    ...
    return contentParent;
    

    ID_ANDROID_CONTENT 这个ID 是什么 ? 跟进去看一下

    Window.java 257行

    /**
     * The ID that the main layout in the XML layout file should have.
     */
    public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
    

    官方注释翻译

    这个 ID 是主布局 xml 文件中应该具有的.

    这个 ID 就是刚才添加到 DecorView 布局中 FrameLayout的 ID , 等于说是 mContentParentDecorView中的一个ViewGroup, 是一个FrameLayout , 最后返回这个 ViewGroup

    我们可以猜想, 根据 Window 不同属性加载的不同布局中, 应该都会有这样一个ViewGroup, 并且ID 为 com.android.internal.R.id.content;


    总结

    现在来梳理一下整体流程.

    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. 流程结束

    用图形来表示一下他们之间的相互关系


    相互之间的关系

    相关文章

      网友评论

        本文标题:02 - [正菜] - Activity.setContentV

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