前言
重构了公司的一个项目,有一处用到了悬浮窗,就是程序后台运行后,悬浮窗依然可以显示到界面上。重构完准备上线,后来发现在6.0手机上有问题,无法显示,百般对比重构之前的代码,总感觉没什么不一样了呀,为什么别人代码写乱七八糟的都可以,我的写么好竟然不行。花了好多时间,百度什么的也找不到原因,真的是花了好多时间才找到了原因,这里记录一下解决问题的思路。
悬浮窗在Android6.0的坑
我有个解决问题的技巧就是写个小Demo,这样就没有其他不相关的代码,Demo里只写关于悬浮窗的代码,新建一个项目,语言选Kotlin,最小SDK为15,其它都默认,我用的是最新版本的AndroidStudio,什么Gradle什么鬼的都是更新到了最新的,项目创建好之后打开MainActivity,写代码,如下:
class MainActivity : AppCompatActivity() {
override fun onTouchEvent(event: MotionEvent): Boolean {
if (event.action == MotionEvent.ACTION_DOWN) {
val button = Button(this).apply { text = "你好"; setOnClickListener {
Toast.makeText(context, "我不好", Toast.LENGTH_SHORT).show()
} }
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
windowManager.addView(button, WindowManager.LayoutParams().apply {
type = WindowManager.LayoutParams.TYPE_TOAST
flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
width = WindowManager.LayoutParams.WRAP_CONTENT
height = WindowManager.LayoutParams.WRAP_CONTENT
gravity = Gravity.START or Gravity.BOTTOM
format = PixelFormat.TRANSLUCENT
})
}
return super.onTouchEvent(event)
}
}
对,你没看错,就是onCreate方法都不要,奇怪吧,我就是要代码最小化,不相关的代码都不要,Activity是可以不要onCreate方法的,长知识了吧。这里只关注Android6.0版本,所以版本权限啊什么的不用管,上面代码中把一个Button设置为悬浮窗的界面内容,Buttion的点击事件是弹出一个Toast,悬浮窗的位置是在右下角,代码特别整洁特别少吧!运行后是一个空白的界面,如下:
点击界面的任意位置,此时在右下角会弹出悬浮窗,如下:
此时点击“你好”是可以弹出Toast的。
按下返回键,我们发现悬浮窗也不见了。重新运行并点击屏幕,再次出现悬浮窗,此时按Home键,这样创建悬浮窗的Activity就没有销毁,此时发现悬浮窗还在,如下:
此时点击“你好”,发现不起作用,但是这个代码运行到Android7.0是完全没问题的,这就是神奇之处,在这里花了我好多时间,百度了好多文章也找不到答案,最后实在没办法又去对比了比了n多次重构前的代码,最后发现有一个地方不同,就是获取WindowManager的地方,如下:
val windowManager = application.getSystemService(Context.WINDOW_SERVICE) as WindowManager
这里是使用application来获取的系统服务,谁能想到啊!!万万也想不到啊,按我之前学习的理解,系统服务一般是单例的,在哪里获取都一样,没想到activity.getSystemService与application.getSystemService竟然是有区别的,一万匹吃草的马跑过,浪费我一天的时间啊,这Android真的是,无处不在的坑!!
使用了application来获取WindowManager之后,问题就解决了,就算结束activity,只要不结束app,悬浮窗都可以显示,而且应用后台运行后,悬浮窗的点击事件也是正常的。
最后试验了下各种上下文获取的WindowManager对象,如下:
println("activity: ${getSystemService(Context.WINDOW_SERVICE)}")
println("activity: ${getSystemService(Context.WINDOW_SERVICE)}")
println("baseContext: ${baseContext.getSystemService(Context.WINDOW_SERVICE)}")
println("application: ${application.getSystemService(Context.WINDOW_SERVICE)}")
println("applicationContext: ${applicationContext.getSystemService(Context.WINDOW_SERVICE)}")
经过经验发现,只要是不同的对象,获取的WindowManager就不是同一个对象,如果是同一个对象,则获取的就是同一个,比如在一个Activity里面多次调用getSystemService来获取WindowManager对象将是同一个,在不同的Activity里面获取的将是不同的对象。而application和applicationContext获取的是相同的,并且它们是全局的,所以不论在哪个Activity中调用application去获取都将得到同一个WindownManager对象。
Android7.1.1悬浮窗
因为我们公司只需要适配Android6.0和Android7.1.1,所以这里把Android7.1.1的悬浮窗实现也记录一下。
上面的代码跑到Android7.1.1中会报如下错误:
WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
这个问题比较好解决,百度也会有相应的答案,从Android7.1.1(api25)开始,需要悬浮窗权限,我们在代码中加入动态权限申请:
// 如果是7.1.1以上的系统(包含7.1.1),则需要判断是否有悬浮窗权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && !Settings.canDrawOverlays(this)) {
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
startActivity(intent)
}
这段代码会判断是否是7.0以上的系统,如果是就判断是否有悬浮窗权限,如果没有就打开系统设置中权限申请界面,如下图:
如上图,需要指定我们的应用为“可出现在其他应用上的应用”,也就是添加悬浮窗权限,但是这里并没有显示出我们的应用,这是因为我们的应用并没有在清单文件中声明悬浮窗权限,如下:
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
添加这个权限之后,系统的申请悬浮窗界面就会列出我们的应用了,如下:
如上图,可以看到我们的应用的悬浮窗权限是“不允许”,我们设置为“允许”,再次运行,发现还是报这个错误:
WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
问题的原因是Android7.1.1及以上的系统不能使用TYPE_TOAST这个类型了,需要使用TYPE_PHONE,代码如下:
// 如果是7.1.1以上的系统(包含7.1.1),则需要使用TYPE_PHONE类型
type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) WindowManager.LayoutParams.TYPE_PHONE
else WindowManager.LayoutParams.TYPE_TOAST
最后有个小提示,我发现我们公司的那个app运行到android7.1.1手机上并不会跳出申请悬浮窗权限的界面,这是因为我们的app设置的targetSdkVersion为21,而动态权限申请是从Android6.0开始的,所以为了兼容旧版本,只要targetSdkVersion小于6.0就不需要动态申请权限,只需要在清单文件中声明即可。不过有些手机可能定制过的,即使targetSdkVersion小于6.0也会弹出隐私权限的动态申请对话框。
来源:CSDN
作者:android_cai_niao
链接:https://blog.csdn.net/android_cai_niao/article/details/104540093