Android窗口机制之由setContentView引发的Window,PhoneWindow,DecorView源码理解
由上一篇文章Activity启动流程源码分析,Activity启动完成最终调用了ActivityThread.handleLaunchActivity->ActivityThread.performLaunchActivity然后回调Activity.onCreate,所以,接着由setContentView引出的Window,PhoneWindow,DecorView源码理解,最近也看了好多相关的文章,记录自己的见解:
从上面的类图看:
Window是个抽象类,定义一些顶层窗口的行为策略,而Window的实现类是PhoneWindow;
DecorView其实是整个Activity窗口的装饰类吧,继承FrameLayout;
PhoneWindow会持有一个DecorView和mContentParent,mContentParent是DecorView的直接ViewGroup的contentView,即:用来显示的主要内容,不包括title之类的。
第一次画图,不是很规范,将就着看吧,我们从Activity的setContentView方法开始:
public void setContentView(@LayoutRes int layoutResID) {
Window window = getWindow();
window.setContentView(layoutResID);
initWindowDecorActionBar();
}
Activity的setContentView方法,通过layoutResID设置Activity的主体内容,不包括标题栏状态栏,这个资源将被填充至activity的顶层view也就是DecorView的contentView中,而这个方法会直接调用Window.setContentView方法,而Window是个抽象类,所以我们看Window的实现类PhoneWindow的setContentView方法:
public void setContentView(int layoutResID) {
if (mContentParent == null) {
//创建DecorView,并内容布局赋值到PhoneWindow的mContentParent上
//也就是说mContentParent是DecorView的直接子View,然后通过将layoutResID的布局有填充的mContentParent上
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
/**
* 经过上面的步骤我们已经获取了DecorView中contectView,那么接下来就是就要将传进来的layoutResID填充contentView的布局了
*/
//是否有过度动画
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
//mContentParent是DecorView的中contentView,就是DecorView的孙子,将layoutResID填充到mContentParent中
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
//回调通知表示完成界面加载
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
整个流程就是先判断mContentParent是否为null,就调用installDecor方法去初始化DecorView,mContentParent是DecorView的内容ViewGroup,如果mContentParent填充完就把layoutResID调用inflate方法将layoutResID的布局填充至mContentParent,刚刚说了installDecor初始化DecorView,我们来看看installDecor方法。
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
//调用该方法创建new一个DecorView
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
//DecorView持有PhoneWindow
mDecor.setWindow(this);
}
//一开始DecorView未加载到mContentParent,所以此时mContentParent=null
if (mContentParent == null) {
//该方法将mDecorView添加到Window上绑定布局
/**
* Decorview需要填充自己的直接布局有可能是LinearLayout或者FrameLayout,
* 然后通过ID_ANDROID_CONTENT获取contentView,最后返回,将contentview复制给phonewindow的mContentParent
*/
mContentParent = generateLayout(mDecor);
// 省略一万行代码................
}
有installDecor方法内容较多,省略了一些,在installDecor方法中,如果mDecor 为null的话就会调用generateDecor方法创建一个DecorView并返回,上面说到mContentParent 是DecorView的内容ViewGroup,所以在如果mContentParent 为null的话将调用generateLayout方法生成mContentParent ,接下来就重点看看generateLayout方法,因为setContentView的大部分内容都在这里做了, Decorview需要填充自己的内容布局有可能是LinearLayout或者FrameLayout,最后返回,将contentview复制给phonewindow的mContentParent然后通过ID_ANDROID_CONTENT获取contentView,代码有点多。
protected ViewGroup generateLayout(DecorView decor) {
//省略............很多代码就看DecorView的布局选择填充............
//填充窗口的装饰
// Inflate the window decor.
int layoutResource;
//根据features选择填充布局
int features = getLocalFeatures();
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
setCloseOnSwipeEnabled(true);
} else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
//悬浮FloatButton
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();
//layoutResource Decorview需要填充自己的内容布局有可能是LinearLayout或者FrameLayout,然后将布局addView给自己
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
//获取真正的contenview
ViewGroup contentParent = (ViewGroup) findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) {
ProgrsetyessBar progress = getCircularProgressBar(false);
if (progress != null) {
progress.setIndeterminate(true);
}
}
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
registerSwipeCallbacks(contentParent);
}
// Remaining setup -- of background and title -- that only applies
// to top-level windows.
if (getContainer() == null) {
final Drawable background;
if (mBackgroundResource != 0) {
background = getContext().getDrawable(mBackgroundResource);
} else {
background = mBackgroundDrawable;
}
mDecor.setWindowBackground(background);
final Drawable frame;
if (mFrameResource != 0) {
frame = getContext().getDrawable(mFrameResource);
} else {
frame = null;
}
mDecor.setWindowFrame(frame);
mDecor.setElevation(mElevation);
mDecor.setClipToOutline(mClipToOutline);
if (mTitle != null) {
setTitle(mTitle);
}
if (mTitleColor == 0) {
mTitleColor = mTextColor;
}
setTitleColor(mTitleColor);
}
mDecor.finishChanging();
return contentParent;
}
别看代码多实际上是根据features选择填充布局,这就是为什么我们要在setContentCiew之前调用requestWindowFeature的原因,先看看DecorView的直接子ViewGroup的布局长什么样:
<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"
android:theme="?attr/actionBarTheme" />
<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>
当然根ViewGroup在不仅仅是LinearLayout ,还有FrameLayout和其他的,这些没什么好说的,就这说填充DecorView的布局吧, Decorview需要填充自己的内容布局有可能是LinearLayout或者FrameLayout,然后调DecorViewon的ResourcesLoaded方法将选择的布局addView给DecorView自己,然后通过findViewById方法找到contentParent,并返回赋值给PhoneWindow的mContentParent,接着回到PhoneWindow的setContentView中将我们从Activity的setContentView传进来的layoutResID,填充到mContentParent,也就是DecorView的内容ViewGroup中,整个流程就完成了布局的填充。
最后看一下setContentView的时序图:
小结
- Window是一个抽象类,提供了各种窗口操作的方法,比如设置背景标题ContentView等等;
- PhoneWindow则是Window的唯一实现类,它里面实现了Window各种各种方法,添加背景主题ContentView等方法,内部通过DecorView来添加顶级视图
每一个Activity上面都有一个Window,可以通过getWindow获取; - DecorView,顶级视图,继承与FramentLayout,setContentView则是添加在它里面的@id/content里
- setContentView里面创建了DecorView,根据Theme,Feature添加了对应的布局文件,当setContentView设置显示后会回调Activity的onContentChanged方法;