Android悬浮窗看这篇就够了

    目录

    悬浮窗的基本原理

    动态添加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创建悬浮窗无法做到任何界面都能显示

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