前几天大致看了下Activity的setContentView方法,现在我们看一下AppCompatActivity的该方法
进入该方法和Activity的setContentView方法有明显的差别,通过名字可以猜测getDelegate()是一个代理方法,它的作用是 代理了一些做兼容的类,因为AppCompatActivity是V7包里的,而v7包就是做兼容的;
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
我们进入 getDelegate(),看看他是一个什么东西,这个方法返回一个AppCompatDelegate
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
我们进入AppCompatDelegate.create(this, this)方法看看它是干什么用的,进入该方法我们发现我们进入AppCompatDelegate是一个抽象类,他有很多实现类:AppCompatDelegateImplV23,AppCompatDelegateImplV14 等
private static AppCompatDelegate create(Context context, Window window,
AppCompatCallback callback) {
final int sdk = Build.VERSION.SDK_INT;
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 AppCompatDelegateImplV7(context, window, callback);
}
}
然后我们发现这个类有setContentView方法;如下
public abstract void setContentView(@LayoutRes int resId);
然后我们随便找一个它的实现类(比如AppCompatDelegateImplV7)进去,就可以找到它实现类的setContentView方法,它有三个重载方法;如下
@Override
public void setContentView(View v) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
contentParent.addView(v);
mOriginalWindowCallback.onContentChanged();
}
@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();
}
@Override
public void setContentView(View v, ViewGroup.LayoutParams lp) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
contentParent.addView(v, lp);
mOriginalWindowCallback.onContentChanged();
}
我们就看第一个就好,首先我们看到这个方法ensureSubDecor(),既然叫subDecor,那我们猜想一下是不是和我们以前讲的Activity的DecorView有某种联系呢??
我们进入ensureSubDecor();继续探索 主要方法createSubDecor();
private void ensureSubDecor() {
if (!mSubDecorInstalled) {
mSubDecor = createSubDecor();
// If a title was set before we installed the decor, propogate 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);
}
}
}
我们进入createSubDecor()方法,可以直观的看到 这个方法返回的是一个ViewGroup
,是不是和我们的 DecorView有点像,但是它找的都是一些AppCompat的属性,如:R.styleable.AppCompatTheme等等。做的都是兼容方面的工作。另外,当我们用了AppCompatActivity,但是在清单文件中没用AppCompatTheme相关的样式,就造成我们的xml文件报错。原因就是在createSubDecor() 的时候,加载的样式都AppCompatTheme相关的样式。相信大家或多或少都有这样的经历。
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.");
}
//在PhoneWindow里是requestFeature而在这里是requestFeature
if (a.getBoolean(R.styleable.AppCompatTheme_windowNoTitle, false)) {
requestWindowFeature(Window.FEATURE_NO_TITLE);
}
。。。省略若干行
// Now let's make sure that the Window has installed its decor by retrieving it
//这里还是初始化Decor,其实还是调用了PhoneWindow的getDecorView()方法
//所以这里的第一步其实也初始化DecorView,和Activiy加载DecorView一样
mWindow.getDecorView();
//之后是根据不同的情来加载subDecor
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;
}
//加载subDecor的布局,xml为abc_screen_toolbar,大家可以点进去看一下,这里就不贴代码了,太多了,下面会分为好多情况来给subDecor加载不同的布局,我们下面分析一个
// 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 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;
}
我们这里分析一个subDecor的一个布局文件的情况,我们来看一下subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);这个里面的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">
//兼容的ViewStub
<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的这个布局u其实就是一个framelayout,和DecorView的xml文件基本一致
<include layout="@layout/abc_screen_content_include" />
</android.support.v7.widget.FitWindowsLinearLayout>
接着createSubDecor() 方法往下讲
我们在这个方法里看到以下代码,发现 R.id.action_bar_activity_content这个Id就是
上面include标签里面framelayout的ID;再往下看它用mWindow获取了Activity里面DDecorView的id为android.R.id.content的framelayout;之后它利用 windowContentView.setId(View.NO_ID);把原来DecorView的id设置为没有id;然后用 contentView.setId(android.R.id.content)把AppCompatActivity的subDecor里面的framelayout的id设置为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);
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
//把subDecor又添加到DecorView里了
//下面的是Activity的DecorView添加布局,用的是decor.addView方法
//View in = mLayoutInflater.inflate(layoutResource, null);
//decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, //MATCH_PARENT));
//这里用的是setContentView(subDecor)方法//这就相当于Activity的setCont//entView方法了,相当于把subDecor作为我们自定义的xml了,这样来兼容。
mWindow.setContentView(subDecor);
contentView.setAttachListener(new ContentFrameLayout.OnAttachListener() {
@Override
public void onAttachedFromWindow() {}
@Override
public void onDetachedFromWindow() {
dismissPopups();
}
});
return subDecor;
之后再回到setContentView,大家发现它是调用了ensureSubDecor()后用findViewById(android.R.id.content)找到这个ViewGroup,但其实这个时候ViewGroup已经做了替换,不再是Activity里DecorView的那个id为content的帧布局了。所以做到了兼容,但是也不影响你用Activity,但是AppCompatActivity在这里做了偷梁换柱。。
@Override
public void setContentView(View v) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
contentParent.addView(v);
mOriginalWindowCallback.onContentChanged();
}
看图更易理解
注释:可以看到继承AppCompatActivity后,里面的ImageView,TextView等都变成兼容的控件了。谷歌悄悄的做了很多兼容的工作。
疑问:
mWindow.getDecorView();产生DecorView的时候,DecorView的xml是如何选择的?原来是根据主题来选择外层的view,但是这里的最外层的view选的是什么?
看代码发现:如果没设置样式的话会产生一个默认的布局R.layout.screen_simple
} else {
// Embedded, so no decoration is needed.
layoutResource = com.android.internal.R.layout.screen_simple;
// System.out.println("Simple!");
}
此外,
1,我们也可以根据AppCompatActivity的id为content的
布局做一些替换,来完成一些自定义控件。比如,在所有Activity都要弹出窗口dialog,我们就可以替换这个id,设置我们自定义的控件,那么这个AppCompatActivity的顶层布局就变成我们自定义的控件了,并且弹窗的同时,不影响弹窗后面的操作。2,SnakBar里面也是会找者id为content的ViewGroup,然后再把snakbar添加到这个ViewGroup里面;