目录
1 前言
1.1 准备知识
1.2 问题概述
2 解决方案
3 代码部分
3.1 动态更新窗口焦点
3.2 窗口监听返回事件
3.3 判断焦点是否在窗口内部
3.4 窗口监听焦点移入/移出
1 前言
1.1 准备知识
1)开发环境:
- 2D开发环境:所有界面或弹窗都在主界面显示;
- 3D开发环境:保留原生Android的主界面,在主界面之外绘制各种窗口,配合3D渲染以实现3D效果。
2)焦点:就是Hover点、中央注视点、可与用户交互的点。
3)窗口:就是系统弹窗,内部有addView,本文窗口监听即View监听。
4)事件分发:正常Android设备使用如下3种,本文采用的第3种setOnHoverListener获取事件。
- setOnTouchListener(MotionEvent::InputEvent):手机、平板、车载等屏幕可触控的2D设备;
- setOnKeyListener(KeyEvent::InputEvent):电视、投影仪等屏幕不可触控的2D设备;
- setOnHoverListener(MotionEvent::InputEvent):AR眼镜等增强现实设备。
5)Hover事件分发:当前View在焦点移出(不再是Hover状态)时,不会立即发送ACTION_HOVER_EXIT退出事件,需要等到下一个View获取到ACTION_HOVER_ENTER状态时才会发送上一个View的ACTION_HOVER_EXIT退出事件。
6)窗口内部View的Hover事件转化过程:
- RootView会先获取到ACTION_HOVER_ENTER事件;
- 当进入ChildView时,ChildView会先获取到ACTION_HOVER_ENTER事件,然后RootView会获取到ACTION_HOVER_EXIT事件;
- 当从ChildView退出时,ChildView会先获取到ACTION_HOVER_EXIT事件,然后RootView会获取到ACTION_HOVER_ENTER事件。
1.2 问题概述
问题描述:在Android悬浮弹窗上双击返回,主界面响应返回事件。
问题原因:悬浮弹窗设置了flag为窗口不可获取焦点即:WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE。
问题分析:
- 悬浮弹窗设置flag为窗口不可获取焦点,是为了不影响主界面的焦点响应(Android默认主界面的窗口是获取焦点的);
- 如果悬浮弹窗设置flag可获取焦点,那么Android的事件分发是无法发送到主界面的,会将事件分发给当前可获取焦点的悬浮窗口;
- 如下图,左侧图1为悬浮窗口,右侧图2为主界面某应用打开一个Activity。图1悬浮窗口是常驻于图2主界面的左侧,且默认不可获取焦点,但在特请情况时可获取焦点(如展开键盘、焦点在此悬浮窗口内部)。
解决方案:当焦点在悬浮窗口内部时,设置窗口flag可获取焦点;当焦点不在悬浮窗口内部时,设置窗口flag不可获取焦点。
2 解决方案
方案主要分为如下几步:
- 窗口默认不可获取焦点;
- 窗口监听焦点的移入/移出事件;
- 窗口监听到焦点移入,判断窗口是否可获取焦点,否——设置窗口可获取焦点,是——不做任何操作;
- 窗口监听到焦点移出,判断焦点是否在窗口内部,否——设置窗口不可获取焦点,是——不做任何操作;
读者可思考如下2个问题,
1)问题1:为什么在窗口监听到焦点移入后,要再判断窗口是否可获取焦点?
2)问题2:为什么在窗口监听到焦点移出后,要再判断焦点是否在窗口内部?
相信本文《1.1 准备知识的第6部分》可以给你一些灵感。
3 代码部分
3.1 动态更新窗口焦点
核心API:
- WindowManager.updateViewLayout;
- WindowManager.LayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
private fun updateNotificationParams(focusable: Boolean) {
initLayoutParams(focusable)
mUiHandler.post {
synchronized(this) {
if (mIsBarWindowAdded) {
try {
mWindowManager.updateViewLayout(mNotificationBar, mLayoutParams)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
}
private fun initLayoutParams(focusable: Boolean) {
mLayoutParams = WindowManager.LayoutParams().apply {
type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
val density = mContext.resources.displayMetrics.density
width = (640 * density).toInt()
height = (640 * density).toInt()
flags =
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
if (!focusable) {
flags = flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
}
format = PixelFormat.RGBA_8888 // 去除默认时有的黑色背景,设置为全透明
gravity = Gravity.TOP or Gravity.START
title = MB_SYSUI_NOTIFICATION
x = (680 * density).toInt() // adb shell wm size 1920x1280
y = 0
setTranslationZ(TRANSLATION_Z_200CM)
setRotationYAroundOrigin(22.0f)
}
}
3.2 窗口监听返回事件
在自定义View中重写dispatchKeyEvent方法,监听keyCode == KeyEvent.KEYCODE_BACK事件即可。
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (event.keyCode == KeyEvent.KEYCODE_BACK) {
Log.i(TAG, "dispatchKeyEvent: KEYCODE_BACK")
LiveDataBus.get().with(Constants.NOTIFICATION_EVENT_BUS_CLOSE_FOLD_PAGE).value = true
}
return super.dispatchKeyEvent(event)
}
3.3 判断焦点是否在窗口内部
mRootView.post {
val locationXY = IntArray(2)
mRootView.getLocationOnScreen(locationXY)
val locationX = locationXY[0]
val locationY = locationXY[1]
val measuredWidth = mRootView.measuredWidth
val measuredHeight = mRootView.measuredHeight
}
/**
* 焦点:就是Hover点、中央注视点、可与用户交互的点。
*
* if (rawX < locationX || rawX > locationX + measuredWidth || rawY < locationY || rawY > locationY + measuredHeight) {
* // 焦点不在View内部
* Log.i(TAG, "isViewNotFocus: 焦点不在View内部")
* } else {
* // 焦点在View内部
* Log.i(TAG, "isViewNotFocus: 焦点在View内部")
* }
*
* @param locationX View相对于屏幕位置X
* @param locationY View相对于屏幕位置Y
* @param measuredWidth View宽
* @param measuredHeight View高
* @param rawX 焦点相对于屏幕位置X
* @param rawY 焦点相对于屏幕位置Y
*
* @return 焦点是否未在View内部
*/
private fun isViewNotFocus(
locationX: Int,
locationY: Int,
measuredWidth: Int,
measuredHeight: Int,
rawX: Float,
rawY: Float
): Boolean {
val density = context.resources.displayMetrics.density
return rawX <= locationX + 50 * density || rawX >= locationX + measuredWidth - 100 * density || rawY <= locationY + 15 * density || rawY >= locationY + measuredHeight - 60 * density
}
3.4 窗口监听焦点移入/移出
// 注:Focus移出时需要包含边界。
mRootView.setOnHoverListener { v, event ->
when (event.action) {
MotionEvent.ACTION_HOVER_ENTER -> {
Log.i(
TAG,
"OnHoverListener: 进入, action = ${event.action},motionX = ${event.rawX},motionY = ${event.rawY}"
)
LiveDataBus.get().with(NOTIFICATION_EVENT_BUS_FOCUSABLE).value?.let {
if (!(it as Boolean)) {
Log.i(TAG, "OnHoverListener: 进入, focus-true-0000")
LiveDataBus.get().with(NOTIFICATION_EVENT_BUS_FOCUSABLE).value =
true
}
} ?: let {
Log.i(TAG, "OnHoverListener: 进入, focus-true-1111")
LiveDataBus.get().with(NOTIFICATION_EVENT_BUS_FOCUSABLE).value = true
}
}
MotionEvent.ACTION_HOVER_MOVE -> {
}
MotionEvent.ACTION_HOVER_EXIT -> {
Log.i(
TAG,
"OnHoverListener: 退出, action = ${event.action},motionX = ${event.rawX},motionY = ${event.rawY}"
)
if (isViewNotFocus(
locationX,
locationY,
measuredWidth,
measuredHeight,
event.rawX,
event.rawY
)
) {
Log.i(TAG, "OnHoverListener: 退出, focus-false")
LiveDataBus.get().with(NOTIFICATION_EVENT_BUS_FOCUSABLE).value = false
}
}
}
false
}