android 悬浮窗踩坑经历
最近公司产品想做一个语音通话需求,需要使用到悬浮窗显示通话,我内心窃喜,心想这不是很简单,这玩意网上肯定很多现成的,随便github找找就可以解决啦,然而才发现这趟踩坑经历才刚刚开始QAQ悬浮窗。
前言
最近公司产品想做一个语音通话需求,需要使用到悬浮窗显示通话,我内心窃喜,心想这不是很简单,这玩意网上肯定很多现成的,随便github找找就可以解决啦,然而才发现这趟踩坑经历才刚刚开始QAQ
准备工作
悬浮窗实现
本着网上有的就绝对不自己造轮子的想法,在github上找了一圈悬浮窗第三方库,发现大部分第三方库都是处于“年久失修”的状态,最后发现轮子哥的EasyWindow还是香喷喷的,不得不赞叹轮子哥真是android领域的一个真神,各种轮子而且一直维护着,issue基本解决
easyWindow
业务场景技术分析
在解决完悬浮窗的实现可以说算是解决了整个业务的一个大头的啦,下面的就是一些细节问题,然而就是这些细节问题非常的繁琐
- 悬浮窗权限的申请
我们都知道要想让悬浮窗悬浮在所有应用的上层必须要获得系统悬浮窗权限,他是一个特殊的权限
//首先要在清单文件中添加悬浮窗权限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
//申请悬浮窗权限(这里我使用了blankj的工具类来请求权限)
private fun requestWindowPermission() {
if (PermissionUtils.isGrantedDrawOverlays()) {
createFloatWindow()
} else {
PermissionUtils.requestDrawOverlays(object : PermissionUtils.SimpleCallback {
override fun onGranted() {
createFloatWindow()
}
override fun onDenied() {
Toast.makeText(this@CallActivity, "请求悬浮窗权限失败", Toast.LENGTH_SHORT)
.show()
}
})
}
}
private fun createFloatWindow(finishCreate:(()->Unit)) {
floatWindow = EasyWindow<EasyWindow<*>>(MyApp.getMyApp()).apply {
setContentView(R.layout.layout_call_float_window)
// 设置成可拖拽的
draggable = SpringDraggable(SpringDraggable.ORIENTATION_HORIZONTAL)
// 设置显示时长
setDuration(1000 * 60 * 60)
setGravity(Gravity.END)
}
floatWindow?.setOnClickListener { window, view ->
if(Build.VERSION.SDK_INT>= Build.VERSION_CODES.O){
startActivity(Intent(this, MainActivity::class.java))
startActivity(Intent(this, CallActivity::class.java))
}else{
moveToFront()
}
floatWindow?.cancel()
MainActivity.isFloatWindowShow = false
}
floatWindow?.show()
moveTaskToBack(true)
MainActivity.isFloatWindowShow = true
finishCreate()
}
- 如何让activity最小化不被销毁只是在后台运行
我们都知道当你activity按了back键后activity就会被销毁掉,这样我们就不能实现保留悬浮窗通话这个逻辑了,所以我们就需要使用到 moveTaskToBack(true) 方法了,这个方法的作用是把当前的任务栈放置到后台运行,这样的话activity就不会被销毁了,但是注意这里说的是把任务栈放到后台,我们在把通话界面最小化后,应该还可以使用app的其他功能,所以这时候我们应该把通话界面的activity的启动模式设置为singleinstance(具体启动模式建议观看官方文档)
到这儿我们基本可以实现悬浮窗功能了
开始踩坑
1.最小化通话界面后点击home发现activity被销毁了
原因就在于由于我们使用了moveTaskToBack方法,他会让activity在后台运行,但是按下home键时系统会清理最近不活动的和application相同的taskAffinity的所有处于后台的栈,taskAffinity默认与application是同一个,所以我们只要给CallActivity设置一个不是包名得taskAffinity,他就不会被系统销毁
<activity
android:name=".CallActivity"
android:exported="false"
android:launchMode="singleInstance"
android:screenOrientation="portrait"
android:taskAffinity=".taskCall"
tools:ignore="LockedOrientationActivity" />
2.当用户在通话界面直接上划回到桌面,在点击桌面app的图标发现返回到的是app的首页而不是通话界面
这是因为我们在androidManifest.xml中设置的默认启动的activity是MainActivity,所以这时候点击app图标他启动的肯定是MainActivity,所以这里我的处理办法是在MainActivity中加一个isCalling flag,用于跳转到我们的通话界面
MainActivity
companion object{
var isFloatWindowShow:Boolean=false
var isCalling:Boolean=false
}
override fun onResume(){
super.onResume()
gotoCallActivity()
}
private fun gotoVoiceMatchActivity() {
if (isCalling && !isFloatWindowShow && lastActivity() is MainActivity) {
view().postDelayed({
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startActivity(this, CallActivity::class.java) {}
} else {
moveToFront()
}
}, 50)
}
}
3.关于AndroidMainfest中的excludeFromRecents属性
我们可以看到在我们设置CallActivity的启动模式为singleInstance后,在我们的后台任务出现了两个我们app的任务,这是由于CallActivity是位于单独的一个activity任务栈,所以就会出现两个任务,这时候我们只需要设置excludeFromRecents=true,就可以隐藏后台的任务,但是最坑爹的来了!!!,我们之前不是设置了taskAffinity这个属性吗,在我自己测试使用的时候没有问题,但是后来我们的测试说,在最小化通话界面后,随便打开一个其他的app后,我们的通话界面就被销毁掉啦。。。这不开玩笑嘛,那这做悬浮窗还有什么意义,找了半天也没有发现原因,后来在网上发现这篇文章:类似微信语音聊天界面,SingleInstance、SingleTask实际遇到的问题:launcher冷启动一个app时会将设置为android:excludeFromRecents="true"并且设置了taskAffinity的task干掉,这时候我们不得不把excludeFromRecents属性去掉,这时候注意悬浮窗的处理
CallActivity
//用户从后台任务中直接点击通话界面任务进入的时候记得销毁悬浮窗
override fun onResume() {
super.onResume()
floatWindow?.let { fw ->
if (fw.isShowing) {
fw.cancel()
MainActivity.isFloatWindowShow=false
}
}
}
4.关于点击悬浮窗返回通话界面低版本手机startActivity方法失效
我们知道在最小化后点击悬浮窗要返回通话界面,按道理直接使用startActivity即可,但是有些低版本的手机调用居然毫无反应,这时候还是使用万能的搜索引擎发现有前辈也遇到这个问题,并且给了一个解决办法:Android SingleInstance的Activity与悬浮窗切换逻辑(startActivity无反应)
private fun moveToFront() {
val manager = getSystemService(ACTIVITY_SERVICE) as ActivityManager?
if (manager != null) {
val recentTasks = manager.getRunningTasks(Int.MAX_VALUE)
if (recentTasks != null && recentTasks.isNotEmpty()) {
for (taskInfo in recentTasks) {
val cpn = taskInfo.baseActivity
if (null != cpn && TextUtils.equals(
CallActivity::class.java.name,
cpn.className
)
) {
manager.moveTaskToFront(
taskInfo.id,
ActivityManager.MOVE_TASK_NO_USER_ACTION
)
break
}
}
}
}
}
最后
关于悬浮窗的需求暂告一个段落,具体代码可以看这:悬浮窗
更多推荐
所有评论(0)