Android 适配——全局悬浮窗的实现

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配置的更新

权限请求

全局悬浮窗需要动态申请权限,主要注意一下两点。

  1. 不管哪个Android系统版本,AndroidManifest.xml文件中必须要声明android.permission.SYSTEM_ALERT_WINDOW权限的使用;

  2. Android 6.0以上,除要声明上述权限外,还要再运行时动态申请权限,权限请求代码如下:

    1
    2
    3
    4
    5
    6
    7
    Intent 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.LayoutParamsgravity的默认值为Gravity.CENTER,如果要想使下面的通过x、y来控制窗口的显示位置,那么久必须设置这个值并且这个值不能是Gravity.CENTER
  • x,设置window的x轴偏移值(在Gravity未设置或者未默认值Gravity.CENTER时不生效);
  • y,设置window的y轴偏移值(在Gravity未设置或者未默认值Gravity.CENTER时不生效);
  1. 通过WindowManager添加的View无法显示在屏幕以外的区域,而通过ViewGroup添加的View可以;
  2. 上面的参数中,同一个悬浮窗的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模式

  1. 支持配置是否可以应用外悬浮(应用外悬浮需要支持动态配置,同时要提供关闭的选项);
  2. 支持配置自动停靠(配置了就会就近停靠到相应的位置);
  3. 支持自定义View;
  4. 支持配置悬浮View的大小;
  5. 支持是否显示关闭按钮(为什么会有这么一个设计呢?因为做demo的时候,发现如果申请了弹窗权限,存在应用销毁了,但是悬浮窗仍存在的现象,这有点流氓)

这里指的就是FloatDragLayout的设计,核心就是一个ViewGroup,需要支持一下功能:

  1. 支持随手指移动;
  2. 支持自动靠边停靠;
  3. 支持几种简单的动效
    1. 上下浮动;
    2. 左右抖动;
    3. 旋转动效

相关文章

问题记录

  1. 调用requestPermission方法获取SYSTEM_ALERT_WINDOW权限一直失败,失败的原因就是这个权限不能通过requestPermission来申请,需要先在AndroidManifest.xml文件中声明,然后通过隐式Intent来请求权限,代码如下:

    1
    2
    3
    4
    5
    6
    7
    Intent 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);
  2. 悬浮窗显示出来后,除了悬浮窗显示的View及物理按键,其他区域都无法响应事件,问题原因是因为WindowManager.LayoutParams中的flags参数设置有问题,需要添加如下设置:

    1
    layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
  3. 应用销毁了,但悬浮窗仍然存在

  4. Android permission denied for window type 2002

    Android M (6.0)全局悬浮窗(通过请求SYSTEM_ALERT_WINDWO权限)window的type不能设置为PHONE,可以设置成APPLICATION_OVERLAY