最全面的-Android-悬浮窗功能实现--[转],开源至上

2023-05-16

(原文作者:csdn 黄林晴)

前言

我们大多数在两种情况下可以看到悬浮窗,一个是视频通话时的悬浮窗,另一个是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是否有悬浮窗权限,首先我们在配置文件中添加,悬浮窗的权限。

(很多文章标题都是悬浮窗如何绕过权限,什么设置类型为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?) {
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.MyBinder
binder.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=0
    wmParams!!.x = winManager!!.defaultDisplay.width
    wmParams!!.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.LayoutParams
get() {
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 or
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR or
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
wmParams!!.width = WindowManager.LayoutParams.WRAP_CONTENT
wmParams!!.height = WindowManager.LayoutParams.WRAP_CONTENT
return 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 = false

private inner class FloatingListener : View.OnTouchListener {

override fun onTouch(v: View, event: MotionEvent): Boolean {
val action = event.action
when (action) {
MotionEvent.ACTION_DOWN -> {
isMove = false
mTouchStartX = event.rawX.toInt()
mTouchStartY = event.rawY.toInt()
mStartX = event.x.toInt()
e: Boolean = false

private inner class FloatingListener : View.OnTouchListener {

override fun onTouch(v: View, event: MotionEvent): Boolean {
val action = event.action
when (action) {
MotionEvent.ACTION_DOWN -> {
isMove = false
mTouchStartX = event.rawX.toInt()
mTouchStartY = event.rawY.toInt()
mStartX = event.x.toInt()

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

最全面的-Android-悬浮窗功能实现--[转],开源至上 的相关文章

  • 短视频文案怎么写?优质短视频文案写作技巧

    抖音短视频的质量仔细推敲起来确实会涉及到非常多的因素 xff0c 但真正决定你视频曝光的关键就是视频文案 xff0c 从剧情的铺垫 冲突 反转 xff0c 这在一定程度上能够吸引不少的观众 又或者小说开篇要么叙述宏达的故事背景 xff0c
  • Windows如何登录linux?

    1 用SSH命令 xff0c windows连接Linux xff08 1 xff09 ssh l root 192 168 5 15 xff08 2 xff09 ssh root 64 192 168 5 10 首次连接yes 即可 2
  • 单链表及其Java代码实现

    众所周知 xff0c 数组和链表都有内存寻址的功能 而数组元素的内存地址是连续的 xff0c 在寻址的时候很容易就可以找到下一个节点的值 而链表中元素的地址不是连续的 xff0c 那我们应该怎么去寻址呢 xff1f 假设现在计算机中有三个节
  • 创建Java Web项目并实现前后端交互

    目录 一 xff1a 使用工具 二 xff1a 创建Java Web项目 三 xff1a 编写代码 一 xff1a 使用工具 Eclipse 版本如下 xff1a 注 xff1a 不同版本的eclipse创建的项目基础目录可能会有所不同 二
  • 不坠华年(大一篇)

    秒速5厘米的樱花 这一年 xff0c 我遇到了想共度一生的人 xff0c 但是 从来没谈过恋爱的我根本不知道如何去追女孩子 xff0c 算了 xff0c 只要她快乐就好 xff0c 喜欢也不一定会在一起吧 xff0c 我总是说愿意等 xff
  • 数据结构之平衡有序二叉树

    我们知道引入树是为了提高数据存储 xff0c 读取的效率 可是有的二叉树并不能提高效率 xff0c 例如下面的这个树 这是一种极端的情况 xff0c 实际上它已经和链表一样了 xff0c 现在对它进行查询 xff0c 时间复杂度已经成为了O
  • 数据结构之B+树

    B 43 树结构特点 1 非叶子节点仅具有索引作用 xff0c 也就是说 xff0c 非叶子节点只能存储Key 不能存储value xff1b 2 树的所有叶节点构成一个有序链表 xff0c 可以按照key排序的次序依次遍历全部数据 B 4
  • MyBatis之Maven配置

    一 什么是Maven Maven是一个项目管理工具 xff0c 它包含了一个对象模型 一组标准集合 xff0c 一个依赖管理系统 和用来运行定义在生命周期阶段中插件目标和逻辑 二 下载配置相关版本的Maven 解压之后做相关的配置 1 配置
  • 数据库三大范式

    第一范式 xff08 1NF xff09 xff1a 在设计数据库时 xff0c 第一范式是对数据库的基本要求 满足第一范式的关系数据库的每一个域均为原子性的 xff0c 即数据库表的每一列都是不可分割的原子项 xff0c 而不能是集合 数
  • 数据库设计规范

    字符集 表格创建规范 所有的表都必须有这几个字段 删除一般都是软删除 xff0c 01删除标志 id等数字类型的字段不能只使用bigint用bigint unsigned xff08 为什么 xff1a xff09 当数据库中该属性添加un
  • git的版本控制流程以及特性分支之间怎样切换

    git xff1a 版本控制工具 xff08 例 xff1a 淘宝每次升级版本号加1 xff0c git来控制版本 xff09 命令 git pull xff1a 用于从远程更新存储库的本地版本 git commit xff1a 主要用于将
  • Android _ ViewBinding 与 Kotlin 委托双剑合璧,从思维图到基础再到深入

    lt LinearLayout tools viewBindingIgnore 61 true gt 3 2 创建绑定类 有三个创建绑定类的 API xff1a fun bind view View T fun inflate inflat
  • Android语音播报、后台播报、语音识别,android程序开发

    lt xml version 61 34 1 0 34 encoding 61 34 gt Android学习笔记总结 43 最新移动架构视频 43 大厂安卓面试真题 43 项目实战源码讲义 gt gt docs qq com doc DS
  • Spring框架介绍和使用

    一 Spring框架介绍 1 Spring简介 Spring是2003年兴起的一个Java轻量级框架 xff0c 它是为解决企业级开发的复杂性而创建的一个框架 它是一个分层的JavaSE EE Full Stack xff08 一站式 xf
  • SpringMVC注解开发运行原理

    服务器初始化过程 xff1a 1 xff1a 服务器初始化 加载继承 AbstractDispatcherServletInitializer即springmvc定义的代替web xml的 类初始化web容器 然后重写三个方法 xff1a
  • Unity3d游戏开发之C#开发(一)

    概要 撰写原因 xff1a 当今市面上关于Unity的教程的脚本几乎都是用js来当做教程语言的 xff0c 因为js比较容易学习 xff0c 所以如果是新学的朋友建议学js的版本 xff0c 如有特殊要求需要学习C 写脚本的话那么这系列文章
  • Spring配置文件详解

    Spring配置文件详解 1 spring的配置文件详解 Bean标签基本配置 用于配置对象让 spring 来创建的 默认情况下它调用的是类中的无参构造函数 如果没有无参构造函数则不能创建成功 属性 xff1a id 给对象在容器之中提供
  • [WinError 10061] 由于目标计算机积极拒绝,无法连接。解决办法

    相信来到这的人一定遇到这样的情况 xff0c 十分的难受 这里给大家总结了三种方法 xff0c 看看能不能解决大家的困难 1 代理问题 大家看看自己是不是因为正在搞东西 xff0c 列如 xff1a burpsuite xff0c 设置了代
  • 快速解决eclipse无法启动,一启动就报错,且整个项目显示红色感叹号和×

    eclipse中由于没有适配的server xff0c 所以需要用Tomcat来支持项目的运行 xff0c 也会出现各种各样的错误 xff0c 比如eclipse无法启动 xff0c 整个项目显示红色 xff01 和红色 该如何解决 xff
  • un7.10:在IDEA中使用若依框架实现增删改查功能。

    在java的世界中 xff0c 有很多框架提供我们使用 xff0c 今天我们就来讲一讲若依框架 若依框架是一款非常便捷的框架 xff0c 他相对于spring boot框架而言 xff0c 显得更为简单 xff0c 更为便捷 xff0c 比

随机推荐

  • docker常用操作命令。

    docker是一款基于Linux之上的容器 xff0c 由于它的抽象层比虚拟机少 xff0c 而且利用的是主机内流 xff0c 不需要价值操作系统的内核 xff0c 所以它比传统的虚拟机更快 xff0c 所以越来越多的人选择用docker了
  • un8.21:用html实现增删改查功能(代码篇)。

    在项目中 xff0c 我们不仅要在后端代码实现增删改查的功能 xff0c 前端也是必不可少的 xff0c 那么在前端页面中应该如何实现完整的一套增删该查呢 xff1f 接下来附上代码 xff0c 之后的文章里会有详解 一 在html中写出大
  • 达梦(DM)数据库介绍。

    达梦数据库管理系统是达梦公司推出的具有完全自主知识产权的高性能数据库管理系统 简称DM 达梦数据库也属于关系型数据库 xff0c 主要有以下几个概念 数据库 实例 用户 表空间 模式 表 角色 数据文件 DM8采用全新的体系架构 xff0c
  • @TableId注解详细介绍

    64 TableId注解是专门用在主键上的注解 xff0c 如果数据库中的主键字段名和实体中的属性名 xff0c 不一样且不是驼峰之类的对应关系 xff0c 可以在实体中表示主键的属性上加 64 Tableid注解 xff0c 并指定 64
  • mybatis-plus自动填充功能

    有些时候我们可能会有这样的需求 xff0c 插入或者更新数据时 xff0c 希望有些字段可以自动填充数据 xff0c 比如密码 version等 在MP中提供了这样的功能 xff0c 可以实现自动填充 一 添加 64 TableField注
  • mybatis-plus之逻辑删除及通用枚举

    开发系统时 xff0c 有时候在实现功能时 xff0c 删除操作需要实现逻辑删除 xff0c 所谓逻辑删除就是将数据标记为删除 xff0c 而并非真正的物理删除 xff08 非delete操作 xff09 xff0c 查询时需要携带状态条件
  • MATLAB R2014b 的安装破解

    注 xff1a 本博文纯供技术学习之用 xff0c 无意侵权 xff0c 请不要传播转载并在破解24小时之内删除 提要 xff1a 因为考虑到数学建模和日常的数学学习只用安装的matlab xff0c 现在我了解的最新的应该就是matlab
  • 解决Field ‘name‘ doesn‘t have a default value报错

    今天写代码的时候报错Field 39 name 39 doesn 39 t have a default value xff0c 如下图 xff1a 是数据库属性字段没有赋初值 xff0c 然后我就去看数据库 xff0c 发现数据库 先说解
  • 仿macos风格的windows11桌面(适用win10,win11,齐全模板与配件,及相关问题解决方案)

    仿macos风格的windows11桌面在一定程度上加大了对win11的美化同时在原有的操作风格上对win11也进行了风格上的统一 xff0c 废话不多说 xff0c 先上效果图 是不是一眼就有所心动呢 xff0c 先介绍一下这个桌面上真正
  • retina屏是什么意思(Retina屏和一般显示屏幕的区别)

    可能许多比较关注电脑方面的信息或者有关于苹果的消息的小伙伴们都听说过 34 Retina 34 这个词 苹果手机 xff0c Retina视网膜屏幕 xff0c 是指人眼在正常观察距离下 xff0c 视网膜无法区分单个像素 xff0c 不再
  • 新员工-产品经理如何上手一个新产品,进行产品规划并开始工作

    产品规划 nbsp nbsp nbsp 是指产品规划人员通过调查研究 在研究市场 探寻客户 消费者需求 分析竞争对手 衡量外在机会与风险 以及对市场和技术发展态势综合研究的基础上 根据公司自身的情况和发展方向 制定出可以把握市场机会 满足消
  • Lottie动画资源放到服务器预下载,2021年Android开发突破20k有哪些有效的路径

    资源打包成zip文件放到了服务器上 xff0c 所以下载下来要解压缩 LottieAnimationUtils unZip download file parentPath downloadCount true override fun o
  • OkHttp的使用之{RequestBody、FormBody,【一步教学,一步到位