阅读 931

Android悬浮窗看这篇就够了

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

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

老规矩先上源码链接gitee.com/AndroidLMY/…

效果图

悬浮窗的基本原理

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

动态添加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

在清单文件添加权限

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW" />
复制代码

上代码:

打开悬浮窗

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<Boolean>()
    //悬浮窗口创建 移除

    var isShowSuspendWindow = MutableLiveData<Boolean>()

    //悬浮窗口显示 隐藏
    var isVisible = MutableLiveData<Boolean>()

}
复制代码

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<ActivityManager.RunningServiceInfo>
        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权限

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.SYSTEM_OVERLAY_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创建悬浮窗无法做到任何界面都能显示

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

文章分类
Android
文章标签