亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Android?懸浮窗開(kāi)發(fā)示例((動(dòng)態(tài)權(quán)限請(qǐng)求?|?前臺(tái)服務(wù)和通知?|?懸浮窗創(chuàng)建?)

 更新時(shí)間:2025年02月21日 11:44:01   作者:韓曙亮  
本文介紹了Android懸浮窗的實(shí)現(xiàn)效果,包括動(dòng)態(tài)權(quán)限請(qǐng)求、前臺(tái)服務(wù)和通知的使用,懸浮窗權(quán)限需要?jiǎng)討B(tài)申請(qǐng)并引導(dǎo)用戶手動(dòng)開(kāi)啟,前臺(tái)服務(wù)用于保證懸浮窗的持續(xù)存活,支持Android不同版本的兼容性,文章還提供了啟動(dòng)前臺(tái)服務(wù)、創(chuàng)建通知和懸浮窗的代碼示例,感興趣的朋友一起看看吧

懸浮窗實(shí)現(xiàn)效果 :

一、懸浮窗 動(dòng)態(tài)權(quán)限請(qǐng)求

1、動(dòng)態(tài)請(qǐng)求權(quán)限

在 Android 開(kāi)發(fā)中 , 自 Android 6.0(API 級(jí)別 23)版本開(kāi)始引入 " 動(dòng)態(tài)權(quán)限 " ,

動(dòng)態(tài)權(quán)限 指的是 在應(yīng)用程序運(yùn)行時(shí)向用戶請(qǐng)求權(quán)限 , 而不是在安裝時(shí)一次性請(qǐng)求所有權(quán)限 , 旨在提高用戶隱私和安全性 ;

動(dòng)態(tài)權(quán)限 請(qǐng)求 流程 :

  • 檢查權(quán)限: 在請(qǐng)求權(quán)限之前,首先檢查是否已經(jīng)擁有該權(quán)限。
  • 請(qǐng)求權(quán)限: 如果沒(méi)有權(quán)限,向用戶請(qǐng)求權(quán)限。
  • 處理權(quán)限請(qǐng)求結(jié)果: 根據(jù)用戶的響應(yīng),執(zhí)行相應(yīng)的操作。

2、懸浮窗權(quán)限說(shuō)明

Settings.ACTION_MANAGE_OVERLAY_PERMISSION 是一個(gè)用于請(qǐng)求和管理 懸浮窗權(quán)限(Overlay Permission) 的系統(tǒng)設(shè)置頁(yè)面 ;

懸浮窗權(quán)限允許應(yīng)用在其他應(yīng)用或系統(tǒng)界面上繪制懸浮窗口(如懸浮球、彈窗等);

由于懸浮窗權(quán)限涉及用戶隱私和安全,Android 要求開(kāi)發(fā)者顯式請(qǐng)求該權(quán)限,并引導(dǎo)用戶手動(dòng)開(kāi)啟。

懸浮窗權(quán)限允許應(yīng)用執(zhí)行以下操作:

  • 在其他應(yīng)用或系統(tǒng)界面上顯示懸浮窗口。
  • 實(shí)現(xiàn)全局彈窗、懸浮按鈕、畫(huà)中畫(huà)等功能。
  • 常用于錄屏工具、懸浮球助手、消息提醒等場(chǎng)景。

3、檢查動(dòng)態(tài)權(quán)限

檢查動(dòng)態(tài)權(quán)限 , Android SDK 23 以上才檢查動(dòng)態(tài)權(quán)限 , 對(duì)應(yīng)的版本是 Android 6.0(Marshmallow)‌‌, 低于該版本不需要 動(dòng)態(tài)權(quán)限 , 直接使用對(duì)應(yīng)功能即可 ,

通過(guò) Build.VERSION.SDK_INT >= Build.VERSION_CODES.M 函數(shù)可以判定是否 當(dāng)前版本 是否高于 Android SDK 23 Android 6.0(Marshmallow)‌‌版本 , 是否需要

通過(guò)調(diào)用 Settings.canDrawOverlays(this) 函數(shù) , 可以檢查是否浮云了 懸浮窗權(quán)限 , 如果是 Android 6.0 以上的系統(tǒng) , 并且沒(méi)有該 動(dòng)態(tài)權(quán)限 , 則 動(dòng)態(tài)請(qǐng)求該權(quán)限 ;

    /**
     * 檢查懸浮窗權(quán)限的方法
     */
    private fun checkOverlayPermission(): Boolean {
        // Android SDK23 對(duì)應(yīng)的版本是 Android 6.0(Marshmallow)??
        // 6.0 以上的 Android 系統(tǒng)需要?jiǎng)討B(tài)申請(qǐng)權(quán)限
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ) {
            /*
                根據(jù)當(dāng)前應(yīng)用是否有懸浮窗權(quán)限進(jìn)行不同的操作
                 - 如果 有 懸浮窗權(quán)限 直接返回 true 顯示懸浮窗
                 - 如果 沒(méi)有懸浮窗權(quán)限, 開(kāi)始請(qǐng)求懸浮窗權(quán)限
             */
            if (!Settings.canDrawOverlays(this)) {
                // 沒(méi)有懸浮窗權(quán)限, 開(kāi)始請(qǐng)求懸浮窗權(quán)限
                requestOverlayPermission()
                return false
            } else {
                // 有 懸浮窗權(quán)限 直接返回 true 顯示懸浮窗
                return true
            }
        } else {
            // 6.0 以下的 Android 系統(tǒng)不需要申請(qǐng)權(quán)限
            // 已經(jīng)請(qǐng)求懸浮窗權(quán)限成功 可進(jìn)行后續(xù)操作
            return true
        }
    }

4、申請(qǐng)動(dòng)態(tài)權(quán)限

申請(qǐng)動(dòng)態(tài)權(quán)限時(shí) , 需要彈出一個(gè)對(duì)話框 , 提示用戶要跳轉(zhuǎn)到指定界面 , 進(jìn)行某個(gè)設(shè)置 ;

這里需要跳轉(zhuǎn)到 Settings.ACTION_MANAGE_OVERLAY_PERMISSION 權(quán)限設(shè)置界面 , 為某個(gè)應(yīng)用開(kāi)啟 " 顯示在其他應(yīng)用的上層 " 權(quán)限 ;

在界面中 , 選中要設(shè)置的應(yīng)用 , 設(shè)置該應(yīng)用可以顯示在其它應(yīng)用的上層 ;

代碼示例 :

    /**
     * 請(qǐng)求懸浮窗權(quán)限
     */
    private fun requestOverlayPermission() {
        // 彈出 " 請(qǐng)?jiān)试S顯示在其他應(yīng)用上方 " 的提示對(duì)話框
        AlertDialog.Builder(this) // 創(chuàng)建AlertDialog構(gòu)建器
            .setTitle("需要懸浮窗權(quán)限") // 設(shè)置標(biāo)題
            .setMessage("請(qǐng)?jiān)试S顯示在其他應(yīng)用上方") // 設(shè)置消息
            .setPositiveButton("去設(shè)置") { _, _ -> // 設(shè)置“去設(shè)置”按鈕
                val intent = Intent(
                    Settings.ACTION_MANAGE_OVERLAY_PERMISSION, // 設(shè)置操作為管理懸浮窗權(quán)限
                    Uri.parse("package:$packageName") // 設(shè)置URI為當(dāng)前應(yīng)用的包名
                )
                startActivityForResult(intent, OVERLAY_PERMISSION_REQUEST_CODE) // 啟動(dòng)設(shè)置界面,等待結(jié)果
            }
            .setNegativeButton("取消", null) // 設(shè)置“取消”按鈕
            .show() // 顯示對(duì)話框
    }

5、權(quán)限設(shè)置完畢后返回處理

設(shè)定一個(gè)請(qǐng)求碼 , 自定義的請(qǐng)求碼 , 用于 跳轉(zhuǎn)到 申請(qǐng) 動(dòng)態(tài)權(quán)限 頁(yè)面 , 返回后判定返回結(jié)果 ;

    /**
     * 請(qǐng)求懸浮窗權(quán)限的請(qǐng)求碼
     */
    private val OVERLAY_PERMISSION_REQUEST_CODE = 1001

設(shè)置完 懸浮窗權(quán)限 后 , 從 Settings.ACTION_MANAGE_OVERLAY_PERMISSION 界面返回 , 會(huì)回調(diào) onActivityResult 函數(shù) , 返回后 再次驗(yàn)證 是否已經(jīng)獲得了 懸浮窗權(quán)限 ,

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == OVERLAY_PERMISSION_REQUEST_CODE) {
            // 如果權(quán)限請(qǐng)求成功, 會(huì)根據(jù) 請(qǐng)求碼 命中該分支
            if (checkOverlayPermission()) { // 檢查是否獲得懸浮窗權(quán)限
                startFloatingService() // 啟動(dòng)懸浮窗服務(wù)
            }
        }
    }

二、懸浮窗 前臺(tái)服務(wù)和通知

1、前臺(tái)服務(wù) 啟動(dòng) 懸浮窗 的必要性

為什么必須用 前臺(tái)服務(wù) 啟動(dòng) 懸浮窗 :

  • 系統(tǒng)兼容性 : Android 8.0+ 禁止后臺(tái)應(yīng)用直接顯示懸浮窗,前臺(tái)服務(wù)是唯一合法途徑。
  • 資源保障 : 前臺(tái)服務(wù)優(yōu)先級(jí)更高,避免懸浮窗因進(jìn)程被回收而消失。
  • 用戶透明度 : 通知欄提示用戶服務(wù)運(yùn)行狀態(tài),符合隱私和設(shè)計(jì)規(guī)范。
  • 權(quán)限合規(guī) : 減少 SYSTEM_ALERT_WINDOW 權(quán)限濫用風(fēng)險(xiǎn),提升應(yīng)用審核通過(guò)率。

如果不使用前臺(tái)服務(wù) , 會(huì)出現(xiàn)以下情況 :

  • 懸浮窗可能在后臺(tái)被系統(tǒng)強(qiáng)制關(guān)閉。
  • 在 Android 12+ 設(shè)備上可能直接崩潰(權(quán)限拒絕)。
  • 用戶可能誤判應(yīng)用為惡意軟件(無(wú)通知提示)。

① 保持懸浮窗存活

Android 懸浮窗開(kāi)發(fā) , 需要 保證 懸浮窗 的持續(xù)存活 ,

  • 當(dāng) 應(yīng)用退到 后臺(tái)時(shí) , 通過(guò) bindService 綁定的服務(wù) 就被系統(tǒng)回收了 , 懸浮窗就會(huì)消失 ;
  • Android 8.0 之后的系統(tǒng) , 無(wú)法在后臺(tái)創(chuàng)建 Activity 或 Window 組件 ;
  • 系統(tǒng)會(huì)限制后臺(tái)的 CPU 和 網(wǎng)絡(luò)資源 , 不定期殺死普通服務(wù) ;
  • 使用 前臺(tái)服務(wù) , 可以避免上述三個(gè)問(wèn)題 , 保證 懸浮窗持續(xù)存在 ;
場(chǎng)景問(wèn)題前臺(tái)服務(wù)的作用
應(yīng)用退到后臺(tái)普通 Service 可能被系統(tǒng)回收 → 懸浮窗消失前臺(tái)服務(wù)優(yōu)先級(jí)更高,系統(tǒng)更傾向于保留(即使內(nèi)存不足) → 懸浮窗持續(xù)顯示
Android 8.0+ 后臺(tái)限制后臺(tái)應(yīng)用無(wú)法創(chuàng)建 ActivityWindow(如 TYPE_APPLICATION_OVERLAY前臺(tái)服務(wù)屬于“用戶可見(jiàn)”狀態(tài) → 允許在后臺(tái)顯示懸浮窗
Doze 模式 / 應(yīng)用待機(jī)系統(tǒng)限制后臺(tái)應(yīng)用的 CPU/網(wǎng)絡(luò)等資源 → 普通服務(wù)可能被中斷前臺(tái)服務(wù)可繞過(guò)部分 Doze 限制 → 懸浮窗邏輯持續(xù)運(yùn)行

② 懸浮窗的要求

在 Android 系統(tǒng)中 , 運(yùn)行了一個(gè) 懸浮在 操作系統(tǒng) 中的 懸浮窗 , 這需要滿足 懸浮窗相關(guān)權(quán)限 和 用戶感知要求 , 要讓用戶知道是哪個(gè)應(yīng)用啟動(dòng)了 懸浮窗 , 并且用戶可以隨時(shí)關(guān)閉該 懸浮窗 ;

使用 前臺(tái)服務(wù) 可以滿足上述要求 ;

要求前臺(tái)服務(wù)的解決方案
權(quán)限依賴懸浮窗需要 SYSTEM_ALERT_WINDOW 權(quán)限,但 Android 10+ 要求動(dòng)態(tài)申請(qǐng)并用戶授權(quán)。前臺(tái)服務(wù)通過(guò)通知欄提示用戶應(yīng)用正在運(yùn)行,減少被系統(tǒng)判定為“濫用權(quán)限”的風(fēng)險(xiǎn)。
用戶可感知性前臺(tái)服務(wù)必須顯示通知欄通知 → 用戶明確知道懸浮窗關(guān)聯(lián)的服務(wù)在運(yùn)行(符合 Android 設(shè)計(jì)規(guī)范)。
避免后臺(tái)限制從 Android 12 開(kāi)始,后臺(tái)應(yīng)用啟動(dòng)前臺(tái)服務(wù)需用戶授權(quán)(START_FOREGROUND_SERVICES 權(quán)限),但啟動(dòng)后系統(tǒng)允許其顯示懸浮窗。

③ 懸浮窗版本兼容

Android 系統(tǒng)中 , 不同的版本中 , 啟動(dòng)懸浮窗各自都有不同的限制 , 只有使用前臺(tái)服務(wù) , 可以滿足所有的限制 , 因此前臺(tái)服務(wù)在不同版本均有關(guān)鍵作用 , 所有的版本都可以使用 前臺(tái)服務(wù) 啟動(dòng) 和 保持 懸浮窗 , 避免了不同 Android 系統(tǒng)版本 開(kāi)發(fā)出的 懸浮窗 不兼容的問(wèn)題 ;

Android 版本前臺(tái)服務(wù)的關(guān)鍵作用
Android 8.0 (API 26)禁止后臺(tái)應(yīng)用創(chuàng)建 Window → 必須通過(guò)前臺(tái)服務(wù)綁定懸浮窗邏輯。
Android 10 (API 29)禁止后臺(tái)應(yīng)用啟動(dòng) Activity → 前臺(tái)服務(wù)可繞過(guò)此限制顯示懸浮窗。
Android 12 (API 31)前臺(tái)服務(wù)需聲明 foregroundServiceType(如 mediaPlayback)→ 明確服務(wù)用途,提升系統(tǒng)信任度。

2、其它類型服務(wù)簡(jiǎn)介

這里需要為 懸浮窗 設(shè)置一個(gè)綁定的服務(wù) , 以確保懸浮窗一直保持存在 ;

服務(wù)類型使用場(chǎng)景特點(diǎn)
前臺(tái)服務(wù)需要在后臺(tái)持續(xù)運(yùn)行且用戶可感知的任務(wù),如播放音樂(lè)、導(dǎo)航等。需要在通知欄顯示持續(xù)的通知,告知用戶服務(wù)正在運(yùn)行。
WorkManager需要可靠執(zhí)行的后臺(tái)任務(wù),即使應(yīng)用退出或設(shè)備重啟后仍需執(zhí)行的任務(wù),如上傳日志、定期同步數(shù)據(jù)等。適用于需要持久性和可靠性的任務(wù),支持鏈?zhǔn)饺蝿?wù)、延遲執(zhí)行、重試機(jī)制等特性。
JobScheduler需要在特定條件下執(zhí)行的后臺(tái)任務(wù),如網(wǎng)絡(luò)連接、設(shè)備充電等條件下執(zhí)行的任務(wù)。適用于 Android 5.0(API 級(jí)別 21)及以上版本,允許在滿足特定條件時(shí)調(diào)度任務(wù)。
AlarmManager需要在特定時(shí)間或周期性執(zhí)行的任務(wù),如定時(shí)提醒、定期同步等。適用于設(shè)置一次性任務(wù)、周期重復(fù)任務(wù)、定時(shí)重復(fù)任務(wù)。

① 前臺(tái)服務(wù)

前臺(tái)服務(wù)(Foreground Service):

  • 使用場(chǎng)景 : 適用于需要在后臺(tái)持續(xù)運(yùn)行且用戶可感知的任務(wù),如音樂(lè)播放、導(dǎo)航等。
  • 特點(diǎn) : 必須顯示一個(gè)持續(xù)的通知,確保用戶知曉服務(wù)的存在。優(yōu)先級(jí)高,不容易被系統(tǒng)殺死。
  • 優(yōu)點(diǎn) : 高優(yōu)先級(jí),系統(tǒng)不容易終止。 適用于需要用戶知曉的長(zhǎng)期運(yùn)行任務(wù) ;
  • 缺點(diǎn) : 需要顯示通知,可能影響用戶體驗(yàn)。不適用于不需要用戶感知的后臺(tái)任務(wù)。

② WorkManager 服務(wù)

WorkManager 服務(wù) :

  • 使用場(chǎng)景 : 適用于需要可靠執(zhí)行的后臺(tái)任務(wù),即使應(yīng)用退出或設(shè)備重啟也能保證執(zhí)行,如數(shù)據(jù)同步、上傳日志等。
  • 特點(diǎn) : 支持鏈?zhǔn)饺蝿?wù)、延遲執(zhí)行、重試機(jī)制等特性。兼容 Android 5.0(API 級(jí)別 21)及以上版本。 自動(dòng)選擇最佳的執(zhí)行方式,適應(yīng)設(shè)備狀態(tài)和系統(tǒng)限制。
  • 優(yōu)點(diǎn) : 高可靠性,適用于需要持久化的任務(wù)。自動(dòng)適配系統(tǒng)限制,確保任務(wù)執(zhí)行。支持任務(wù)鏈?zhǔn)綀?zhí)行,方便管理復(fù)雜任務(wù)。
  • 缺點(diǎn) : 相較于其他方式,可能引入額外的庫(kù)和復(fù)雜性。對(duì)于簡(jiǎn)單的后臺(tái)任務(wù),可能顯得過(guò)于復(fù)雜。

③ JobScheduler 服務(wù)

JobScheduler 服務(wù) :

  • 使用場(chǎng)景 : 適用于需要在特定條件下執(zhí)行的后臺(tái)任務(wù),如網(wǎng)絡(luò)連接、充電狀態(tài)等。
  • 特點(diǎn) : 在 Android 5.0(API 級(jí)別 21)引入。允許根據(jù)設(shè)備狀態(tài)和約束條件調(diào)度任務(wù)。
  • 優(yōu)點(diǎn) : 節(jié)省電池和資源,避免不必要的后臺(tái)任務(wù)。適用于需要在特定條件下執(zhí)行的任務(wù)。
  • 缺點(diǎn) : 僅適用于 Android 5.0 及以上版本。功能相對(duì)有限,不如 WorkManager 靈活。

④ AlarmManager 服務(wù)

AlarmManager 服務(wù) :

  • 使用場(chǎng)景 : 適用于需要在特定時(shí)間或周期性執(zhí)行的任務(wù),如定時(shí)提醒、定期同步等。
  • 特點(diǎn) : 允許在指定時(shí)間或周期性觸發(fā)任務(wù)。會(huì)喚醒設(shè)備執(zhí)行任務(wù),可能影響電池壽命。
  • 優(yōu)點(diǎn) : 適用于精確的定時(shí)任務(wù)。簡(jiǎn)單易用,適合定時(shí)提醒等場(chǎng)景。
  • 缺點(diǎn) : 可能導(dǎo)致設(shè)備從低電耗模式中喚醒,影響電池壽命。在設(shè)備處于 Doze 模式或應(yīng)用被限制時(shí),可能無(wú)法按時(shí)執(zhí)行任務(wù)。

三、前臺(tái)服務(wù) 創(chuàng)建 通知 和 懸浮窗

1、啟動(dòng)前臺(tái)服務(wù)

Android SDK 版本大于 26, Android 8.0 (Oreo) 需要 調(diào)用 startForegroundService 函數(shù) 啟動(dòng) 前臺(tái)服務(wù) , 前臺(tái)服務(wù) 是 Android 8.0 之后才有的概念 , 之前 全都是 普通的 服務(wù) , 只是通過(guò) startService 和 bindService 兩種啟動(dòng)方式 區(qū)別服務(wù) ;

如果 Android 的 SDK 版本低于 26, Android 8.0 (Oreo) 則直接 調(diào)用 startService 函數(shù) 啟動(dòng)普通服務(wù)即可 ;

啟動(dòng)懸浮窗前臺(tái)服務(wù)代碼 :

    /**
     * 啟動(dòng)懸浮窗服務(wù)
     */
    private fun startFloatingService() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // 如果 SDK 版本大于 26,  Android 8.0 (Oreo) 需要啟動(dòng)前臺(tái)服務(wù)
            startForegroundService(Intent(this, FloatingWindowService::class.java)) // 啟動(dòng)前臺(tái)服務(wù)
        } else {
            // 如果 SDK 版本低于 26,  Android 8.0 (Oreo) 則直接啟動(dòng)普通服務(wù)即可
            startService(Intent(this, FloatingWindowService::class.java)) // 啟動(dòng)普通服務(wù)
        }
    }

2、前臺(tái)服務(wù)通知

Android SDK 版本大于 26 , 對(duì)應(yīng)的系統(tǒng)版本是 Android 8.0 (Oreo) , 通過(guò)調(diào)用 startForegroundService 函數(shù) 啟動(dòng) 前臺(tái)服務(wù) , 必須在 啟動(dòng)服務(wù) 的 5 秒內(nèi) , 啟動(dòng) 前臺(tái)通知 , 否則應(yīng)用會(huì)崩潰退出 ;

啟動(dòng)通知代碼如下 :

        // SDK 版本大于 26,  Android 8.0 (Oreo) , 才創(chuàng)建通知渠道, 并啟動(dòng)前臺(tái)應(yīng)用
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            createNotificationChannel()
            val notification = buildNotification()
            // 啟動(dòng)服務(wù)后, 必須在 5 秒內(nèi)設(shè)置 前臺(tái)服務(wù)通知信息
            startForeground(NOTIFICATION_ID, notification)
        }

首先 , 要?jiǎng)?chuàng)建 通知渠道 :

    /**
     * 創(chuàng)建通知渠道
     *  通知渠道是 SDK 26 Android 8.0 (Oreo) 引入的新特性
     */
    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            // 創(chuàng)建通知渠道
            val channel = NotificationChannel(
                CHANNEL_ID,
                "懸浮窗",
                NotificationManager.IMPORTANCE_LOW
            )
            // 注冊(cè)通知渠道
            getSystemService(NotificationManager::class.java)
                .createNotificationChannel(channel)
        }
    }

然后 , 創(chuàng)建通知 :

    /**
     * 創(chuàng)建通知
     */
    private fun buildNotification(): Notification {
        return NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle("懸浮窗") // 設(shè)置通知標(biāo)題
            .setContentText("顯示前臺(tái)懸浮窗服務(wù)") // 設(shè)置通知內(nèi)容
            .setSmallIcon(R.mipmap.ic_launcher) // 設(shè)置通知小圖標(biāo)
            .setPriority(NotificationCompat.PRIORITY_LOW) // 設(shè)置通知優(yōu)先級(jí)
            .build() // 構(gòu)建并返回通知
    }

3、創(chuàng)建浮動(dòng)窗口

創(chuàng)建浮動(dòng)窗口流程 :

① 設(shè)置布局類型 :

  • Android SDK 26 Android 8.0 (Oreo) 及以上的版本 , 需要設(shè)置 WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY 類型布局 ;
  • SDK 25 及以下的版本使用 WindowManager.LayoutParams.TYPE_PHONE 布局 ;
        // 獲取 WindowManager 實(shí)例
        windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
        // 設(shè)置布局類型
        val layoutFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // 如果 SDK 版本大于等于 O
            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY // 設(shè)置布局類型為應(yīng)用覆蓋層
        } else {
            WindowManager.LayoutParams.TYPE_PHONE // 設(shè)置布局類型為電話
        }

② 設(shè)置布局參數(shù) :

        // 設(shè)置布局參數(shù)
        val params = WindowManager.LayoutParams(
            WindowManager.LayoutParams.WRAP_CONTENT, // 寬度自適應(yīng)
            WindowManager.LayoutParams.WRAP_CONTENT, // 高度自適應(yīng)
            layoutFlag, // 布局類型
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, // 不獲取焦點(diǎn)
            PixelFormat.TRANSLUCENT // 半透明
        ).apply {
            gravity = Gravity.TOP or Gravity.START // 設(shè)置重力為頂部和左側(cè)
            x = 0 // 設(shè)置X坐標(biāo)
            y = 0 // 設(shè)置Y坐標(biāo), 將浮動(dòng)窗口顯示在左上角
        }

③ 加載浮動(dòng)窗口布局 :

        // 加載 浮動(dòng)窗口 布局
        val inflater = getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater // 獲取LayoutInflater實(shí)例
        floatingView = inflater.inflate(R.layout.floating_window, null)      // 加載懸浮窗布局

④ 設(shè)置浮動(dòng)窗口事件 :

        // 設(shè)置關(guān)閉按鈕的點(diǎn)擊事件
        floatingView.findViewById<Button>(R.id.close_btn).setOnClickListener {
            stopSelf() // 停止服務(wù)
        }
        // 設(shè)置拖動(dòng)事件
        floatingView.setOnTouchListener { view, event ->
            when (event.action) {
                MotionEvent.ACTION_DOWN -> { // 按下事件
                    initialX = params.x // 記錄初始X坐標(biāo)
                    initialY = params.y // 記錄初始Y坐標(biāo)
                    initialTouchX = event.rawX // 記錄初始觸摸X坐標(biāo)
                    initialTouchY = event.rawY // 記錄初始觸摸Y坐標(biāo)
                    true
                }
                MotionEvent.ACTION_MOVE -> { // 移動(dòng)事件
                    params.x = initialX + (event.rawX - initialTouchX).toInt() // 更新X坐標(biāo)
                    params.y = initialY + (event.rawY - initialTouchY).toInt() // 更新Y坐標(biāo)
                    windowManager.updateViewLayout(floatingView, params) // 更新懸浮窗位置
                    true
                }
                else -> false
            }
        }

⑤ 添加浮動(dòng)窗口 :

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

完整代碼如下 :

    /**
     * 創(chuàng)建懸浮窗口
     */
    private fun createFloatingWindow() { // 創(chuàng)建懸浮窗的方法
        // 獲取 WindowManager 實(shí)例
        windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
        // 設(shè)置布局類型
        val layoutFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // 如果 SDK 版本大于等于 O
            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY // 設(shè)置布局類型為應(yīng)用覆蓋層
        } else {
            WindowManager.LayoutParams.TYPE_PHONE // 設(shè)置布局類型為電話
        }
        // 設(shè)置布局參數(shù)
        val params = WindowManager.LayoutParams(
            WindowManager.LayoutParams.WRAP_CONTENT, // 寬度自適應(yīng)
            WindowManager.LayoutParams.WRAP_CONTENT, // 高度自適應(yīng)
            layoutFlag, // 布局類型
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, // 不獲取焦點(diǎn)
            PixelFormat.TRANSLUCENT // 半透明
        ).apply {
            gravity = Gravity.TOP or Gravity.START // 設(shè)置重力為頂部和左側(cè)
            x = 0 // 設(shè)置X坐標(biāo)
            y = 0 // 設(shè)置Y坐標(biāo), 將浮動(dòng)窗口顯示在左上角
        }
        // 加載 浮動(dòng)窗口 布局
        val inflater = getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater // 獲取LayoutInflater實(shí)例
        floatingView = inflater.inflate(R.layout.floating_window, null)      // 加載懸浮窗布局
        // 設(shè)置關(guān)閉按鈕的點(diǎn)擊事件
        floatingView.findViewById<Button>(R.id.close_btn).setOnClickListener {
            stopSelf() // 停止服務(wù)
        }
        // 設(shè)置拖動(dòng)事件
        floatingView.setOnTouchListener { view, event ->
            when (event.action) {
                MotionEvent.ACTION_DOWN -> { // 按下事件
                    initialX = params.x // 記錄初始X坐標(biāo)
                    initialY = params.y // 記錄初始Y坐標(biāo)
                    initialTouchX = event.rawX // 記錄初始觸摸X坐標(biāo)
                    initialTouchY = event.rawY // 記錄初始觸摸Y坐標(biāo)
                    true
                }
                MotionEvent.ACTION_MOVE -> { // 移動(dòng)事件
                    params.x = initialX + (event.rawX - initialTouchX).toInt() // 更新X坐標(biāo)
                    params.y = initialY + (event.rawY - initialTouchY).toInt() // 更新Y坐標(biāo)
                    windowManager.updateViewLayout(floatingView, params) // 更新懸浮窗位置
                    true
                }
                else -> false
            }
        }
        // 正式添加懸浮窗到窗口
        windowManager.addView(floatingView, params)
    }

四、完整代碼示例

1、Service 浮動(dòng)窗口服務(wù)代碼

浮動(dòng)窗口所在 前臺(tái)服務(wù) 代碼 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
    /*
        聲明 浮動(dòng)窗口 的 初始坐標(biāo)
     */
    private var initialX = 0
    private var initialY = 0
    /*
        聲明 浮動(dòng)窗口 的 初始觸摸坐標(biāo)
     */
    private var initialTouchX = 0f
    private var initialTouchY = 0f
    /**
     * 定義通知 ID
     */
    private val NOTIFICATION_ID = 1001
    /**
     * 定義通知渠道 ID, 通知渠道需要
     *  調(diào)用 Service.createNotificationChannel 函數(shù)創(chuàng)建
     */
    private val CHANNEL_ID = "floating_window_channel"
    /**
     * 重寫(xiě) onBind 函數(shù), 返回 null
     */
    override fun onBind(intent: Intent?): IBinder? = null
    override fun onCreate() {
        super.onCreate()
        // SDK 版本大于 26,  Android 8.0 (Oreo) , 才創(chuàng)建通知渠道, 并啟動(dòng)前臺(tái)應(yīng)用
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            createNotificationChannel()
            val notification = buildNotification()
            // 啟動(dòng)服務(wù)后, 必須在 5 秒內(nèi)設(shè)置 前臺(tái)服務(wù)通知信息
            startForeground(NOTIFICATION_ID, notification)
        }
        // 創(chuàng)建懸浮窗
        createFloatingWindow()
    }
    /**
     * 創(chuàng)建通知渠道
     *  通知渠道是 SDK 26 Android 8.0 (Oreo) 引入的新特性
     */
    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            // 創(chuàng)建通知渠道
            val channel = NotificationChannel(
                CHANNEL_ID,
                "懸浮窗",
                NotificationManager.IMPORTANCE_LOW
            )
            // 注冊(cè)通知渠道
            getSystemService(NotificationManager::class.java)
                .createNotificationChannel(channel)
        }
    }
    /**
     * 創(chuàng)建通知
     */
    private fun buildNotification(): Notification {
        return NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle("懸浮窗") // 設(shè)置通知標(biāo)題
            .setContentText("顯示前臺(tái)懸浮窗服務(wù)") // 設(shè)置通知內(nèi)容
            .setSmallIcon(R.mipmap.ic_launcher) // 設(shè)置通知小圖標(biāo)
            .setPriority(NotificationCompat.PRIORITY_LOW) // 設(shè)置通知優(yōu)先級(jí)
            .build() // 構(gòu)建并返回通知
    }
    /**
     * 創(chuàng)建懸浮窗口
     */
    private fun createFloatingWindow() { // 創(chuàng)建懸浮窗的方法
        // 獲取 WindowManager 實(shí)例
        windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
        // 設(shè)置布局類型
        val layoutFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // 如果 SDK 版本大于等于 O
            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY // 設(shè)置布局類型為應(yīng)用覆蓋層
        } else {
            WindowManager.LayoutParams.TYPE_PHONE // 設(shè)置布局類型為電話
        }
        // 設(shè)置布局參數(shù)
        val params = WindowManager.LayoutParams(
            WindowManager.LayoutParams.WRAP_CONTENT, // 寬度自適應(yīng)
            WindowManager.LayoutParams.WRAP_CONTENT, // 高度自適應(yīng)
            layoutFlag, // 布局類型
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, // 不獲取焦點(diǎn)
            PixelFormat.TRANSLUCENT // 半透明
        ).apply {
            gravity = Gravity.TOP or Gravity.START // 設(shè)置重力為頂部和左側(cè)
            x = 0 // 設(shè)置X坐標(biāo)
            y = 0 // 設(shè)置Y坐標(biāo), 將浮動(dòng)窗口顯示在左上角
        }
        // 加載 浮動(dòng)窗口 布局
        val inflater = getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater // 獲取LayoutInflater實(shí)例
        floatingView = inflater.inflate(R.layout.floating_window, null)      // 加載懸浮窗布局
        // 設(shè)置關(guān)閉按鈕的點(diǎn)擊事件
        floatingView.findViewById<Button>(R.id.close_btn).setOnClickListener {
            stopSelf() // 停止服務(wù)
        }
        // 設(shè)置拖動(dòng)事件
        floatingView.setOnTouchListener { view, event ->
            when (event.action) {
                MotionEvent.ACTION_DOWN -> { // 按下事件
                    initialX = params.x // 記錄初始X坐標(biāo)
                    initialY = params.y // 記錄初始Y坐標(biāo)
                    initialTouchX = event.rawX // 記錄初始觸摸X坐標(biāo)
                    initialTouchY = event.rawY // 記錄初始觸摸Y坐標(biāo)
                    true
                }
                MotionEvent.ACTION_MOVE -> { // 移動(dòng)事件
                    params.x = initialX + (event.rawX - initialTouchX).toInt() // 更新X坐標(biāo)
                    params.y = initialY + (event.rawY - initialTouchY).toInt() // 更新Y坐標(biāo)
                    windowManager.updateViewLayout(floatingView, params) // 更新懸浮窗位置
                    true
                }
                else -> false
            }
        }
        // 正式添加懸浮窗到窗口
        windowManager.addView(floatingView, params)
    }
    /**
     * 重寫(xiě) onDestroy 方法
     */
    override fun onDestroy() {
        super.onDestroy()
        if (::floatingView.isInitialized) { // 如果 floatingView 已初始化
            windowManager.removeView(floatingView) // 移除懸浮窗
        }
    }
}

2、Activity 主界面代碼

下面是 Activity 主界面代碼 MainActivity.kt , 主要作用就是 申請(qǐng) 浮動(dòng)窗口所需權(quán)限 和 啟動(dòng)前臺(tái)服務(wù) ;

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() {
    /**
     * 請(qǐng)求懸浮窗權(quán)限的請(qǐng)求碼
     */
    private val OVERLAY_PERMISSION_REQUEST_CODE = 1001
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 檢查是否具有懸浮窗權(quán)限
        if (checkOverlayPermission()) {
            // 啟動(dòng)懸浮窗服務(wù)
            startFloatingService()
        }
    }
    /**
     * 檢查懸浮窗權(quán)限的方法
     */
    private fun checkOverlayPermission(): Boolean {
        // Android SDK23 對(duì)應(yīng)的版本是 Android 6.0(Marshmallow)??
        // 6.0 以上的 Android 系統(tǒng)需要?jiǎng)討B(tài)申請(qǐng)權(quán)限
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ) {
            /*
                根據(jù)當(dāng)前應(yīng)用是否有懸浮窗權(quán)限進(jìn)行不同的操作
                 - 如果 有 懸浮窗權(quán)限 直接返回 true 顯示懸浮窗
                 - 如果 沒(méi)有懸浮窗權(quán)限, 開(kāi)始請(qǐng)求懸浮窗權(quán)限
             */
            if (!Settings.canDrawOverlays(this)) {
                // 沒(méi)有懸浮窗權(quán)限, 開(kāi)始請(qǐng)求懸浮窗權(quán)限
                requestOverlayPermission()
                return false
            } else {
                // 有 懸浮窗權(quán)限 直接返回 true 顯示懸浮窗
                return true
            }
        } else {
            // 6.0 以下的 Android 系統(tǒng)不需要申請(qǐng)權(quán)限
            // 已經(jīng)請(qǐng)求懸浮窗權(quán)限成功 可進(jìn)行后續(xù)操作
            return true
        }
    }
    /**
     * 請(qǐng)求懸浮窗權(quán)限
     */
    private fun requestOverlayPermission() {
        // 彈出 " 請(qǐng)?jiān)试S顯示在其他應(yīng)用上方 " 的提示對(duì)話框
        AlertDialog.Builder(this) // 創(chuàng)建AlertDialog構(gòu)建器
            .setTitle("需要懸浮窗權(quán)限") // 設(shè)置標(biāo)題
            .setMessage("請(qǐng)?jiān)试S顯示在其他應(yīng)用上方") // 設(shè)置消息
            .setPositiveButton("去設(shè)置") { _, _ -> // 設(shè)置“去設(shè)置”按鈕
                val intent = Intent(
                    Settings.ACTION_MANAGE_OVERLAY_PERMISSION, // 設(shè)置操作為管理懸浮窗權(quán)限
                    Uri.parse("package:$packageName") // 設(shè)置URI為當(dāng)前應(yīng)用的包名
                )
                startActivityForResult(intent, OVERLAY_PERMISSION_REQUEST_CODE) // 啟動(dòng)設(shè)置界面,等待結(jié)果
            }
            .setNegativeButton("取消", null) // 設(shè)置“取消”按鈕
            .show() // 顯示對(duì)話框
    }
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == OVERLAY_PERMISSION_REQUEST_CODE) {
            // 如果權(quán)限請(qǐng)求成功, 會(huì)根據(jù) 請(qǐng)求碼 命中該分支
            if (checkOverlayPermission()) { // 檢查是否獲得懸浮窗權(quán)限
                startFloatingService() // 啟動(dòng)懸浮窗服務(wù)
            }
        }
    }
    /**
     * 啟動(dòng)懸浮窗服務(wù)
     */
    private fun startFloatingService() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // 如果 SDK 版本大于 26,  Android 8.0 (Oreo) 需要啟動(dòng)前臺(tái)服務(wù)
            startForegroundService(Intent(this, FloatingWindowService::class.java)) // 啟動(dòng)前臺(tái)服務(wù)
        } else {
            // 如果 SDK 版本低于 26,  Android 8.0 (Oreo) 則直接啟動(dòng)普通服務(wù)即可
            startService(Intent(this, FloatingWindowService::class.java)) // 啟動(dòng)普通服務(wù)
        }
    }
}

3、AndroidManifest.xml 配置文件代碼

在該 AndroidManifest.xml 配置文件中 , 主要需要聲明 :

  • 權(quán)限聲明 : 浮動(dòng)窗口權(quán)限 和 前臺(tái)服務(wù)權(quán)限 ;
  • Activity 組件聲明
  • Service 組件聲明
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="hsl.floatingwindow">
    <!-- 浮動(dòng)窗口權(quán)限 -->
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    <!-- 前臺(tái)服務(wù)權(quán)限 -->
    <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 組件注冊(cè), 注意必須配置 android:exported="true" 屬性, 否則報(bào)錯(cuò) -->
        <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 組件注冊(cè) -->
        <service android:name=".FloatingWindowService" />
    </application>
</manifest>

4、布局文件

浮動(dòng)窗口布局文件 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、執(zhí)行結(jié)果

執(zhí)行效果 :

到此這篇關(guān)于Android 懸浮窗開(kāi)發(fā) ((動(dòng)態(tài)權(quán)限請(qǐng)求 | 前臺(tái)服務(wù)和通知 | 懸浮窗創(chuàng)建 )的文章就介紹到這了,更多相關(guān)Android 懸浮窗內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論