分享好友 最新动态首页 最新动态分类 切换频道
Android-悬浮窗功能的实现(附Java、KT实现源码)(1)
2024-12-26 23:13

最后

很多程序员,整天沉浸在业务代码的 CRUD 中,业务中没有大量数据做并发,缺少实战经验,对并发仅仅停留在了解,做不到精通,所以总是与大厂擦肩而过。

我把私藏的这套并发体系的笔记和思维脑图分享出来,理论知识与项目实战的结合,我觉得只要你肯花时间用心学完这些,一定可以快速掌握并发编程。

不管是查缺补漏还是深度学习都能有非常不错的成效,需要的话记得帮忙点个赞支持一下

整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

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的完整代码如下所示


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()
mStartY = event.y.toInt()
}
MotionEvent.ACTION_MOVE -> {
mTouchCurrentX = event.rawX.toInt()
mTouchCurrentY = event.rawY.toInt()
wmParams!!.x += mTouchCurrentX - mTouchStartX
wmParams!!.y += mTouchCurrentY - mTouchStartY
winManager!!.updateViewLayout(mFloatingLayout, wmParams)
mTouchStartX = mTouchCurrentX
mTouchStartY = 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? = null
private var wmParams: WindowManager.LayoutParams? = null
private var inflater: LayoutInflater? = null
//浮动布局
private var mFloatingLayout: View? = null
private var linearLayout: LinearLayout? = null
private var chronometer: Chronometer? = null

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

inner class MyBinder : Binder() {
val service: FloatWinfowServices
get() = this@FloatWinfowServices
}

override fun onCreate() {
super.onCreate()
}


private fun initFloating() {
linearLayout = mFloatingLayout!!.findViewById(R.id.line1)
linearLayout!!.setOnClickListener { startActivity(Intent(this@FloatWinfowServices, Main2Activity::class.java)) }
//悬浮框触摸事件,设置悬浮框可拖动
linearLayout!!.setOnTouchListener(FloatingListener())
}

我个人认为,如果你想靠着背面试题来获得心仪的offer,用癞蛤蟆想吃天鹅肉形容完全不过分。想必大家能感受到面试越来越难,想找到心仪的工作也是越来越难,高薪工作羡慕不来,却又对自己目前的薪资不太满意,工作几年甚至连一个应届生的薪资都比不上,终究是错付了,错付了自己没有去提升技术。

这些面试题分享给大家的目的,其实是希望大家通过大厂面试题分析自己的技术栈,给自己梳理一个更加明确的学习方向,当你准备好去面试大厂,你心里有底,大概知道面试官会问多广,多深,避免面试的时候一问三不知。

大家可以把Java基础,JVM,并发编程,MySQL,Redis,Spring,Spring cloud等等做一个知识总结以及延伸,再去进行操作,不然光记是学不会的,这里我也提供一些脑图分享给大家

希望你看完这篇文章后,不要犹豫,抓紧学习,复习知识,准备在明年的金三银四拿到心仪的offer,加油,打工人

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

结以及延伸,再去进行操作,不然光记是学不会的,这里我也提供一些脑图分享给大家

[外链图片转存中…(img-7wp118A5-1715808027369)]

[外链图片转存中…(img-oULttCbz-1715808027370)]

[外链图片转存中…(img-UWuKkAj6-1715808027370)]

希望你看完这篇文章后,不要犹豫,抓紧学习,复习知识,准备在明年的金三银四拿到心仪的offer,加油,打工人

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

最新文章
用AI轻松生成你的专属高清美女写真,玩法大揭秘!
DALL-E 2:OpenAI推出的这款工具以其强大的创作能力而著称,能够生成多种风格和主题的图像,包括超写实的美女照片。用户只需要提供简单的文本描述,DALL-E 2便能巧妙地将其转化为一幅幅令人惊艳的艺术作品。不过,由于其使用门槛和有限的访
SEO分析报告中,如何解释反向链接的作用和价值?提升网站权威性与排名
反向链接的定义及其在SEO中的基本作用反向链接,通常被称为“外部链接”或“入站链接”,是指从其他网站指向您网站的链接。在搜索引擎优化(SEO)中,反向链接扮演着至关重要的角色,因为它们被视为对网站内容质量和可信度的投票。换句话说
【Android -- 学习】学习资料汇总
项目名称项目简介Google I/O 2014Google I/O Android App 使用了当时最新推出的 Material Design 设计Google play music一个跨多个平台音乐播放器Google Santa Tracker for AndroidGoogle 开源的一个儿童教育和娱乐的 Appgithub客户端开源
爬虫工具之selenium(五)-建立代理IP池
IP网站为了防止被爬取,会有反爬机制,对于同一个IP地址的大量同类型的访问,会封锁IP,过一段时间后,才能继续访问,有几种简单的应对套路:1.修改请求头,模拟浏览器(而不是代码去直接访问)去访问2.采用代理IP并轮换3.设置访问时间间隔
文件转换成PDF格式的常用方法与工具介绍
文件怎么转换成PDF格式How to Convert Files to PDF Format在现代办公和学习中,PDF(便携式文档格式)已经成为一种广泛使用的文件格式。PDF文件具有良好的跨平台兼容性,能够保持文件的格式和排版,因此在分享和打印文档时非常受欢迎。本
搜索功能升级又免费!OpenAI强势出击,挑战谷歌搜索霸权
当地时间12月16日,OpenAI宣布向全球所有用户免费开放ChatGPT的实时搜索功能。这是其为期12天的“Shipmas”活动的第八场直播内容,在此前的活动中,该公司已经展示并发布了一系列全新的AI产品与功能。搜索功能全面开放OpenAI在此次“Shipma
淘宝无忧退货是什么意思?卖家能拒绝退款吗
现在许多小伙伴们都非常愿意在淘宝购物,因为在淘宝购物它的退货是非常方便的,并且不仅有七天无理由退货,还有无忧退货,这都给消费者提供了不错的服务,但是也有些朋友不理解淘宝无忧退货的意思,那么接下来就一起往下看。一、淘宝无忧退
综合能源服务的市场潜力分析
当前,我国能源消费供给、能源结构转型、能源系统形态呈现新的发展趋势。随着互联网信息技术、可再生能源技术以及电力改革进程加快,开展综合能源服务已成为提升能源效率、降低 用能成本的重要发展方向,也成为各企业新的战略竞争和合作焦
用AI画美女,让我的数字女友一键生成!这些工具你不能错过!
步骤二:注册账户这里你可以选择快速注册,使用邮箱或手机号即可创建一个新账户。步骤三:选择模板进入主界面后,你会看到多种生成模板,选择适合你的美女写真风格。步骤四:输入参数根据自己想要的效果修改画风、服饰、发型等选项,尽量做
权限列表【梦幻AI绘画-免费AI作图AI换脸】
● 访问大概位置允许应用访问大概位置● 精确位置权限允许应用访问精确位置。● 查看网络状态允许应用程序查看所有网络的状态。● 开关WLAN允许应用开启或关闭WLAN● 拍摄照片和视频允许应用拍摄照片或录制视频● 更改网络连接性允许应用程
相关文章
推荐文章
发表评论
0评