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

news2025/4/19 10:31:30

文章目录

  • 一、悬浮窗 动态权限请求
    • 1、动态请求权限
    • 2、悬浮窗权限说明
    • 3、检查动态权限
    • 4、申请动态权限
    • 5、权限设置完毕后返回处理
  • 二、悬浮窗 前台服务和通知
    • 1、前台服务 启动 悬浮窗 的必要性
      • ① 保持悬浮窗存活
      • ② 悬浮窗的要求
      • ③ 悬浮窗版本兼容
    • 2、其它类型服务简介
      • ① 前台服务
      • ② WorkManager 服务
      • ③ JobScheduler 服务
      • ④ AlarmManager 服务
  • 三、前台服务 创建 通知 和 悬浮窗
    • 1、启动前台服务
    • 2、前台服务通知
    • 3、创建浮动窗口
  • 四、完整代码示例
    • 1、Service 浮动窗口服务代码
    • 2、Activity 主界面代码
    • 3、AndroidManifest.xml 配置文件代码
    • 4、布局文件
    • 5、执行结果

悬浮窗实现效果 :

在这里插入图片描述





一、悬浮窗 动态权限请求




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 以上的系统 , 并且没有该 动态权限 , 则 动态请求该权限 ;

    /**
     * 检查悬浮窗权限的方法
     */
    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 权限设置界面 , 为某个应用开启 " 显示在其他应用的上层 " 权限 ;

在这里插入图片描述

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

在这里插入图片描述

代码示例 :

    /**
     * 请求悬浮窗权限
     */
    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、权限设置完毕后返回处理


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

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

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

    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 函数 启动普通服务即可 ;


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

    /**
     * 启动悬浮窗服务
     */
    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 秒内 , 启动 前台通知 , 否则应用会崩溃退出 ;

启动通知代码如下 :

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

首先 , 要创建 通知渠道 :

    /**
     * 创建通知渠道
     *  通知渠道是 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() // 构建并返回通知
    }

3、创建浮动窗口


创建浮动窗口流程 :

  • ① 设置布局类型 :
    • Android SDK 26 Android 8.0 (Oreo) 及以上的版本 , 需要设置 WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY 类型布局 ;
    • SDK 25 及以下的版本使用 WindowManager.LayoutParams.TYPE_PHONE 布局 ;
        // 获取 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)

完整代码如下 :

    /**
     * 创建悬浮窗口
     */
    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 :

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 , 主要作用就是 申请 浮动窗口所需权限 和 启动前台服务 ;

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 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 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 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、执行结果


执行效果 :

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2301890.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python高级语法之jsonpathBeautifulSoup解析器

目录&#xff1a; 1、jsonPath的使用2、使用jsonpath解析淘票票网页3、BeautifulSoup解析器的使用4、BeautifulSoup层级选择器的使用 1、jsonPath的使用 2、使用jsonpath解析淘票票网页 3、BeautifulSoup解析器的使用 4、BeautifulSoup层级选择器的使用

工业安卓主板在智慧粮仓设备中发挥着至关重要的作用

工业安卓主板在智慧粮仓设备中发挥着至关重要的作用。以下是关于其作用的具体分析&#xff1a; 一、提供稳定可靠的运行平台 智慧粮仓设备需要长时间稳定运行&#xff0c;以实现对粮食储存环境的实时监测和精准控制。工业安卓主板采用高性能的处理器和大容量的存储空间&#…

ECMAScript6----var、let、const

ECMAScript6----var、let、const 1.var2.let3.const 1.var &#xff08;1&#xff09;在相同作用域下可重复声明 var a 20 var a 30 console.log(a) // 30&#xff08;2&#xff09;存在变量提升 console.log(a) // undefined var a 20&#xff08;3&#xff09;可修改声…

【ST-LINK未能被keil识别STM32 ST-LINK Utility出现“Can not connect to target】

针对各种品牌32MCU boot0拉高&#xff0c;boot1拉低进入系统存储器&#xff0c;对Flash先擦除在下载 针对STM32f103 通过32复位和stlink Utilit解决 https://blog.csdn.net/Donglutao/article/details/129086960 https://www.bilibili.com/video/BV1F94y1g7be/?spm_id_…

Android Http-server 本地 web 服务

时间&#xff1a;2025年2月16日 地点&#xff1a;深圳.前海湾 需求 我们都知道 webview 可加载 URI&#xff0c;他有自己的协议 scheme&#xff1a; content:// 标识数据由 Content Provider 管理file:// 本地文件 http:// 网络资源 特别的&#xff0c;如果你想直接…

python实践-实现实时语音转文字本地部署版(二)

一、技术栈 python 3.10.6 vosk 需下载对应模型&#xff08;vosk-model-cn-0.22&#xff09;模型下载慢的同学看最后的资源链接。 pyaudio keyboard 二、实现功能 本地化实现麦克风语音录入&#xff0c;实时生成文字&#xff0c;并保存至本地文档。 三、实现代码 fro…

tortoiseSVN 如何克隆项目到本地

导入项目成功&#xff0c;如下图&#xff1a;

解决“QString的split()函数分割中文“报错

在使用Qt平台的QString类里的split()函数&#xff0c;分割.txt文件里中文的字符串时&#xff0c;发现中文会乱码。     问题原因&#xff1a;中文使用UTF-16编码。     解决方法&#xff1a;将.txt文件保存为UTF-16编码&#xff0c;然后使用split()去分割对应的字符串即可。…

云平台结合DeepSeek的AI模型优化实践:技术突破与应用革新

目录 前言 一、技术架构&#xff1a;算力与算法的协同基石 1. 蓝耘平台的核心优势 2. DeepSeek的模型创新 二、应用场景&#xff1a;垂直领域的智能化落地 1. 商业领域&#xff1a;智能推荐与客服 2. 工业领域&#xff1a;质检与流程优化 3. 智慧城市与医…

蓝桥杯(B组)-每日一题(1093字符逆序)

c中函数&#xff1a; reverse(首位置&#xff0c;尾位置&#xff09; reverse(s.begin(),s.end()) 头文件&#xff1a;<algorithm> #include<iostream> #include<algorithm>//运用reverse函数的头文件 using namespace std; int main() {string s;//定义一…

jsherp importItemExcel接口存在SQL注入

一、漏洞简介 很多人说管伊佳ERP&#xff08;原名&#xff1a;华夏ERP&#xff0c;英文名&#xff1a;jshERP&#xff09;是目前人气领先的国产ERP系统虽然目前只有进销存财务生产的功能&#xff0c;但后面将会推出ERP的全部功能&#xff0c;有兴趣请帮点一下 二、漏洞影响 …

一文讲清 AIO BIO NIO的区别

引言 在 Java 编程中&#xff0c;BIO&#xff08;Blocking I/O&#xff09;、NIO&#xff08;Non-blocking I/O&#xff09;和 AIO&#xff08;Asynchronous I/O&#xff09;是三种不同的 I/O 模型&#xff0c;它们在处理输入输出操作时有着不同的机制和特点&#xff0c;但是市…

文心一言大模型的“三级跳”:从收费到免费再到开源,一场AI生态的重构实验

2025年2月&#xff0c;百度文心大模型接连抛出两枚“重磅炸弹”&#xff1a;4月1日起全面免费&#xff0c;6月30日正式开源文心大模型4.5系列。这一系列动作不仅颠覆了李彦宏此前坚持的“闭源优势论”13&#xff0c;更标志着中国AI大模型竞争进入了一个全新的阶段——从技术壁垒…

Uniapp 从入门到精通:基础篇 - 搭建开发环境

Uniapp 从入门到精通:基础篇 - 搭建开发环境 前言一、Uniapp 简介1.1 什么是 Uniapp1.2 Uniapp 的优势二、搭建开发环境前的准备2.1 安装 Node.js2.2 安装 HBuilderX三、创建第一个 Uniapp 项目3.1 打开 HBuilderX 并创建项目3.2 项目结构介绍3.3 运行项目四、配置项目4.1 配置…

CSDN文章质量分查询系统【赠python爬虫、提分攻略】

CSDN文章质量分查询系统 https://www.csdn.net/qc 点击链接-----> CSDN文章质量分查询系统 <------点击链接 点击链接-----> https://www.csdn.net/qc <------点击链接 点击链接-----> CSDN文章质量分查询系统 <------点击链接 点击链…

GPT-SoVITS更新V3 win整合包

GPT-SoVITS 是由社区开发者联合打造的开源语音生成框架&#xff0c;其创新性地融合了GPT语言模型与SoVITS&#xff08;Singing Voice Inference and Timbre Synthesis&#xff09;语音合成技术&#xff0c;实现了仅需5秒语音样本即可生成高保真目标音色的突破。该项目凭借其开箱…

CSS三大特性——继承、优先级与层叠

1. 层叠性 概念&#xff1a;如果发生了样式冲突&#xff0c;那就会根据一定的规则&#xff08;选择器优先级&#xff09;&#xff0c;进行样式的层叠&#xff08;覆 盖&#xff09;。 什么是样式冲突&#xff1f; ——— 元素的同一个样式名&#xff0c;被设置了不同的值&…

敏捷开发06:用户故事估算方法介绍

估算介绍 在以前开发 IT 软件时&#xff0c;使用较多的衡量软件开发工作量的单位是&#xff1a;小时、人天 或 人月。它是预估开发时间。比如&#xff1a;这个功能张三一个人开发需要 3 天时间完成。 这种 “人天” 估算只是 “理想人天” 的估算&#xff0c;有时与实际开发完…

环境变量与本地变量

目录 本地变量的创建 环境变量VS本地变量 认识完了环境变量我们来认识一下本地变量。 本地变量的创建 我们如果直接env是看不到本地变量的&#xff0c;因为本地变量和环境变量都具有独立性&#xff0c;环境变量是系统提供的具有全局属性的变量&#xff0c;都存在bash进程的…

Visual Studio中打开多个项目

1) 找到解决方案窗口 2) 右键添加→ 选择现有项目 3) 选择.vcxproj文件打开即可