Android悬浮窗看这篇就够了

365万博首页 2025-08-27 22:46:01 admin

目录

悬浮窗的基本原理

动态添加View

悬浮窗原理

应用内悬浮窗

应用内悬浮窗实现流程

效果

应用外悬浮窗(有局限性)

效果

悬浮窗权限的适配

权限配置和请求

LayoutParam的坑!!!!

无障碍悬浮窗

总结

之前想要实现个全局全浮球的效果,找遍了网上大佬的博客,踩了不少坑,但是还是有一些问题没有解决,比如个别手机设置界面的部分二级界面无法显示(例如:MIUI设置-关于手机[狗头保命])

索性在此总结一篇关于悬浮窗使用以及适配的详细博客(Kotlin代码)

老规矩先上源码链接https://gitee.com/AndroidLMY/SuspendedWindow

效果图

悬浮窗的基本原理

首先我们来说下悬浮窗的基本原理是什么

动态添加View

我们都知道我们想动态的添加View到界面上无非是

实例化一个View然后添加到某个布局中 例如:

val view = LayoutInflater.from(this).inflate(R.layout.activity_float_item, null)

ll_all.addView(view)

那么此时我们想在当前Activity不依赖任何布局添加View时 我们只需要获取WindowManager来添加我们的View

例如:

val view = LayoutInflater.from(this).inflate(R.layout.activity_float_item, null)

var layoutParam = WindowManager.LayoutParams().apply {

//设置大小 自适应

width = WRAP_CONTENT

height = WRAP_CONTENT

}

windowManager.addView(view,layoutParam)

悬浮窗原理

获取WindowManager

创建View

添加到WindowManager中

应用内悬浮窗

应用内悬浮窗实现流程

获取WindowManager

创建悬浮View

设置悬浮View的拖拽事件

添加View到WindowManager中

代码如下:

var layoutParam = WindowManager.LayoutParams().apply {

//设置大小 自适应

width = WRAP_CONTENT

height = WRAP_CONTENT

flags =

WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE

}

// 新建悬浮窗控件

floatRootView = LayoutInflater.from(this).inflate(R.layout.activity_float_item, null)

//设置拖动事件

floatRootView?.setOnTouchListener(ItemViewTouchListener(layoutParam, windowManager))

// 将悬浮窗控件添加到WindowManager

windowManager.addView(floatRootView, layoutParam)

拖拽监听ItemViewTouchListener代码如下:

class ItemViewTouchListener(val wl: WindowManager.LayoutParams, val windowManager: WindowManager) :

View.OnTouchListener {

private var x = 0

private var y = 0

override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {

when (motionEvent.action) {

MotionEvent.ACTION_DOWN -> {

x = motionEvent.rawX.toInt()

y = motionEvent.rawY.toInt()

}

MotionEvent.ACTION_MOVE -> {

val nowX = motionEvent.rawX.toInt()

val nowY = motionEvent.rawY.toInt()

val movedX = nowX - x

val movedY = nowY - y

x = nowX

y = nowY

wl.apply {

x += movedX

y += movedY

}

//更新悬浮球控件位置

windowManager?.updateViewLayout(view, wl)

}

else -> {

}

}

return false

}

}

效果

应用外悬浮窗(有局限性)

应用外悬浮窗实现流程 这里我使用了LivaData来进行和Service的通信

申请悬浮窗权限

创建Service

获取WindowManager

创建悬浮View

设置悬浮View的拖拽事件

添加View到WindowManager

在清单文件添加权限

上代码:

打开悬浮窗

startService(Intent(this, SuspendwindowService::class.java))

Utils.checkSuspendedWindowPermission(this) {

isReceptionShow = false

ViewModleMain.isShowSuspendWindow.postValue(true)

}

SuspendwindowService代码如下

package com.lmy.suspendedwindow.service

import android.annotation.SuppressLint

import android.graphics.PixelFormat

import android.os.Build

import android.util.DisplayMetrics

import android.view.*

import android.view.ViewGroup.LayoutParams.WRAP_CONTENT

import androidx.lifecycle.LifecycleService

import com.lmy.suspendedwindow.R

import com.lmy.suspendedwindow.utils.Utils

import com.lmy.suspendedwindow.utils.ViewModleMain

import com.lmy.suspendedwindow.utils.ItemViewTouchListener

/**

* @功能:应用外打开Service 有局限性 特殊界面无法显示

* @User Lmy

* @Creat 4/15/21 5:28 PM

* @Compony 永远相信美好的事情即将发生

*/

class SuspendwindowService : LifecycleService() {

private lateinit var windowManager: WindowManager

private var floatRootView: View? = null//悬浮窗View

override fun onCreate() {

super.onCreate()

initObserve()

}

private fun initObserve() {

ViewModleMain.apply {

isVisible.observe(this@SuspendwindowService, {

floatRootView?.visibility = if (it) View.VISIBLE else View.GONE

})

isShowSuspendWindow.observe(this@SuspendwindowService, {

if (it) {

showWindow()

} else {

if (!Utils.isNull(floatRootView)) {

if (!Utils.isNull(floatRootView?.windowToken)) {

if (!Utils.isNull(windowManager)) {

windowManager?.removeView(floatRootView)

}

}

}

}

})

}

}

@SuppressLint("ClickableViewAccessibility")

private fun showWindow() {

windowManager = getSystemService(WINDOW_SERVICE) as WindowManager

val outMetrics = DisplayMetrics()

windowManager.defaultDisplay.getMetrics(outMetrics)

var layoutParam = WindowManager.LayoutParams().apply {

type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY

} else {

WindowManager.LayoutParams.TYPE_PHONE

}

format = PixelFormat.RGBA_8888

flags =

WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE

//位置大小设置

width = WRAP_CONTENT

height = WRAP_CONTENT

gravity = Gravity.LEFT or Gravity.TOP

//设置剧中屏幕显示

x = outMetrics.widthPixels / 2 - width / 2

y = outMetrics.heightPixels / 2 - height / 2

}

// 新建悬浮窗控件

floatRootView = LayoutInflater.from(this).inflate(R.layout.activity_float_item, null)

floatRootView?.setOnTouchListener(ItemViewTouchListener(layoutParam, windowManager))

// 将悬浮窗控件添加到WindowManager

windowManager.addView(floatRootView, layoutParam)

}

}

ViewModleMain代码如下

package com.lmy.suspendedwindow.utils

import androidx.lifecycle.AndroidViewModel

import androidx.lifecycle.MutableLiveData

import androidx.lifecycle.ViewModel

/**

* @功能: 用于和Service通信

* @User Lmy

* @Creat 4/16/21 8:37 AM

* @Compony 永远相信美好的事情即将发生

*/

object ViewModleMain : ViewModel() {

//悬浮窗口创建 移除 基于无障碍服务

var isShowWindow = MutableLiveData()

//悬浮窗口创建 移除

var isShowSuspendWindow = MutableLiveData()

//悬浮窗口显示 隐藏

var isVisible = MutableLiveData()

}

Utils代码如下:

package com.lmy.suspendedwindow.utils

import android.app.Activity

import android.app.ActivityManager

import android.content.Context

import android.content.Intent

import android.net.Uri

import android.os.Build

import android.provider.Settings

import android.text.TextUtils

import android.util.Log

import android.widget.Toast

import com.lmy.suspendedwindow.service.WorkAccessibilityService

import java.util.*

/**

* @功能: 工具类

* @User Lmy

* @Creat 4/16/21 8:33 AM

* @Compony 永远相信美好的事情即将发生

*/

object Utils {

const val REQUEST_FLOAT_CODE=1001

/**

* 跳转到设置页面申请打开无障碍辅助功能

*/

private fun accessibilityToSettingPage(context: Context) {

//开启辅助功能页面

try {

val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)

intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK

context.startActivity(intent)

} catch (e: Exception) {

val intent = Intent(Settings.ACTION_SETTINGS)

intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK

context.startActivity(intent)

e.printStackTrace()

}

}

/**

* 判断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

for (i in runningService.indices) {

if (runningService[i].service.className == ServiceName) {

return true

}

}

return false

}

/**

* 判断悬浮窗权限权限

*/

private 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) {

Log.e("ServiceUtils", Log.getStackTraceString(e))

}

}

return result

}

/**

* 检查悬浮窗权限是否开启

*/

fun checkSuspendedWindowPermission(context: Activity, block: () -> Unit) {

if (commonROMPermissionCheck(cont ext)) {

block()

} else {

Toast.makeText(context, "请开启悬浮窗权限", Toast.LENGTH_SHORT).show()

context.startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION).apply {

data = Uri.parse("package:${context.packageName}")

}, REQUEST_FLOAT_CODE)

}

}

/**

* 检查无障碍服务权限是否开启

*/

fun checkAccessibilityPermission(context: Activity, block: () -> Unit) {

if (isServiceRunning(context, WorkAccessibilityService::class.java.canonicalName)) {

block()

} else {

accessibilityToSettingPage(context)

}

}

fun isNull(any: Any?): Boolean = any == null

}

效果

悬浮窗权限的适配

权限配置和请求

这一块倒是没什么坑

在当Android7.0以上的时候,需要在AndroidManefest.xml文件中声明SYSTEM_ALERT_WINDOW权限

LayoutParam的坑!!!!

WindowManager的addView方法有两个参数,一个是需要加入的控件对象,另一个参数是WindowManager.LayoutParam对象。

LayoutParam里的type变量。有大坑!!!!!!,这个变量是用来指定窗口类型的。在设置这个变量时,需要对不同版本的Android系统进行适配。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;

} else {

layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;

}

在Android 8.0之前,悬浮窗口设置可以为TYPE_PHONE,这种类型是用于提供用户交互操作的非应用窗口。

但是Android 8.0以上版本你继续使用TYPE_PHONE类型的悬浮窗口,则会出现如下异常信息:

android.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRootImpl$W@f8ec928 -- permission denied for window type 2002

Android 8.0以后不允许使用一下窗口类型来在其他应用和窗口上方显示提醒窗口,这些类型包括:

TYPE_PHONE

TYPE_PRIORITY_PHONE

TYPE_SYSTEM_ALERT

TYPE_SYSTEM_OVERLAY

TYPE_SYSTEM_ERROR

如果需要实现在其他应用和窗口上方显示提醒窗口,那么必须该为TYPE_APPLICATION_OVERLAY的类型。

但是这个TYPE_APPLICATION_OVERLAY类型无法在所有界面上进行显示

就像是这样

有的同学会问这什么鬼操作啊?这怎么解决

不要慌 只要耐心找总会找到的答案的 百度不行那就谷歌

经过不懈的努力终于找到了解决办法

使用另外一个类型TYPE_ACCESSIBILITY_OVERLAY就可以解决此问题

但是当你开开心心使用的时候你会发现以下错误

经过查证一些资料之后发现这个类型必须和无障碍 AccessibilityService搭配使用

无障碍悬浮窗

配置无障碍流程见我另一篇博客基于无障碍服务实现自动跳过APP启动页广告

无障碍悬浮窗实现流程

配置无障碍服务

在AccessibilityService中获取WindowManager

创建悬浮View

设置悬浮View的拖拽事件

添加View到WindowManager

启动悬浮窗:

Utils.checkAccessibilityPermission(this) {

ViewModleMain.isShowWindow.postValue(true)

}

WorkAccessibilityService代码如下

package com.lmy.suspendedwindow.service

import android.accessibilityservice.AccessibilityService

import android.annotation.SuppressLint

import android.content.Intent

import android.graphics.PixelFormat

import android.os.Build

import android.util.DisplayMetrics

import android.view.*

import android.view.accessibility.AccessibilityEvent

import androidx.lifecycle.Lifecycle

import androidx.lifecycle.LifecycleOwner

import androidx.lifecycle.LifecycleRegistry

import com.lmy.suspendedwindow.R

import com.lmy.suspendedwindow.utils.ItemViewTouchListener

import com.lmy.suspendedwindow.utils.Utils.isNull

import com.lmy.suspendedwindow.utils.ViewModleMain

/**

* @功能:利用无障碍打开悬浮窗口 无局限性 任何界面可以显示

* @User Lmy

* @Creat 4/15/21 5:57 PM

* @Compony 永远相信美好的事情即将发生

*/

class WorkAccessibilityService : AccessibilityService(), LifecycleOwner {

private lateinit var windowManager: WindowManager

private var floatRootView: View? = null//悬浮窗View

private val mLifecycleRegistry = LifecycleRegistry(this)

override fun onCreate() {

super.onCreate()

mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);

initObserve()

}

/**

* 打开关闭的订阅

*/

private fun initObserve() {

ViewModleMain.isShowWindow.observe(this, {

if (it) {

showWindow()

} else {

if (!isNull(floatRootView)) {

if (!isNull(floatRootView?.windowToken)) {

if (!isNull(windowManager)) {

windowManager?.removeView(floatRootView)

}

}

}

}

})

}

@SuppressLint("ClickableViewAccessibility")

private fun showWindow() {

// 设置LayoutParam

// 获取WindowManager服务

windowManager = getSystemService(WINDOW_SERVICE) as WindowManager

val outMetrics = DisplayMetrics()

windowManager.defaultDisplay.getMetrics(outMetrics)

var layoutParam = WindowManager.LayoutParams()

layoutParam.apply {

//显示的位置

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY

//刘海屏延伸到刘海里面

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {

layoutInDisplayCutoutMode =

WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES

}

} else {

type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT

}

flags =

WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE

width = WindowManager.LayoutParams.WRAP_CONTENT

height = WindowManager.LayoutParams.WRAP_CONTENT

format = PixelFormat.TRANSPARENT

}

floatRootView = LayoutInflater.from(this).inflate(R.layout.activity_float_item, null)

floatRootView?.setOnTouchListener(ItemViewTouchListener(layoutParam, windowManager))

windowManager.addView(floatRootView, layoutParam)

}

override fun onServiceConnected() {

super.onServiceConnected()

mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)

}

override fun getLifecycle(): Lifecycle = mLifecycleRegistry

override fun onStart(intent: Intent?, startId: Int) {

super.onStart(intent, startId)

mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)

}

override fun onUnbind(intent: Intent?): Boolean {

mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)

return super.onUnbind(intent)

}

override fun onDestroy() {

mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)

super.onDestroy()

}

override fun onAccessibilityEvent(event: AccessibilityEvent?) {

}

override fun onInterrupt() {

}

}

总结

使用普通的Service创建悬浮窗无法做到任何界面都能显示

利用无障碍服务可以做到任何界面悬浮