Android-SetContentView内部原理
点击上方蓝字关注
布局结构分析
在分析AppCompatActivity.setContentView原理之前,我们先看一下它的布局结构,
我们做一个这样的布局,布局中只有TextView,并且设置AppCompatActivity主题为noTitle的:
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
</TextView>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
然后我通过Layout Inspector查看布局结构:
源码分析
setContentView(R.layout.activity_main);
点击进入查询源码:
@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()
,点进去查看:|
@NonNull
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
我们又发现了AppCompatDelegate
,先看一下此类的继承关系:
怎么查看继承关系?
Windows:ctrl + h
Mac:control + h
下面看一下继承关系图:
然后看一下AppCompatDelegate create
方法:
public static AppCompatDelegate create(Dialog dialog, AppCompatCallback callback) {
return create(dialog.getContext(), dialog.getWindow(), callback);
}
private static AppCompatDelegate create(Context context, Window window,
AppCompatCallback callback) {
if (Build.VERSION.SDK_INT >= 24) {
return new AppCompatDelegateImplN(context, window, callback);
} else if (Build.VERSION.SDK_INT >= 23) {
return new AppCompatDelegateImplV23(context, window, callback);
} else {
return new AppCompatDelegateImplV14(context, window, callback);
}
}
}
我们终于发现在create方法中是根据版本的不同来创建AppCompatDelegate
实现类的。为什么这样做呢?当然是为了兼容问题!
现在我们具体看一下setContentView
中到底写了什么?通过寻找,发现AppCompatDelegateImplV9
中进行了重写:
我们会发现三个方法中都执行了 ensureSubDecor();
,
我们来分析一下此段代码:
@Override
public void setContentView(int resId) {
//一会详细去解读此方法
ensureSubDecor();
//生成viewGroup contentParent
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
//生成布局
LayoutInflater.from(mContext).inflate(resId, contentParent);
mOriginalWindowCallback.onContentChanged();
}
解读ensureSubDecor();
,我们打开:
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)) {
onTitleChanged(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 (!isDestroyed() && (st == null || st.menu == null)) {
invalidatePanelMenu(FEATURE_SUPPORT_ACTION_BAR);
}
}
}
我们注意到了 mSubDecor = createSubDecor();
,那mSubDecor
是什么?我们索引一下,其实它是ViewGroup
private ViewGroup mSubDecor;
继续看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.");
}
if (a.getBoolean(R.styleable.AppCompatTheme_windowNoTitle, false)) {
requestWindowFeature(Window.FEATURE_NO_TITLE);
} else if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBar, false)) {
// Don't allow an action bar if there is no title.
requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR);
}
if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBarOverlay, false)) {
requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY);
}
if (a.getBoolean(R.styleable.AppCompatTheme_windowActionModeOverlay, false)) {
requestWindowFeature(FEATURE_ACTION_MODE_OVERLAY);
}
mIsFloating = a.getBoolean(R.styleable.AppCompatTheme_android_windowIsFloating, false);
a.recycle();
// 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;
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) {
/**
* 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());
/**
* Propagate features to DecorContentParent
*/
if (mOverlayActionBar) {
mDecorContentParent.initFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY);
}
if (mFeatureProgress) {
mDecorContentParent.initFeature(Window.FEATURE_PROGRESS);
}
if (mFeatureIndeterminateProgress) {
mDecorContentParent.initFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
}
}
} 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 (Build.VERSION.SDK_INT >= 21) {
// If we're running on L or above, we can rely on ViewCompat's
// setOnApplyWindowInsetsListener
ViewCompat.setOnApplyWindowInsetsListener(subDecor,
new OnApplyWindowInsetsListener() {
@Override
public WindowInsetsCompat onApplyWindowInsets(View v,
WindowInsetsCompat insets) {
final int top = insets.getSystemWindowInsetTop();
final int newTop = updateStatusGuard(top);
if (top != newTop) {
insets = insets.replaceSystemWindowInsets(
insets.getSystemWindowInsetLeft(),
newTop,
insets.getSystemWindowInsetRight(),
insets.getSystemWindowInsetBottom());
}
// Now apply the insets on our view
return ViewCompat.onApplyWindowInsets(v, insets);
}
});
} else {
// Else, we need to use our own FitWindowsViewGroup handling
((FitWindowsViewGroup) subDecor).setOnFitSystemWindowsListener(
new FitWindowsViewGroup.OnFitSystemWindowsListener() {
@Override
public void onFitSystemWindows(Rect insets) {
insets.top = updateStatusGuard(insets.top);
}
});
}
}
if (subDecor == null) {
throw new IllegalArgumentException(
"AppCompat does not support the current theme features: { "
+ "windowActionBar: " + mHasActionBar
+ ", windowActionBarOverlay: "+ mOverlayActionBar
+ ", android:windowIsFloating: " + mIsFloating
+ ", windowActionModeOverlay: " + mOverlayActionMode
+ ", windowNoTitle: " + mWindowNoTitle
+ " }");
}
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);
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);
contentView.setAttachListener(new ContentFrameLayout.OnAttachListener() {
@Override
public void onAttachedFromWindow() {}
@Override
public void onDetachedFromWindow() {
dismissPopups();
}
});
return subDecor;
}
createSubDecor
方法我们要好好分析一下,首先要知道我们当前研究的AppCompatActivity
是no title的。然后初始化了DecorView
:
mWindow.getDecorView();
mWindow.getDecorView()
我们放在后面讲,继续看代码,然后走到这个地方,发现有个mWindowNoTitle
判断
mWindowNoTitle
到底是什么?我们继续查一下
原来是判断activity是否有title,这就明白了,我们显然是没有,直接走else,继续往下看代码:
从图上看mOverlayActionMode
,经过在本类AppCompatDelegateImplV9
查找,发现在requestWindowFeature(FEATURE_ACTION_MODE_OVERLAY)
才会执行进来,所以直接走else
:
else {
subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
}
subDecor
前面已经提到了是ViewGroup
,加载了abc_screen_simple
布局,我们来看一下abc_screen_simple
布局是什么样子:
<?xml version="1.0" encoding="utf-8"?>
<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>
发现布局中有include
布局,打开发现就是一个ContentFrameLayout
,注意它的ID:
<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>
看完这些我们继续回归createSubDecor()
,继续往下看,首先加载ActionBarContextView
然后会发现ContentFrameLayout
以及windowContentView (ViewGroup)
的实例化,windowContentView
加载了android.R.id.content
的布局:
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
R.id.action_bar_activity_content);
final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
继续往下看,我们将将 ContentFrameLayout
的 id 更改成了 content
:
windowContentView.setId(View.NO_ID);
contentView.setId(android.R.id.content);
下面就剩下我们还没搞清楚的两行代码了:
// Now let's make sure that the Window has installed its decor by retrieving it
mWindow.getDecorView();
// Now set the Window's content view with the decor
mWindow.setContentView(subDecor);
首先我们从代码中看出是初始化了DecorView
内部控件,打开 mWindow.getDecorView()
,发现是在Window
类下:
public abstract View getDecorView();
我们查询下实现类PhoneWindow
:
@Override
public final View getDecorView() {
if (mDecor == null || mForceDecorInstall) {
installDecor();
}
return mDecor;
}
mDecor
就是DecorView
,然后我们看一下installDecor();
方法,因为代码太多,我就截图重要部分:
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
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) {
mContentParent = generateLayout(mDecor);
//....
}
//....
}
我们逐步分析上面的代码,我们最先看到的就是实例化mDecor
:
mDecor = generateDecor(-1);
然后我们继续往下看,对mContentParent
做了判断,mContentParent
其实就是ViewGroup
:
我们打开generateLayout(mDecor)
看看,因为代码实在太多,我就粘贴比较重要的部分吧:
protected ViewGroup generateLayout(DecorView decor) {
// Inflate the window decor.
int layoutResource;
int features = getLocalFeatures();
// System.out.println("Features: 0x" + Integer.toHexString(features));
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
setCloseOnSwipeEnabled(true);
}
//....
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);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
//.....
mDecor.finishChanging();
return contentParent;
}
从上面代码得知加载了R.layout.screen_simple
布局,并在onResourcesLoaded
方法中添加到了DecorView
里,然后找到此布局的R.id.content控件 ,并直接return contentParent
。
看一下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" />
<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>
现在mWindow.getDecorView();
弄清楚了,接下来就剩下mWindow.setContentView(subDecor);
了。我们继续研究:
mWindow.setContentView(subDecor);
subDecor
熟悉吧?我们在上面createSubDecor()
方法中都仔细的分析完了 继续分析setContentView
:
public abstract void setContentView(View view);
从PhoneWindow
中我们可以看到setContentView
具体的实现方法:
@Override
public void setContentView(View view) {
setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
@Override
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;
}
我们一起分析上面代码,首先是执行:
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
mContentParent
知道吧?上面已经讲了在installDecor()
方法中,就是ViewGroup
,具体是什么大家去上面仔细看。然后继续往下看:
mContentParent.addView(view, params);
非常重要的一行代码,是把subDecor
添加到mContentParent
中,subDecor
是什么?上边已经仔细讲过了。
至此AppCompatDelegateImplV9
类setContentView
方法中的ensureSubDecor();
全部讲完了。我们先回顾一下AppCompatDelegateImplV9
类setContentView
方法:
@Override
public void setContentView(int resId) {
//上面讲完了
ensureSubDecor();
//获得还是id为content的ViewGroup
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
//将布局id映射成View并且放到contentParent下
LayoutInflater.from(mContext).inflate(resId, contentParent);
mOriginalWindowCallback.onContentChanged();
}
如果你还是比较懵逼,我这写一个方法流程图,
里面写的不是很全,只是一个思路,希望大家能静下心去翻源码:
投票统计
现在公众号是以分享Android技术文为主,Android市场招聘要求越来越高,我也是在学习跨平台技术,所以想统计大家比较想看到技术文:
—————END—————