Kotlin實現(xiàn)Android系統(tǒng)懸浮窗詳解
Android 彈窗淺談
我們知道 Android 彈窗中,有一類彈窗會在應(yīng)用之外也顯示,這是因為他被申明成了系統(tǒng)彈窗,除此之外還有2類彈窗分別是:子彈窗與應(yīng)用彈窗。
應(yīng)用彈窗:就是我們常規(guī)使用的 Dialog 之類彈窗,依賴于應(yīng)用的 Activity;子彈窗:依賴于父窗口,比如 PopupWindow;系統(tǒng)彈窗:比如狀態(tài)欄、Toast等,本文所講的系統(tǒng)懸浮窗就是系統(tǒng)彈窗。
系統(tǒng)懸浮窗具體實現(xiàn)
權(quán)限申請
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW" />
代碼設(shè)計
下面的包結(jié)構(gòu)截圖,簡單展示了實現(xiàn)系統(tǒng)懸浮窗的代碼結(jié)構(gòu),更復雜的業(yè)務(wù)需要可在此基礎(chǔ)上進行擴展。

FloatWindowService:系統(tǒng)懸浮窗在此 Service 中彈出;
FloatWindowManager:系統(tǒng)懸浮窗管理類;
FloatLayout:系統(tǒng)懸浮窗布局;
HomeKeyObserverReceiver:
監(jiān)聽 Home 鍵;
FloatWindowUtils:系統(tǒng)懸浮窗工具類。
具體實現(xiàn)
FloatWindowService 類
class FloatWindowService : Service() {
private val TAG = FloatWindowService::class.java.simpleName
private var mFloatWindowManager: FloatWindowManager? = null
private var mHomeKeyObserverReceiver: HomeKeyObserverReceiver? = null
override fun onCreate() {
TLogUtils.i(TAG, "onCreate: ")
mFloatWindowManager = FloatWindowManager(applicationContext)
mHomeKeyObserverReceiver = HomeKeyObserverReceiver()
registerReceiver(mHomeKeyObserverReceiver, IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
mFloatWindowManager!!.createWindow()
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return START_NOT_STICKY
}
override fun onDestroy() {
TLogUtils.i(TAG, "onDestroy: ")
mFloatWindowManager?.removeWindow()
if (mHomeKeyObserverReceiver != null) {
unregisterReceiver(mHomeKeyObserverReceiver)
}
}
}
FloatWindowManager 類
包括系統(tǒng)懸浮窗的創(chuàng)建、顯示、銷毀(以及更新)。
public void addView(View view, ViewGroup.LayoutParams params); // 添加 View 到 Window public void updateViewLayout(View view, ViewGroup.LayoutParams params); //更新 View 在 Window 中的位置 public void removeView(View view); //刪除 View
FloatWindowManager 類代碼
class FloatWindowManager constructor(context: Context) {
var isShowing = false
private val TAG = FloatWindowManager::class.java.simpleName
private var mContext: Context = context
private var mFloatLayout = FloatLayout(mContext)
private var mLayoutParams: WindowManager.LayoutParams? = null
private var mWindowManager: WindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
fun createWindow() {
TLogUtils.i(TAG, "createWindow: start...")
// 對象配置操作使用apply,額外的處理使用also
mLayoutParams = WindowManager.LayoutParams().apply {
type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Android 8.0以后需要使用TYPE_APPLICATION_OVERLAY,不允許使用以下窗口類型來在其他應(yīng)用和窗口上方顯示提醒窗口:TYPE_PHONE、TYPE_PRIORITY_PHONE、TYPE_SYSTEM_ALERT、TYPE_SYSTEM_OVERLAY、TYPE_SYSTEM_ERROR。
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
// 在Android 8.0之前,懸浮窗口設(shè)置可以為TYPE_PHONE,這種類型是用于提供用戶交互操作的非應(yīng)用窗口。
// 在API Level = 23的時候,需要在Android Manifest.xml文件中聲明權(quán)限SYSTEM_ALERT_WINDOW才能在其他應(yīng)用上繪制控件
WindowManager.LayoutParams.TYPE_PHONE
}
// 設(shè)置圖片格式,效果為背景透明
format = PixelFormat.RGBA_8888
// 設(shè)置浮動窗口不可聚焦(實現(xiàn)操作除浮動窗口外的其他可見窗口的操作)
flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
// 調(diào)整懸浮窗顯示的??课恢脼橛覀?cè)置頂
gravity = Gravity.TOP or Gravity.END
width = 800
height = 200
x = 20
y = 40
}
mWindowManager.addView(mFloatLayout, mLayoutParams)
TLogUtils.i(TAG, "createWindow: end...")
isShowing = true
}
fun showWindow() {
TLogUtils.i(TAG, "showWindow: isShowing = $isShowing")
if (!isShowing) {
if (mLayoutParams == null) {
createWindow()
} else {
mWindowManager.addView(mFloatLayout, mLayoutParams)
isShowing = true
}
}
}
fun removeWindow() {
TLogUtils.i(TAG, "removeWindow: isShowing = $isShowing")
mWindowManager.removeView(mFloatLayout)
isShowing = false
}
}
FloatLayout 類及其 Layout
系統(tǒng)懸浮窗自定義View:FloatLayout
class FloatLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0) :
ConstraintLayout(context, attrs, defStyleAttr, defStyleRes) {
private var mTime: TCLTextView
private var mDistance: TCLTextView
private var mSpeed: TCLTextView
private var mCalories: TCLTextView
init {
val view = LayoutInflater.from(context).inflate(R.layout.do_exercise_view_float_layout, this, true)
mTime = view.findViewById(R.id.float_layout_tv_time)
mDistance = view.findViewById(R.id.float_layout_tv_distance)
mSpeed = view.findViewById(R.id.float_layout_tv_speed)
mCalories = view.findViewById(R.id.float_layout_tv_calories)
}
}
布局文件:float_layout_tv_time
HomeKeyObserverReceiver 類
class HomeKeyObserverReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
try {
val action = intent!!.action
val reason = intent.getStringExtra("reason")
TLogUtils.d(TAG, "HomeKeyObserverReceiver: action = $action,reason = $reason")
if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS == action && "homekey" == reason) {
val keyCode = intent.getIntExtra("keycode", KeyEvent.KEYCODE_UNKNOWN)
TLogUtils.d(TAG, "keyCode = $keyCode")
context?.stopService(Intent(context, FloatWindowService::class.java))
}
} catch (ex: Exception) {
ex.printStackTrace()
}
}
companion object {
private val TAG = HomeKeyObserverReceiver::class.java.simpleName
}
}
FloatWindowUtils 類
object FloatWindowUtils {
const val REQUEST_FLOAT_CODE = 1000
private val TAG = FloatWindowUtils::class.java.simpleName
/**
* 判斷Service是否開啟
*/
fun isServiceRunning(context: Context, ServiceName: String): Boolean {
if (TextUtils.isEmpty(ServiceName)) {
return false
}
val myManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val runningService = myManager.getRunningServices(1000) as ArrayList<ActivityManager.RunningServiceInfo>
runningService.forEach {
if (it.service.className == ServiceName) {
return true
}
}
return false
}
/**
* 檢查懸浮窗權(quán)限是否開啟
*/
@SuppressLint("NewApi")
fun checkSuspendedWindowPermission(context: Activity, block: () -> Unit) {
if (commonROMPermissionCheck(context)) {
block()
} else {
Toast.makeText(context, "請開啟懸浮窗權(quán)限", Toast.LENGTH_SHORT).show()
context.startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION).apply {
data = Uri.parse("package:${context.packageName}")
}, REQUEST_FLOAT_CODE)
}
}
/**
* 判斷懸浮窗權(quán)限權(quán)限
*/
fun commonROMPermissionCheck(context: Context?): Boolean {
var result = true
if (Build.VERSION.SDK_INT >= 23) {
try {
val clazz: Class<*> = Settings::class.java
val canDrawOverlays = clazz.getDeclaredMethod("canDrawOverlays", Context::class.java)
result = canDrawOverlays.invoke(null, context) as Boolean
} catch (e: Exception) {
TLogUtils.e(TAG, e)
}
}
return result
}
}
總結(jié)
本文并未詳細討論系統(tǒng)懸浮窗的拖動功能,實現(xiàn)系統(tǒng)懸浮穿基本功能可以總結(jié)為以下幾個步驟:
1. 聲明及申請權(quán)限;
2. 構(gòu)建懸浮窗需要的控件 Service、Receiver、Manager、Layout、Util;
3. 使用 WindowManager 創(chuàng)建、顯示、銷毀(以及更新)Layout。
到此這篇關(guān)于Kotlin實現(xiàn)Android系統(tǒng)懸浮窗詳解的文章就介紹到這了,更多相關(guān)Android Kotlin系統(tǒng)懸浮窗內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android Walker登錄記住密碼頁面功能實現(xiàn)
這篇文章主要為大家詳細介紹了Android Walker登錄記住密碼頁面功能的實現(xiàn),具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-05-05
Flutter實現(xiàn)固定header底部滑動頁效果示例
這篇文章主要為大家介紹了Flutter實現(xiàn)固定header底部滑動頁效果示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-12-12
Android實現(xiàn)漸變啟動頁和帶有指示器的引導頁
這篇文章主要為大家詳細介紹了Android實現(xiàn)漸變啟動頁和帶有指示器的引導頁,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-09-09
android檢測網(wǎng)絡(luò)連接狀態(tài)示例講解
網(wǎng)絡(luò)的時候,并不是每次都能連接到網(wǎng)絡(luò),因此在程序啟動中需要對網(wǎng)絡(luò)的狀態(tài)進行判斷,如果沒有網(wǎng)絡(luò)則提醒用戶進行設(shè)置2014-02-02

