Android悬浮窗的实现
*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
本文也发布于本人的知乎文章:https://zhuanlan.zhihu.com/p/394211126
0. 前言
现在很多应用都使用到悬浮窗,例如微信在视频的时候,点击Home键,视频小窗口仍然会在屏幕上显示。这个功能在很多情况下都非常有用。那么今天我们就来实现一下Android悬浮窗,以及探索一下实现悬浮窗时的易错点。
1. 实现原理
1.1 悬浮窗插入接口
在实现悬浮窗之前,我们需要知道通过什么接口,能够将一个控件放入到屏幕中去。
Android的界面绘制,都是通过WindowMananger
的服务来实现的。那么,既然要实现一个能够在自身应用以外的界面上的悬浮窗,我们就要利用WindowManager
来“做手脚”。
(frameworks/base/core/java/android/view/WindowMananger.java)
@SystemService(Context.WINDOW_SERVICE)
public interface WindowManager extends ViewManager {
...
}
WindowManager
实现了ViewManager
接口,可以通过获取WINDOW_SERVICE
系统服务得到。而ViewManager
接口有addView
方法,我们就是通过这个方法将悬浮窗控件加入到屏幕中去。
1.2 权限设置及请求
悬浮窗需要在别的应用之上显示控件,很显然,这需要某些权限才可以。
在API Level >= 23
的时候,需要在AndroidManefest.xml文件中声明权限SYSTEM_ALERT_WINDOW
才能在其他应用上绘制控件。
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
除了这个权限外,我们还需要在系统设置里面对本应用进行设置悬浮窗权限。该权限在应用中需要启动Settings.ACTION_MANAGE_OVERLAY_PERMISSION
来让用户手动设置权限。
startActivityForResult(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())), REQUEST_CODE);
1.3 LayoutParam设置
WindowManager
的addView
方法有两个参数,一个是需要加入的控件对象,另一个参数是WindowManager.LayoutParam
对象。
这里需要着重说明的是LayoutParam
里的type
变量。这个变量是用来指定窗口类型的。在设置这个变量时,需要注意一个坑,那就是需要对不同版本的Android系统进行适配。
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
,这种类型是用于提供用户交互操作的非应用窗口。
而Android 8.0对系统和API行为做了修改,包括使用SYSTEM_ALERT_WINDOW
权限的应用无法再使用一下窗口类型来在其他应用和窗口上方显示提醒窗口:
- TYPE_PHONE
- TYPE_PRIORITY_PHONE
- TYPE_SYSTEM_ALERT
- TYPE_SYSTEM_OVERLAY
- TYPE_SYSTEM_ERROR
如果需要实现在其他应用和窗口上方显示提醒窗口,那么必须该为TYPE_APPLICATION_OVERLAY
的新类型。
如果在Android 8.0以上版本仍然使用TYPE_PHONE
类型的悬浮窗口,则会出现如下异常信息:
android.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRootImpl$W@f8ec928 -- permission denied for window type 2002
2. 具体实现
下面来讲解一下悬浮窗的具体实现方式。
完整的源码地址:https://github.com/dongzhong/TestForFloatingWindow
为了让悬浮窗与Activity
脱离,使其在应用处于后台时悬浮窗仍然可以正常运行,这里使用Service
来启动悬浮窗并做为其背后逻辑支撑。
在启动服务之前,需要先判断一下当前是否允许开启悬浮窗。
(MainActivity.java)
public void startFloatingService(View view) {
...
if (!Settings.canDrawOverlays(this)) {
Toast.makeText(this, "当前无权限,请授权", Toast.LENGTH_SHORT);
startActivityForResult(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())), 0);
} else {
startService(new Intent