美文网首页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