Window和WindowManager
为了分析Window的工作机制,我们看下如何用 WindowManager 添加一个 Window。
mFloatingButton = new Button(this);
mFloatingButton.setText("button");
mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
mLayoutParams = new WindowManager.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 0, 0, PixelFormat.TRANSPARENT);
mLayoutParams.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL| LayoutParams.FLAG_NOT_FOCUSABLE| LayoutParams.FLAG_SHOW_WHEN_LOCKED;
mLayoutParams.type = LayoutParams.TYPE_SYSTEM_ALERT;
mLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
mLayoutParams.x = 100;
mLayoutParams.y = 300;
mWindowManager.addView(mFloatingButton, mLayoutParams);
以上代码将一个Button添加到屏幕坐标为(100,300)的位置上,要在Manifest.xml中添加权限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />。
WindowManager中的 flags、type 这两个参数比较重要。
1. 几个flag属性
WindowManager.LayoutParams.FLAG_SECURE
不允许截屏;设置了这个属性的窗口,在窗口可见的情况下,是会禁用系统的截图功能的。那么问题来了:假如有一天,你的公司要求写一个类似于‘阅后即焚’功能的页面的话,不妨在activity中获得WindowManager.LayoutParams并添加该属性,轻轻松松搞定。WindowManager.LayoutParams.FLAG_BLUR_BEHIND
背景模糊;假如你的窗口设置了这个属性,并且这个窗口可见,在这窗口之后的所有背景都会被模糊化,但我还没有发现一个属性是可以控制模糊程度的。WindowManager.LayoutParams.FLAG_DIM_BEHIND
背景变暗;设置这个效果的窗口,在窗口可见的情况下,窗口后方的背景会相应的变暗,这个属性需要配合参数dimAmount一起使用,dimAmount会在后文中介绍。WindowManager.LayoutParams.FLAG_FULLSCREEN
设置全屏;这个属性也许是大家接触的最多的一个属性,很多应用开发过程中会要求有些页面需要动态设置Activity为全屏,而我们只需要获得Activity的WindowManager.LayoutParams并设置WindowManager.LayoutParams.FLAG_FULLSCREEN属性就行。WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
设备常亮;设置这个属性的窗口,在窗口可见的情况下,整个屏幕会处于常亮并且高亮度的状态,并且不受待机时间的约束。WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
布局不受限制;设置这个属性的窗口,将不再受设备显示范围边界 的约束,通俗点讲,就是窗口可以出设备之外,然后移除部分不可见。具体会在坐标参数中讲到。WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
不设置聚焦;关于焦点获得我有必要说明一下,如果窗口获得焦点的话,只要窗口处于可视化状态,当前设备的物理按键点击事件都会被这个窗口接收,但是如果不设置窗口的焦点的话,直接传递到之后窗口进行接收。这就导致一个问题,如果你的需求要求你写的悬浮窗点击返回键能够关闭或是进行其他操作的话,你就必须让你的窗口获得焦点,并为当前View设置按键监听事件。WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
取消触摸事件; 设置这个属性的窗口将不再处理任何Touch事件,就算显示的View设置了onTouch事件,那么这个窗口就会是一个僵尸窗口。WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
不知道怎么去归纳,这个属性还是比较有意思的,设置这个属性的窗口,在窗口可见的情况下,就算窗口没有设置属性FLAG_NOT_FOCUSABLE,也就是在窗口获得焦点的情况下,当触摸事件是在窗口之外区域的时候,窗口不在拦截触摸事件,而是将事件往下传递,也算是解决聚焦后的事件拦截问题吧。WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER
显示壁纸;官方文档说明是在窗口之后显示系统壁纸,但是我亲测,似乎并没有这个想效果,还是这个属性需要配合其他的属性设置一起使用,希望有设置成功的小伙伴能够在评论区分享你的结果。WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
锁屏显示;关于这个属性官方文档给出的说明是在锁屏的时候显示的窗口,但是,实在惭愧,在下还是没有能够有一个实验结果,不知道是需要给权限呢还是需要同时进行其他设置。同样,还是很希望有知道的小伙伴能够在评论区向大家分享。WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
点亮屏幕;设置这个属性的窗口,当窗口显示的时候,如果设备处于待机状态,会点亮设备。这个应该在很多锁屏窗口中用的比较多,比如收到消息点亮屏幕。WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
这个也不知道怎么去归纳,也是一个比较有意思的属性,之前我们说到FLAG_NOT_TOUCH_MODAL,在窗口获得焦点的情况下,当触摸事件是在窗口之外区域的时候,窗口不在拦截触摸事件,而是将事件往下传递,而如果再设置这个属性,窗口能在MotionEvent.ACTION_OUTSIDE中收获窗口之外的点击事件,遗憾的是不能进行屏蔽,也就是说事件依然会向下传递。
2. type表示Window的类型
Window有三种类型,分别为应用Window、子Window、系统Window。Window是分层的,每个Window都有对应的 z-ordered.层级大的覆盖在层级小的上面。
| 说 明 | 层级(z-order)
-----------| ---------------| ------------
应用Window | 对应一个Activity| 1-99
子Window | 常见的Dialog | 1000-1999
系统Window | Toast,系统状态栏 | 2000-2999
Window的内部机制
Window是一个抽象概念,每一个Window都对应着一个View和ViewRootImpl,Window 和 View 通过ViewRootImpl来建立联系。
下图显示了Activity的Window和Wms的关系
Window的添加过程
Window 的添加需要 WindowManager的addView来实现,WindowManager是一个接口,它的实现是在 WindowManagerImpl类。
public interface WindowManager extends ViewManager{} //WindowManager 是一个接口
public interface ViewManager{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}
WindowManagerImpl并没有直接实现 Window的三大操作,而是全部交给WindowManagerGlobal来处理,WindowManagerGlobal是典型的单例。
public final class WindowManagerImpl implements WindowManager {
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
...
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
@Override
public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.updateViewLayout(view, params);
}
@Override
public void removeView(View view) {
mGlobal.removeView(view, false);
}
}
WindowManagerGlobal 中
1. 检查参数是否合法
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");
}
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
}
2. 创建ViewRootImpl并将View添加到列表中
WindowManagerGlobal 内部有几个列表比较重要:
- 存储的是所有 window 所对应的 View
private final ArrayList<View> mViews = new ArrayList<View>(); - 所有的 Window 对应的 ViewRootImpl
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>(); - 所有 Window 所对应的布局参数
private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<WindowManager.LayoutParams>();
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
3.通过ViewRootImpl来更新界面并完成 Window的添加
这个步骤由ViewRootImpl的setView方法完成。
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
synchronized (mLock) {
final int index = findViewLocked(view, false);
if (index >= 0) {
removeViewLocked(index, true);
}
}
throw e;
}
setView内部会通过 requestLayout 来完成异步刷新请求。scheduleTraversals 其实就是View绘制的入口。
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
接着会通过WindowSession 最终来完成 Window 的添加过程。在下面的代码中 mWindowSession 类型是一个 IWindowSession,它是一个Binder对象,真正的实现是 Session,也就是说Window的添加过程是一次IPC调用。
try {
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
} catch (RemoteException e) {
mAdded = false;
mView = null;
mAttachInfo.mRootView = null;
mInputChannel = null;
mFallbackEventHandler.setView(null);
unscheduleTraversals();
setAccessibilityFocus(null, null);
throw new RuntimeException("Adding window failed", e);
}
在Session内部会通过WindowManagerService来实现Window的添加。
final class Session extends IWindowSession.Stub
implements IBinder.DeathRecipient {
...
@Override
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
Rect outOutsets, InputChannel outInputChannel) {
return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
outContentInsets, outStableInsets, outOutsets, outInputChannel);
}
}
如此一来,Window的添加请求就交给了WindowManagerService处理了。在WindowManagerService内部会为每一个应用保留一个单独的Session。
Window的创建过程
1.Activity的Window创建过程
从源码分析Activity的启动过程
在ActivityThread 的 performLaunchActivity
/**
* 4.创建 ContextImpl 对象并通过 Activity 的 attach 方法来完成一些数据的初始化
* 将 appContext 对象 attach 到 Activity 中
*/
Context appContext = createBaseContextForActivity(r, activity);
CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
Configuration config = new Configuration(mCompatConfiguration);
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity "
+ r.activityInfo.name + " with config " + config);
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor);
中,在Activity的 attach方法中,系统会创建Activity所属的Window对象并为其设置回调接口。由于Activity实现了Window的Callback接口,因此当Window接受到外界的状态改变时就会回调Activity方法。Callback接口中方法很多,常见的有:onAttachedToWindow、onDetachedFromWindow、onWindowFocusChanged等等。Api23的代码如下:
mWindow = new PhoneWindow(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
mWindow.setSoftInputMode(info.softInputMode);
}
if (info.uiOptions != 0) {
mWindow.setUiOptions(info.uiOptions);
}
Activity视图是如何依附在Window上面?
由于Activity视图由setContentView方法提供,我们只要看setContentView即可。
public void setContentView(View view, ViewGroup.LayoutParams params) {
getWindow().setContentView(view, params);
initWindowDecorActionBar();
}
从Activity的setContentView看出,Activity将具体的实现交给了Window处理,而Window的具体的实现是PhoneWindow。来看PhoneWindow的setContentView。
- 如果没有DecorView,就创建它。
- 将View添加到DecorView的mContentParent中。
- 回调Activity的 onContentChanged方法,通知Activity视图改变了。
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
view.setLayoutParams(params);
final Scene newScene = new Scene(mContentParent, view);
transitionTo(newScene);
} else {
mContentParent.addView(view, params);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
经过了上面的三个步骤,到这里DecorView已经创建并初始化完毕。Activity的布局文件也被成功添加到DecorView的mContentParent中,但这时候DecorView还没被WindowManager正式添加到Window中。在ActivityThread的handleResumeActivity中,首先会调用Activity的onResume方法,接着会调用Activity的makeVisible方法,正是这个makeVisible方法中,DecorView真正完成了添加和显示这两个过程。
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
参考资料及推荐阅读
- 本文主要参考了《Android开发艺术探索》
- 示例:做一个炫酷的悬浮迷你音乐盒
- 为什么Dialog不能用Application的Context