Android?懸浮窗開發(fā)示例((動態(tài)權(quán)限請求?|?前臺服務(wù)和通知?|?懸浮窗創(chuàng)建?)
懸浮窗實現(xiàn)效果 :
一、懸浮窗 動態(tài)權(quán)限請求
1、動態(tài)請求權(quán)限
在 Android 開發(fā)中 , 自 Android 6.0(API 級別 23)版本開始引入 " 動態(tài)權(quán)限 " ,
動態(tài)權(quán)限 指的是 在應(yīng)用程序運行時向用戶請求權(quán)限 , 而不是在安裝時一次性請求所有權(quán)限 , 旨在提高用戶隱私和安全性 ;
動態(tài)權(quán)限 請求 流程 :
- 檢查權(quán)限: 在請求權(quán)限之前,首先檢查是否已經(jīng)擁有該權(quán)限。
- 請求權(quán)限: 如果沒有權(quán)限,向用戶請求權(quán)限。
- 處理權(quán)限請求結(jié)果: 根據(jù)用戶的響應(yīng),執(zhí)行相應(yīng)的操作。
2、懸浮窗權(quán)限說明
Settings.ACTION_MANAGE_OVERLAY_PERMISSION 是一個用于請求和管理 懸浮窗權(quán)限(Overlay Permission) 的系統(tǒng)設(shè)置頁面 ;
懸浮窗權(quán)限允許應(yīng)用在其他應(yīng)用或系統(tǒng)界面上繪制懸浮窗口(如懸浮球、彈窗等);
由于懸浮窗權(quán)限涉及用戶隱私和安全,Android 要求開發(fā)者顯式請求該權(quán)限,并引導(dǎo)用戶手動開啟。
懸浮窗權(quán)限允許應(yīng)用執(zhí)行以下操作:
- 在其他應(yīng)用或系統(tǒng)界面上顯示懸浮窗口。
- 實現(xiàn)全局彈窗、懸浮按鈕、畫中畫等功能。
- 常用于錄屏工具、懸浮球助手、消息提醒等場景。
3、檢查動態(tài)權(quán)限
檢查動態(tài)權(quán)限 , Android SDK 23 以上才檢查動態(tài)權(quán)限 , 對應(yīng)的版本是 Android 6.0(Marshmallow), 低于該版本不需要 動態(tài)權(quán)限 , 直接使用對應(yīng)功能即可 ,
通過 Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
函數(shù)可以判定是否 當(dāng)前版本 是否高于 Android SDK 23 Android 6.0(Marshmallow)版本 , 是否需要
通過調(diào)用 Settings.canDrawOverlays(this)
函數(shù) , 可以檢查是否浮云了 懸浮窗權(quán)限 , 如果是 Android 6.0 以上的系統(tǒng) , 并且沒有該 動態(tài)權(quán)限 , 則 動態(tài)請求該權(quán)限 ;
/** * 檢查懸浮窗權(quán)限的方法 */ private fun checkOverlayPermission(): Boolean { // Android SDK23 對應(yīng)的版本是 Android 6.0(Marshmallow)?? // 6.0 以上的 Android 系統(tǒng)需要動態(tài)申請權(quán)限 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ) { /* 根據(jù)當(dāng)前應(yīng)用是否有懸浮窗權(quán)限進(jìn)行不同的操作 - 如果 有 懸浮窗權(quán)限 直接返回 true 顯示懸浮窗 - 如果 沒有懸浮窗權(quán)限, 開始請求懸浮窗權(quán)限 */ if (!Settings.canDrawOverlays(this)) { // 沒有懸浮窗權(quán)限, 開始請求懸浮窗權(quán)限 requestOverlayPermission() return false } else { // 有 懸浮窗權(quán)限 直接返回 true 顯示懸浮窗 return true } } else { // 6.0 以下的 Android 系統(tǒng)不需要申請權(quán)限 // 已經(jīng)請求懸浮窗權(quán)限成功 可進(jìn)行后續(xù)操作 return true } }
4、申請動態(tài)權(quán)限
申請動態(tài)權(quán)限時 , 需要彈出一個對話框 , 提示用戶要跳轉(zhuǎn)到指定界面 , 進(jìn)行某個設(shè)置 ;
這里需要跳轉(zhuǎn)到 Settings.ACTION_MANAGE_OVERLAY_PERMISSION 權(quán)限設(shè)置界面 , 為某個應(yīng)用開啟 " 顯示在其他應(yīng)用的上層 " 權(quán)限 ;
在界面中 , 選中要設(shè)置的應(yīng)用 , 設(shè)置該應(yīng)用可以顯示在其它應(yīng)用的上層 ;
代碼示例 :
/** * 請求懸浮窗權(quán)限 */ private fun requestOverlayPermission() { // 彈出 " 請允許顯示在其他應(yīng)用上方 " 的提示對話框 AlertDialog.Builder(this) // 創(chuàng)建AlertDialog構(gòu)建器 .setTitle("需要懸浮窗權(quán)限") // 設(shè)置標(biāo)題 .setMessage("請允許顯示在其他應(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) // 啟動設(shè)置界面,等待結(jié)果 } .setNegativeButton("取消", null) // 設(shè)置“取消”按鈕 .show() // 顯示對話框 }
5、權(quán)限設(shè)置完畢后返回處理
設(shè)定一個請求碼 , 自定義的請求碼 , 用于 跳轉(zhuǎn)到 申請 動態(tài)權(quán)限 頁面 , 返回后判定返回結(jié)果 ;
/** * 請求懸浮窗權(quán)限的請求碼 */ private val OVERLAY_PERMISSION_REQUEST_CODE = 1001
設(shè)置完 懸浮窗權(quán)限 后 , 從 Settings.ACTION_MANAGE_OVERLAY_PERMISSION 界面返回 , 會回調(diào) onActivityResult 函數(shù) , 返回后 再次驗證 是否已經(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)限請求成功, 會根據(jù) 請求碼 命中該分支 if (checkOverlayPermission()) { // 檢查是否獲得懸浮窗權(quán)限 startFloatingService() // 啟動懸浮窗服務(wù) } } }
二、懸浮窗 前臺服務(wù)和通知
1、前臺服務(wù) 啟動 懸浮窗 的必要性
為什么必須用 前臺服務(wù) 啟動 懸浮窗 :
- 系統(tǒng)兼容性 : Android 8.0+ 禁止后臺應(yīng)用直接顯示懸浮窗,前臺服務(wù)是唯一合法途徑。
- 資源保障 : 前臺服務(wù)優(yōu)先級更高,避免懸浮窗因進(jìn)程被回收而消失。
- 用戶透明度 : 通知欄提示用戶服務(wù)運行狀態(tài),符合隱私和設(shè)計規(guī)范。
- 權(quán)限合規(guī) : 減少 SYSTEM_ALERT_WINDOW 權(quán)限濫用風(fēng)險,提升應(yīng)用審核通過率。
如果不使用前臺服務(wù) , 會出現(xiàn)以下情況 :
- 懸浮窗可能在后臺被系統(tǒng)強制關(guān)閉。
- 在 Android 12+ 設(shè)備上可能直接崩潰(權(quán)限拒絕)。
- 用戶可能誤判應(yīng)用為惡意軟件(無通知提示)。
① 保持懸浮窗存活
Android 懸浮窗開發(fā) , 需要 保證 懸浮窗 的持續(xù)存活 ,
- 當(dāng) 應(yīng)用退到 后臺時 , 通過 bindService 綁定的服務(wù) 就被系統(tǒng)回收了 , 懸浮窗就會消失 ;
- Android 8.0 之后的系統(tǒng) , 無法在后臺創(chuàng)建 Activity 或 Window 組件 ;
- 系統(tǒng)會限制后臺的 CPU 和 網(wǎng)絡(luò)資源 , 不定期殺死普通服務(wù) ;
- 使用 前臺服務(wù) , 可以避免上述三個問題 , 保證 懸浮窗持續(xù)存在 ;
場景 | 問題 | 前臺服務(wù)的作用 |
---|---|---|
應(yīng)用退到后臺 | 普通 Service 可能被系統(tǒng)回收 → 懸浮窗消失 | 前臺服務(wù)優(yōu)先級更高,系統(tǒng)更傾向于保留(即使內(nèi)存不足) → 懸浮窗持續(xù)顯示 |
Android 8.0+ 后臺限制 | 后臺應(yīng)用無法創(chuàng)建 Activity 或 Window (如 TYPE_APPLICATION_OVERLAY ) | 前臺服務(wù)屬于“用戶可見”狀態(tài) → 允許在后臺顯示懸浮窗 |
Doze 模式 / 應(yīng)用待機 | 系統(tǒng)限制后臺應(yīng)用的 CPU/網(wǎng)絡(luò)等資源 → 普通服務(wù)可能被中斷 | 前臺服務(wù)可繞過部分 Doze 限制 → 懸浮窗邏輯持續(xù)運行 |
② 懸浮窗的要求
在 Android 系統(tǒng)中 , 運行了一個 懸浮在 操作系統(tǒng) 中的 懸浮窗 , 這需要滿足 懸浮窗相關(guān)權(quán)限 和 用戶感知要求 , 要讓用戶知道是哪個應(yīng)用啟動了 懸浮窗 , 并且用戶可以隨時關(guān)閉該 懸浮窗 ;
使用 前臺服務(wù) 可以滿足上述要求 ;
要求 | 前臺服務(wù)的解決方案 |
---|---|
權(quán)限依賴 | 懸浮窗需要 SYSTEM_ALERT_WINDOW 權(quán)限,但 Android 10+ 要求動態(tài)申請并用戶授權(quán)。前臺服務(wù)通過通知欄提示用戶應(yīng)用正在運行,減少被系統(tǒng)判定為“濫用權(quán)限”的風(fēng)險。 |
用戶可感知性 | 前臺服務(wù)必須顯示通知欄通知 → 用戶明確知道懸浮窗關(guān)聯(lián)的服務(wù)在運行(符合 Android 設(shè)計規(guī)范)。 |
避免后臺限制 | 從 Android 12 開始,后臺應(yīng)用啟動前臺服務(wù)需用戶授權(quán)(START_FOREGROUND_SERVICES 權(quán)限),但啟動后系統(tǒng)允許其顯示懸浮窗。 |
③ 懸浮窗版本兼容
Android 系統(tǒng)中 , 不同的版本中 , 啟動懸浮窗各自都有不同的限制 , 只有使用前臺服務(wù) , 可以滿足所有的限制 , 因此前臺服務(wù)在不同版本均有關(guān)鍵作用 , 所有的版本都可以使用 前臺服務(wù) 啟動 和 保持 懸浮窗 , 避免了不同 Android 系統(tǒng)版本 開發(fā)出的 懸浮窗 不兼容的問題 ;
Android 版本 | 前臺服務(wù)的關(guān)鍵作用 |
---|---|
Android 8.0 (API 26) | 禁止后臺應(yīng)用創(chuàng)建 Window → 必須通過前臺服務(wù)綁定懸浮窗邏輯。 |
Android 10 (API 29) | 禁止后臺應(yīng)用啟動 Activity → 前臺服務(wù)可繞過此限制顯示懸浮窗。 |
Android 12 (API 31) | 前臺服務(wù)需聲明 foregroundServiceType (如 mediaPlayback )→ 明確服務(wù)用途,提升系統(tǒng)信任度。 |
2、其它類型服務(wù)簡介
這里需要為 懸浮窗 設(shè)置一個綁定的服務(wù) , 以確保懸浮窗一直保持存在 ;
服務(wù)類型 | 使用場景 | 特點 |
---|---|---|
前臺服務(wù) | 需要在后臺持續(xù)運行且用戶可感知的任務(wù),如播放音樂、導(dǎo)航等。 | 需要在通知欄顯示持續(xù)的通知,告知用戶服務(wù)正在運行。 |
WorkManager | 需要可靠執(zhí)行的后臺任務(wù),即使應(yīng)用退出或設(shè)備重啟后仍需執(zhí)行的任務(wù),如上傳日志、定期同步數(shù)據(jù)等。 | 適用于需要持久性和可靠性的任務(wù),支持鏈?zhǔn)饺蝿?wù)、延遲執(zhí)行、重試機制等特性。 |
JobScheduler | 需要在特定條件下執(zhí)行的后臺任務(wù),如網(wǎng)絡(luò)連接、設(shè)備充電等條件下執(zhí)行的任務(wù)。 | 適用于 Android 5.0(API 級別 21)及以上版本,允許在滿足特定條件時調(diào)度任務(wù)。 |
AlarmManager | 需要在特定時間或周期性執(zhí)行的任務(wù),如定時提醒、定期同步等。 | 適用于設(shè)置一次性任務(wù)、周期重復(fù)任務(wù)、定時重復(fù)任務(wù)。 |
① 前臺服務(wù)
前臺服務(wù)(Foreground Service):
- 使用場景 : 適用于需要在后臺持續(xù)運行且用戶可感知的任務(wù),如音樂播放、導(dǎo)航等。
- 特點 : 必須顯示一個持續(xù)的通知,確保用戶知曉服務(wù)的存在。優(yōu)先級高,不容易被系統(tǒng)殺死。
- 優(yōu)點 : 高優(yōu)先級,系統(tǒng)不容易終止。 適用于需要用戶知曉的長期運行任務(wù) ;
- 缺點 : 需要顯示通知,可能影響用戶體驗。不適用于不需要用戶感知的后臺任務(wù)。
② WorkManager 服務(wù)
WorkManager 服務(wù) :
- 使用場景 : 適用于需要可靠執(zhí)行的后臺任務(wù),即使應(yīng)用退出或設(shè)備重啟也能保證執(zhí)行,如數(shù)據(jù)同步、上傳日志等。
- 特點 : 支持鏈?zhǔn)饺蝿?wù)、延遲執(zhí)行、重試機制等特性。兼容 Android 5.0(API 級別 21)及以上版本。 自動選擇最佳的執(zhí)行方式,適應(yīng)設(shè)備狀態(tài)和系統(tǒng)限制。
- 優(yōu)點 : 高可靠性,適用于需要持久化的任務(wù)。自動適配系統(tǒng)限制,確保任務(wù)執(zhí)行。支持任務(wù)鏈?zhǔn)綀?zhí)行,方便管理復(fù)雜任務(wù)。
- 缺點 : 相較于其他方式,可能引入額外的庫和復(fù)雜性。對于簡單的后臺任務(wù),可能顯得過于復(fù)雜。
③ JobScheduler 服務(wù)
JobScheduler 服務(wù) :
- 使用場景 : 適用于需要在特定條件下執(zhí)行的后臺任務(wù),如網(wǎng)絡(luò)連接、充電狀態(tài)等。
- 特點 : 在 Android 5.0(API 級別 21)引入。允許根據(jù)設(shè)備狀態(tài)和約束條件調(diào)度任務(wù)。
- 優(yōu)點 : 節(jié)省電池和資源,避免不必要的后臺任務(wù)。適用于需要在特定條件下執(zhí)行的任務(wù)。
- 缺點 : 僅適用于 Android 5.0 及以上版本。功能相對有限,不如 WorkManager 靈活。
④ AlarmManager 服務(wù)
AlarmManager 服務(wù) :
- 使用場景 : 適用于需要在特定時間或周期性執(zhí)行的任務(wù),如定時提醒、定期同步等。
- 特點 : 允許在指定時間或周期性觸發(fā)任務(wù)。會喚醒設(shè)備執(zhí)行任務(wù),可能影響電池壽命。
- 優(yōu)點 : 適用于精確的定時任務(wù)。簡單易用,適合定時提醒等場景。
- 缺點 : 可能導(dǎo)致設(shè)備從低電耗模式中喚醒,影響電池壽命。在設(shè)備處于 Doze 模式或應(yīng)用被限制時,可能無法按時執(zhí)行任務(wù)。
三、前臺服務(wù) 創(chuàng)建 通知 和 懸浮窗
1、啟動前臺服務(wù)
Android SDK 版本大于 26, Android 8.0 (Oreo) 需要 調(diào)用 startForegroundService 函數(shù) 啟動 前臺服務(wù) , 前臺服務(wù) 是 Android 8.0 之后才有的概念 , 之前 全都是 普通的 服務(wù) , 只是通過 startService 和 bindService 兩種啟動方式 區(qū)別服務(wù) ;
如果 Android 的 SDK 版本低于 26, Android 8.0 (Oreo) 則直接 調(diào)用 startService 函數(shù) 啟動普通服務(wù)即可 ;
啟動懸浮窗前臺服務(wù)代碼 :
/** * 啟動懸浮窗服務(wù) */ private fun startFloatingService() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // 如果 SDK 版本大于 26, Android 8.0 (Oreo) 需要啟動前臺服務(wù) startForegroundService(Intent(this, FloatingWindowService::class.java)) // 啟動前臺服務(wù) } else { // 如果 SDK 版本低于 26, Android 8.0 (Oreo) 則直接啟動普通服務(wù)即可 startService(Intent(this, FloatingWindowService::class.java)) // 啟動普通服務(wù) } }
2、前臺服務(wù)通知
Android SDK 版本大于 26 , 對應(yīng)的系統(tǒng)版本是 Android 8.0 (Oreo) , 通過調(diào)用 startForegroundService 函數(shù) 啟動 前臺服務(wù) , 必須在 啟動服務(wù) 的 5 秒內(nèi) , 啟動 前臺通知 , 否則應(yīng)用會崩潰退出 ;
啟動通知代碼如下 :
// SDK 版本大于 26, Android 8.0 (Oreo) , 才創(chuàng)建通知渠道, 并啟動前臺應(yīng)用 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { createNotificationChannel() val notification = buildNotification() // 啟動服務(wù)后, 必須在 5 秒內(nèi)設(shè)置 前臺服務(wù)通知信息 startForeground(NOTIFICATION_ID, notification) }
首先 , 要創(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 ) // 注冊通知渠道 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("顯示前臺懸浮窗服務(wù)") // 設(shè)置通知內(nèi)容 .setSmallIcon(R.mipmap.ic_launcher) // 設(shè)置通知小圖標(biāo) .setPriority(NotificationCompat.PRIORITY_LOW) // 設(shè)置通知優(yōu)先級 .build() // 構(gòu)建并返回通知 }
3、創(chuàng)建浮動窗口
創(chuàng)建浮動窗口流程 :
① 設(shè)置布局類型 :
- Android SDK 26 Android 8.0 (Oreo) 及以上的版本 , 需要設(shè)置 WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY 類型布局 ;
- SDK 25 及以下的版本使用 WindowManager.LayoutParams.TYPE_PHONE 布局 ;
// 獲取 WindowManager 實例 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, // 不獲取焦點 PixelFormat.TRANSLUCENT // 半透明 ).apply { gravity = Gravity.TOP or Gravity.START // 設(shè)置重力為頂部和左側(cè) x = 0 // 設(shè)置X坐標(biāo) y = 0 // 設(shè)置Y坐標(biāo), 將浮動窗口顯示在左上角 }
③ 加載浮動窗口布局 :
// 加載 浮動窗口 布局 val inflater = getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater // 獲取LayoutInflater實例 floatingView = inflater.inflate(R.layout.floating_window, null) // 加載懸浮窗布局
④ 設(shè)置浮動窗口事件 :
// 設(shè)置關(guān)閉按鈕的點擊事件 floatingView.findViewById<Button>(R.id.close_btn).setOnClickListener { stopSelf() // 停止服務(wù) } // 設(shè)置拖動事件 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 -> { // 移動事件 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)
完整代碼如下 :
/** * 創(chuàng)建懸浮窗口 */ private fun createFloatingWindow() { // 創(chuàng)建懸浮窗的方法 // 獲取 WindowManager 實例 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, // 不獲取焦點 PixelFormat.TRANSLUCENT // 半透明 ).apply { gravity = Gravity.TOP or Gravity.START // 設(shè)置重力為頂部和左側(cè) x = 0 // 設(shè)置X坐標(biāo) y = 0 // 設(shè)置Y坐標(biāo), 將浮動窗口顯示在左上角 } // 加載 浮動窗口 布局 val inflater = getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater // 獲取LayoutInflater實例 floatingView = inflater.inflate(R.layout.floating_window, null) // 加載懸浮窗布局 // 設(shè)置關(guān)閉按鈕的點擊事件 floatingView.findViewById<Button>(R.id.close_btn).setOnClickListener { stopSelf() // 停止服務(wù) } // 設(shè)置拖動事件 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 -> { // 移動事件 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 浮動窗口服務(wù)代碼
浮動窗口所在 前臺服務(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 /* 聲明 浮動窗口 的 初始坐標(biāo) */ private var initialX = 0 private var initialY = 0 /* 聲明 浮動窗口 的 初始觸摸坐標(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" /** * 重寫 onBind 函數(shù), 返回 null */ override fun onBind(intent: Intent?): IBinder? = null override fun onCreate() { super.onCreate() // SDK 版本大于 26, Android 8.0 (Oreo) , 才創(chuàng)建通知渠道, 并啟動前臺應(yīng)用 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { createNotificationChannel() val notification = buildNotification() // 啟動服務(wù)后, 必須在 5 秒內(nèi)設(shè)置 前臺服務(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 ) // 注冊通知渠道 getSystemService(NotificationManager::class.java) .createNotificationChannel(channel) } } /** * 創(chuàng)建通知 */ private fun buildNotification(): Notification { return NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("懸浮窗") // 設(shè)置通知標(biāo)題 .setContentText("顯示前臺懸浮窗服務(wù)") // 設(shè)置通知內(nèi)容 .setSmallIcon(R.mipmap.ic_launcher) // 設(shè)置通知小圖標(biāo) .setPriority(NotificationCompat.PRIORITY_LOW) // 設(shè)置通知優(yōu)先級 .build() // 構(gòu)建并返回通知 } /** * 創(chuàng)建懸浮窗口 */ private fun createFloatingWindow() { // 創(chuàng)建懸浮窗的方法 // 獲取 WindowManager 實例 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, // 不獲取焦點 PixelFormat.TRANSLUCENT // 半透明 ).apply { gravity = Gravity.TOP or Gravity.START // 設(shè)置重力為頂部和左側(cè) x = 0 // 設(shè)置X坐標(biāo) y = 0 // 設(shè)置Y坐標(biāo), 將浮動窗口顯示在左上角 } // 加載 浮動窗口 布局 val inflater = getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater // 獲取LayoutInflater實例 floatingView = inflater.inflate(R.layout.floating_window, null) // 加載懸浮窗布局 // 設(shè)置關(guān)閉按鈕的點擊事件 floatingView.findViewById<Button>(R.id.close_btn).setOnClickListener { stopSelf() // 停止服務(wù) } // 設(shè)置拖動事件 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 -> { // 移動事件 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) } /** * 重寫 onDestroy 方法 */ override fun onDestroy() { super.onDestroy() if (::floatingView.isInitialized) { // 如果 floatingView 已初始化 windowManager.removeView(floatingView) // 移除懸浮窗 } } }
2、Activity 主界面代碼
下面是 Activity 主界面代碼 MainActivity.kt , 主要作用就是 申請 浮動窗口所需權(quán)限 和 啟動前臺服務(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() { /** * 請求懸浮窗權(quán)限的請求碼 */ private val OVERLAY_PERMISSION_REQUEST_CODE = 1001 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // 檢查是否具有懸浮窗權(quán)限 if (checkOverlayPermission()) { // 啟動懸浮窗服務(wù) startFloatingService() } } /** * 檢查懸浮窗權(quán)限的方法 */ private fun checkOverlayPermission(): Boolean { // Android SDK23 對應(yīng)的版本是 Android 6.0(Marshmallow)?? // 6.0 以上的 Android 系統(tǒng)需要動態(tài)申請權(quán)限 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ) { /* 根據(jù)當(dāng)前應(yīng)用是否有懸浮窗權(quán)限進(jìn)行不同的操作 - 如果 有 懸浮窗權(quán)限 直接返回 true 顯示懸浮窗 - 如果 沒有懸浮窗權(quán)限, 開始請求懸浮窗權(quán)限 */ if (!Settings.canDrawOverlays(this)) { // 沒有懸浮窗權(quán)限, 開始請求懸浮窗權(quán)限 requestOverlayPermission() return false } else { // 有 懸浮窗權(quán)限 直接返回 true 顯示懸浮窗 return true } } else { // 6.0 以下的 Android 系統(tǒng)不需要申請權(quán)限 // 已經(jīng)請求懸浮窗權(quán)限成功 可進(jìn)行后續(xù)操作 return true } } /** * 請求懸浮窗權(quán)限 */ private fun requestOverlayPermission() { // 彈出 " 請允許顯示在其他應(yīng)用上方 " 的提示對話框 AlertDialog.Builder(this) // 創(chuàng)建AlertDialog構(gòu)建器 .setTitle("需要懸浮窗權(quán)限") // 設(shè)置標(biāo)題 .setMessage("請允許顯示在其他應(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) // 啟動設(shè)置界面,等待結(jié)果 } .setNegativeButton("取消", null) // 設(shè)置“取消”按鈕 .show() // 顯示對話框 } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == OVERLAY_PERMISSION_REQUEST_CODE) { // 如果權(quán)限請求成功, 會根據(jù) 請求碼 命中該分支 if (checkOverlayPermission()) { // 檢查是否獲得懸浮窗權(quán)限 startFloatingService() // 啟動懸浮窗服務(wù) } } } /** * 啟動懸浮窗服務(wù) */ private fun startFloatingService() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // 如果 SDK 版本大于 26, Android 8.0 (Oreo) 需要啟動前臺服務(wù) startForegroundService(Intent(this, FloatingWindowService::class.java)) // 啟動前臺服務(wù) } else { // 如果 SDK 版本低于 26, Android 8.0 (Oreo) 則直接啟動普通服務(wù)即可 startService(Intent(this, FloatingWindowService::class.java)) // 啟動普通服務(wù) } } }
3、AndroidManifest.xml 配置文件代碼
在該 AndroidManifest.xml 配置文件中 , 主要需要聲明 :
- 權(quán)限聲明 : 浮動窗口權(quán)限 和 前臺服務(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"> <!-- 浮動窗口權(quán)限 --> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <!-- 前臺服務(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 組件注冊, 注意必須配置 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、執(zhí)行結(jié)果
執(zhí)行效果 :
到此這篇關(guān)于Android 懸浮窗開發(fā) ((動態(tài)權(quán)限請求 | 前臺服務(wù)和通知 | 懸浮窗創(chuàng)建 )的文章就介紹到這了,更多相關(guān)Android 懸浮窗內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android中ViewFlipper的使用及設(shè)置動畫效果實例詳解
這篇文章主要介紹了Android中ViewFlipper的使用及設(shè)置動畫效果的方法,以實例形式較為詳細(xì)的分析了ViewFlipper的功能、原理及設(shè)置與使用技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-10-10Android觸摸事件和mousedown、mouseup、click事件之間的關(guān)系
今天小編就為大家分享一篇關(guān)于Android觸摸事件和mousedown、mouseup、click事件之間的關(guān)系,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-01-01Android 設(shè)置Edittext獲取焦點并彈出軟鍵盤
本文主要介紹了Android設(shè)置Edittext獲取焦點并彈出軟鍵盤的實現(xiàn)代碼。具有很好的參考價值。下面跟著小編一起來看下吧2017-04-04Android開發(fā)實現(xiàn)ListView點擊item改變顏色功能示例
這篇文章主要介紹了Android開發(fā)實現(xiàn)ListView點擊item改變顏色功能,涉及Android布局及響應(yīng)事件動態(tài)變換元素屬性相關(guān)操作技巧,需要的朋友可以參考下2017-11-11