从setContentView开始阅读源码
写在前面的话
setContentView作为Activity的入口,极为适合当作源码练习的第一课。阅读源码当然是比较痛苦的,但是在当下的就业环境里,会阅读源码已经是迫在眉睫。
这张图很直观的展示了Activity,Window,DecorView的关系,也能更好的帮助我们理解setContentView。
图片来源于网络
正文开始
Activity中 setContentView指向 AppCompatDelegate,而AppCompatDelegate是个抽象类,唯一实现类为: AppCompatDelegateImpl
所以,我们来到AppCompatDelegateImpl的setContentView,代码如下:
@Override
public void setContentView(int resId) {
ensureSubDecor();//确保 DecorView已初始化完毕
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);//添加布局
mAppCompatWindowCallback.getWrapped().onContentChanged();//通知window刷新
}
这个方法 里只有5行代码,我们依次来分析一下:
ensureSubDecor() ,从名字上来看,是确保Decor已经添加了,目的是保证 mSubDecor 对象非空,接下来要用。
mSubDecor 是一个ViewGroup对象,是Activity的跟布局。你或许在面试里经常听到它的另一个称呼:DecorView。 所以,第二行代码就是获取content的Viewgroup,
第三行 清除。确保根布局是干净的,无任何子view.
第四行,添加我们的布局,到content上。
第五行,通过windowCallback,刷新内容。
好了,xml已经显示到窗口上了。
ensureSubDecor
private void ensureSubDecor() {
//先判断是否已安装过 mSubDecor
if (!mSubDecorInstalled) {
//创建 DecorView,我们稍后来看这个方法的实现
mSubDecor = createSubDecor();
//这里是判断是否有title,暂且按下不表,我们继续看后面的方法
// 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 (!mIsDestroyed && (st == null || st.menu == null)) {
invalidatePanelMenu(FEATURE_SUPPORT_ACTION_BAR);
}
}
}
这个方法暴露的有效信息不多,但是逻辑在这里有一个闭环,即先判断onSubDecorInstalled是否为false,随后在执行完createSubDecor和onSubDecorInstalled方法之后,将onSubDecorInstalled置为了true。为了阅读源码方便,我们提炼一下有效代码:
if (!mSubDecorInstalled) {
mSubDecor = createSubDecor();
applyFixedSizeWindow();
onSubDecorInstalled(mSubDecor);
mSubDecorInstalled = true;
}
所以,下一步,我们来看一下createSubDecor和onSubDecorInstalled。
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
ensureWindow();
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;
}
从第一行到第23行,这一段看着是不是很熟悉?我们在自定义View里获取自定义属性的时候经常见到,与我们的主线无关,略过。 第26,27这2行有专门的注释:确保window已经安装好了decor
而33行开始一直到119行,这么大的if~else代码块,只干了一件事情,对subDecor 进行赋值。在121行对 subDecor进行了判空,如果为空,抛出异常,我们的逻辑戛然而止。
在137行,有一句注释:确保window与decor相匹配。
而132行,139行,通过subDecor分别获取了2个view,一个TextView(R.id.title),一个ContentFrameLayout(R.id.action_bar_activity_content)
142行,从window中获取windowContentView (R.id.content)
重点来了!从143行开始,先判断windowContentView 有无子view,如果有,把所有的子view剪贴到subDecor的content上。然后把content的布局id改为R.id.content。这一步执行之后,window的content上没有了子view。
165行,把subdecor添加到window上。
最后,返回subdecor。
把这个方法精简一下,有效代码如下:
private ViewGroup createSubDecor() {
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
//省略次要代码
a.recycle();
//确保 window 已经安装了 decor
ensureWindow();
mWindow.getDecorView();
final LayoutInflater inflater = LayoutInflater.from(mContext);
ViewGroup subDecor = null;
//省略 subDecor 的赋值以及 判空
if (mDecorContentParent == null) {
mTitleView = (TextView) subDecor.findViewById(R.id.title);
}
//确保 subDecor 与window 相统一
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) {
while (windowContentView.getChildCount() > 0) {
final View child = windowContentView.getChildAt(0);
windowContentView.removeViewAt(0);
contentView.addView(child);
}
windowContentView.setId(View.NO_ID);
contentView.setId(android.R.id.content);
}
mWindow.setContentView(subDecor);
return subDecor;
}
applyFixedSizeWindow()
方法相对比较简单,获取到R.id.content,随后设置了内边距,然后重新布局 requestLayout,这一部分与我们的主线关联不大,按下不表。
private void applyFixedSizeWindow() {
ContentFrameLayout cfl = (ContentFrameLayout) mSubDecor.findViewById(android.R.id.content);
// This is a bit weird. In the framework, the window sizing attributes control
// the decor view's size, meaning that any padding is inset for the min/max widths below.
// We don't control measurement at that level, so we need to workaround it by making sure
// that the decor view's padding is taken into account.
final View windowDecor = mWindow.getDecorView();
cfl.setDecorPadding(windowDecor.getPaddingLeft(),
windowDecor.getPaddingTop(), windowDecor.getPaddingRight(),
windowDecor.getPaddingBottom());
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
a.getValue(R.styleable.AppCompatTheme_windowMinWidthMajor, cfl.getMinWidthMajor());
a.getValue(R.styleable.AppCompatTheme_windowMinWidthMinor, cfl.getMinWidthMinor());
if (a.hasValue(R.styleable.AppCompatTheme_windowFixedWidthMajor)) {
a.getValue(R.styleable.AppCompatTheme_windowFixedWidthMajor,
cfl.getFixedWidthMajor());
}
if (a.hasValue(R.styleable.AppCompatTheme_windowFixedWidthMinor)) {
a.getValue(R.styleable.AppCompatTheme_windowFixedWidthMinor,
cfl.getFixedWidthMinor());
}
if (a.hasValue(R.styleable.AppCompatTheme_windowFixedHeightMajor)) {
a.getValue(R.styleable.AppCompatTheme_windowFixedHeightMajor,
cfl.getFixedHeightMajor());
}
if (a.hasValue(R.styleable.AppCompatTheme_windowFixedHeightMinor)) {
a.getValue(R.styleable.AppCompatTheme_windowFixedHeightMinor,
cfl.getFixedHeightMinor());
}
a.recycle();
cfl.requestLayout();
}
onSubDecorInstalled()
void onSubDecorInstalled(ViewGroup subDecor) {}
一个空方法,原来这就是惊喜啊!
写在后面的话
setContentView 作为Activity的入口,其地位不可谓不重要。其中涉及的Actvity,window,DecorView 三者的关系更是在面试中常被问及。理解了setContentView,对于Activity的理解也就更近了一步。
留一个小尾巴:LayoutInflater.from(mContext).inflate(resId, contentParent) 我们下篇文章再见!
转载自:https://juejin.cn/post/7000916643136143391