https://rainmonth.github.io/posts/A200312.html
摘要
悬浮窗是App开发中比较常用的功能,本文就探究下Android中悬浮窗的实现方案。这里主要讨论以下两种方案:全局悬浮窗和应用内悬浮窗。两种实现方式各有优缺点(后面会有所比较),本文先对两种方案的实现思路分析,然后给出具体的编码实现,最后将二者封装完成,做成轮子可以按需使用。
要实现的功能
封装后的悬浮窗需具备以下功能:
- [x] 支持应用内所有页面悬浮;
- [x] 支持应用外全局悬浮;
- [x] 支持可跟随手指移动;
- [ ] 支持配置吸附效果;
- [x] 支持设置悬浮View动效;
- [x] 悬浮窗权限适配问题
- [ ] 显示黑名单与白名单
- [ ] 支持数据绑定(采用泛型实现)
- [x] 同时支持多个悬浮窗,并且可以对他们进行管理
- [x] 支持自定义视图
需要解决的问题
要完成上述功能,需要解决以下几个问题
- 权限适配的问题(全局悬浮窗在Android API 在23(Android 6.0)以上时需要动态请求权限,其他情况下不需要请求权限);
- 悬浮窗的降级处理(在权限未获取到的情况下,如果请求的时全局悬浮窗,则直接采用应用内悬浮窗实现,目前好多主流大厂都是这样实现的);
- 多个悬浮窗的管理(悬浮窗管理类中维护一个Map,key为悬浮窗的id,value为悬浮窗的配置,id唯一,可以通过这个id来对悬浮窗进行管理);
- 悬浮窗的内存管理(管理类采用单例模式实现,而这个单例里面存在悬浮View的使用,所以要注意内存的管理,主要是要防止内存泄漏);
全局悬浮窗
实现方式
请求权限,然后通过WindowManager的addView来添加悬浮View,同时通过WindowManager.LayoutParams类进行View配置的更新
权限请求
全局悬浮窗需要动态申请权限,主要注意一下两点。
不管哪个Android系统版本,AndroidManifest.xml文件中必须要声明
android.permission.SYSTEM_ALERT_WINDOW
权限的使用;Android 6.0以上,除要声明上述权限外,还要再运行时动态申请权限,权限请求代码如下:
1
2
3
4
5
6
7Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
intent.setData(Uri.parse("package:" + Utils.getApp().getPackageName()));
if (!UtilsBridge.isIntentAvailable(intent)) {
launchAppDetailsSettings();
return;
}
activity.startActivityForResult(intent, requestCode);注意:Android 6.0后系统对某些敏感权限要求动态请求,仅仅在AndroidManifest.xml中声明是远远不够的,需要运行时动态申请,而这种动态请求权限中又有两个权限的请求方式比较特殊,悬浮窗权限就是其中之一,不能通过Activity.requestPermissions方法来完成,而是需要通过Intent打开页面来进行设置
全局悬浮View的配置
全局悬浮View的显示配置主要通过WindowManager.LayoutParams来进行控制,对于这个WindowManager.LayoutParams,有几点需要注意:
type
指定的时悬浮窗的类型,这里需要注意的时Android 6.0以上这个值需要指定为TYPE_APPLICATION_OVERLAY
,其他指定为TYPE_PHONE
即可;gravity
默认值为Gravity.START | Gravity.TOP,采用默认值可以避免不必要的坐标转换计算,WindowManager.LayoutParams
中gravity
的默认值为Gravity.CENTER
,如果要想使下面的通过x、y来控制窗口的显示位置,那么久必须设置这个值并且这个值不能是Gravity.CENTER
;x
,设置window的x轴偏移值(在Gravity未设置或者未默认值Gravity.CENTER时不生效);y
,设置window的y轴偏移值(在Gravity未设置或者未默认值Gravity.CENTER时不生效);
- 通过WindowManager添加的View无法显示在屏幕以外的区域,而通过ViewGroup添加的View可以;
- 上面的参数中,同一个悬浮窗的
type
是不能更改的;
View的配置例如位置更新等,主要通过WindowManager的updateViewLayout(View view, ViewGroup.LayoutParams params)
方法来实现,其中有一个参数需要注意
应用内全局悬浮
实现方式
通过动态的向Activity根布局添加或移除View来实现悬浮窗;
悬浮View的配置
通过FrameLayout.LayoutParams类进行View配置(位置相关的配置)的更新。
两种方式总结
全局悬浮
- 优点,可以始终显示在最上端,不存在应用内悬浮页面切换闪烁的问题;
- 缺点,6.0以上需要动态申请权限,如果用户拒绝该权限,需要做相应的降级处理
应用内悬浮
- 优点,无需申请权限,所有版本完美运行;
- 缺点,不能做到应用外悬浮(即应用推到后台后不能悬浮),由于是动态的添加和移除View,在切面切换时会出现闪烁的现象(需要在旧的页面移除,然后在新的页面添加),5.0之后可以共享元素来进行避免。
具体实现
关键的类
FloatViewManager
,悬浮View管理(采用单例模式),FloatViewManager
中根据悬浮窗id来记录相应的FloatView,同时也根据悬浮窗id记录相应的FloatView配置;FloatViewConfig
,悬浮View配置项管理(采用Builder模式)FloatViewContainer
,主要负责实现随手指滑动、停靠功能,这里全局悬浮和应用内悬浮需要分开设置,应为全局悬浮更新的时Window
的坐标,而应用内悬浮跟新的时View的坐标,每个FloatViewContainer
都有一个与之对应的FloatViewConfig- FloatPermissionUtils,主要负责全县请求方面的工作
- Utils,主要负责实现常用的工具方法
悬浮View的设计
悬浮View的配置
悬浮View的配置交给FloatViewConfig
类来实现,将上面要实现的功能
采用Builder模式
- 支持配置是否可以应用外悬浮(应用外悬浮需要支持动态配置,同时要提供关闭的选项);
- 支持配置自动停靠(配置了就会就近停靠到相应的位置);
- 支持自定义View;
- 支持配置悬浮View的大小;
- 支持是否显示关闭按钮(为什么会有这么一个设计呢?因为做demo的时候,发现如果申请了弹窗权限,存在应用销毁了,但是悬浮窗仍存在的现象,这有点流氓)
这里指的就是FloatDragLayout
的设计,核心就是一个ViewGroup,需要支持一下功能:
- 支持随手指移动;
- 支持自动靠边停靠;
- 支持几种简单的动效
- 上下浮动;
- 左右抖动;
- 旋转动效
相关文章
- Android应用内悬浮窗的实现方案
- Android应用内悬浮窗从入门到放弃/妥协
- Andorid 应用内悬浮控件实践方案总结
- Andorid 应用内悬浮控件实践方案总结
- 应用内悬浮窗(不请求权限))
- Android无需权限显示悬浮窗, 兼谈逆向分析app
问题记录
调用
requestPermission
方法获取SYSTEM_ALERT_WINDOW
权限一直失败,失败的原因就是这个权限不能通过requestPermission
来申请,需要先在AndroidManifest.xml
文件中声明,然后通过隐式Intent
来请求权限,代码如下:1
2
3
4
5
6
7Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
intent.setData(Uri.parse("package:" + Utils.getApp().getPackageName()));
if (!UtilsBridge.isIntentAvailable(intent)) {
launchAppDetailsSettings();
return;
}
activity.startActivityForResult(intent, requestCode);悬浮窗显示出来后,除了悬浮窗显示的View及物理按键,其他区域都无法响应事件,问题原因是因为WindowManager.LayoutParams中的flags参数设置有问题,需要添加如下设置:
1
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
应用销毁了,但悬浮窗仍然存在
Android permission denied for window type 2002
Android M (6.0)全局悬浮窗(通过请求SYSTEM_ALERT_WINDWO权限)window的type不能设置为PHONE,可以设置成
APPLICATION_OVERLAY