Android 悬浮View实现-WindowManger

2,967 阅读7分钟

一、WindowManger addView 添加窗口视图

1.1、步骤:

  • 创建悬浮View实例,设置View的点击,触摸等等事件
  • 创建WindowManager.LayoutParams实例
  • 获取WindowManager实例对象,调用addView方法将视图添加到窗口中

代码示例:

//1.创建View实例
val imageView = ImageView(context)
imageView.setImageResource(R.drawable.icon_joker_doge)

//2.创建WindowManager.LayoutParams实例
val lp = WindowManager.LayoutParams().apply {
    width = 200
    height = 200
		//不指定,默认值为TYPE_APPLICATION
    type = WindowManager.LayoutParams.TYPE_APPLICATION
    flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
}

//3.获取WindowManager实例对象,调用addView方法将视图添加到窗口中
requireActivity().windowManager.addView(imageView, lp)

上述代码即可添加一个悬浮View到Window,有两个参数比较重要typeflags,下面分别说明下这两个参数

1.2、flags标识

多选项,常用的几个如下:

flag说明
FLAG_NOT_FOCUSABLE表示 Window 不需要获取焦点,也不需要各种输入事件,此标记通同时启用 FLAG_NOT_TOUCH_MODAL。最终事件会直接传递给下层具有焦点的 Window。
FLAG_NOT_TOUCH_MODAL将 Window 区域以外的单击事件传递给底层的 Window,当前 Window 内的单击事件自己处理, 一般都要开启此事件,否则其他 Window 无法收到单击事件
FLAG_SHOW_WHEN_LOCKED可以将 Window 显示在锁屏的界面上
FLAG_TURN_SCREEN_ONWindow 显示时将屏幕点亮

其他Flag含义:WindowManager.LayoutParams的各种flag含义

1.3、TYPE窗口属性

type参数表示Window的类型,共有三种类型

  • 应用窗口(Application Window)

    取值范围 1~99

    常量常量值说明
    FIRST_APPLICATION_WINDOW1应用窗口类型从1开始
    TYPE_BASE_APPLICATION1基础窗口,所有其他类型的应用窗口将出现在它的顶部;activity会使用该类型
    TYPE_APPLICATION2普通应用窗口;dialog会使用此类型
    TYPE_APPLICATION_STARTING3应用程序启动时显示的特殊应用程序窗口。不供应用程序本身使用;系统使用它来显示某些内容,直到应用程序可以显示自己的窗口
    TYPE_DRAWN_APPLICATION4TYPE_APPLICATION 的变体,确保窗口管理器将在显示应用程序之前等待绘制此窗口
    LAST_APPLICATION_WINDOW99应用窗口类型到99结束

    正常情况下我们自定义应用级别的窗口使用TYPE_APPLICATION 即可,其他类型基本用不到,当然我们也可以指定Type为其他数值,实现效果一样

  • 子窗口(Sub Window)

    取值范围:1000~1999

    说明:必须依附在其他窗口上,比如PopWindow必须依附Activity

    常量常量值说明
    FIRST_SUB_WINDOW1000子窗口类型从1000开始
    TYPE_APPLICATION_PANEL1000应用程序窗口顶部的面板。显示在其附加窗口的顶部
    LAST_SUB_WINDOW1999结束
  • 系统窗口(System Window)

    取值范围:2000~2999

    说明:需要声明权限才能创建的窗口,比如Toast,系统状态栏,软键盘等等都是系统窗口

    常量常量值说明
    FIRST_SYSTEM_WINDOW2000系统窗口类型从2000开始
    TYPE_STATUS_BAR2000状态栏窗口只能有一个;它位于屏幕的顶部,所有其他窗口都向下移动,因此它们位于它的下方。在多用户系统中显示在所有用户的窗口上。
    …其他
    LAST_SYSTEM_WINDOW2999结束

详细的各个窗口类型值说明可以查看这篇文章:Android悬浮窗级别

总结

  • 因为官方暴露给我们的方法只能通过WindowManger addView 的方式去新增展示视图,因此对我们应用层开发来讲不管设置应用窗口类型,还是子窗口类型,最终呈现的效果是没有区别的。最终结果都是往已经存在的窗口基础上去新增一个窗口,例如使用ActivityWindowManger去添加,只能在当前Activity中展示。

  • 特别注意的是所有上述窗口类型需要在Activity finish时候调用removeViewImmediate方法移除,否则会抛出android.view.WindowLeaked异常。这和DialogPopWindow是一样的,需要在父窗口销毁先调用dismiss方法移除

  • 如果仅仅是想在某个Acitivity中增加一个悬浮View(大部分需求是这样),直接在对应的Activity的相关视图上添加悬浮子视图View即可,并不需要使用到WindowManger addView的方式。所有该方式比较多的应用场景是添加应用层级或者系统层级的视图,这就需要用的系统窗口类型了 具体使用如下:二、系统层级悬浮View(系统窗口)

二、系统层级悬浮View(系统窗口)

2.1、步骤:

a、声明权限和申请悬浮窗权限

  • manifest文件声明权限

    <!-- 显示系统窗口权限 -->
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    
    <!-- 在 屏幕最顶部显示addview-->
    <uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW" /
    

    Android 8.0以上需要使用TYPE_APPLICATION_OVERLAY 的新窗口类型,需要配置SYSTEM_OVERLAY_WINDOW 权限。具体查看Android 8.0窗口变更

  • 打开权限设置页

    private val launcher = registerForActivityResult(
    		ActivityResultContracts.StartActivityForResult()) { result ->
            if (result.resultCode == Activity.RESULT_OK) {
                showFloatView(x, y)
            }
        }
    
    private fun checkWindowPermission(): Boolean {
            return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                if (Settings.canDrawOverlays(this.applicationContext)) {
                    true
                } else {
                    val intent = Intent(ACTION_MANAGE_OVERLAY_PERMISSION);
                    intent.data = Uri.parse("package:$packageName")
    		            //打开悬浮窗权限设置页
                    launcher.launch(intent)
                    false
                }
            } else {
                true
            }
        }
    

b、addView 展示悬浮View

//...省略...
binding.showFloatView.setOnClickListener {
       if (checkWindowPermission()) {
           showFloatView(0, 0)
       }
}

private fun showFloatView(xo: Int, yo: Int) {
		//1.创建View实例
    val imageView = ImageView(this.applicationContext)
    imageView.setImageResource(R.drawable.icon_joker_doge)
    //2.创建WindowManager.LayoutParams实例
    val lp = WindowManager.LayoutParams().apply {
				width = 250
        height = 250
        type = if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.O) {
            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
				} else {
            WindowManager.LayoutParams.TYPE_PHONE
				}
        flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or 
								WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
				x = xo
        y = yo
		}
		//3.获取WindowManager实例对象,调用addView方法将视图添加到窗口中
		val windowManager = applicationContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager
		//这里直接使用Activity的windowManager据说在Android6.0上有问题(关闭界面后不展示?),
    //这里我没有试过,有低版本手机的同学可以试一下
    imageView.setOnTouchListener(ItemViewTouchListener(lp,windowManager))
		windowManager.addView(imageView, lp)
}

说明:关于窗口类型type

Android8.0(包含)以上: 必须新系统窗口类型TYPE_APPLICATION_OVERLAYTYPE_SYSTEM_ALERTTYPE_PHONE 等等类型即使配置弹窗权限也无法展示,会抛出permission denied for window type xxxx异常

Android8.0以下:选一个使用即可,一般用TYPE_PHONE类型:用于提供用户交互操作的非应用窗口;或者TYPE_SYSTEM_ALERT类型:警告类型窗口。

具体查看Android 8.0窗口变更

c、效果

ccaa9961-eeeb-4f1c-8e09-fbf94fccccce.gif

d、问题在部分设置界面上无法展示

如下:

9169d64c-44af-4faf-8c6f-8ad79d8f1584.gif

测试了一下只有在某些系统的设置页面才会被屏蔽,一般情况下上述的弹窗功能就足够了。如果一定要在这些界面也能展示,可以使用TYPE_ACCESSIBILITY_OVERLAY 类型弹窗,需要结合无障碍服务一起使用。

无障碍功能

//context传入无障碍服务的Context
fun showFloatView(context: Context, xo: Int = 0, yo: Int = 0) {
            //1.创建View实例
            val imageView = ImageView(context)
            imageView.setImageResource(R.drawable.icon_joker_doge)
            //2.创建WindowManager.LayoutParams实例
            val lp = WindowManager.LayoutParams().apply {
                width = 250
                height = 250
                //刘海屏延伸到刘海里
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                    layoutInDisplayCutoutMode =
                        WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
                }
                type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY
                } else {
                    WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
                }
                flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                x = xo
                y = yo
            }
            //3.获取WindowManager实例对象,调用addView方法将视图添加到窗口中
            val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
            imageView.setOnTouchListener(ItemViewTouchListener(lp, windowManager))
            windowManager.addView(imageView, lp)
        }
}

//开启服务,然后展示悬浮窗口,省略其他代码...
if (checkWindowPermission()) {
    val service = MyAccessibilityService.service
    if (service != null) {
        MyAccessibilityService.showFloatView(service)
       } else {
	         startAccessibilityActivity()
       }
}

//打开无障碍服务设置界面
private fun startAccessibilityActivity() {

   try {
       val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
       intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
       startActivity(intent)
        } catch (e: Exception) {
       startActivity(Intent(Settings.ACTION_SETTINGS))
       }
    }

效果:

63af3d79-01a3-4374-80fb-68d30b10ee56.gif

可以看到使用TYPE_ACCESSIBILITY_OVERLAY 后可以在之前不能展示的界面展示了

总结:

  • 使用Activity或普通Service+TYPE_APPLICATION_OVERLAY 类型弹窗可以在大部分页面展示悬浮弹窗,可以满足绝大部分需求
  • 如果要在所有界面展示悬浮弹窗,可以使用无障碍Service+TYPE_ACCESSIBILITY_OVERLAY 类型弹窗的方式实现悬浮弹窗

三、Window窗口和android.view.Window类

问题:为什么上面没有创建PhoneWindow对象,最终也能添加一个窗口,PhoneWindow不是Window类的唯一实现吗?

我的理解

WindowManger 管理器的addView 最终会调用ViewRootImpl实例的setView方法

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView, int userId) {
			res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
											getHostVisibility(), mDisplay.getDisplayId(), userId,
                      mInsetsController.getRequestedVisibilities(), 
                      inputChannel, mTempInsets,mTempControls);
}

mWindowSession 通过IPC跨进程通信最终会调用WindowManagerServiceaddWindow 方法去显示窗口视图,展示到屏幕上。这里addWindow就是我们常说的去新增一个窗口,最终展示开始窗口里面的视图,可以看到我们其实并没有创建一个真正的android.view.Window类对象。

可以怎么理解:Window 是一个抽象概念,每一个 Window都对应着一个 View 和一个 ViewRootImplWIndowView 通过 ViewRootImpl 建立联系,因此 WindowView 的形式存在,View 才是 Window 存在的实体。这里说的Window 窗口和android.view.Window类并不是一个东西

那么android.view.Window类以及它的子类PhoneWindow是做什么的?

Abstract base class for a top-level window look and behavior policy. An instance of this class should be used as the top-level view added to the window manager. It provides standard UI policies such as a background, title area, default key processing, etc

官方注释对该类的解释是:顶级窗口外观和行为策略的抽象基类。此类的实例应该用作添加到窗口管理器的顶级视图。它提供标准的 UI 策略,例如背景、标题区域、默认键处理等。它是去管理添加到窗口管理器的顶级视图,和最终我们展示在屏幕中的窗口不是一个概念。

参考文章

官方文档:

博客: