一、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,有两个参数比较重要type
和flags
,下面分别说明下这两个参数
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_ON | Window 显示时将屏幕点亮 |
其他Flag含义:WindowManager.LayoutParams的各种flag含义
1.3、TYPE窗口属性
type参数表示Window的类型,共有三种类型
-
应用窗口(Application Window)
取值范围 1~99
常量 常量值 说明 FIRST_APPLICATION_WINDOW 1 应用窗口类型从1开始 TYPE_BASE_APPLICATION 1 基础窗口,所有其他类型的应用窗口将出现在它的顶部;activity会使用该类型 TYPE_APPLICATION 2 普通应用窗口;dialog会使用此类型 TYPE_APPLICATION_STARTING 3 应用程序启动时显示的特殊应用程序窗口。不供应用程序本身使用;系统使用它来显示某些内容,直到应用程序可以显示自己的窗口 TYPE_DRAWN_APPLICATION 4 TYPE_APPLICATION 的变体,确保窗口管理器将在显示应用程序之前等待绘制此窗口 LAST_APPLICATION_WINDOW 99 应用窗口类型到99结束 正常情况下我们自定义应用级别的窗口使用
TYPE_APPLICATION
即可,其他类型基本用不到,当然我们也可以指定Type为其他数值,实现效果一样 -
子窗口(Sub Window)
取值范围:1000~1999
说明:必须依附在其他窗口上,比如PopWindow必须依附Activity
常量 常量值 说明 FIRST_SUB_WINDOW 1000 子窗口类型从1000开始 TYPE_APPLICATION_PANEL 1000 应用程序窗口顶部的面板。显示在其附加窗口的顶部 … … … LAST_SUB_WINDOW 1999 结束 -
系统窗口(System Window)
取值范围:2000~2999
说明:需要声明权限才能创建的窗口,比如Toast,系统状态栏,软键盘等等都是系统窗口
常量 常量值 说明 FIRST_SYSTEM_WINDOW 2000 系统窗口类型从2000开始 TYPE_STATUS_BAR 2000 状态栏窗口只能有一个;它位于屏幕的顶部,所有其他窗口都向下移动,因此它们位于它的下方。在多用户系统中显示在所有用户的窗口上。 … … …其他 LAST_SYSTEM_WINDOW 2999 结束
详细的各个窗口类型值说明可以查看这篇文章:Android悬浮窗级别
总结:
-
因为官方暴露给我们的方法只能通过
WindowManger
addView
的方式去新增展示视图,因此对我们应用层开发来讲不管设置应用窗口类型,还是子窗口类型,最终呈现的效果是没有区别的。最终结果都是往已经存在的窗口基础上去新增一个窗口,例如使用Activity
的WindowManger
去添加,只能在当前Activity
中展示。 -
特别注意的是所有上述窗口类型需要在
Activity
finish
时候调用removeViewImmediate
方法移除,否则会抛出android.view.WindowLeaked
异常。这和Dialog
和PopWindow
是一样的,需要在父窗口销毁先调用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_OVERLAY
。TYPE_SYSTEM_ALERT
、TYPE_PHONE
等等类型即使配置弹窗权限也无法展示,会抛出permission denied for window type xxxx
异常
Android8.0以下:选一个使用即可,一般用TYPE_PHONE
类型:用于提供用户交互操作的非应用窗口;或者TYPE_SYSTEM_ALERT
类型:警告类型窗口。
具体查看Android 8.0窗口变更
c、效果
d、问题在部分设置界面上无法展示
如下:
测试了一下只有在某些系统的设置页面才会被屏蔽,一般情况下上述的弹窗功能就足够了。如果一定要在这些界面也能展示,可以使用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))
}
}
效果:
可以看到使用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跨进程通信最终会调用WindowManagerService
的addWindow
方法去显示窗口视图,展示到屏幕上。这里addWindow就是我们常说的去新增一个窗口,最终展示开始窗口里面的视图,可以看到我们其实并没有创建一个真正的android.view.Window
类对象。
可以怎么理解:Window
是一个抽象概念,每一个 Window
都对应着一个 View
和一个 ViewRootImpl
,WIndow
和 View
通过 ViewRootImpl
建立联系,因此 Window
以 View
的形式存在,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 策略,例如背景、标题区域、默认键处理等。它是去管理添加到窗口管理器的顶级视图,和最终我们展示在屏幕中的窗口不是一个概念。
参考文章
官方文档:
博客: