前⾔
我们⼤多数在两种情况下可以看到悬浮窗,⼀个是视频通话时的悬浮窗,另⼀个是360卫⼠的悬浮球,实现此功能的⽅式⽐较多,这⾥以视频通话悬浮窗中的需求为例。编码实现使⽤Kotlin。Java版本留⾔邮箱即可。
业务场景
以视频通话为例,在视频通话时,我们打开其他应⽤或点击Home键退出时或点击缩放图标,悬浮窗会显⽰在其他应⽤之上,给⼈的假象是通话页⾯变⼩了,点击悬浮窗回到通过页⾯,悬浮窗消失。退出通话页⾯悬浮窗消失。
业务场景技术分析
在编码之前,我们必须将流程整理好,这样更有利于编码的实现。实现⼀个功能如果需要10分钟,思考的时间是7分钟,编码占⽤的时间只是三分钟。
1.悬浮窗可以显⽰在其他应⽤或launchers之上,这个肯定需要悬浮窗权限,⽽悬浮窗权限属于特殊权限,
所以只能通过引导⽤户去打开⽆法像危险权限那样直接申请。可以做到后台显⽰则说明悬浮窗是⼀个Service。
2.通话页⾯隐藏时悬浮窗显⽰,通话页⾯显⽰时悬浮窗隐藏,可以看出悬浮窗和Activity的⽣命周期相关联,所以悬浮窗的Service和通话页⾯的Activity是通过bind去绑定的。
3.既然Service和Activity是通过bind去绑定的,说明当悬浮窗显⽰的时候,通话Activity虽然不可见但仍在运⾏。
结合上述技术问题分析,我们倒叙⼀⼀通过编码实现
悬浮窗实现⽅案
实现效果
准备⼯作
⾸先我们新建⼀个项⽬,项⽬中有两个Activity,我们在第⼆个Activity编写通话模拟页⾯。在第⼆个页⾯的原因我们后⾯会讲到。
如何将acitivity置于后台
其实很简单,我们调⽤⼀个⽅法即可
moveTaskToBack(true);
这个⽅法的含义就是将当前的任务战置于后台,so,为什么我要在第⼆个Activity中实现的原因之⼀,因为默认的Activity的启动模式是标准模式,⽽上⾯⽅法会将任务栈置于后台⽽不是⼀个单独的Activity,所以我们为了显⽰悬浮窗时不影响操作软件的其他功能,我们要将通话页⾯的Activity设置为singleInstance,这样当调⽤上⾯⽅法的时候只是将通话页⾯所在的Activity栈置于后台,如果你还不了解启动模式可以移步⾄上⼀篇⽂章:。
我们现在在右上⽅的点击事件中添加上述代码,可以看到通话页⾯的Activity的已经在后台运⾏了。
判断是否有悬浮窗权限
点击左上⾓图标时,我们要先判断当前app是否有悬浮窗权限,⾸先我们在配置⽂件中添加,悬浮窗的权限。
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
(很多⽂章标题都是悬浮窗如何绕过权限,什么设置类型为TOAST或者PHONE,我想说不可能的事,TOAST类型的虽然部分机型可以显⽰但是就是⼀个普通的TOSAT会⾃动消失)
那么我们如何判断是否有悬浮窗权限呢,这⼀块不同⼚商处理⽅案可能不⼀样,这⾥我们⽤⼀种通⽤的处理⽅案,测试表明除了(vivo部分)⽆效,其他多数机型都ok。并且vivo部分机型通话也不会弹出提⽰(这我就放⼼了~)
fun zoom(v: View) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!Settings.canDrawOverlays(this)) {
Toast.makeText(this, "当前⽆权限,请授权", Toast.LENGTH_SHORT)
GlobalDialogSingle(this, "", "当前未获取悬浮窗权限", "去开启", DialogInterface.OnClickListener { dialog, which ->
dialog.dismiss()
startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + packageName)), 0)
}).show()
} else {
moveTaskToBack(true)
val intent = Intent(this@Main2Activity, FloatWinfowServices::class.java)
hasBind = bindService(intent, mVideoServiceConnection, Context.BIND_AUTO_CREATE)
}
}
}
我们通过Settings.canDrawOverlays(this)来判断当前应⽤是否有悬浮窗权限,如果没有,我们弹窗提⽰,通过startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + packageName)), 0)
跳转到开启悬浮窗权限页⾯。如果悬浮窗权限已开启,直接将当前任务栈置于后台,开启服务即可。
其实回调⽅法,并没有直接告诉我们是否授权成功,所以我们需要在回调中再次判断
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
if (requestCode == 0) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!Settings.canDrawOverlays(this)) {
Toast.makeText(this, "授权失败", Toast.LENGTH_SHORT).show()
} else {
Handler().postDelayed({
val intent = Intent(this@Main2Activity, FloatWinfowServices::class.java)
intent.putExtra("rangeTime", rangeTime)
hasBind = bindService(intent, mVideoServiceConnection, Context.BIND_AUTO_CREATE)
moveTaskToBack(true)
}, 1000)
}
}
}
}
这⾥我们可以看到回调中延迟了1秒,因为测试发现某些机型反应“过快”,收到回调的时候还以为没有授权成功,其实已经成功了。
绑定Service我们需要⼀个ServiceConnection对象
internal var mVideoServiceConnection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
// 获取服务的操作对象
val binder = service as FloatWinfowServices.MyBinder
binder.service
}
override fun onServiceDisconnected(name: ComponentName) {}
}
Main2Activity的完整代码如下所⽰:
/**
* @author Huanglinqing
*/
class Main2Activity : AppCompatActivity() {
private val chronometer: Chronometer? = null
private var hasBind = false
private val rangeTime: Long = 0
override fun onCreate(savedInstanceState: Bundle?) {
setContentView(R.layout.activity_main2)
}
fun zoom(v: View) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!Settings.canDrawOverlays(this)) {
Toast.makeText(this, "当前⽆权限,请授权", Toast.LENGTH_SHORT)
去云南旅游什么地方最好玩呢GlobalDialogSingle(this, "", "当前未获取悬浮窗权限", "去开启", DialogInterface.OnClickListener { dialog, which ->
dialog.dismiss()
startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + packageName)), 0) }).show()
} else {
moveTaskToBack(true)
val intent = Intent(this@Main2Activity, FloatWinfowServices::class.java)
hasBind = bindService(intent, mVideoServiceConnection, Context.BIND_AUTO_CREATE)
}
}
}
internal var mVideoServiceConnection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
// 获取服务的操作对象
val binder = service as FloatWinfowServices.MyBinder
binder.service
}
个税app怎么退税override fun onServiceDisconnected(name: ComponentName) {}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
if (requestCode == 0) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!Settings.canDrawOverlays(this)) {
Toast.makeText(this, "授权失败", Toast.LENGTH_SHORT).show()
} else {
} else {
Handler().postDelayed({
什么是透明图片val intent = Intent(this@Main2Activity, FloatWinfowServices::class.java)
intent.putExtra("rangeTime", rangeTime)
hasBind = bindService(intent, mVideoServiceConnection, Context.BIND_AUTO_CREATE)
moveTaskToBack(true)
}, 1000)
}
}
一年级下册数学期中考试试卷}
}
override fun onRestart() {
Log.d("RemoteView", "重新显⽰了")
//不显⽰悬浮框
if (hasBind) {
徐峥出轨
unbindService(mVideoServiceConnection)
hasBind = false
}
}
override fun onNewIntent(intent: Intent) {
}
override fun onDestroy() {
}
}
新建悬浮窗Service
新建悬浮窗Service FloatWinfowServices,因为我们使⽤的BindService,我们在onBind⽅法中初始化service中的布局
override fun onBind(intent: Intent): IBinder? {
任嘉伦老婆大闹横店initWindow()
//悬浮框点击事件的处理
initFloating()
return MyBinder()
}
service中我们通过WindowManager来添加⼀个布局显⽰。
发布评论