从源码角度解析悬浮窗
今天我们从源码角度来一探 Android 悬浮窗的究竟。
一、如何创建一个悬浮窗
- 在 AndroidManifest.xml 中声明权限 android.permission.SYSTEM_ALERT_WINDOW
- 新建一个 Service, 在其 onCreate() 函数中添加如下代码:1234567891011121314151617181920212223242526272829303132public void onCreate() {super.onCreate();// WindowManagerfinal WindowManager windowManager =(WindowManager) getSystemService(Context.WINDOW_SERVICE);// 定义一个按钮final Button btn = new Button(getBaseContext());btn.setText("Dismiss Me");btn.setOnClickListener(new View.OnClickListener() {public void onClick(View v) {wm.removeView(btn);stopSelf();}});// 布局参数WindowManager.LayoutParams p = new WindowManager.LayoutParams();p.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;p.gravity = Gravity.TOP;p.width = WRAP_CONTENT;p.height = WRAP_CONTENT;p.x = 0;p.y = 0;p.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;// 添加按钮到 WindowManagerwindowManager.addView(btn, p);}
当我们的 Service 被启动后,手机屏幕顶部就会显示一个按钮,点击此按钮后,它会消失。
二、如何定制悬浮窗?
从上述代码片段可知,定制悬浮窗的关键在于 WindowManager.LayoutParams 这个类,
我们来看看几个主要的成员。
- x, y
窗口的绝对位置(考虑gravity属性) - width, height
窗口的尺寸 - gravity
窗口的布局方式 type
窗口的类型,值越大显示的位置越在上面123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107// type 共分三大类:/* --------- Application Window ---------- */// id 从 1 到 99, 目前有效值 1~3// Activity -> TYPE_APPLICATION(2)(需要 token 设置成 Activity 的 token)//开始应用程序窗口public static final int FIRST_APPLICATION_WINDOW = 1;//所有程序窗口的base窗口,其他应用程序窗口都显示在它上面public static final int TYPE_BASE_APPLICATION = 1;//普通应用程序窗口,token必须设置为Activity的token来指定窗口属于谁public static final int TYPE_APPLICATION = 2;//应用程序启动时所显示的窗口,应用自己不要使用这种类型,它被系统用来显示一些信息,直到应用程序可以开启自己的窗口为止public static final int TYPE_APPLICATION_STARTING = 3;//结束应用程序窗口public static final int LAST_APPLICATION_WINDOW = 99;/* --------- Sub Window ---------- */// id 从 1000 到 1999, 目前有效值 1000~1005// (与顶层窗口相关联,需将 token 设置成它所附着宿主窗口的 token)// PopupWindow -> TYPE_APPLICATION_PANEL(1000)//SubWindows子窗口,子窗口的Z序和坐标空间都依赖于他们的宿主窗口public static final int FIRST_SUB_WINDOW = 1000;// 面板窗口,显示于宿主窗口的上层public static final int TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW;//媒体窗口(例如视频),显示于宿主窗口下层public static final int TYPE_APPLICATION_MEDIA = FIRST_SUB_WINDOW+1;//应用程序窗口的子面板,显示于所有面板窗口的上层public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW+2;//对话框,类似于面板窗口,绘制类似于顶层窗口,而不是宿主的子窗口public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW+3;//媒体信息,显示在媒体层和程序窗口之间,需要实现半透明效果public static final int TYPE_APPLICATION_MEDIA_OVERLAY = FIRST_SUB_WINDOW+4;//子窗口结束public static final int LAST_SUB_WINDOW = 1999;/* --------- System Window ---------- */// id 从 2000 到 2999, 目前有效值 2000~2033// 系统功能使用,不能用于应用程序,使用时需要特殊权限,它是特定的系统功能才能使用;//系统窗口,非应用程序创建public static final int FIRST_SYSTEM_WINDOW = 2000;//状态栏,只能有一个状态栏,位于屏幕顶端,其他窗口都位于它下方public static final int TYPE_STATUS_BAR = FIRST_SYSTEM_WINDOW;//搜索栏,只能有一个搜索栏,位于屏幕上方public static final int TYPE_SEARCH_BAR = FIRST_SYSTEM_WINDOW+1;//电话窗口,它用于电话交互(特别是呼入),置于所有应用程序之上,状态栏之下public static final int TYPE_PHONE = FIRST_SYSTEM_WINDOW+2;//系统提示,出现在应用程序窗口之上public static final int TYPE_SYSTEM_ALERT = FIRST_SYSTEM_WINDOW+3;//锁屏窗口public static final int TYPE_KEYGUARD = FIRST_SYSTEM_WINDOW+4;//信息窗口,用于显示Toastpublic static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5;//系统顶层窗口,显示在其他一切内容之上,此窗口不能获得输入焦点,否则影响锁屏public static final int TYPE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW+6;//电话优先,当锁屏时显示,此窗口不能获得输入焦点,否则影响锁屏public static final int TYPE_PRIORITY_PHONE = FIRST_SYSTEM_WINDOW+7;//系统对话框public static final int TYPE_SYSTEM_DIALOG = FIRST_SYSTEM_WINDOW+8;//锁屏时显示的对话框public static final int TYPE_KEYGUARD_DIALOG = FIRST_SYSTEM_WINDOW+9;//系统内部错误提示,显示于所有内容之上public static final int TYPE_SYSTEM_ERROR = FIRST_SYSTEM_WINDOW+10;//内部输入法窗口,显示于普通UI之上,应用程序可重新布局以免被此窗口覆盖public static final int TYPE_INPUT_METHOD = FIRST_SYSTEM_WINDOW+11;//内部输入法对话框,显示于当前输入法窗口之上public static final int TYPE_INPUT_METHOD_DIALOG= FIRST_SYSTEM_WINDOW+12;//墙纸窗口public static final int TYPE_WALLPAPER = FIRST_SYSTEM_WINDOW+13;//状态栏的滑动面板public static final int TYPE_STATUS_BAR_PANEL = FIRST_SYSTEM_WINDOW+14;//安全系统覆盖窗口,这些窗户必须不带输入焦点,否则会干扰键盘public static final int TYPE_SECURE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW+15;//拖放伪窗口,只有一个阻力层(最多),它被放置在所有其他窗口上面public static final int TYPE_DRAG = FIRST_SYSTEM_WINDOW+16;//状态栏下拉面板public static final int TYPE_STATUS_BAR_SUB_PANEL = FIRST_SYSTEM_WINDOW+17;//鼠标指针public static final int TYPE_POINTER = FIRST_SYSTEM_WINDOW+18;//导航栏(有别于状态栏时)public static final int TYPE_NAVIGATION_BAR = FIRST_SYSTEM_WINDOW+19;//音量级别的覆盖对话框,显示当用户更改系统音量大小public static final int TYPE_VOLUME_OVERLAY = FIRST_SYSTEM_WINDOW+20;//起机进度框,在一切之上public static final int TYPE_BOOT_PROGRESS = FIRST_SYSTEM_WINDOW+21;//假窗,消费导航栏隐藏时触摸事件public static final int TYPE_HIDDEN_NAV_CONSUMER = FIRST_SYSTEM_WINDOW+22;//梦想(屏保)窗口,略高于键盘public static final int TYPE_DREAM = FIRST_SYSTEM_WINDOW+23;//导航栏面板(不同于状态栏的导航栏)public static final int TYPE_NAVIGATION_BAR_PANEL = FIRST_SYSTEM_WINDOW+24;//universe背后真正的窗户public static final int TYPE_UNIVERSE_BACKGROUND = FIRST_SYSTEM_WINDOW+25;//显示窗口覆盖,用于模拟辅助显示设备public static final int TYPE_DISPLAY_OVERLAY = FIRST_SYSTEM_WINDOW+26;//放大窗口覆盖,用于突出显示的放大部分可访问性放大时启用public static final int TYPE_MAGNIFICATION_OVERLAY = FIRST_SYSTEM_WINDOW+27;//......public static final int TYPE_KEYGUARD_SCRIM = FIRST_SYSTEM_WINDOW+29;public static final int TYPE_PRIVATE_PRESENTATION = FIRST_SYSTEM_WINDOW+30;public static final int TYPE_VOICE_INTERACTION = FIRST_SYSTEM_WINDOW+31;public static final int TYPE_ACCESSIBILITY_OVERLAY = FIRST_SYSTEM_WINDOW+32;//系统窗口结束public static final int LAST_SYSTEM_WINDOW = 2999;flags
flags 和 type 可以共同决定 Window 的表示和行为123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384// FLAG_ALLOW_LOCK_WHILE_SCREEN_ON:// 当窗口可见时,允计锁屏界面出现。// FLAG_DIM_BEHIND:// 窗口之后的内容变暗// FLAG_NOT_FOCUSABLE:// 不能获得按键输入焦点,所以不能向它发送按键或按钮事件。那些事件将发送给它后面的可以获得焦点的窗口。// 此选项还会设置 FLAG_NOT_TOUCH_MODAL 选项。设置此选项,意味着窗口不能与软输入法进行交互,// 所以它的 Z 序独立于任何活动的输入法(它可以全屏显示,如果需要的话,可覆盖输入法窗口)。// 要修改这一行为,可参考 FLAG_ALT_FOCUSALBE_IM 选项。// FLAG_NOT_TOUCHABLE:// 不接受触摸屏事件// FLAG_NOT_TOUCH_MODAL:// 当窗口可以获得焦点(没有设置 FLAG_NOT_FOCUSALBE 选项)时,仍然将窗口范围之外的点设备事件//(鼠标、触摸屏)发送给后面的窗口处理。否则它将独占所有的点设备事件,而不管它们是不是发生在窗口范围之内。// FLAG_TOUCHABLE_WHEN_WAKING:// 如果设置了这个标志,当设备休眠时,点击触摸屏,设备将收到这个第一触摸事件。// 通常第一触摸事件被系统所消耗,用户不会看到他们点击屏幕有什么反应。// FLAG_KEEP_SCREEN_ON// 当此窗口为用户可见时,保持设备常开,并保持亮度不变。// FLAG_LAYOUT_IN_SCREEN:// 窗口占满整个屏幕,忽略周围的装饰边框(例如状态栏)。此窗口需考虑到装饰边框的内容。// FLAG_LAYOUT_NO_LIMITS:// 允许窗口扩展到屏幕之外// FLAG_FULLSCREEN:// 窗口显示时,隐藏所有的屏幕装饰(例如状态条)。使窗口占用整个显示区域。// FLAG_FORCE_NOT_FULLSCREEN:// 此选项将覆盖 FLAG_FULLSCREEN 选项,并强制屏幕装饰(如状态条)弹出。// FLAG_SECURE:// 不允许屏幕截图// FLAG_SCALED:// 一种特殊模式,布局参数用于指示显示比例// FLAG_IGNORE_CHEEK_PRESSES:// 当屏幕有可能贴着脸时,这一选项可防止面颊对屏幕造成误操作// FLAG_LAYOUT_INSET_DECOR:// 当请求布局时,你的窗口可能出现在状态栏的上面或下面,从而造成遮挡。// 当设置这一选项后,窗口管理器将确保窗口内容不会被装饰条(状态栏)盖住。// public static final int FLAG_ALT_FOCUSABLE_IM:// 如果同时设置了FLAG_NOT_FOCUSABLE选项和本选项,窗口将能够与输入法交互,允许输入法窗口覆盖;// 如果FLAG_NOT_FOCUSABLE没有设置而设置了本选项,窗口不能与输入法交互,可以覆盖输入法窗口。// FLAG_WATCH_OUTSIDE_TOUCH:// 如果你设置了FLAG_NOT_TOUCH_MODAL,那么当触屏事件发生在窗口之外时,// 可以通过设置此标志接收到一个MotionEvent.ACTION_OUTSIDE事件。// 注意,你不会收到完整的down/move/up事件,只有第一次down事件时可以收到ACTION_OUTSIDE。// FLAG_SHOW_WHEN_LOCKED:// 当屏幕锁定时,窗口可以被看到。这使得应用程序窗口优先于锁屏界面。// 可配合 FLAG_KEEP_SCREEN_ON 选项点亮屏幕并直接显示在锁屏界面之前。// 可使用FLAG_DISMISS_KEYGUARD选项直接解除非加锁的锁屏状态。此选项只用于最顶层的全屏幕窗口。// FLAG_SHOW_WALLPAPER:// 请求系统墙纸显示在你的窗口后面。窗口必须是半透明的。// FLAG_TURN_SCREEN_ON:// 窗口一旦显示出来,系统将点亮屏幕,正如用户唤醒设备那样。// FLAG_DISMISS_KEYGUARD:// 解除锁屏。只有锁屏界面不是加密的才能解锁。// 如果锁屏界面是加密的,那么用户解锁之后才能看到此窗口,除非设置了FLAG_SHOW_WHEN_LOCKED选项。// FLAG_SPLIT_TOUCH:// FLAG_HARDWARE_ACCELERATED:// 硬件加速// FLAG_LOCAL_FOCUS_MODE:// FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS:示例:
- 使悬浮窗可以覆盖通知栏
type = TYPE_SYSTEM_ERROR
flags = FLAG_LAYOUT_IN_SCREEN - 使悬浮窗不遮挡输入法
FLAG_ALT_FOCUSABLE_IM - 使悬浮窗不可点击(可以获得焦点,会拦截 Back, Home 键)
FLAG_NOT_TOUCHABLE - 使悬浮窗不可获得焦点(不拦截 Back, Home 键)
FLAG_NOT_FOCUSABLE
- 使悬浮窗可以覆盖通知栏
三、悬浮窗是如何显示到屏幕上的?
我们知道视图是通过 WindowManager 添加到屏幕上的,所以第一个要了解的类便是 WindowManager。
我们看到 WindowManager 继承自 ViewManager, 有一个获取 DefaultDisplay 的方法和删除 View 的方法;
再来看看 ViewManager 的代码:
非常简洁的三个函数:添加视图,更新视图,删除视图。
如果再查看一个 ViewManager 的其它实现类,还会发现 ViewGroup 也实现了这个接口。
我们再来看看 WindowManager 的实现者是谁?
原来是 WindowManagerImpl,如果有同学觉见得打 Log 的方式太偷懒了,可以阅读这篇文章来了解 getSystemService(…) 的实现原理。
那我们就来看看 WindowManagerImpl 的 addView(…), updateViewLayout(…), removeView(…) 这三个函数吧:
这几个函数无一例外地将实现都转移给了 WindowManagerGlobal:
毫无疑问,WindowManagerGlobal 使用了单例,所以一个进程中最多只有一个 WindowManager,所有 WindowManager 实例都是这个 WindowManagerGlobal 实例的代理。
到此,我们对 WindowManager 的结构体系便有了一个概括的了解:
四、 WindowManagerGlobal 详解
|
|
WindowManagerGlobal 有 4 个重要的集合:
- mView : 根视图集合
- mRoots : ViewRootImpl 集合
- mParams : 根视图的布局的集合
- mDyingViews : 要移除和根视图的集合
我们先来分析 添加、删除、更新 3 种操作对这 4 个集合的影响。
1. 添加视图
|
|
添加视图时,做了以下几件事:
- 将 view, viewRootImpl, layoutParams 分别添加到对应的集合中;
- 将 layoutParams 设置给 view;
- 将 view 设置给 viewRootImpl;
2. 更新视图
|
|
更新视图比较简单
- 设置 layoutParams 给 view;
- 更新 mParams 集合中对应的 layoutParams;
- 更新 对应 viewRootImpl 的 laoutParams;
3. 删除视图
|
|
我们看到,删除视图最终交给了 removeViewLocked 函数:
removeViewLocked 函数会调用 ViewRootImpl 的 die() 函数,该函数返回一个 boolean 值,标识是否需要延迟删除?如果需要延迟,则将 view 加入 mDyingViews 这个集合中。
接着我们看看 ViewRootImpl 的 die 函数:
我们看到,所谓的延迟删除,其实就是使用了 Handler 消息队列机制,来调用 doDie() 函数;
如果是非延迟删除,则立即调用 doDie() 函数:
|
|
doDie() 函数会发送 DetachedFromWindow 消息给 View;然后调用 WindowManagerGlobal 的 doRemoveView() 函数:
doRemoveView() 函数的功能一目了然,即从 mRoots, mParams, mDyingViews 中将 数据移除掉。
五、总结
- 使用悬浮窗时,要声明权限(个别机型有单独的悬浮窗权限管理,如小米);
- 悬浮窗是使用 WindowManager 添加到屏幕上的,WindowManaget.LayoutParams 可以定制悬浮窗的表现和行为;
- WindowManager 的实际实现是 WindowManagerGlobal;
- WindowManagerGlobal 使用 4 个列表保存了窗口的关键数据,并配合 ViewRootImpl 类完成窗口的添加、更新、删除操作;
- 关于 WindowManager 分析的部分不局限于悬浮窗,它也是整个 Android 系统的窗口管理器。