安卓全局浮层方案调研

安卓全局浮层方案调研

二月 07, 2022

背景:

为满足开机广告丰富样式,7.76.0新闻客户端接入LongView广告样式。目前的LongView效果实现是复用之前一镜到底的视频缩放动画,与集成了sax广告SDK的倒计时相关组件。最后用单独的一个Activity层级的浮层Window来承载后续小窗内容。

这样做的方式目前满足了第一版的需求,但是前置动画与小窗目前是侵入式埋点在主页面的onCreate中,后续如果想扩充其他场景需要再继续埋入相应代码,不利于统一维护且容易遗漏,并且当前的浮层不能 “跨页面”

悬浮窗基本原理

类似于动态添加View,但是由于悬浮窗是不依赖具体某个View的所以需要WindowManager介入。

  • 获取WindowManger
  • 创建悬浮窗View [可额外处理悬浮窗View的拖拽事件等]
  • 添加到WindowManager

1. 应用内添加悬浮窗(页面级别)

优点:方便添加无需权限申请,快速创建。
缺点:Activity层级无法全局,退后台后不能显示。
整体流程伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
var layoutParam = WindowManager.LayoutParams().apply {
//设置大小 自适应
width = WRAP_CONTENT
height = WRAP_CONTENT
flags =
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
}
// 新建悬浮窗控件
floatRootView = LayoutInflater.from(this).inflate(R.layout.float_view, null)
//设置拖动事件
floatRootView?.setOnTouchListener(ItemViewTouchListener(layoutParam, windowManager))
// 将悬浮窗控件添加到WindowManager
windowManager.addView(floatRootView, layoutParam)

2. 系统类全局悬浮窗(需申请权限)

优点:可以在应用后台后依然显示,官方支持,限制较少。
缺点:需要权限申请考虑用户授权率打开率或许较低,且如果想覆盖所有场景,需要无障碍敏感权限。由于有后台操作,所以接口需要用到Service参与。
(总结:使用普通的Service创建悬浮窗无法做到任何界面都能显示,利用无障碍服务可以做到任何界面悬浮)

1
2
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW" />

相应权限判断代码省略。(6.0以下声明权限即默认拥有,但华为小米OPPO等手机有自己的畸形悬浮窗权限管理,仍旧需要考虑适配。)

注意点WindowManager.LayoutParam中的type需要留意区分版本

1
2
3
4
5
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
}

Android 8.0之前可以通过TYPE_PHONE来提供用户交互窗口,但是8.0及以上会抛出异常:

1
android.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRootImpl$W@f8ec928 -- permission denied for window type 2002

同时以下类型也被禁止:
Android8.0-TYPE规范

注意点:

综上所以我们必须添加TYPE_APPLICATION_OVERLAY类型来达到显示目的,但是某些情况下悬浮窗效果仍旧会失效不显示。
所以此时如果仍旧想要覆盖此种场景(如小米手机查看系统信息页面),需要添加TYPE_ACCESSIBILITY_OVERLAY类型,则必须搭配AccessibilityService无障碍服务使用。

过程概述:

  • 配置无障碍服务
  • AccessibilityService中获取WindowManager
  • 创建悬浮View[设置悬浮View的拖拽事件]
  • 添加View到WindowManager

3. 应用内全局悬浮窗

优点:无需权限申请,可满足大部分场景,相对较少的侵入代码。
缺点:无法后台,且本质上仍旧是Activity页面级别在页面切换时,需要更新token依附会有闪动

关键点是type类型中有一个TYPE_APPLICATION_PANEL适合用于应用内的悬浮窗开发。所以可以在BaseActivity基类中埋入相关代码,不断切换悬浮窗的token,也可用LifeCycle监听解耦基类代码。

过程伪代码:

1
2
3
4
5
6
WindowManager windowManager = (WindowManager)  applicationContext.getSystemService(Context.WINDOW_SERVICE);
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
// 必须
layoutParams.token = activity.getWindow().getDecorView().getWindowToken();
windowManager.addView(view, layoutParams);

由于关键点仍旧依附于页面,所以切换Activity时会跟随动画一起位移且会有闪动。

之后在生命周期时做token 切换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
override fun onActivityResumed(activity: Activity?) {
activity?.window?.decorView?.let { decorView ->
decorView.viewTreeObserver?.let { viewTree ->
if (decorView.windowToken != null) {
FloatWindowUtils.bindDebugPanelFloatWindow(activity, decorView.windowToken)
weakGlobalListener?.get()?.let { globalListener ->
decorView.viewTreeObserver.removeOnGlobalLayoutListener(globalListener)
}
} else {
val globalListener = object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
activity.window?.decorView?.windowToken?.let {
FloatWindowUtils.bindDebugPanelFloatWindow(activity, it)
}
decorView.viewTreeObserver.removeOnGlobalLayoutListener(this)
}
}
viewTree.addOnGlobalLayoutListener(globalListener)
weakGlobalListener = WeakReference(globalListener)
}
}
}
}

override fun onActivityPaused(activity: Activity?) {
activity?.let {
FloatWindowUtils.unbindDebugPanelFloatWindow(activity)
}
}

总结

对比上述方案,授予权限仍旧是最佳的方案,但由于广告的特殊性用户授予权限场景低,这样会导致LongView的全局浮层几乎失效。
那么第三种方案,将会是首选,目前看到也有通过自定义toast方案来解决的,但是对于一些手机系统如小米ROM不稳定等问题需要进一步考量。

参考链接: