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的参数,关于其详细参数可以参考这篇文章。 先来看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的测量、布局、绘制流程。关于该方法详细内容可以参考、这两篇文章。后者主要向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
在这里就不详细讲解了,在中已经讲解的很清楚了。 总体上来说,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艺术探索》