【Android】Android 悬浮窗开发 ( 动态权限请求 | 前台服务和通知 | 悬浮窗创建 )

文章目录

悬浮窗实现效果 :

一、悬浮窗 动态权限请求


1、动态请求权限

在 Android 开发中 , 自 Android 6.0(API 级别 23)版本开始引入 " 动态权限 " ,

动态权限 指的是 在应用程序运行时向用户请求权限 , 而不是在安装时一次性请求所有权限 , 旨在提高用户隐私和安全性 ;

动态权限 请求 流程 :

  • 检查权限: 在请求权限之前,首先检查是否已经拥有该权限。
  • 请求权限: 如果没有权限,向用户请求权限。
  • 处理权限请求结果: 根据用户的响应,执行相应的操作。

2、悬浮窗权限说明

Settings.ACTION_MANAGE_OVERLAY_PERMISSION 是一个用于请求和管理 悬浮窗权限(Overlay Permission) 的系统设置页面 ;

悬浮窗权限允许应用在其他应用或系统界面上绘制悬浮窗口(如悬浮球、弹窗等);

由于悬浮窗权限涉及用户隐私和安全,Android 要求开发者显式请求该权限,并引导用户手动开启。

悬浮窗权限允许应用执行以下操作:

  • 在其他应用或系统界面上显示悬浮窗口。
  • 实现全局弹窗、悬浮按钮、画中画等功能。
  • 常用于录屏工具、悬浮球助手、消息提醒等场景。

3、检查动态权限

检查动态权限 , Android SDK 23 以上才检查动态权限 , 对应的版本是 Android 6.0(Marshmallow)‌‌, 低于该版本不需要 动态权限 , 直接使用对应功能即可 ,

通过 Build.VERSION.SDK_INT >= Build.VERSION_CODES.M 函数可以判定是否 当前版本 是否高于 Android SDK 23 Android 6.0(Marshmallow)‌‌版本 , 是否需要

通过调用 Settings.canDrawOverlays(this) 函数 , 可以检查是否浮云了 悬浮窗权限 , 如果是 Android 6.0 以上的系统 , 并且没有该 动态权限 , 则 动态请求该权限 ;

kt 复制代码
    /**
     * 检查悬浮窗权限的方法
     */
    private fun checkOverlayPermission(): Boolean {
        // Android SDK23 对应的版本是 Android 6.0(Marshmallow)‌‌
        // 6.0 以上的 Android 系统需要动态申请权限
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ) {
            /*
                根据当前应用是否有悬浮窗权限进行不同的操作
                 - 如果 有 悬浮窗权限 直接返回 true 显示悬浮窗
                 - 如果 没有悬浮窗权限, 开始请求悬浮窗权限
             */
            if (!Settings.canDrawOverlays(this)) {
                // 没有悬浮窗权限, 开始请求悬浮窗权限
                requestOverlayPermission()
                return false
            } else {
                // 有 悬浮窗权限 直接返回 true 显示悬浮窗
                return true
            }
        } else {
            // 6.0 以下的 Android 系统不需要申请权限
            // 已经请求悬浮窗权限成功 可进行后续操作
            return true
        }
    }

4、申请动态权限

申请动态权限时 , 需要弹出一个对话框 , 提示用户要跳转到指定界面 , 进行某个设置 ;

这里需要跳转到 Settings.ACTION_MANAGE_OVERLAY_PERMISSION 权限设置界面 , 为某个应用开启 " 显示在其他应用的上层 " 权限 ;

在界面中 , 选中要设置的应用 , 设置该应用可以显示在其它应用的上层 ;

代码示例 :

kotlin 复制代码
    /**
     * 请求悬浮窗权限
     */
    private fun requestOverlayPermission() {
        // 弹出 " 请允许显示在其他应用上方 " 的提示对话框
        AlertDialog.Builder(this) // 创建AlertDialog构建器
            .setTitle("需要悬浮窗权限") // 设置标题
            .setMessage("请允许显示在其他应用上方") // 设置消息
            .setPositiveButton("去设置") { _, _ -> // 设置"去设置"按钮
                val intent = Intent(
                    Settings.ACTION_MANAGE_OVERLAY_PERMISSION, // 设置操作为管理悬浮窗权限
                    Uri.parse("package:$packageName") // 设置URI为当前应用的包名
                )
                startActivityForResult(intent, OVERLAY_PERMISSION_REQUEST_CODE) // 启动设置界面,等待结果
            }
            .setNegativeButton("取消", null) // 设置"取消"按钮
            .show() // 显示对话框
    }

5、权限设置完毕后返回处理

设定一个请求码 , 自定义的请求码 , 用于 跳转到 申请 动态权限 页面 , 返回后判定返回结果 ;

kt 复制代码
    /**
     * 请求悬浮窗权限的请求码
     */
    private val OVERLAY_PERMISSION_REQUEST_CODE = 1001

设置完 悬浮窗权限 后 , 从 Settings.ACTION_MANAGE_OVERLAY_PERMISSION 界面返回 , 会回调 onActivityResult 函数 , 返回后 再次验证 是否已经获得了 悬浮窗权限 ,

kotlin 复制代码
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == OVERLAY_PERMISSION_REQUEST_CODE) {
            // 如果权限请求成功, 会根据 请求码 命中该分支
            if (checkOverlayPermission()) { // 检查是否获得悬浮窗权限
                startFloatingService() // 启动悬浮窗服务
            }
        }
    }

二、悬浮窗 前台服务和通知


1、前台服务 启动 悬浮窗 的必要性

为什么必须用 前台服务 启动 悬浮窗 :

  • 系统兼容性 : Android 8.0+ 禁止后台应用直接显示悬浮窗,前台服务是唯一合法途径。
  • 资源保障 : 前台服务优先级更高,避免悬浮窗因进程被回收而消失。
  • 用户透明度 : 通知栏提示用户服务运行状态,符合隐私和设计规范。
  • 权限合规 : 减少 SYSTEM_ALERT_WINDOW 权限滥用风险,提升应用审核通过率。

如果不使用前台服务 , 会出现以下情况 :

  • 悬浮窗可能在后台被系统强制关闭。
  • 在 Android 12+ 设备上可能直接崩溃(权限拒绝)。
  • 用户可能误判应用为恶意软件(无通知提示)。

① 保持悬浮窗存活

Android 悬浮窗开发 , 需要 保证 悬浮窗 的持续存活 ,

  • 当 应用退到 后台时 , 通过 bindService 绑定的服务 就被系统回收了 , 悬浮窗就会消失 ;
  • Android 8.0 之后的系统 , 无法在后台创建 Activity 或 Window 组件 ;
  • 系统会限制后台的 CPU 和 网络资源 , 不定期杀死普通服务 ;
  • 使用 前台服务 , 可以避免上述三个问题 , 保证 悬浮窗持续存在 ;
场景 问题 前台服务的作用
应用退到后台 普通 Service 可能被系统回收 → 悬浮窗消失 前台服务优先级更高,系统更倾向于保留(即使内存不足) → 悬浮窗持续显示
Android 8.0+ 后台限制 后台应用无法创建 ActivityWindow(如 TYPE_APPLICATION_OVERLAY 前台服务属于"用户可见"状态 → 允许在后台显示悬浮窗
Doze 模式 / 应用待机 系统限制后台应用的 CPU/网络等资源 → 普通服务可能被中断 前台服务可绕过部分 Doze 限制 → 悬浮窗逻辑持续运行

② 悬浮窗的要求

在 Android 系统中 , 运行了一个 悬浮在 操作系统 中的 悬浮窗 , 这需要满足 悬浮窗相关权限 和 用户感知要求 , 要让用户知道是哪个应用启动了 悬浮窗 , 并且用户可以随时关闭该 悬浮窗 ;

使用 前台服务 可以满足上述要求 ;

要求 前台服务的解决方案
权限依赖 悬浮窗需要 SYSTEM_ALERT_WINDOW 权限,但 Android 10+ 要求动态申请并用户授权。前台服务通过通知栏提示用户应用正在运行,减少被系统判定为"滥用权限"的风险。
用户可感知性 前台服务必须显示通知栏通知 → 用户明确知道悬浮窗关联的服务在运行(符合 Android 设计规范)。
避免后台限制 从 Android 12 开始,后台应用启动前台服务需用户授权(START_FOREGROUND_SERVICES 权限),但启动后系统允许其显示悬浮窗。

③ 悬浮窗版本兼容

Android 系统中 , 不同的版本中 , 启动悬浮窗各自都有不同的限制 , 只有使用前台服务 , 可以满足所有的限制 , 因此前台服务在不同版本均有关键作用 , 所有的版本都可以使用 前台服务 启动 和 保持 悬浮窗 , 避免了不同 Android 系统版本 开发出的 悬浮窗 不兼容的问题 ;

Android 版本 前台服务的关键作用
Android 8.0 (API 26) 禁止后台应用创建 Window → 必须通过前台服务绑定悬浮窗逻辑。
Android 10 (API 29) 禁止后台应用启动 Activity → 前台服务可绕过此限制显示悬浮窗。
Android 12 (API 31) 前台服务需声明 foregroundServiceType(如 mediaPlayback)→ 明确服务用途,提升系统信任度。

2、其它类型服务简介

这里需要为 悬浮窗 设置一个绑定的服务 , 以确保悬浮窗一直保持存在 ;

服务类型 使用场景 特点
前台服务 需要在后台持续运行且用户可感知的任务,如播放音乐、导航等。 需要在通知栏显示持续的通知,告知用户服务正在运行。
WorkManager 需要可靠执行的后台任务,即使应用退出或设备重启后仍需执行的任务,如上传日志、定期同步数据等。 适用于需要持久性和可靠性的任务,支持链式任务、延迟执行、重试机制等特性。
JobScheduler 需要在特定条件下执行的后台任务,如网络连接、设备充电等条件下执行的任务。 适用于 Android 5.0(API 级别 21)及以上版本,允许在满足特定条件时调度任务。
AlarmManager 需要在特定时间或周期性执行的任务,如定时提醒、定期同步等。 适用于设置一次性任务、周期重复任务、定时重复任务。

① 前台服务

前台服务(Foreground Service):

  • 使用场景 : 适用于需要在后台持续运行且用户可感知的任务,如音乐播放、导航等。
  • 特点 : 必须显示一个持续的通知,确保用户知晓服务的存在。优先级高,不容易被系统杀死。
  • 优点 : 高优先级,系统不容易终止。 适用于需要用户知晓的长期运行任务 ;
  • 缺点 : 需要显示通知,可能影响用户体验。不适用于不需要用户感知的后台任务。

② WorkManager 服务

WorkManager 服务 :

  • 使用场景 : 适用于需要可靠执行的后台任务,即使应用退出或设备重启也能保证执行,如数据同步、上传日志等。
  • 特点 : 支持链式任务、延迟执行、重试机制等特性。兼容 Android 5.0(API 级别 21)及以上版本。 自动选择最佳的执行方式,适应设备状态和系统限制。
  • 优点 : 高可靠性,适用于需要持久化的任务。自动适配系统限制,确保任务执行。支持任务链式执行,方便管理复杂任务。
  • 缺点 : 相较于其他方式,可能引入额外的库和复杂性。对于简单的后台任务,可能显得过于复杂。

③ JobScheduler 服务

JobScheduler 服务 :

  • 使用场景 : 适用于需要在特定条件下执行的后台任务,如网络连接、充电状态等。
  • 特点 : 在 Android 5.0(API 级别 21)引入。允许根据设备状态和约束条件调度任务。
  • 优点 : 节省电池和资源,避免不必要的后台任务。适用于需要在特定条件下执行的任务。
  • 缺点 : 仅适用于 Android 5.0 及以上版本。功能相对有限,不如 WorkManager 灵活。

④ AlarmManager 服务

AlarmManager 服务 :

  • 使用场景 : 适用于需要在特定时间或周期性执行的任务,如定时提醒、定期同步等。
  • 特点 : 允许在指定时间或周期性触发任务。会唤醒设备执行任务,可能影响电池寿命。
  • 优点 : 适用于精确的定时任务。简单易用,适合定时提醒等场景。
  • 缺点 : 可能导致设备从低电耗模式中唤醒,影响电池寿命。在设备处于 Doze 模式或应用被限制时,可能无法按时执行任务。

三、前台服务 创建 通知 和 悬浮窗


1、启动前台服务

Android SDK 版本大于 26, Android 8.0 (Oreo) 需要 调用 startForegroundService 函数 启动 前台服务 , 前台服务 是 Android 8.0 之后才有的概念 , 之前 全都是 普通的 服务 , 只是通过 startService 和 bindService 两种启动方式 区别服务 ;

如果 Android 的 SDK 版本低于 26, Android 8.0 (Oreo) 则直接 调用 startService 函数 启动普通服务即可 ;

启动悬浮窗前台服务代码 :

kotlin 复制代码
    /**
     * 启动悬浮窗服务
     */
    private fun startFloatingService() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // 如果 SDK 版本大于 26,  Android 8.0 (Oreo) 需要启动前台服务
            startForegroundService(Intent(this, FloatingWindowService::class.java)) // 启动前台服务
        } else {
            // 如果 SDK 版本低于 26,  Android 8.0 (Oreo) 则直接启动普通服务即可
            startService(Intent(this, FloatingWindowService::class.java)) // 启动普通服务
        }
    }

2、前台服务通知

Android SDK 版本大于 26 , 对应的系统版本是 Android 8.0 (Oreo) , 通过调用 startForegroundService 函数 启动 前台服务 , 必须在 启动服务 的 5 秒内 , 启动 前台通知 , 否则应用会崩溃退出 ;

启动通知代码如下 :

kotlin 复制代码
        // SDK 版本大于 26,  Android 8.0 (Oreo) , 才创建通知渠道, 并启动前台应用
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            createNotificationChannel()
            val notification = buildNotification()
            // 启动服务后, 必须在 5 秒内设置 前台服务通知信息
            startForeground(NOTIFICATION_ID, notification)
        }

首先 , 要创建 通知渠道 :

kotlin 复制代码
    /**
     * 创建通知渠道
     *  通知渠道是 SDK 26 Android 8.0 (Oreo) 引入的新特性
     */
    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            // 创建通知渠道
            val channel = NotificationChannel(
                CHANNEL_ID,
                "悬浮窗",
                NotificationManager.IMPORTANCE_LOW
            )

            // 注册通知渠道
            getSystemService(NotificationManager::class.java)
                .createNotificationChannel(channel)
        }
    }

然后 , 创建通知 :

kotlin 复制代码
    /**
     * 创建通知
     */
    private fun buildNotification(): Notification {
        return NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle("悬浮窗") // 设置通知标题
            .setContentText("显示前台悬浮窗服务") // 设置通知内容
            .setSmallIcon(R.mipmap.ic_launcher) // 设置通知小图标
            .setPriority(NotificationCompat.PRIORITY_LOW) // 设置通知优先级
            .build() // 构建并返回通知
    }

3、创建浮动窗口

创建浮动窗口流程 :

  • ① 设置布局类型 :
    • Android SDK 26 Android 8.0 (Oreo) 及以上的版本 , 需要设置 WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY 类型布局 ;
    • SDK 25 及以下的版本使用 WindowManager.LayoutParams.TYPE_PHONE 布局 ;
kotlin 复制代码
        // 获取 WindowManager 实例
        windowManager = getSystemService(WINDOW_SERVICE) as WindowManager

        // 设置布局类型
        val layoutFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // 如果 SDK 版本大于等于 O
            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY // 设置布局类型为应用覆盖层
        } else {
            WindowManager.LayoutParams.TYPE_PHONE // 设置布局类型为电话
        }
  • ② 设置布局参数 :
kotlin 复制代码
        // 设置布局参数
        val params = WindowManager.LayoutParams(
            WindowManager.LayoutParams.WRAP_CONTENT, // 宽度自适应
            WindowManager.LayoutParams.WRAP_CONTENT, // 高度自适应
            layoutFlag, // 布局类型
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, // 不获取焦点
            PixelFormat.TRANSLUCENT // 半透明
        ).apply {
            gravity = Gravity.TOP or Gravity.START // 设置重力为顶部和左侧
            x = 0 // 设置X坐标
            y = 0 // 设置Y坐标, 将浮动窗口显示在左上角
        }
  • ③ 加载浮动窗口布局 :
kotlin 复制代码
        // 加载 浮动窗口 布局
        val inflater = getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater // 获取LayoutInflater实例
        floatingView = inflater.inflate(R.layout.floating_window, null)      // 加载悬浮窗布局
  • ④ 设置浮动窗口事件 :
kotlin 复制代码
        // 设置关闭按钮的点击事件
        floatingView.findViewById<Button>(R.id.close_btn).setOnClickListener {
            stopSelf() // 停止服务
        }

        // 设置拖动事件
        floatingView.setOnTouchListener { view, event ->
            when (event.action) {
                MotionEvent.ACTION_DOWN -> { // 按下事件
                    initialX = params.x // 记录初始X坐标
                    initialY = params.y // 记录初始Y坐标
                    initialTouchX = event.rawX // 记录初始触摸X坐标
                    initialTouchY = event.rawY // 记录初始触摸Y坐标
                    true
                }
                MotionEvent.ACTION_MOVE -> { // 移动事件
                    params.x = initialX + (event.rawX - initialTouchX).toInt() // 更新X坐标
                    params.y = initialY + (event.rawY - initialTouchY).toInt() // 更新Y坐标
                    windowManager.updateViewLayout(floatingView, params) // 更新悬浮窗位置
                    true
                }
                else -> false
            }
        }
  • ⑤ 添加浮动窗口 :
kotlin 复制代码
        // 正式添加悬浮窗到窗口
        windowManager.addView(floatingView, params)

完整代码如下 :

kotlin 复制代码
    /**
     * 创建悬浮窗口
     */
    private fun createFloatingWindow() { // 创建悬浮窗的方法
        // 获取 WindowManager 实例
        windowManager = getSystemService(WINDOW_SERVICE) as WindowManager

        // 设置布局类型
        val layoutFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // 如果 SDK 版本大于等于 O
            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY // 设置布局类型为应用覆盖层
        } else {
            WindowManager.LayoutParams.TYPE_PHONE // 设置布局类型为电话
        }

        // 设置布局参数
        val params = WindowManager.LayoutParams(
            WindowManager.LayoutParams.WRAP_CONTENT, // 宽度自适应
            WindowManager.LayoutParams.WRAP_CONTENT, // 高度自适应
            layoutFlag, // 布局类型
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, // 不获取焦点
            PixelFormat.TRANSLUCENT // 半透明
        ).apply {
            gravity = Gravity.TOP or Gravity.START // 设置重力为顶部和左侧
            x = 0 // 设置X坐标
            y = 0 // 设置Y坐标, 将浮动窗口显示在左上角
        }

        // 加载 浮动窗口 布局
        val inflater = getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater // 获取LayoutInflater实例
        floatingView = inflater.inflate(R.layout.floating_window, null)      // 加载悬浮窗布局

        // 设置关闭按钮的点击事件
        floatingView.findViewById<Button>(R.id.close_btn).setOnClickListener {
            stopSelf() // 停止服务
        }

        // 设置拖动事件
        floatingView.setOnTouchListener { view, event ->
            when (event.action) {
                MotionEvent.ACTION_DOWN -> { // 按下事件
                    initialX = params.x // 记录初始X坐标
                    initialY = params.y // 记录初始Y坐标
                    initialTouchX = event.rawX // 记录初始触摸X坐标
                    initialTouchY = event.rawY // 记录初始触摸Y坐标
                    true
                }
                MotionEvent.ACTION_MOVE -> { // 移动事件
                    params.x = initialX + (event.rawX - initialTouchX).toInt() // 更新X坐标
                    params.y = initialY + (event.rawY - initialTouchY).toInt() // 更新Y坐标
                    windowManager.updateViewLayout(floatingView, params) // 更新悬浮窗位置
                    true
                }
                else -> false
            }
        }

        // 正式添加悬浮窗到窗口
        windowManager.addView(floatingView, params)
    }

四、完整代码示例


1、Service 浮动窗口服务代码

浮动窗口所在 前台服务 代码 FloatingWindowService.kt :

kt 复制代码
package hsl.floatingwindow

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.graphics.PixelFormat
import android.os.Build
import android.os.IBinder
import android.view.*
import android.widget.Button
import androidx.core.app.NotificationCompat

class FloatingWindowService : Service() {

    /**
     * 窗口管理器
     */
    private lateinit var windowManager: WindowManager

    /**
     * 悬浮窗组件
     */
    private lateinit var floatingView: View

    /*
        声明 浮动窗口 的 初始坐标
     */
    private var initialX = 0
    private var initialY = 0

    /*
        声明 浮动窗口 的 初始触摸坐标
     */
    private var initialTouchX = 0f
    private var initialTouchY = 0f

    /**
     * 定义通知 ID
     */
    private val NOTIFICATION_ID = 1001

    /**
     * 定义通知渠道 ID, 通知渠道需要
     *  调用 Service.createNotificationChannel 函数创建
     */
    private val CHANNEL_ID = "floating_window_channel"

    /**
     * 重写 onBind 函数, 返回 null
     */
    override fun onBind(intent: Intent?): IBinder? = null

    override fun onCreate() {
        super.onCreate()

        // SDK 版本大于 26,  Android 8.0 (Oreo) , 才创建通知渠道, 并启动前台应用
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            createNotificationChannel()
            val notification = buildNotification()
            // 启动服务后, 必须在 5 秒内设置 前台服务通知信息
            startForeground(NOTIFICATION_ID, notification)
        }

        // 创建悬浮窗
        createFloatingWindow()
    }

    /**
     * 创建通知渠道
     *  通知渠道是 SDK 26 Android 8.0 (Oreo) 引入的新特性
     */
    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            // 创建通知渠道
            val channel = NotificationChannel(
                CHANNEL_ID,
                "悬浮窗",
                NotificationManager.IMPORTANCE_LOW
            )

            // 注册通知渠道
            getSystemService(NotificationManager::class.java)
                .createNotificationChannel(channel)
        }
    }

    /**
     * 创建通知
     */
    private fun buildNotification(): Notification {
        return NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle("悬浮窗") // 设置通知标题
            .setContentText("显示前台悬浮窗服务") // 设置通知内容
            .setSmallIcon(R.mipmap.ic_launcher) // 设置通知小图标
            .setPriority(NotificationCompat.PRIORITY_LOW) // 设置通知优先级
            .build() // 构建并返回通知
    }

    /**
     * 创建悬浮窗口
     */
    private fun createFloatingWindow() { // 创建悬浮窗的方法
        // 获取 WindowManager 实例
        windowManager = getSystemService(WINDOW_SERVICE) as WindowManager

        // 设置布局类型
        val layoutFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // 如果 SDK 版本大于等于 O
            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY // 设置布局类型为应用覆盖层
        } else {
            WindowManager.LayoutParams.TYPE_PHONE // 设置布局类型为电话
        }

        // 设置布局参数
        val params = WindowManager.LayoutParams(
            WindowManager.LayoutParams.WRAP_CONTENT, // 宽度自适应
            WindowManager.LayoutParams.WRAP_CONTENT, // 高度自适应
            layoutFlag, // 布局类型
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, // 不获取焦点
            PixelFormat.TRANSLUCENT // 半透明
        ).apply {
            gravity = Gravity.TOP or Gravity.START // 设置重力为顶部和左侧
            x = 0 // 设置X坐标
            y = 0 // 设置Y坐标, 将浮动窗口显示在左上角
        }

        // 加载 浮动窗口 布局
        val inflater = getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater // 获取LayoutInflater实例
        floatingView = inflater.inflate(R.layout.floating_window, null)      // 加载悬浮窗布局

        // 设置关闭按钮的点击事件
        floatingView.findViewById<Button>(R.id.close_btn).setOnClickListener {
            stopSelf() // 停止服务
        }

        // 设置拖动事件
        floatingView.setOnTouchListener { view, event ->
            when (event.action) {
                MotionEvent.ACTION_DOWN -> { // 按下事件
                    initialX = params.x // 记录初始X坐标
                    initialY = params.y // 记录初始Y坐标
                    initialTouchX = event.rawX // 记录初始触摸X坐标
                    initialTouchY = event.rawY // 记录初始触摸Y坐标
                    true
                }
                MotionEvent.ACTION_MOVE -> { // 移动事件
                    params.x = initialX + (event.rawX - initialTouchX).toInt() // 更新X坐标
                    params.y = initialY + (event.rawY - initialTouchY).toInt() // 更新Y坐标
                    windowManager.updateViewLayout(floatingView, params) // 更新悬浮窗位置
                    true
                }
                else -> false
            }
        }

        // 正式添加悬浮窗到窗口
        windowManager.addView(floatingView, params)
    }

    /**
     * 重写 onDestroy 方法
     */
    override fun onDestroy() {
        super.onDestroy()
        if (::floatingView.isInitialized) { // 如果 floatingView 已初始化
            windowManager.removeView(floatingView) // 移除悬浮窗
        }
    }
}

2、Activity 主界面代码

下面是 Activity 主界面代码 MainActivity.kt , 主要作用就是 申请 浮动窗口所需权限 和 启动前台服务 ;

cpp 复制代码
package hsl.floatingwindow

import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity


class MainActivity : AppCompatActivity() {

    /**
     * 请求悬浮窗权限的请求码
     */
    private val OVERLAY_PERMISSION_REQUEST_CODE = 1001

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 检查是否具有悬浮窗权限
        if (checkOverlayPermission()) {
            // 启动悬浮窗服务
            startFloatingService()
        }
    }

    /**
     * 检查悬浮窗权限的方法
     */
    private fun checkOverlayPermission(): Boolean {
        // Android SDK23 对应的版本是 Android 6.0(Marshmallow)‌‌
        // 6.0 以上的 Android 系统需要动态申请权限
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ) {
            /*
                根据当前应用是否有悬浮窗权限进行不同的操作
                 - 如果 有 悬浮窗权限 直接返回 true 显示悬浮窗
                 - 如果 没有悬浮窗权限, 开始请求悬浮窗权限
             */
            if (!Settings.canDrawOverlays(this)) {
                // 没有悬浮窗权限, 开始请求悬浮窗权限
                requestOverlayPermission()
                return false
            } else {
                // 有 悬浮窗权限 直接返回 true 显示悬浮窗
                return true
            }
        } else {
            // 6.0 以下的 Android 系统不需要申请权限
            // 已经请求悬浮窗权限成功 可进行后续操作
            return true
        }
    }

    /**
     * 请求悬浮窗权限
     */
    private fun requestOverlayPermission() {
        // 弹出 " 请允许显示在其他应用上方 " 的提示对话框
        AlertDialog.Builder(this) // 创建AlertDialog构建器
            .setTitle("需要悬浮窗权限") // 设置标题
            .setMessage("请允许显示在其他应用上方") // 设置消息
            .setPositiveButton("去设置") { _, _ -> // 设置"去设置"按钮
                val intent = Intent(
                    Settings.ACTION_MANAGE_OVERLAY_PERMISSION, // 设置操作为管理悬浮窗权限
                    Uri.parse("package:$packageName") // 设置URI为当前应用的包名
                )
                startActivityForResult(intent, OVERLAY_PERMISSION_REQUEST_CODE) // 启动设置界面,等待结果
            }
            .setNegativeButton("取消", null) // 设置"取消"按钮
            .show() // 显示对话框
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == OVERLAY_PERMISSION_REQUEST_CODE) {
            // 如果权限请求成功, 会根据 请求码 命中该分支
            if (checkOverlayPermission()) { // 检查是否获得悬浮窗权限
                startFloatingService() // 启动悬浮窗服务
            }
        }
    }

    /**
     * 启动悬浮窗服务
     */
    private fun startFloatingService() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // 如果 SDK 版本大于 26,  Android 8.0 (Oreo) 需要启动前台服务
            startForegroundService(Intent(this, FloatingWindowService::class.java)) // 启动前台服务
        } else {
            // 如果 SDK 版本低于 26,  Android 8.0 (Oreo) 则直接启动普通服务即可
            startService(Intent(this, FloatingWindowService::class.java)) // 启动普通服务
        }
    }
}

3、AndroidManifest.xml 配置文件代码

在该 AndroidManifest.xml 配置文件中 , 主要需要声明 :

  • 权限声明 : 浮动窗口权限 和 前台服务权限 ;
  • Activity 组件声明
  • Service 组件声明
xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="hsl.floatingwindow">

    <!-- 浮动窗口权限 -->
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    <!-- 前台服务权限 -->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.FloatingWindow">

        <!-- Activity 组件注册, 注意必须配置 android:exported="true" 属性, 否则报错 -->
        <activity android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <!-- Service 组件注册 -->
        <service android:name=".FloatingWindowService" />

    </application>

</manifest>

4、布局文件

浮动窗口布局文件 floating_window.xml :

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/floating_layout"
    android:layout_width="200dp"
    android:layout_height="100dp"
    android:orientation="vertical"
    android:background="#80FFFFFF"
    android:padding="8dp">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Floating Window"
        android:textSize="18sp"/>

    <Button
        android:id="@+id/close_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Close"/>

</LinearLayout>

Activity 组件布局文件 activity_main.xml :

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

5、执行结果

执行效果 :

相关推荐
daifgFuture2 小时前
Android 3D球形水平圆形旋转,旋转动态更换图片
android·3d
二流小码农4 小时前
鸿蒙开发:loading动画的几种实现方式
android·ios·harmonyos
爱吃西红柿!4 小时前
fastadmin fildList 动态下拉框默认选中
android·前端·javascript
悠哉清闲5 小时前
工厂模式与多态结合
android·java
大耳猫6 小时前
Android SharedFlow 详解
android·kotlin·sharedflow
火柴就是我6 小时前
升级 Android Studio 后报错 Error loading build artifacts from redirect.txt
android
androidwork8 小时前
掌握 MotionLayout:交互动画开发
android·kotlin·交互
奔跑吧 android8 小时前
【android bluetooth 协议分析 14】【HFP详解 1】【案例一: 手机侧显示来电,但车机侧没有显示来电: 讲解AT+CLCC命令】
android·hfp·aosp13·telecom·ag·hf·headsetclient
Chenyu_3108 小时前
09.MySQL内外连接
android·数据库·mysql
砖厂小工8 小时前
Kotlin Flow 全面解析:从基础到高级
android