AppCompatActivity的setContentView
前言
上一篇我们分析了Activity的setContentView()的源码,这篇我们分析一下AppCompatActivity的setContentView()源码,我们都知道Android Studio创建一个Activity后默认都是继承AppCompatActivity的,话不多说直接分析。
AppCompatActivity的setContentView()方法
@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(),字面意思就是代理,采用了代理模式对自身对象资源的访问,看下是如何实现的
/**
* @return The {@link AppCompatDelegate} being used by this Activity.
*/
@NonNull
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
就是创建一个自身代理并返回,看下具体创建过程
private static AppCompatDelegate create(Context context, Window window,
AppCompatCallback callback) {
final int sdk = Build.VERSION.SDK_INT;
if (BuildCompat.isAtLeastN()) {
return new AppCompatDelegateImplN(context, window, callback);
} else 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 AppCompatDelegateImplV9(context, window, callback);
}
}
我们发现针对不同版本,AppCompatDelegate 有很多子类,下面我们看下AppCompatDelegateImplV9的setContentView()方法。
@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();
}
createSubDecor()
我们发现首先调用的是ensureSubDecor(),看到名字我们是不是想到DecorView了呢?我们点进去发现调用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.");
} //我们自定义主题必须继承AppTheme,否则会抛出异常
...
...
// Now let's make sure that the Window has installed its decor by retrieving it
mWindow.getDecorView();
//填充subDecor
final LayoutInflater inflater = LayoutInflater.from(mContext);
ViewGroup subDecor = null;
...
subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
...
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
R.id.action_bar_activity_content); // 在subDecor 中找到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);
return subDecor;
}
这里调用了mWindow.getDecorView(); 通过上一篇文章Activity的setContentView()源码的分析,我们知道就是创建generateDecor和generateLayout的过程,接着再往下看,subDecor也会根据不同的属性加载不同的XML布局,我们看下布局abc_screen_simple的布局
<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>
incluse引入一个id为abc_screen_content_include的布局,我们再看下abc_screen_content_include的布局,
<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>
发现它就是一个id为action_bar_activity_content的兼容FrameLayout,那有人就问了,既然完全类似,为什么不直接只用Activity呢?别急,下面是重点,首先mWindow会找到一个id为content 的布局,相信大家对这个布局已经很熟悉了,它就是Activity显示的布局加载到这个布局里面, 再往下通过while循环删除FrameLayout里面的所用控件,然后添加到这个兼容的FrameLayout里面,最后将subDecor作为参数传到PhoneWindow的setContentView()方法中;
windowContentView.setId(View.NO_ID);
contentView.setId(android.R.id.content);
我们发现用了一个巧妙的技巧把id给偷梁换柱了,DecorView中FrameLayout的id设置为空,将subDecor中兼容的ContentFrameLayout重新设置为content 了,我们再看下AppCompatDelegateImplV9的setContentView()方法中有这样一句
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
在这里获得还是id为content的ViewGroup,对外表现的还是获取content,其实内部已经偷偷的替换了,我们在其他地方获取根节点,通过content还是可以获取的到,这就是为了做兼容,我们看一张图片
这就是AppCompatActivity的布局结构。那了解了这个content后,有什么作用呢,相信大家都用到过每一个Activity都需要弹出一个菜单,我们可以通过这个启发,将我们的菜单布局设置到content上面,说到这里大家有没有想到Snackbar呢,下面我看看下Snackbar的创建过程
Snackbar源码
@NonNull
public static Snackbar make(@NonNull View view, @NonNull CharSequence text,
@Duration int duration) {
Snackbar snackbar = new Snackbar(findSuitableParent(view));
snackbar.setText(text);
snackbar.setDuration(duration);
return snackbar;
}
private static ViewGroup findSuitableParent(View view) {
ViewGroup fallback = null;
do {
if (view instanceof CoordinatorLayout) {
// We've found a CoordinatorLayout, use it
return (ViewGroup) view;
} else if (view instanceof FrameLayout) {
if (view.getId() == android.R.id.content) {
// If we've hit the decor content view, then we didn't find a CoL in the
// hierarchy, so use it.
return (ViewGroup) view;
} else {
// It's not the content view but we'll use it as our fallback
fallback = (ViewGroup) view;
}
}
if (view != null) {
// Else, we will loop and crawl up the view hierarchy and try to find a parent
final ViewParent parent = view.getParent();
view = parent instanceof View ? (View) parent : null;
}
} while (view != null);
// If we reach here then we didn't find a CoL or a suitable content view so we'll fallback
return fallback;
}
在findSuitableParent()方法中,会不断地循环去找ViewParent parent = view.getParent();直到是CoordinatorLayout或者id为content的FrameLayout,找到后返回这个ViewGroup,我们接着看下Snackbar的构造方法
private Snackbar(ViewGroup parent) {
mParent = parent;
mContext = parent.getContext();
LayoutInflater inflater = LayoutInflater.from(mContext);
mView = (SnackbarLayout) inflater.inflate(R.layout.design_layout_snackbar, mParent, false);
}
在这里将返回id为content或者CoordinatorLayout作为Snackbar的根布局,谷歌工程师封装的Snackbar就是这个思想,这就是我们通过分析源码分析到的!!!下一篇我们分析下系统如何将DecorView添加到Window。