前言
目前我们开发Activity时都会继承AppCompatActivity,本文会从该类为入口,分析我们onCreate()方法中调用的setContentView(View)做了些什么,到底将我们的View设置到了哪里。
Activity的层级结构
Activity:负责生命周期的管理,Window的创建
Window:代表一个窗口,包括Background、Titile、事件等
View:安卓中所有可显示的组件都继承自View
由于Window本身不是View,只负责管理、处理事件,所以内部包含一个DecorView用来显示真正的UI,根据用户不同的配置,如是否有ActionBar等,DecorView内部的组织结构也不一样,用户setContentView设置的content位置也不同。
AppCompatActivity
该类是一个代理类,当我们调用setContentView时会创建一个AppCompatActivityDelateImpl对象,然后调用对应的setContentView();
public void setContentView(View view) {
initViewTreeOwners();
getDelegate().setContentView(view);
}
AppCompatActivityDelateImpl
下面是具体setContentView中的代码,很直观,先确保subDecor已经创建过,然后通过findViewById根据contentParent,清楚其中的view,再我们指定的View设置进去即可。
public void setContentView(View v) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
contentParent.addView(v);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
顺着这个逻辑,现在我们有两个个问题:
- 什么是subDecor?
- contentParent是什么?
什么是SubDecor?
首先mSubDecor是一个位于DecorView中的ViewGroup,专门用来存放用户自己创建的content。
AppCompatActivityDelateImpl.ensureSubDecor会先检测subdecor是否被创建过,如果没有就先创建。
// true if we have installed a window sub-decor layout.
private boolean mSubDecorInstalled;
private ViewGroup mSubDecor;
private void ensureSubDecor() {
if (!mSubDecorInstalled) {
mSubDecor = createSubDecor();
// If a title was set before we installed the decor, propagate it now
CharSequence title = getTitle();
if (!TextUtils.isEmpty(title)) {
if (mDecorContentParent != null) {
mDecorContentParent.setWindowTitle(title);
} else if (peekSupportActionBar() != null) {
peekSupportActionBar().setWindowTitle(title);
} else if (mTitleView != null) {
mTitleView.setText(title);
}
}
applyFixedSizeWindow();
onSubDecorInstalled(mSubDecor);
mSubDecorInstalled = true;
// Invalidate if the panel menu hasn't been created before this.
// Panel menu invalidation is deferred avoiding application onCreateOptionsMenu
// being called in the middle of onCreate or similar.
// A pending invalidation will typically be resolved before the posted message
// would run normally in order to satisfy instance state restoration.
PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);
if (!mDestroyed && (st == null || st.menu == null)) {
invalidatePanelMenu(FEATURE_SUPPORT_ACTION_BAR);
}
}
}
在createSubDector的过程中会再调用ensureWindow和getDectorView()。这两个步骤主要是为了保证Window和DecorView已经被创建过。里面的代码很长,但主要只干了两件事:
- 确保Window和DecorView已经被创建
- 根据对应的配置,如是否有Titile,是否悬浮等选择对应的layout文件,inflate后生成subDecor
- 调用mWindow.setContentView(View),将对应的subDecor设置进去
private ViewGroup createSubDecor() {
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
/**
* 经典错误,如果Activity在manifest中没有指定theme就会报错
*/
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.");
}
//。。。
// Now let's make sure that the Window has installed its decor by retrieving it
//确保DecorView已经创建过
mWindow.getDecorView();
final LayoutInflater inflater = LayoutInflater.from(mContext);
ViewGroup subDecor = null;
//根据配置如是否有title,是否悬浮,是否有actionbar等选择layout文件,生成subDecor
if (!mWindowNoTitle) {
if (mIsFloating) {
// If we're floating, inflate the dialog title decor
subDecor = (ViewGroup) inflater.inflate(
R.layout.abc_dialog_title_material, null);
// Floating windows can never have an action bar, reset the flags
mHasActionBar = mOverlayActionBar = false;
} else if (mHasActionBar) { //是否有ActionBar
/**
* This needs some explanation. As we can not use the android:theme attribute
* pre-L, we emulate it by manually creating a LayoutInflater using a
* ContextThemeWrapper pointing to actionBarTheme.
*/
TypedValue outValue = new TypedValue();
mContext.getTheme().resolveAttribute(R.attr.actionBarTheme, outValue, true);
Context themedContext;
if (outValue.resourceId != 0) {
themedContext = new ContextThemeWrapper(mContext, outValue.resourceId);
} else {
themedContext = mContext;
}
// Now inflate the view using the themed context and set it as the content view
subDecor = (ViewGroup) LayoutInflater.from(themedContext)
.inflate(R.layout.abc_screen_toolbar, null);
mDecorContentParent = (DecorContentParent) subDecor
.findViewById(R.id.decor_content_parent);
mDecorContentParent.setWindowCallback(getWindowCallback());
//...
}
} else {
if (mOverlayActionMode) {
subDecor = (ViewGroup) inflater.inflate(
R.layout.abc_screen_simple_overlay_action_mode, null);
} else {
subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
}
//...
}
//...
if (mDecorContentParent == null) {
mTitleView = (TextView) subDecor.findViewById(R.id.title);
}
// Make the decor optionally fit system windows, like the window's decor
ViewUtils.makeOptionalFitsSystemWindows(subDecor);
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
R.id.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);
//关键,将其id设置为R.id.content
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;
}
什么是contentParent
contentParent是通过subDecor.findViewById(android.R.id.content) 获取到的,所以我们创建subDecor的布局中应该有一个id为android.R.id.content的布局控件才对。
以有ActionBar时的为例,查看R.layout.abc_screen_toolbar的结构 。
<androidx.appcompat.widget.ActionBarOverlayLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/decor_content_parent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<!--存放content的地方-->
<include layout="@layout/abc_screen_content_include"/>
<androidx.appcompat.widget.ActionBarContainer
android:id="@+id/action_bar_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
style="?attr/actionBarStyle"
android:touchscreenBlocksFocus="true"
android:gravity="top">
<androidx.appcompat.widget.Toolbar
android:id="@+id/action_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:navigationContentDescription="@string/abc_action_bar_up_description"
style="?attr/toolbarStyle"/>
<androidx.appcompat.widget.ActionBarContextView
android:id="@+id/action_context_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:theme="?attr/actionModeTheme"
style="?attr/actionModeStyle"/>
</androidx.appcompat.widget.ActionBarContainer>
</androidx.appcompat.widget.ActionBarOverlayLayout>
layout/abc_screen_content_include代码结构:
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<androidx.appcompat.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为android.R.id.content ****的控件,只有一个action_bar_activity_content。
我们可以回头查看一下createSubDecor中最后一部分加黑的代码:
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
R.id.action_bar_activity_content);
final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
先分别获取subDecor中R.id.action_bar_activity_content的布局和mWindow中android.R.id.content的布局。注意!Window并不继承于view,其findViewById的方法是委托为DecorView的。
// Change our content FrameLayout to use the android.R.id.content id.
// Useful for fragments.
windowContentView.setId(View.NO_ID);
//关键,将其id设置为R.id.content
contentView.setId(android.R.id.content);
然后将DecorView中原有的android.R.id.content设置为NO_ID,然后再将R.id.action_bar_activity_content设置为android.R.id.content。这样subDecor中就有了android.R.id.content的控件,也就是我们后面setContentView中的contentParent,其本身是一个ContentFrameLayout类型的对象。
到这里逻辑就很清晰了,setContentView会根据我们的主题配置选择对应的layout,生成subDecor,然后将我们的View添加到对应layout的content布局处。不同的layout其内容放置的位置也不同。这种机制就可以让安卓方便的给我们提供多样的父布局。
但以上还有两个问题:
- mWindow.setContentView()的流程,即设置subDecor的流程。
- DecorView是怎么建立的?
- DecorView中的布局是怎样的,其中是否有android.R.id.content。毕竟是要先检测到DecorView中有android.R.id.content时才进行替换。
PhoneWindow.setContentView的流程
这块代码比较简单,分为两部分:
- 先判断DecorView是否初始化过,如果没有先初始化DecorView
- 原view的清除和新view的添加。先判断是否设置了动画,如果没有动画就直接removeAllViews()和addView(),如果有就使用Scene对象来运行动画。
public void setContentView(View view, ViewGroup.LayoutParams params) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
view.setLayoutParams(params);
final Scene newScene = new Scene(mContentParent, view);
transitionTo(newScene);
} else {
mContentParent.addView(view, params);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
Window并不是View,而安卓中所有的显示组件都是View类型,所以Window中存在一个DecorView类型的实例。而DecorView的根布局也存在不同类型,显示content的位置也不一样,而mContentParent引用的便是DecorView中添加contentView的控件。
所以在setContentView时添加和删除都在mContentParent上进行操作。
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
// This is the view in which the window contents are placed. It is either
// mDecor itself, or a child of mDecor where the contents go.
ViewGroup mContentParent;
mContentParent的创建我们会在DecorView的创建过程中说明。
DecorView的创建过程
DecorView的创建在PhoneWindow.installDecor()中,当我们调用PhoneWindow.getDecorView或者直接调用installDecor()时会创建DecorView。
//PhoneWindow.getDecorView();
public final @NonNull View getDecorView() {
if (mDecor == null || mForceDecorInstall) {
installDecor();
}
return mDecor;
}
installDecor()中使用generateDecor(-1)来创建DecorView,然后使用generateLayout(mDecor)来创建mContentParent;
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
//创建对应的DecorView
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
//初始化contentParent
mContentParent = generateLayout(mDecor);
// Set up decor part of UI to ignore fitsSystemWindows if appropriate.
mDecor.makeFrameworkOptionalFitsSystemWindows();
final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
R.id.decor_content_parent);
if (decorContentParent != null) {
mDecorContentParent = decorContentParent;
mDecorContentParent.setWindowCallback(getCallback());
if (mDecorContentParent.getTitle() == null) {
mDecorContentParent.setWindowTitle(mTitle);
}
//...
} else {
mTitleView = findViewById(R.id.title);
if (mTitleView != null) {
if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
final View titleContainer = findViewById(R.id.title_container);
if (titleContainer != null) {
titleContainer.setVisibility(View.GONE);
} else {
mTitleView.setVisibility(View.GONE);
}
mContentParent.setForeground(null);
} else {
mTitleView.setText(mTitle);
}
}
}
if (mDecor.getBackground() == null && mBackgroundFallbackDrawable != null) {
mDecor.setBackgroundFallback(mBackgroundFallbackDrawable);
}
//创建动画
// Only inflate or create a new TransitionManager if the caller hasn't
// already set a custom one.
if (hasFeature(FEATURE_ACTIVITY_TRANSITIONS)) {
if (mTransitionManager == null) {
final int transitionRes = getWindowStyle().getResourceId(
R.styleable.Window_windowContentTransitionManager,
0);
if (transitionRes != 0) {
final TransitionInflater inflater = TransitionInflater.from(getContext());
mTransitionManager = inflater.inflateTransitionManager(transitionRes,
mContentParent);
} else {
mTransitionManager = new TransitionManager();
}
}
//...
}
}
}
创建DecorView的逻辑在generateDecor() 中,逻辑很简单,主要是构造一个DecorContext和DecorView,代码如下:
//PhoneWindow.generateDecor()
protected DecorView generateDecor(int featureId) {
// System process doesn't have application context and in that case we need to directly use
// the context we have. Otherwise we want the application context, so we don't cling to the
// activity.
Context context;
if (mUseDecorContext) {
Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {
context = getContext();
} else {
context = new DecorContext(applicationContext, this);
if (mTheme != -1) {
context.setTheme(mTheme);
}
}
} else {
context = getContext();
}
return new DecorView(context, featureId, this, getAttributes());
}
创建mContentParent的代码在generateLayout()中,代码很长,我们只关注主要逻辑。
其中主要就干了三件事:
- 先通过各种条件和配置确定对应layout的资源Id,
- 然后通过mDecor.onResourcesLoaded(mLayoutInflater, layoutResource)来加载布局文件。
- 调用
findViewById(``ID_ANDROID_CONTENT``)获取对应的控件返回。
//PhoneWindow.generateLayout
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
//...
// Inflate the window decor.
//查找对应的资源文件
int layoutResource;
int features = getLocalFeatures();
// System.out.println("Features: 0x" + Integer.toHexString(features));
if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleIconsDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_title_icons;
}
// XXX Remove this once action bar supports these features.
removeFeature(FEATURE_ACTION_BAR);
// System.out.println("Title Icons!");
} else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
&& (features & (1 << FEATURE_ACTION_BAR)) == 0) {
// Special case for a window with only a progress bar (and title).
// XXX Need to have a no-title version of embedded windows.
layoutResource = R.layout.screen_progress;
// System.out.println("Progress!");
} else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
// Special case for a window with a custom title.
// If the window is floating, we need a dialog layout
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogCustomTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_custom_title;
}
// XXX Remove this once action bar supports these features.
removeFeature(FEATURE_ACTION_BAR);
} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
// If no other features and not embedded, only need a title.
// If the window is floating, we need a dialog layout
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
layoutResource = a.getResourceId(
R.styleable.Window_windowActionBarFullscreenDecorLayout,
R.layout.screen_action_bar);
} else {
layoutResource = R.layout.screen_title;
}
// System.out.println("Title!");
} else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
layoutResource = R.layout.screen_simple_overlay_action_mode;
} else {
// Embedded, so no decoration is needed.
layoutResource = R.layout.screen_simple;
// System.out.println("Simple!");
}
mDecor.startChanging();
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
ViewGroup contentParent = (ViewGroup)findViewById( ID_ANDROID_CONTENT );
//...
mDecor.finishChanging();
return contentParent;
}
其中有个很重要的代码:mDecor.onResourcesLoaded(mLayoutInflater, layoutResource),这里会初始化DecorView的根布局,添加,并赋值给mContentRoot。
//DecorView.java
//加载布局文件并赋值给mContentRoot。
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
if (mBackdropFrameRenderer != null) {
loadBackgroundDrawablesIfNeeded();
mBackdropFrameRenderer.onResourcesLoaded(
this, mResizingBackgroundDrawable, mCaptionBackgroundDrawable,
mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState),
getCurrentColor(mNavigationColorViewState));
}
mDecorCaptionView = createDecorCaptionView(inflater);
final View root = inflater.inflate(layoutResource, null);
if (mDecorCaptionView != null) {
if (mDecorCaptionView.getParent() == null) {
addView(mDecorCaptionView,
new ViewGroup.LayoutParams( MATCH_PARENT , MATCH_PARENT ));
}
mDecorCaptionView.addView(root,
new ViewGroup.MarginLayoutParams( MATCH_PARENT , MATCH_PARENT ));
} else {
// Put it below the color views.
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mContentRoot = (ViewGroup) root;
initializeElevation();
}
到此DecorView的初始化就完成了,但其中android.R.id.content在哪儿呢?
其实创建mContentParent里面的布局文件都会包含一个android.R.id.content的FrameLayout布局来放置我们的content内容。
总结
总体流程:
- 创建代理类AppCompatActivityDelateImpl,调用对应的setContentView(View)
- 调用ensureSubdecor
- 然后创建DecorView,根据主题、类型查找对应的layout文件,查找到id为
com.android.internal.R.id.content的控件,设为contentParent - 创建subDecor,根据主题、对应的类型查找layout文件创建subDecor
- 调用mWindow.setContentView()将subDecor设置到DecorView中
- 从subDecor中查找id为
android.R.id.content的控件,将用户的view添加进去