1. 前言

现在很多应用都有小悬浮窗的功能,比如看直播的时候,通过Home键返回桌面,直播的小窗口仍可以在屏幕上显示。下面将介绍下悬浮窗的的一种简单实现方式。

2.原理

Window我们应该很熟悉,它是一个接口类,具体的实现类为PhoneWindow,它可以对View进行管理。WindowManager是一个接口类,继承自ViewManager,从名称就知道它是用来管理Window的,它的实现类是WindowManagerImpl。如果我们想要对Window(View)进行添加、更新和删除操作就可以使用WindowManager,WindowManager会将具体的工作交由WindowManagerService处理。这里我们只需要知道WindowManager能用来管理Window就好。

  WindowManager是一个接口类,继承自ViewManager,ViewManager中定义了3个方法,分布用来添加、更新和删除View,如下所示:

public interface ViewManager {public void addView(View view, ViewGroup.LayoutParams params);public void updateViewLayout(View view, ViewGroup.LayoutParams params);public void removeView(View view);
}

WindowManager也继承了这些方法,而这些方法传入的参数都是View类型,说明了Window是以View的形式存在的。

3.具体实现

3.1浮窗布局

  悬浮窗的简易布局如下的可参考下面的layout_floating_window.xml文件。顶层深色部分的FrameLayout布局是用来实现悬浮窗的拖拽功能的,点击右上角ImageView可以实现关闭悬浮窗,剩下区域显示内容,这里只是简单地显示文本内容,不做复杂的东西,故只设置TextView。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><FrameLayoutandroid:id="@+id/layout_drag"android:layout_width="match_parent"android:layout_height="15dp"android:background="#dddddd"><androidx.appcompat.widget.AppCompatImageViewandroid:id="@+id/iv_close"android:layout_width="15dp"android:layout_height="15dp"android:layout_gravity="end"android:src="@drawable/img_delete"/></FrameLayout><androidx.appcompat.widget.AppCompatTextViewandroid:id="@+id/tv_content"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_gravity="center_horizontal"android:background="#eeeeee"android:scrollbars="vertical"/>
</LinearLayout>

3.2 悬浮窗的实现

1. 使用服务Service

  Service 是一种可在后台执行长时间运行操作而不提供界面的应用组件,可由其他应用组件启动,而且即使用户切换到其他应用,仍将在后台继续运行。要保证应用在后台时,悬浮窗仍然可以正常显示,所以这里可以使用Service。

2. 获取WindowManager并设置LayoutParams

private lateinit var windowManager: WindowManager
private lateinit var layoutParams: WindowManager.LayoutParams
override fun onCreate() {// 获取WindowManagerwindowManager = getSystemService(WINDOW_SERVICE) as WindowManagerlayoutParams = WindowManager.LayoutParams().apply {// 实现在其他应用和窗口上方显示浮窗type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY} else {WindowManager.LayoutParams.TYPE_PHONE}format = PixelFormat.RGBA_8888// 设置浮窗的大小和位置gravity = Gravity.START or Gravity.TOPflags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLEwidth = 600height = 600x = 300y = 300}
}

3. 创建View并添加到WindowManager

private lateinit var floatingView: View
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {if (Settings.canDrawOverlays(this)) {floatingView = LayoutInflater.from(this).inflate(R.layout.layout_floating_window.xml, null)windowManager.addView(floatingView, layoutParams)}  return super.onStartCommand(intent, flags, startId)
}

4. 实现悬浮窗的拖拽和关闭功能

// 浮窗的坐标
private var x = 0
private var y = 0override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {   if (Settings.canDrawOverlays(this)) {floatingView = LayoutInflater.from(this).inflate(R.layout.layout_floating_window.xml, null)windowManager.addView(floatingView, layoutParams)// 点击浮窗的右上角关闭按钮可以关闭浮窗floatingView.findViewById<AppCompatImageView>(R.id.iv_close).setOnClickListener {windowManager.removeView(floatingView)}// 实现浮窗的拖动功能, 通过改变layoutParams来实现floatingView.findViewById<AppCompatImageView>(R.id.layout_drag).setOnTouchListener { v, event ->when (event.action) {MotionEvent.ACTION_DOWN -> {x = event.rawX.toInt()y = event.rawY.toInt()}MotionEvent.ACTION_MOVE -> {val currentX = event.rawX.toInt()val currentY = event.rawY.toInt()val offsetX = currentX - xval offsetY = currentY - yx = currentXy = currentYlayoutParams.x = layoutParams.x + offsetXlayoutParams.y = layoutParams.y + offsetY// 更新floatingViewwindowManager.updateViewLayout(floatingView, layoutParams)}}true}return super.onStartCommand(intent, flags, startId)
}

5. 利用广播进行通信

private var receiver: MyReceiver? = null
override fun onCreate() {// 注册广播receiver = MyReceiver()val filter = IntentFilter()filter.addAction("android.intent.action.MyReceiver")registerReceiver(receiver, filter)
}inner class MyReceiver : BroadcastReceiver() {override fun onReceive(context: Context, intent: Intent) {val content = intent.getStringExtra("content") ?: ""// 通过Handler更新UIval message = Message.obtain()message.what = 0message.obj = contenthandler.sendMessage(message)}
}val handler = Handler(this.mainLooper) { msg ->tvContent.text = msg.obj as Stringfalse
}

可以在Activity中通过广播给Service发送信息

fun sendMessage(view: View?) {Intent("android.intent.action.MyReceiver").apply {putExtra("content", "Hello, World!")sendBroadcast(this)}
}

6. 设置权限

悬浮窗的显示需要权限,在AndroidManefest.xml中添加:

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
此外,还要通过Settings.ACTION_MANAGE_OVERLAY_PERMISSION来让动态设置权限,在Activity中设置。

// MainActivity.kt
fun startWindow(view: View?) {if (!Settings.canDrawOverlays(this)) {startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName")), 0)} else {startService(Intent(this@MainActivity, FloatingWindowService::class.java))}
}
​
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {super.onActivityResult(requestCode, resultCode, data)if (requestCode == 0) {if (Settings.canDrawOverlays(this)) {Toast.makeText(this, "悬浮窗权限授权成功", Toast.LENGTH_SHORT).show()startService(Intent(this@MainActivity, FloatingWindowService::class.java))}}
}

3.3 完整代码

class FloatingWindowService : Service() {private lateinit var windowManager: WindowManagerprivate lateinit var layoutParams: WindowManager.LayoutParamsprivate lateinit var tvContent: AppCompatTextViewprivate lateinit var handler: Handlerprivate var receiver: MyReceiver? = nullprivate var floatingView: View? = nullprivate val stringBuilder = StringBuilder()private var x = 0private var y = 0// 用来判断floatingView是否attached 到 window manager,防止二次removeView导致崩溃private var attached = falseoverride fun onCreate() {super.onCreate()// 注册广播receiver = MyReceiver()val filter = IntentFilter()filter.addAction("android.intent.action.MyReceiver")registerReceiver(receiver, filter);// 获取windowManager并设置layoutParamswindowManager = getSystemService(WINDOW_SERVICE) as WindowManagerlayoutParams = WindowManager.LayoutParams().apply {type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY} else {WindowManager.LayoutParams.TYPE_PHONE}format = PixelFormat.RGBA_8888
//            format = PixelFormat.TRANSPARENTgravity = Gravity.START or Gravity.TOPflags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLEwidth = 600height = 600x = 300y = 300}handler = Handler(this.mainLooper) { msg ->tvContent.text = msg.obj as String// 当文本超出屏幕自动滚动,保证文本处于最底部val offset = tvContent.lineCount * tvContent.lineHeightfloatingView?.apply {if (offset > height) {tvContent.scrollTo(0, offset - height)}}false}}override fun onBind(intent: Intent?): IBinder? {return null}@SuppressLint("ClickableViewAccessibility")override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {if (Settings.canDrawOverlays(this)) {floatingView = LayoutInflater.from(this).inflate(R.layout.layout_show_log, null)tvContent = floatingView!!.findViewById(R.id.tv_log)floatingView!!.findViewById<AppCompatImageView>(R.id.iv_close).setOnClickListener {stringBuilder.clear()windowManager.removeView(floatingView)attached = false}// 设置TextView滚动tvContent.movementMethod = ScrollingMovementMethod.getInstance()floatingView!!.findViewById<FrameLayout>(R.id.layout_drag).setOnTouchListener { v, event ->when (event.action) {MotionEvent.ACTION_DOWN -> {x = event.rawX.toInt()y = event.rawY.toInt()}MotionEvent.ACTION_MOVE -> {val currentX = event.rawX.toInt()val currentY = event.rawY.toInt()val offsetX = currentX - xval offsetY = currentY - yx = currentXy = currentYlayoutParams.x = layoutParams.x + offsetXlayoutParams.y = layoutParams.y + offsetYwindowManager.updateViewLayout(floatingView, layoutParams)}}true}windowManager.addView(floatingView, layoutParams)attached = true}return super.onStartCommand(intent, flags, startId)}override fun onDestroy() {// 注销广播并删除浮窗unregisterReceiver(receiver)receiver = nullif (attached) {windowManager.removeView(floatingView)}}inner class MyReceiver : BroadcastReceiver() {override fun onReceive(context: Context, intent: Intent) {val content = intent.getStringExtra("content") ?: ""stringBuilder.append(content).append("\n")val message = Message.obtain()message.what = 0message.obj = stringBuilder.toString()handler.sendMessage(message)}}
}

4. 总结

以上就是Android悬浮窗的一个简单实现方式。如果需要实现其他复杂一点的功能,比如播放视频,也可以在此基础上完成。

Android悬浮窗的简单实现相关推荐

  1. Android 悬浮窗的简单实现

    概述 利用WindowManager(窗口管理器)和WindowManager.LayoutParams(参数属性)来实现一个小悬浮窗. 这是效果图: 布局很简单就略了.下面是代码: java源码 p ...

  2. android悬浮窗语音识别demo

    带有android悬浮窗的语音识别语义理解demo 如发现代码排版问题,请访问CSDN博客 Android桌面悬浮窗实现比较简单,本篇以一个语音识别,语义理解的demo来演示如何实现android悬浮 ...

  3. Android 悬浮窗功能的实现

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

  4. Android悬浮窗的实现

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

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

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

  6. Android 悬浮窗的使用(2)

    上一个使用的时候是写到了悬浮窗的简单使用,但是绝大多数情况下不止这点,我在这里做一下延申,这里根据上一篇文章的基础上做出了代码的增加. 增加悬浮窗的拖动. floatView.setOnTouchLi ...

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

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

  8. Android悬浮窗

    今天给大家写一个这个Android悬浮窗的功能,这个功能一般在360,酷狗(桌面歌词),网易云音乐(桌面歌词)上面用到,一般开发是很少碰到这个功能的,但是这个悬浮窗功能可以帮助理解Android的绘制 ...

  9. Android 悬浮窗全系统版本实现

    悬浮窗是在系统上显示的内容,好像微信视频聊天时的小窗口一样,在退出软件后依然存在的一个窗口,本博客以窗口中放一个button组件为例,简单展示悬浮窗,其中包括了对Android 6.0以下.Andro ...

最新文章

  1. 如何提高模型性能?这几个方法值得尝试 | CSDN 博文精选
  2. Docker知识6:实战!将一个tensorflow项目制作成Docker image
  3. Android插件化开发之解决OpenAtlas组件在宿主的注冊问题
  4. golang 开源代理
  5. python学生名片系统_基于python的学生信息管理系统!听说好多人的作业都是这个...
  6. ireport参数传递json_ssm中iReport报表使用json数据源过程体会
  7. 2021年7月国产数据库排行榜:openGauss成绩依旧亮眼,Kingbase向Top 10发起冲刺
  8. Linux下关于gcc、vim、readelf、rpm、yum、彩色进度条的问题
  9. iPhone 13系列或将涨价?业内人士:可能性不大,原因有这几点
  10. 程序员界大杯具:蜗居中的小贝是搞C++ 的 !
  11. SSO之CAS单点登录实例演示
  12. 一个注册表清理工具Advanced Uninstaller PRO 12
  13. 卜若的代码笔记-机器学习基础-UCI数据库简介与Iris数据集分析
  14. Python 保留字和关键字的用法
  15. c#获取当前日期时间
  16. 基础篇:6.9)形位公差-检测方法Measurement
  17. 将文件复制到临时文件夹
  18. HTML免费在线文档批量翻译工具
  19. 直播电商,小红书的商业化“解药”?
  20. ————《metasploit 魔鬼训练营》学习笔记序言

热门文章

  1. 工作三年程序员收入到底多高?透露收入:网友:哇,真的好高呀!
  2. 《安富莱嵌入式周报》第221期:2021.07.12--2021.07.18
  3. 什么录音软件可以录制电影对白
  4. 《当下的启蒙》的概述和精华
  5. 更改Pycharm的配置文件的存放路径
  6. SpringCloud Alibaba实战第九课 分布式事务理论、DevOps运维
  7. 电影《肖申克的救赎》给你最深的感受是什么?
  8. 转:怎么判断自己在不在一家好公司?
  9. Java设计模式:1.1.认识六个原则(开闭,里氏替代,依赖颠倒)
  10. 中学生学习心理:学习心理