Window表示一个窗口的概念,在日常开发中直接接触Window的机会并不多,但却会经常用到Window,activity
、toast
、dialog
、PopupWindow
、状态栏等都是Window。在Android中Window是个抽象类,并且仅有一个实现类PhoneWindow
。
1、Window
Android中,Window有应用Window、子Window及系统Window三种类型,分别对应不同的层级范围,层级越高,显示越靠前,这里的“靠前”是指层级大的Window会覆盖在层级小的Window上面。
- 应用Window:对应层级范围是1~99,每个activity就对应一个应用Window,如果在activity中创建了一个应用Window,那么当跳转到另外一个Activity时,该Window会被覆盖。应用Window的高度不受状态栏影响。
- 子Window:对应层级范围是1000~1999,PopupWindow默认就是一个子Window(可以修改PopupWindow的Window类型),如果在activity中创建了一个子Window,那么当跳转到另外一个Activity时,该Window也会被覆盖。子Window的高度受状态栏影响。
- 系统Window:对应层级范围是2000~2999,
toast
、状态栏等都是系统Window,如果创建了一个系统Window,那么只有当该应用被销毁时,该Window才被会关闭(排除主动关闭),所以可以用系统Window实现像360那样的悬浮小球。系统Window需要设置<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
权限,否则会抛异常,在6.0以上需要动态申请。系统Window的高度不受状态栏影响。
前面说了Window的层级,下面就来看一个示例。
//代码参考了PopupWindow的源代码。
private void startWindow() {
//拿到activity中的wm对象,在attach中创建,是一个WindowManagerImpl对象
wm = (WindowManager) this.getSystemService(Context.WINDOW_SERVICE);
frame = new PopupDecorView(this);
frame.setLayoutParams(new ActivityzhoLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
View view = View.inflate(this, R.layout.window_layout, null);
Button bt = view.findViewById(R.id.window_layout_button);
bt.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
dismiss();
}
});
//重新设置WindowManager.LayoutParams的值
WindowManager.LayoutParams p = createPopupLayoutParams(frame.getWindowToken());
frame.addView(view);
wm.addView(frame, p);
}
private LayoutParams createPopupLayoutParams(IBinder windowToken) {
final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
//设置Window gravity。gravity 表示居中,top表示位于顶部
p.gravity = Gravity.CENTER|Gravity.TOP;
p.flags = computeFlags(p.flags);
//设置Window的类型,其实这里我们也可以设置1~99、1000~1999、2000~2999之间的任意数字
p.type = LayoutParams.TYPE_APPLICATION;
//设置Window Token
p.token = windowToken;
//设置输入法模式
p.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED;
//设置Window动画
p.windowAnimations = 0;
//设置Window像素格式
p.format = PixelFormat.TRANSLUCENT;
// Used for debugging.
p.setTitle("PopupWindow:" + Integer.toHexString(hashCode()));
//设置Window宽
p.width = LayoutParams.MATCH_PARENT;
//设置Window高
p.height = LayoutParams.WRAP_CONTENT;
return p;
}
private int computeFlags(int curFlags) {
curFlags &= ~(
WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES |
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE |
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH |
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS |
WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM |
WindowManager.LayoutParams.FLAG_SPLIT_TOUCH);
curFlags |= WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES;
return curFlags;
}
//关闭Window
private void dismiss() {
wm.removeView(frame);
}
private class PopupDecorView extends FrameLayout {
public PopupDecorView(Context context) {
super(context);
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
if (getKeyDispatcherState() == null) {
return super.dispatchKeyEvent(event);
}
if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
final KeyEvent.DispatcherState state = getKeyDispatcherState();
if (state != null) {
state.startTracking(event, this);
}
return true;
} else if (event.getAction() == KeyEvent.ACTION_UP) {
final KeyEvent.DispatcherState state = getKeyDispatcherState();
if (state != null && state.isTracking(event) && !event.isCanceled()) {
dismiss();
return true;
}
}
return super.dispatchKeyEvent(event);
} else {
return super.dispatchKeyEvent(event);
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// if (mTouchInterceptor != null && mTouchInterceptor.onTouch(this, ev)) {
// return true;
// }
return super.dispatchTouchEvent(ev);
}
}
复制代码
WindowManager.LayoutParams
用于描述Window的参数,关于其详细参数可以参考Android Activity应用窗口的创建过程分析这篇文章。 先来看WindowManager.LayoutParams
的参数type
,也就是Window类型。下面来看不同Window类型的显示效果
粉色部分就是创建的Window,可以看出系统及应用Window不受状态栏影响,而子Window却因为状态栏导致按钮超出Window范围。所以可以认为子Window的高度被被状态栏占去一部分,而其他类型Window则不受此影响,让WIndow居中时,子Window在手机中的位置也会比其他类型Window的位置高一些,这里就不验证了,至于子Window为什么在状态栏的下面,那是因为状态栏的层级比子Window层级要高。
WindowManager.LayoutParams
的flags
也是一个非常重要的参数,由于类型比较多,这里就主要介绍以下几个类型。
- FLAG_NOT_TOUCH_MODAL:在此模式下,系统会将当前Window区域以外的单击事件传递给底层的Window,当前Window区域内的单击事件则自己处理。一般都需要开启此标记
- FLAG_NOT_FOCUSABLE:在此模式下,Window不能获取焦点,也不能接受各种输入事件,此标记会同时开启FLAG_NOT_TOUCH_MODAL,最终事件会直接传递给下层的具有焦点的Window。所以如果Window中有EditText等输入控件时,就不应该启用此标记。
- FLAG_SHOW_WHEN_LOCKED:开启此模式可以让Window显示在锁屏的界面。
WindowManager.LayoutParams
中比较常用的参数就上面两个,当然也可以设置Window的宽高、动画、token等等,这里就不一一叙述了。 从上面示例可以看出,Window并不实际存在,它是以一个View的形式展示在屏幕上。
2、WindowManager
WindowManager
的主要功能是提供简单的API使得使用者可以方便地将一个View作为一个窗口添加到系统中,它是一个接口,继承自ViewManager
接口,ViewManager
接口比较简单,只有以下三个方法。
public interface ViewManager
{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}
复制代码
从方法名也可以看出对Window的增删改就是针对View的增删改。方法虽然只有三个,但已经完全够用了。WindowManager
的具体实现是WindowManagerImpl
。
public final class WindowManagerImpl implements WindowManager {
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
private final Context mContext;
//父Window
private final Window mParentWindow;
private IBinder mDefaultToken;
...
//添加View
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
//更新View
@Override
public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.updateViewLayout(view, params);
}
...
//异步移除View
@Override
public void removeView(View view) {
mGlobal.removeView(view, false);
}
//同步移除View
@Override
public void removeViewImmediate(View view) {
mGlobal.removeView(view, true);
}
...
}
复制代码
这里采用了代理模式,将所有操作交给WindowManagerGlobal
来执行。首先来看Window的添加。
2.1、添加Window
在前面的例子中可以看到,创建一个Window就是向WindowManagerImpl
中添加一个View,而WindowManagerImpl
又将操作交给了WindowManagerGlobal
来处理,下面就来看看WindowManagerGlobal
中addView
的实现。
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
//检查参数
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
//拿到Window的宽高、type等布局参数
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
...
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
...
//查找View是否已经存在,WindowManager不允许同一个View被添加两次
int index = findViewLocked(view, false);
if (index >= 0) {
//如果View已在被销毁的列表中,那么就先销毁列表中存在的View
if (mDyingViews.contains(view)) {
// Don't wait for MSG_DIE to make it's way through root's queue.
mRoots.get(index).doDie();
} else {
//很常见的一个异常,表示不能重复添加同一View
throw new IllegalStateException("View " + view
+ " has already been added to the window manager.");
}
// The previous removeView() had not completed executing. Now it has.
}
//如果是子Window则需要先找到它的父View
if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
final int count = mViews.size();
for (int i = 0; i < count; i++) {
if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
panelParentView = mViews.get(i);
}
}
}
//创建一个新的ViewRootImpl
root = new ViewRootImpl(view.getContext(), display);
//给View设置参数
view.setLayoutParams(wparams);
//保存View
mViews.add(view);
//保存ViewRootImpl
mRoots.add(root);
//保存参数
mParams.add(wparams);
//绘制View、添加Window
try {
// 将作为窗口的控件设置给ViewRootImpl。这个动作将导致ViewRootImpl向WMS添加新的窗口、申请Surface以及托管控件在Surface上的重绘动作。这才是真正意义上完成了窗口的添加操作
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
复制代码
在addView
方法中主要做了参数检查、查找子Window的父View、创建ViewRootImpl
对象并通过ViewRootImpl
的setView
方法来实现View的绘制及Window添加操作。下面来看ViewRootImpl
中setView
方法的实现。
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
//保存当前View
mView = view;
...
//保存参数
attrs = mWindowAttributes;
...
//绘制View。
requestLayout();
...
try {
...
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
} catch (RemoteException e) {
....
} finally {
if (restore) {
attrs.restore();
}
}
...
//添加失败
if (res < WindowManagerGlobal.ADD_OKAY) {
mAttachInfo.mRootView = null;
//添加失败
mAdded = false;
mFallbackEventHandler.setView(null);
unscheduleTraversals();
setAccessibilityFocus(null, null);
//返回错误的原因,相比很多错误信息大家都会遇到过
switch (res) {
//token出错
case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not valid; is your activity running?");
case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not for an application");
case WindowManagerGlobal.ADD_APP_EXITING:
throw new WindowManager.BadTokenException(
"Unable to add window -- app for token " + attrs.token
+ " is exiting");
//添加Window已存在
case WindowManagerGlobal.ADD_DUPLICATE_ADD:
throw new WindowManager.BadTokenException(
"Unable to add window -- window " + mWindow
+ " has already been added");
case WindowManagerGlobal.ADD_STARTING_NOT_NEEDED:
// Silently ignore -- we would have just removed it
// right away, anyway.
return;
case WindowManagerGlobal.ADD_MULTIPLE_SINGLETON:
throw new WindowManager.BadTokenException("Unable to add window "
+ mWindow + " -- another window of type "
+ mWindowAttributes.type + " already exists");
//未申请权限,当创建系统Window时是需要申请权限的
case WindowManagerGlobal.ADD_PERMISSION_DENIED:
throw new WindowManager.BadTokenException("Unable to add window "
+ mWindow + " -- permission denied for window type "
+ mWindowAttributes.type);
case WindowManagerGlobal.ADD_INVALID_DISPLAY:
throw new WindowManager.InvalidDisplayException("Unable to add window "
+ mWindow + " -- the specified display can not be found");
//window类型未在1~99,1000~1999,2000~2999这个范围内。
case WindowManagerGlobal.ADD_INVALID_TYPE:
throw new WindowManager.InvalidDisplayException("Unable to add window "
+ mWindow + " -- the specified window type "
+ mWindowAttributes.type + " is not valid");
}
throw new RuntimeException(
"Unable to add window -- unknown error code " + res);
}
...
}
}
}
复制代码
该方法真正意义上完成了View的绘制及Window的添加操作,来看requestLayout
与mWindowSession.addToDisplay
这两个方法。前者主要是申请Surface以及托管控件在Surface上的重绘动作,即View的测量、布局、绘制流程。关于该方法详细内容可以参考Android源码分析之View绘制流程、《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统这两篇文章。后者主要向WindowManagerService(WMS)
添加新的窗口。 总体来说,WindowManagerGlobal
通过父窗口调整了布局参数之后,将新建的ViewRootImpl
、控件以及布局参数保存在mRoots
,mViews
及mParams
这三个数组中,然后将View交给新建的ViewRootImpl
进行处理,从而完成了窗口的添加。 WindowManagerGlobal
管理窗口的原理如下图所示。
2.2、更新Window
相对于添加Window,更新Window就简单很多了,主要是修改布局参数,然后调用ViewRootImpl.setLayoutParams
来更新View。
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
//修改view的布局参数
view.setLayoutParams(wparams);
synchronized (mLock) {
int index = findViewLocked(view, true);
//查找view对应的ViewRootImpl
ViewRootImpl root = mRoots.get(index);
//移除旧的布局参数
mParams.remove(index);
//添加新的布局参数
mParams.add(index, wparams);
//更新布局参数
root.setLayoutParams(wparams, false);
}
}
复制代码
代码还是比较简单的,下面就来看ViewRootImpl
中setLayoutParams
方法的实现。
void setLayoutParams(WindowManager.LayoutParams attrs, boolean newView) {
synchronized (this) {
//修改布局参数的操作
...
//对View进行重新测量、布局、绘制
mWindowAttributesChanged = true;
scheduleTraversals();
}
}
复制代码
该方法也比较简单,主要就是调用scheduleTraversals
方法来对View进行重新测量、布局及绘制。scheduleTraversals
在这里就不详细讲解了,在View的绘制流程中已经讲解的很清楚了。 总体上来说,Window的更新操作就是对View的重新测量、布局及绘制。
2.2、关闭Window
关闭Window调用的是WindowManagerGlobal
的removeView
方法。
public void removeView(View view, boolean immediate) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
synchronized (mLock) {
int index = findViewLocked(view, true);
View curView = mRoots.get(index).getView();
removeViewLocked(index, immediate);
if (curView == view) {
return;
}
throw new IllegalStateException("Calling with view " + view
+ " but the ViewAncestor is attached to " + curView);
}
}
//移除View
private void removeViewLocked(int index, boolean immediate) {
ViewRootImpl root = mRoots.get(index);
View view = root.getView();
if (view != null) {
//拿到输入法管理
InputMethodManager imm = InputMethodManager.getInstance();
if (imm != null) {
//关闭输入法Window
imm.windowDismissed(mViews.get(index).getWindowToken());
}
}
//返回true表示异步删除,false表示同步删除
boolean deferred = root.die(immediate);
if (view != null) {
view.assignParent(null);
if (deferred) {
//异步删除只是将view添加到mDyingViews这个集合即可。
mDyingViews.add(view);
}
}
}
//该方法在ViewRootImpl中
boolean die(boolean immediate) {
//立即移除View
if (immediate && !mIsInTraversal) {
doDie();
return false;
}
...
//异步移除View,
mHandler.sendEmptyMessage(MSG_DIE);
return true;
}
复制代码
最终还是通过ViewRootImpl
来实现的Window的关闭,immediate
为true
时则代表立即删除当前Window的信息及资源释放,否则异步执行。当异步移除View时,也是调用了ViewRootImpl
的doDie
方法,只不过异步需要排队而已。
void doDie() {
//如果在非UI线程则报错
checkThread();
...
synchronized (this) {
if (mRemoved) {
return;
}
mRemoved = true;
if (mAdded) {
//资源释放
dispatchDetachedFromWindow();
}
if (mAdded && !mFirst) {
destroyHardwareRenderer();
...
}
mAdded = false;
}
//从mRoots、mViews及mParams这三个数组中移除信息
WindowManagerGlobal.getInstance().doRemoveView(this);
}
复制代码
在该方法里主是调用dispatchDetachedFromWindow
进行资源释放,在dispatchDetachedFromWindow
中会释放Surface所占内存、从WMS中移除Window、停止动画、线程等。最后刷新WindowManagerGlobal
中mRoots
、mViews
及mParams
这三个数组的数据。 当调用ViewRootImpl
的doDie
方法后,该ViewRootImpl
也就完成了自己的使命了,等待被GC回收。因此可以得出这样一个结论:ViewRootImpl的生命从setView()开始,到die()结束。
3、总结
到这里,相必对WIndow及WindowManager就有了较深入的了解,主要总结以下几点。
- Window分为应用Window、子Window及系统Window,不同类型的Window对应着不同的层级范围,层级越高,显示越靠前。
- 子Window的高度受状态栏的影响。而系统Window及应用Window则无此限制,所以实现一个子Window需要考虑状态栏的高度
- 一个Window对应着一个
ViewRootImpl
,也就是说ViewRootImpl
与Window同生共死。 - Window的更新其实对View的重新执行测量、布局及绘制。
【参考资料】 《Android艺术探索》 Android Activity应用窗口的创建过程分析 Android Window 机制探索 《深入理解Android 卷III》第四章 深入理解WindowManagerService 《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统 [深入理解Android卷一全文-第八章]深入理解Surface系统