一、PhoneWindow和Window的关系
PhoneWindow是com.view.Window这个抽象类的唯一具体实现类,它从更高级别的层次上描述了一个窗口的特性。
1)当开发者通过WindowManager、LayoutParams以及控件树创建一个窗口时,需要手动初始化LayoutParams以及自行构建控件树。虽说这种方式已经比直接使用WMS、IWindow与Surface这些更底层的方式进步不少,但是仍显繁琐。现今窗口中的内容往往都有不成文的规范,比如在指定的位置有标题栏、动作栏、图标等,手动创建符合这些规范的控件树绝不是一件令人愉快的事情。为此Android提供了com.view.Window类,以便在更高的级别上操作窗口。
2)那么Window类究竟负责做什么呢?Window类中有三个最为核心的组件:WindowManager.LayoutParams、一棵控件树以及Window.Callback。因此它的作用也要从这三个组件说起。
-
a)WindowManager.LayoutParams参数
Window类提供了一系列set方法用于设置LayoutParams属性。这项工作看似没什么用处,还不如开发者自行管理LayoutParams来的方便。不过Window类的优势在于它可以根据用例初始化Layoutparams中的属性,例如窗口令牌、窗口名称以及FLAG等。 -
b)Window所包含的控件树
Window为使用者提供了多种多样的控件树模板。这些模板可以为窗口提供形式多样却风格统一的展示方式以及辅助功能,例如标题栏、图标、顶部进度条、动作栏等,甚至还为其嵌套在模板的合适位置。这一模板就是最终显示时的窗口外观。Window类提供了接口用于模板选择、指定期望显示的内容以及修改模板如标题、图标、进度条等常见的属性。 -
c)Window.Callback接口
Window的使用者可以实现这个接口并注册到Window中,于是每当窗口中发生变化时都能收到通知。可以获得的通知内容有输入事件、窗口属性变更、菜单的弹出/选择等。
简单来说,Window类就是一个模板,它大大简化了一个符合使用习惯的控件树的创建过程。使得使用者只需要关注控件树中真正感兴趣的部分,并且仅需少量的工作就可以使这部分嵌套在一个漂亮且专业的窗口外观下,而不用关心这一窗口外观的控件树的构成。另外由于Window类是一个抽象类,因此使用不同的Window类的实现时还可以在不修改应用程序原有逻辑的情况下提供完全不同的窗口外观。
PhoneWindow时Window的唯一实现类,Window抽象类中提供了用于修改LayoutParams的接口等通用功能的实现,而PhoneWindow类则负责具体的外观模板的实现。
二、选择Activity对应的窗口外观
我们经常会在Activity的onCreate方法中调用requestWindowFeature方法来设置一些窗口属性,比如设置Activity所对应的窗口是否拥有标题栏。换言之,Activity的requestWindowFeature方法决定了窗口的外观模板。
1、来看下Activity的requestWindowFeature方法。
/frameworks/base/core/java/android/app/Activity.java
public class Activity extends ContextThemeWrapper
implements LayoutInflater.Factory2,
Window.Callback, KeyEvent.Callback,
OnCreateContextMenuListener, ComponentCallbacks2,
Window.OnWindowDismissedCallback, WindowControllerCallback,
AutofillManager.AutofillClient {
//直接将请求转发给Window
public final boolean requestWindowFeature(int featureId) {
return getWindow().requestFeature(featureId);
}
}
通过上面的代码可以知道,当我们在Activity的onCreate方法中调用requestWindowFeature方法的时候,最终Activity是把请求转发给Window来完成的,而PhoneWindow是Window的唯一具体实现类。
2、PhoneWindow的requestWindowFeature方法:
/frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java
@Override
public boolean requestFeature(int featureId) {
if (mContentParentExplicitlySet) {
throw new AndroidRuntimeException("requestFeature() must be called before adding content");
}
final int features = getFeatures();
final int newFeatures = features | (1 << featureId);
if ((newFeatures & (1 << FEATURE_CUSTOM_TITLE)) != 0 &&
(newFeatures & ~CUSTOM_TITLE_COMPATIBLE_FEATURES) != 0) {
throw new AndroidRuntimeException(
"You cannot combine custom titles with other title features");
}
if ((features & (1 << FEATURE_NO_TITLE)) != 0 && featureId == FEATURE_ACTION_BAR) {
return false; // Ignore. No title dominates.
}
if ((features & (1 << FEATURE_ACTION_BAR)) != 0 && featureId == FEATURE_NO_TITLE) {
// Remove the action bar feature if we have no title. No title dominates.
removeFeature(FEATURE_ACTION_BAR);
}
if ((features & (1 << FEATURE_ACTION_BAR)) != 0 && featureId == FEATURE_SWIPE_TO_DISMISS) {
throw new AndroidRuntimeException(
"You cannot combine swipe dismissal and the action bar.");
}
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0 && featureId == FEATURE_ACTION_BAR) {
throw new AndroidRuntimeException(
"You cannot combine swipe dismissal and the action bar.");
}
if (featureId == FEATURE_INDETERMINATE_PROGRESS &&
getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) {
throw new AndroidRuntimeException("You cannot use indeterminate progress on a watch.");
}
return super.requestFeature(featureId);
}
PhoneWindow的requestWindowFeature方法在进行一系列验证之后,会继续调用父类Window的requestWindowFeature方法。
3、Window的requestWindowFeature方法
/frameworks/base/core/java/android/view/Window.java
public boolean requestFeature(int featureId) {
//与处理触控点ID的方式类似,Window以bit的方式存储feature列表
final int flag = 1<<featureId;
//将feature添加到mFeatures成员中
mFeatures |= flag;
mLocalFeatures |= mContainer != null ? (flag&~mContainer.mFeatures) : flag;
return (mFeatures&flag) != 0;
}
通过以上代码可以发现,设置窗口特性的工作十分简单,窗口特性最终被保存在Window.mFeatures成员之中。requestFeature()方法并没有立刻创建外观模板,但是mFeatures成员将会为创建外观模板提供依据。
三、设置Activity对应的显示内容
在Activity的onCreate方法中调用完requestWindowFeature方法之后,一般还需要调用setContentView来设置Activity所对应的布局文件。
1、来看下Activity的setContentView方法。
public class Activity extends ContextThemeWrapper
implements LayoutInflater.Factory2,
Window.Callback, KeyEvent.Callback,
OnCreateContextMenuListener, ComponentCallbacks2,
Window.OnWindowDismissedCallback, WindowControllerCallback,
AutofillManager.AutofillClient {
//直接将请求转发给Window
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
}
Activity的setContentView方法也是直接转发给了PhoneWindow。
2、继续来看PhoneWindow的setContentView方法:
@Override
public void setContentView(int layoutResID) {
//首先为窗口准备外观模板
if (mContentParent == null) {
//当mContentParent为null时,表明外观模板尚未创建,此时通过installDecor方法创建一个外观模板。
//并且会让mContentParent和外观模板中id为R.id.content的ViewGroup关联起来,以便其作为新的控件树的父控件
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
//倘若外观模板已经创建,则清空mContentParent的子控件,使其准备好作为新的控件树的父控件
mContentParent.removeAllViews();
}
//窗口视图内容是否是透明的
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
//将使用者给定的layout实例化为一棵控件树,然后作为子控件保存在mContentParent中
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
//Callback接口被Window用来向使用者通知其内部所发生的变化。
//作为Window的使用者,Activity类实现了这一接口,因此开发者可以通过重写Activity的这一方法从而对这些变化做出反应
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
通过上面代码可以发现,Window控件树的创建时在Window的setContentView方法中完成的。其创建过程分为创建外观模板,以及将Activity所对应的控件树添加到模板中两个步骤。在完成控件树的创建后,Window会通过Callback接口将这一变化通知给其使用者(Activity)。
实例化使用者所提供的控件树并没有什么可讨论的地方,这里我们更关心的是PhoneWindow是如何创建外观模板的,而创建外观模板的关键就在PhoneWindow#的installDecor方法中。
3、来看下PhoneWindow的installDecor方法:
//构建DecorView视图对象
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
//调用generateDecor方法创建DecorView根控件
mDecor = generateDecor(-1);
//设置根控件的焦点优先级为子控件优先
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
//设置mDecor为RootNamespace,这个时让根控件区别于其他控件的一个重要手段,在进行焦点控件查找的时候有用
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
//让DecorView持有当前PhoneWindow的引用
mDecor.setWindow(this);
}
//生成外观模板
if (mContentParent == null) {
//根控件mDecor创建完成后,mDecor之中没有任何内容的,
//generateLayout方法将会完成外观模板的创建,并将自己作为子控件添加到mDecor中
//而且还会将mContentParent和DecorView中id为com.android.internal.R.id.content的FrameLayout控件进行关联
mContentParent = generateLayout(mDecor);
...代码省略...
} else {
//获取标题
mTitleView = findViewById(R.id.title);
if (mTitleView != null) {
if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
final View titleContainer = findViewById(R.id.title_container);
if (titleContainer != null) {
titleContainer.setVisibility(View.GONE);
} else {
mTitleView.setVisibility(View.GONE);
}
mContentParent.setForeground(null);
} else {
mTitleView.setText(mTitle);
}
}
}
...代码省略...
}
}
}
在此方法中出现了另一个十分重要的成员mDecor,它是整个控件树的根控件,它是一个由generaeDecor方法创建的类型为DecorView的一个ViewGroup。
4、generateDecor方法如下所示:
//构建DecorView视图对象
protected DecorView generateDecor(int featureId) {
//获取设备上下文
Context context;
if (mUseDecorContext) {
Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {
context = getContext();
} else {
context = new DecorContext(applicationContext, getContext());
if (mTheme != -1) {
context.setTheme(mTheme);
}
}
} else {
context = getContext();
}
//创建DecorView实例对象
return new DecorView(context, featureId, this, getAttributes());
}
DecorView是继承自FrameLayout的一个控件类,它除了担当一个ViewGroup应有的责任外,还和PhoneWindow有非常紧密的联系。
在创建完DecorView之后,还会继续调用generateLayout方法进行外观模板的创建。外观模板的创建时一个非常繁琐的过程,因此不仅受前文所述窗口特性的影响,而且还需要考虑窗口的样式,Android的版本等。generateLayout会首先对这些因素进行集中解析。
5、先看下generateLayout方法的前半部分
protected ViewGroup generateLayout(DecorView decor) {
//首先解析窗口样式表。所谓样式其实时定义在资源系统中的一个xml文件,指定了窗口各种各样的属性。
//比如窗口时浮动(对话框)还是全屏(Activity),最小尺寸,是否具有标题栏,是否显示壁纸等。
//这些属性设置一部分影响了前文所提到的窗口特性(如标题栏),
// 一部分影响了窗口的LayoutParams中的属性(如是否浮动,是否显示壁纸等)
//还有一部分影响了控件树的工作方式(如最小尺寸,它会影响根控件DecorView的测量)
TypedArray a = getWindowStyle();//获取窗口的样式表并存储在变量a中
//首先以检查样式表中是否定义窗口为浮动窗口(非全屏)为例,这个样式影响了LayoutParams中的属性
mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR)
& (~getForcedWindowFlags());
if (mIsFloating) {
//对于浮动窗口来说,其LayoutParams.width/height必须时WRAP_CONTENT
setLayout(WRAP_CONTENT, WRAP_CONTENT);
//并且LayoutParams.flags中的IN_SCREEN和INSET_DECOR标记必须被移除。
// 因为浮动窗口在布局的时候不能被状态栏导航栏遮挡
setFlags(0, flagsToUpdate);
} else {
//对非浮动窗口来说,其LayoutParams.width/height将保持默认的MATCH_PARENT并且需要增加IN_SCREEN和INSET_DECOR标记
setFlags(FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
}
//然后检查样式表中是否定义了无标题栏或拥有动作栏两个样式。这两个样式影响了窗口的特性
if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {//是否没有标题
requestFeature(FEATURE_NO_TITLE);
} else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {//是否拥有ActionBar
requestFeature(FEATURE_ACTION_BAR);
}
//继续进行其他样式的检查,同样会引发修改LayoutParams以及窗口特性等操作
if (a.getBoolean(R.styleable.Window_windowActionBarOverlay, false)) {
requestFeature(FEATURE_ACTION_BAR_OVERLAY);
}
if (a.getBoolean(R.styleable.Window_windowActionModeOverlay, false)) {
requestFeature(FEATURE_ACTION_MODE_OVERLAY);
}
if (a.getBoolean(R.styleable.Window_windowSwipeToDismiss, false)) {
requestFeature(FEATURE_SWIPE_TO_DISMISS);
}
if (a.getBoolean(R.styleable.Window_windowFullscreen, false)) {//是否隐藏状态栏全屏显示
setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN & (~getForcedWindowFlags()));
}
if (a.getBoolean(R.styleable.Window_windowTranslucentStatus,//状态栏是否透明
false)) {
setFlags(FLAG_TRANSLUCENT_STATUS, FLAG_TRANSLUCENT_STATUS
& (~getForcedWindowFlags()));
}
if (a.getBoolean(R.styleable.Window_windowTranslucentNavigation,//导航栏是否透明
false)) {
setFlags(FLAG_TRANSLUCENT_NAVIGATION, FLAG_TRANSLUCENT_NAVIGATION
& (~getForcedWindowFlags()));
}
if (a.getBoolean(R.styleable.Window_windowOverscan, false)) {
setFlags(FLAG_LAYOUT_IN_OVERSCAN, FLAG_LAYOUT_IN_OVERSCAN & (~getForcedWindowFlags()));
}
if (a.getBoolean(R.styleable.Window_windowShowWallpaper, false)) {
setFlags(FLAG_SHOW_WALLPAPER, FLAG_SHOW_WALLPAPER & (~getForcedWindowFlags()));
}
if (a.getBoolean(R.styleable.Window_windowEnableSplitTouch,
getContext().getApplicationInfo().targetSdkVersion
>= android.os.Build.VERSION_CODES.HONEYCOMB)) {
setFlags(FLAG_SPLIT_TOUCH, FLAG_SPLIT_TOUCH & (~getForcedWindowFlags()));
}
/**
* 检查样式中是否定义了最小宽度。这张样式影响了DecorView测量时的计算。因此其效果并不会体现在这里。
* 将样式中的值保存在mMinWidthMajor/Minor成员中,并在需要的时候使用。
* 其中Major的含义是在横屏情况下的最小宽度,而Minor则是在竖屏情况下的最小宽度。
*/
a.getValue(R.styleable.Window_windowMinWidthMajor, mMinWidthMajor);
a.getValue(R.styleable.Window_windowMinWidthMinor, mMinWidthMinor);
if (DEBUG) Log.d(TAG, "Min width minor: " + mMinWidthMinor.coerceToString()
+ ", major: " + mMinWidthMajor.coerceToString());
if (a.hasValue(R.styleable.Window_windowFixedWidthMajor)) {
if (mFixedWidthMajor == null) mFixedWidthMajor = new TypedValue();
a.getValue(R.styleable.Window_windowFixedWidthMajor,
mFixedWidthMajor);
}
if (a.hasValue(R.styleable.Window_windowFixedWidthMinor)) {
if (mFixedWidthMinor == null) mFixedWidthMinor = new TypedValue();
a.getValue(R.styleable.Window_windowFixedWidthMinor,
mFixedWidthMinor);
}
if (a.hasValue(R.styleable.Window_windowFixedHeightMajor)) {
if (mFixedHeightMajor == null) mFixedHeightMajor = new TypedValue();
a.getValue(R.styleable.Window_windowFixedHeightMajor,
mFixedHeightMajor);
}
if (a.hasValue(R.styleable.Window_windowFixedHeightMinor)) {
if (mFixedHeightMinor == null) mFixedHeightMinor = new TypedValue();
a.getValue(R.styleable.Window_windowFixedHeightMinor,
mFixedHeightMinor);
}
if (a.getBoolean(R.styleable.Window_windowContentTransitions, false)) {
requestFeature(FEATURE_CONTENT_TRANSITIONS);
}
if (a.getBoolean(R.styleable.Window_windowActivityTransitions, false)) {
requestFeature(FEATURE_ACTIVITY_TRANSITIONS);
}
mIsTranslucent = a.getBoolean(R.styleable.Window_windowIsTranslucent, false);
//接下来是影响外观模板的另外一种因素,即Android的版本
final Context context = getContext();
final int targetSdk = context.getApplicationInfo().targetSdkVersion;
final boolean targetPreHoneycomb = targetSdk < android.os.Build.VERSION_CODES.HONEYCOMB;
final boolean targetPreIcs = targetSdk < android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH;
final boolean targetPreL = targetSdk < android.os.Build.VERSION_CODES.LOLLIPOP;
final boolean targetHcNeedsOptions = context.getResources().getBoolean(
R.bool.target_honeycomb_needs_options_menu);
final boolean noActionBar = !hasFeature(FEATURE_ACTION_BAR) || hasFeature(FEATURE_NO_TITLE);
if (targetPreHoneycomb || (targetPreIcs && targetHcNeedsOptions && noActionBar)) {
/**
* 在Honeycomb之前的版本中,选项菜单的呼出动作由菜单键完成,因此需要选项菜单时需要导航栏提供虚拟的菜单键。
* 将NEEDS_MENU_KEY标记放入LayoutParams中,当此窗口处于焦点状态时,WSM会向SystemUI请求显示虚拟菜单键
*/
setNeedsMenuKey(WindowManager.LayoutParams.NEEDS_MENU_SET_TRUE);
} else {
/**
* 而在Honeycomb之后的版本中,选项菜单由动作栏中的菜单按钮完成,因此需要将标记移除
*/
setNeedsMenuKey(WindowManager.LayoutParams.NEEDS_MENU_SET_FALSE);
}
...代码省略...
//创建外观模板的代码
}
generateLayout方法的前半部分,在创建外观模板之前会先解析样式表以及Android的版本,再根据这些因素修改LayoutParams中的设置,申请或删除窗口特性,或者保存一些信息以备后用。这部分代码体现了使用Window创建控件树以及管理LayoutParams的优越性。使用者仅需声明其所需的样式,而具体的工作都是Window完成的。事实上,窗口所需的样式往往已经被Android事先定义好了,因此再默认情况下使用者甚至都不用关心样式的存在。
6、继续来看generateLayout方法的后半部分,后半部分主要是进行创建窗口外观的工作
protected ViewGroup generateLayout(DecorView decor) {
//解析样式表和Android版本的代码
...代码省略...
if (!mForcedStatusBarColor) {
//状态栏颜色
mStatusBarColor = a.getColor(R.styleable.Window_statusBarColor, 0xFF000000);
}
if (!mForcedNavigationBarColor) {
//导航栏颜色
mNavigationBarColor = a.getColor(R.styleable.Window_navigationBarColor, 0xFF000000);
//导航栏分割线颜色
mNavigationBarDividerColor = a.getColor(R.styleable.Window_navigationBarDividerColor,
0x00000000);
}
...代码省略...
/**
* 首先是选择合适的外观模板,构建视图内容
* 所有的窗口外观模板都已经被定义再系统资源中。
* 后续部分的代码就是根据窗口特性来选择一个合适的外观模板的资源id。
* layoutResource变量保存了选择的结果。
*/
int layoutResource;
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) {
//是否是悬浮窗
if (mIsFloating) {
//如果是悬浮窗,创建弹窗外观模板
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleIconsDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
// 如果不是悬浮窗,则创建带有自定义标题的外观模板
layoutResource = R.layout.screen_title_icons;
}
//移除支持actionbar的属性
removeFeature(FEATURE_ACTION_BAR);
} else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
&& (features & (1 << FEATURE_ACTION_BAR)) == 0) {
//带有进度条的外观模板
layoutResource = R.layout.screen_progress;
} else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
//是否是悬浮窗
if (mIsFloating) {
//如果是悬浮窗,创建弹窗外观模板
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogCustomTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
// 如果不是悬浮窗,创建带有自定义标题的外观模板
layoutResource = R.layout.screen_custom_title;
}
//移除支持actionbar的属性
removeFeature(FEATURE_ACTION_BAR);
} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
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;
}
} else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
layoutResource = R.layout.screen_simple_overlay_action_mode;
} else {
layoutResource = R.layout.screen_simple;
}
mDecor.startChanging();
//构建layoutResource所对应的控件树并添加到DecorView中,这便是最终的外观模板
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
//从外观模板控件树种获取作为使用者控件树的父控件contentParent ,contentParent 的id是ID_ANDROID_CONTENT。无论那种模板,都必须存在一个拥有此Id的ViewGroup。否则generateLayout将会抛出异常
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
...代码省略...
return contentParent;
总的来说,PhoneWindow的generateLayout方法花费了较大的精力根据各种窗口特性,最终会选择一个系统定义好的外观模板的布局资源,然后将其实例化后作为子控件添加到根控件DecorView种。
网友评论