前言

我们大多数在两种情况下可以看到悬浮窗,一个是视频通话时的悬浮窗,另一个是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的启动模式。

我们现在在右上方的点击事件中添加上述代码,可以看到通话页面的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.MyBinderbinder.service}override fun onServiceDisconnected(name: ComponentName) {}
}

Main2Activity的完整代码如下所示:

/*** @author Huanglinqing*/
class Main2Activity : AppCompatActivity() {private val chronometer: Chronometer? = nullprivate var hasBind = falseprivate val rangeTime: Long = 0override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)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.MyBinderbinder.service}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 {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() {super.onRestart()Log.d("RemoteView", "重新显示了")//不显示悬浮框if (hasBind) {unbindService(mVideoServiceConnection)hasBind = false}}override fun onNewIntent(intent: Intent) {super.onNewIntent(intent)}override fun onDestroy() {super.onDestroy()}
}
  • 新建悬浮窗Service

新建悬浮窗Service FloatWinfowServices,因为我们使用的BindService,我们在onBind方法中初始化service中的布局

override fun onBind(intent: Intent): IBinder? {initWindow()//悬浮框点击事件的处理initFloating()return MyBinder()
}

service中我们通过WindowManager来添加一个布局显示。

/*** 初始化窗口*/
private fun initWindow() {winManager = application.getSystemService(Context.WINDOW_SERVICE) as WindowManager//设置好悬浮窗的参数wmParams = params// 悬浮窗默认显示以左上角为起始坐标wmParams!!.gravity = Gravity.LEFT or Gravity.TOP//悬浮窗的开始位置,因为设置的是从左上角开始,所以屏幕左上角是x=0;y=0wmParams!!.x = winManager!!.defaultDisplay.widthwmParams!!.y = 210//得到容器,通过这个inflater来获得悬浮窗控件inflater = LayoutInflater.from(applicationContext)// 获取浮动窗口视图所在布局mFloatingLayout = inflater!!.inflate(R.layout.remoteview, null)// 添加悬浮窗的视图winManager!!.addView(mFloatingLayout, wmParams)
}

悬浮窗的参数主要设置悬浮窗的类型为

WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY

8.0 以下可设置为:

wmParams!!.type = WindowManager.LayoutParams.TYPE_PHONE

代码如下所示:

private //设置window type 下面变量2002是在屏幕区域显示,2003则可以显示在状态栏之上//设置可以显示在状态栏上//设置悬浮窗口长宽数据
val params: WindowManager.LayoutParamsget() {wmParams = WindowManager.LayoutParams()if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {wmParams!!.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY} else {wmParams!!.type = WindowManager.LayoutParams.TYPE_PHONE}wmParams!!.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL orWindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR orWindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCHwmParams!!.width = WindowManager.LayoutParams.WRAP_CONTENTwmParams!!.height = WindowManager.LayoutParams.WRAP_CONTENTreturn wmParams}

当点击悬浮窗的时候回到Activity2页面,并且悬浮窗消失,所以我们只需要给悬浮窗添加点击事件

linearLayout!!.setOnClickListener { startActivity(Intent(this@FloatWinfowServices, Main2Activity::class.java)) }

当Service走到onDestory的时候将view移除,对于Activity2页面来说 当onResume的时候 解绑Service,当onstop的时候 绑定Service。

从效果图中我们可以看到悬浮窗可以拖拽的,所以还要设置触摸事件,当移动距离超过某个值的时候让onTouch消费事件,这样就不会触发点击事件了。这个算是view比较基础的知识,相信大家都明白了。

//开始触控的坐标,移动时的坐标(相对于屏幕左上角的坐标)
private var mTouchStartX: Int = 0
private var mTouchStartY: Int = 0
private var mTouchCurrentX: Int = 0
private var mTouchCurrentY: Int = 0
//开始时的坐标和结束时的坐标(相对于自身控件的坐标)
private var mStartX: Int = 0
private var mStartY: Int = 0
private var mStopX: Int = 0
private var mStopY: Int = 0
//判断悬浮窗口是否移动,这里做个标记,防止移动后松手触发了点击事件
private var isMove: Boolean = falseprivate inner class FloatingListener : View.OnTouchListener {override fun onTouch(v: View, event: MotionEvent): Boolean {val action = event.actionwhen (action) {MotionEvent.ACTION_DOWN -> {isMove = falsemTouchStartX = event.rawX.toInt()mTouchStartY = event.rawY.toInt()mStartX = event.x.toInt()mStartY = event.y.toInt()}MotionEvent.ACTION_MOVE -> {mTouchCurrentX = event.rawX.toInt()mTouchCurrentY = event.rawY.toInt()wmParams!!.x += mTouchCurrentX - mTouchStartXwmParams!!.y += mTouchCurrentY - mTouchStartYwinManager!!.updateViewLayout(mFloatingLayout, wmParams)mTouchStartX = mTouchCurrentXmTouchStartY = mTouchCurrentY}MotionEvent.ACTION_UP -> {mStopX = event.x.toInt()mStopY = event.y.toInt()if (Math.abs(mStartX - mStopX) >= 1 || Math.abs(mStartY - mStopY) >= 1) {isMove = true}}else -> {}}//如果是移动事件不触发OnClick事件,防止移动的时候一放手形成点击事件return isMove}
}

FloatWinfowServices所有代码如下所示:

class FloatWinfowServices : Service() {private var winManager: WindowManager? = nullprivate var wmParams: WindowManager.LayoutParams? = nullprivate var inflater: LayoutInflater? = null//浮动布局private var mFloatingLayout: View? = nullprivate var linearLayout: LinearLayout? = nullprivate var chronometer: Chronometer? = nulloverride fun onBind(intent: Intent): IBinder? {initWindow()//悬浮框点击事件的处理initFloating()return MyBinder()}inner class MyBinder : Binder() {val service: FloatWinfowServicesget() = this@FloatWinfowServices}override fun onCreate() {super.onCreate()}/*** 悬浮窗点击事件*/private fun initFloating() {linearLayout = mFloatingLayout!!.findViewById<LinearLayout>(R.id.line1)linearLayout!!.setOnClickListener { startActivity(Intent(this@FloatWinfowServices, Main2Activity::class.java)) }//悬浮框触摸事件,设置悬浮框可拖动linearLayout!!.setOnTouchListener(FloatingListener())}//开始触控的坐标,移动时的坐标(相对于屏幕左上角的坐标)private var mTouchStartX: Int = 0private var mTouchStartY: Int = 0private var mTouchCurrentX: Int = 0private var mTouchCurrentY: Int = 0//开始时的坐标和结束时的坐标(相对于自身控件的坐标)private var mStartX: Int = 0private var mStartY: Int = 0private var mStopX: Int = 0private var mStopY: Int = 0//判断悬浮窗口是否移动,这里做个标记,防止移动后松手触发了点击事件private var isMove: Boolean = falseprivate inner class FloatingListener : View.OnTouchListener {override fun onTouch(v: View, event: MotionEvent): Boolean {val action = event.actionwhen (action) {MotionEvent.ACTION_DOWN -> {isMove = falsemTouchStartX = event.rawX.toInt()mTouchStartY = event.rawY.toInt()mStartX = event.x.toInt()mStartY = event.y.toInt()}MotionEvent.ACTION_MOVE -> {mTouchCurrentX = event.rawX.toInt()mTouchCurrentY = event.rawY.toInt()wmParams!!.x += mTouchCurrentX - mTouchStartXwmParams!!.y += mTouchCurrentY - mTouchStartYwinManager!!.updateViewLayout(mFloatingLayout, wmParams)mTouchStartX = mTouchCurrentXmTouchStartY = mTouchCurrentY}MotionEvent.ACTION_UP -> {mStopX = event.x.toInt()mStopY = event.y.toInt()if (Math.abs(mStartX - mStopX) >= 1 || Math.abs(mStartY - mStopY) >= 1) {isMove = true}}else -> {}}//如果是移动事件不触发OnClick事件,防止移动的时候一放手形成点击事件return isMove}}/*** 初始化窗口*/private fun initWindow() {winManager = application.getSystemService(Context.WINDOW_SERVICE) as WindowManager//设置好悬浮窗的参数wmParams = params// 悬浮窗默认显示以左上角为起始坐标wmParams!!.gravity = Gravity.LEFT or Gravity.TOP//悬浮窗的开始位置,因为设置的是从左上角开始,所以屏幕左上角是x=0;y=0wmParams!!.x = winManager!!.defaultDisplay.widthwmParams!!.y = 210//得到容器,通过这个inflater来获得悬浮窗控件inflater = LayoutInflater.from(applicationContext)// 获取浮动窗口视图所在布局mFloatingLayout = inflater!!.inflate(R.layout.remoteview, null)chronometer = mFloatingLayout!!.findViewById<Chronometer>(R.id.chronometer)chronometer!!.start()// 添加悬浮窗的视图winManager!!.addView(mFloatingLayout, wmParams)}private //设置window type 下面变量2002是在屏幕区域显示,2003则可以显示在状态栏之上//设置可以显示在状态栏上//设置悬浮窗口长宽数据val params: WindowManager.LayoutParamsget() {wmParams = WindowManager.LayoutParams()if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {wmParams!!.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY} else {wmParams!!.type = WindowManager.LayoutParams.TYPE_PHONE}wmParams!!.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL orWindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR orWindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCHwmParams!!.width = WindowManager.LayoutParams.WRAP_CONTENTwmParams!!.height = WindowManager.LayoutParams.WRAP_CONTENTreturn wmParams}override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {return super.onStartCommand(intent, flags, startId)}override fun onDestroy() {super.onDestroy()winManager!!.removeView(mFloatingLayout)}
}
  • 实际应用中需要考虑的一些其他问题

在使用使用的过程中,我们肯定会遇到其他问题:

1.用户使用过程中,可能会直接按Home键,这个时候如何提示呢?

产生问题原因:因为用户按Home键之后,开发者无法重写Home键逻辑,此时应用不在前台运行,无法弹窗提醒,此时用户点击APP图标进入的是第一个栈,这个时候用户就没有进入通话页面的入口了。

解决方案:

第一种解决方案 我们可以仿照微信那样去做,就是在整个通话过程中开启一个前台通知,用户点击通知时进入通话页面。

第二种解决方案 就是检测应用是否在前台,当通话页面在运行的时候,并且应用重新回到前台,我们广播到其他页面,提示权限引导即可。

2.用户在通话页面(singleInstance模式),点击Home键

应用在后台运行的时候,通话结束,Activity被finish,此时从任务程序中切回应用你会发现打开的竟然是通话页面!

这个问题简单的说就是,如果你在通话页面呼叫某人,通话过程中按Home键,然后电话挂断,此时你从任务程序中切回应用,会再次呼叫这个人,也就是这种状态下重新回到了onCreate方法。

问题产生原因:

1.因为通话页面是singleInstance模式,此时有两个任务栈,按Home键后再从任务程序中切回,此时应用只保留了第二个任务栈,已经失去了和第一个任务栈的关系,finish之后无法在回到第一个任务栈。

解决方案:

1.(不推荐)通话页面不使用singleInstance模式,这种情况下,在通话过程中无法操作软件的其他功能,一般都不采取。

2.(我目前的解决方案)设置一个标记位,标记当前是否在通话,在onCreate中如果通话已经结束了,跳转到一个过渡页面(标准模式),过渡页面中finish,就可以了,添加过渡页面的原因是我们不知道上一个页面是哪里,因为我们收到来电可能是任意页面,我们我们在过渡页面finsh之后,就再次回到了第一个任务栈。

如果有其他好的解决方案 欢迎留言。

如果需要Java版本的小伙伴 ,留言邮箱就可以了,我看到会发到邮箱哦!

-------2020年6月2日更新------

Java版本源码已提交至github

GitHub - huanglinqing123/RemoteView: Android 悬浮窗,视频通话缩放最小

欢迎start 和Issues

Android 悬浮窗功能的实现相关推荐

  1. Android 悬浮窗功能实现(微信语音通话悬浮窗效果实现)

    目录 1.基本介绍 2.代码示例 3.实现效果及便捷工具类 4.仿微信语音通话悬浮窗效果实现 4.1 需求分析及效果展示 4.2 实现 5.最后 1.基本介绍 Android 界面绘制都是通过 Win ...

  2. Android展开悬浮窗功能,Android 悬浮窗 (附圆形菜单悬浮窗)

    序言 Android悬浮窗的实现,主要有四个步骤: 1. 声明及申请权限 2. 构建悬浮窗需要的控件 3. 将控件添加到WindowManager 4. 必要时更新WindowManager的布局 一 ...

  3. Android 悬浮窗语音识别功能开发详解

    笔者是一个普通不能再普通的程序员,本着出处兴趣,花时间研究了一下,想实现手机的悬浮窗语音识别功能,这样不影响自己其它操作的,语音识别技术是用百度云语音sdk,应该不难实现,很难实现就是核心语音识别技术 ...

  4. android 仿微信来电_Android 悬浮窗功能实现(微信语音通话悬浮窗效果实现)

    目录 1.基本介绍 2.代码示例 3.实现效果及便捷工具类 4.仿微信语音通话悬浮窗效果实现 4.1 需求分析及效果展示 4.2 实现 5.最后 1.基本介绍 Android 界面绘制都是通过 Win ...

  5. Android悬浮窗的简单实现

    1. 前言 现在很多应用都有小悬浮窗的功能,比如看直播的时候,通过Home键返回桌面,直播的小窗口仍可以在屏幕上显示.下面将介绍下悬浮窗的的一种简单实现方式. 2.原理 Window我们应该很熟悉,它 ...

  6. Android悬浮窗的实现

    Android悬浮窗的实现 *本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 本文也发布于本人的知乎文章:https://zhuanlan.zhihu.com/p/39421112 ...

  7. Android悬浮窗原理解析(Window)[源码]

    悬浮窗,在大多数应用中还是很少见的,目前我们接触到的悬浮窗,差不多都是一些系统级的应用软件,例如:360安全卫士,腾讯手机管家等:在某些服务行业如金融,餐饮等,也会在应用中添加悬浮窗,例如:美团的偷红 ...

  8. Android 悬浮窗的使用(1)

    悬浮窗功能基本代码: //创建悬浮窗的图片,这里当然也可以用自定义的View,这里之用了简单的图片var imageView = ImageView(this)imageView.setImageRe ...

  9. 安卓股票悬浮窗_Android 实现悬浮窗功能

    前言 我们大多数在两种情况下可以看到悬浮窗,一个是视频通话时的悬浮窗,另一个是360卫士的悬浮球,实现此功能的方式比较多,这里以视频通话悬浮窗中的需求为例.编码实现使用Kotlin.Java版本留言邮 ...

最新文章

  1. Eclipse不编译解决方案
  2. vs内存泄露检测方法
  3. 6年,终拿腾讯 offer!
  4. linux——rpm的详细说明
  5. linux mkfs 源码,mkfs工具怎么移植到嵌入式平台下面
  6. 三丰三坐标编程基本步骤_数控车床编程,经典实例教程
  7. 2020年中国.NET开发者大会第二天 WorkShop
  8. Paint the Tree CodeForces - 1244D(看似是树,其实是条链)
  9. php时间操作函数总结,基于php常用函数总结(数组,字符串,时间,文件操作)
  10. 从硬核科幻小说《三体》中看嵌入式
  11. github flow
  12. CentOS:linux开放指定端口命令
  13. 《恋上数据结构第1季》动态数组实现栈
  14. 2020 年帮你加薪的 7 个小习惯
  15. 单片机项目开发一般步骤
  16. java数组整组处理_java – 使它漂亮:同时处理数组
  17. linux pdf 合并 脚本,在Linux中使用脚本结合多个pdf文件?
  18. 【时间序列预测】基于matlab LMS麦基玻璃时间序列预测【含Matlab源码 1443期】
  19. 菜鸟教程php在线编程器,菜鸟教程在线工具
  20. socket编程之socket()

热门文章

  1. SAP中税码、税率、税务科目的几个表及其中的勾稽关系
  2. cebemax hal库 stm32 OLED移植 解析
  3. 学生用计算机隐藏功能,学生计算器怎么去掉d
  4. 罗永浩与王自如的约战,有不少看头
  5. 玩到全身僵直!07年最强的9款PC游戏
  6. python数据分析(六)—数据清洗2
  7. 【C语言】 《弹跳的小球》游戏编写过程及思路,清屏函数、Sleep()函数的应用
  8. 网易Airtest跨平台的UI自动化测试框架
  9. 三亚三姐,三亚海鲜的代言人
  10. python三维建模和cad比较_对比Revit和CAD三维建模的不同