如何处理Android悬浮弹窗双击返回事件?
目录
1 前言
1.1 准备知识
1.2 问题概述
2 解决方案
3 代码部分
3.1 动态更新窗口焦点
3.2 窗口监听返回事件
3.3 判断焦点是否在窗口内部
3.4 窗口监听焦点移入/移出
4 注意事项
4.1 窗口范围
4.2 空隙处的返回事件处理
1 前言
1.1 准备知识
1)开发环境:
- 2D开发环境:所有界面或窗口都在主界面显示;
- 3D开发环境:保留原生Android的主界面,在主界面之外绘制各种窗口,配合3D渲染以实现3D效果。
2)焦点:就是Hover点、中央注视点、可与用户交互的点。
3)窗口:就是系统窗口、悬浮弹窗,内部通过addView方法去添加View,本文窗口监听指的就是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 准备知识的Hover事件分发部分》可以给你一些灵感。
3 代码部分
3.1 动态更新窗口焦点
核心API:
- WindowManager.updateViewLayout;
- WindowManager.LayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
private fun initLiveDataBus() {LiveDataBus.get().with(Constants.NOTIFICATION_EVENT_BUS_FOCUSABLE, Boolean::class.java).observeForever { focusable: Boolean ->Log.d(TAG, "onChanged: $focusable")updateNotificationParams(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_OVERLAYval density = mContext.resources.displayMetrics.densitywidth = (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_LIMITSif (!focusable) {flags = flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE}format = PixelFormat.RGBA_8888 // 去除默认时有的黑色背景,设置为全透明gravity = Gravity.TOP or Gravity.STARTtitle = SYSUI_NOTIFICATIONx = -(640 * density).toInt()y = 0}}
3.2 窗口监听返回事件
窗口设置可获得焦点后,内部View会获取到事件分发的事件,在此View中重写dispatchKeyEvent方法,监听keyCode == KeyEvent.KEYCODE_BACK事件,就可对返回事件进行处理。
override fun dispatchKeyEvent(event: KeyEvent): Boolean {if (event.keyCode == KeyEvent.KEYCODE_BACK) {Log.i(TAG, "dispatchKeyEvent: KEYCODE_BACK")// 窗口设置可获得焦点后,内部View会获取到事件分发的事件,并可对返回事件进行处理}return super.dispatchKeyEvent(event)}
3.3 判断焦点是否在窗口内部
通过View相对于屏幕位置X/Y、以及View宽高,共同确定View的边界。
mRootView.post {val locationXY = IntArray(2)mRootView.getLocationOnScreen(locationXY)val locationX = locationXY[0]val locationY = locationXY[1]val measuredWidth = mRootView.measuredWidthval measuredHeight = mRootView.measuredHeight}/*** 焦点:就是Hover点、中央注视点、可与用户交互的点。** @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) =if (rawX <= locationX || rawX >= locationX + measuredWidth || rawY <= locationY || rawY >= locationY + measuredHeight) {// 焦点不在View内部Log.i(TAG, "isViewNotFocus: 焦点不在View内部")true} else {// 焦点在View内部Log.i(TAG, "isViewNotFocus: 焦点在View内部")false}
3.4 窗口监听焦点移入/移出
- 窗口监听到焦点移入,判断窗口是否可获取焦点,否——设置窗口可获取焦点,是——不做任何操作;
- 窗口监听到焦点移出,判断焦点是否在窗口内部,否——设置窗口不可获取焦点,是——不做任何操作;
- 最后,通过发送NOTIFICATION_EVENT_BUS_FOCUSABLE事件,进而设置窗口的是否可获取焦点。
// 注: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-again")LiveDataBus.get().with(NOTIFICATION_EVENT_BUS_FOCUSABLE).value =true}} ?: let {Log.i(TAG, "OnHoverListener: 进入, focus-true-init")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}
4 注意事项
4.1 窗口范围
在判断焦点是否在窗口内部时,需要确认窗口范围,如果窗口内部的View有设置Padding或Margin,应该将其去掉。
如:本文的窗口大小是640*640,但View大小是540*580,所以计算时需要去掉相应Padding或Margin,重写isViewNotFocus()方法如下:
private fun isViewNotFocus(locationX: Int,locationY: Int,measuredWidth: Int,measuredHeight: Int,rawX: Float,rawY: Float): Boolean {val density = context.resources.displayMetrics.densityreturn rawX <= locationX + 50 * density || rawX >= locationX + measuredWidth - 100 * density || rawY <= locationY + 15 * density || rawY >= locationY + measuredHeight - 60 * density}
4.2 空隙处的返回事件处理
1)从窗口移出到空隙处
通过本文1.1准备知识的第5部分《Hover事件分发》,我们知道,从窗口移出但还未有下一个View获取焦点时,此时窗口还是会接收到返回事件。
2)从View移出到空隙处
从当前View移出但还未有下一个View获取焦点时,此时当前View还是会接收到返回事件。
那么,如何处理这种空隙处的返回事件呢?
核心:从系统层拦截此种情况下的返回事件 。
- 渲染层:提供接口,返回焦点移入移出时当前layer的名称,是否有碰撞窗口等信息;
- 系统层:当没有碰撞窗口时,从系统层拦截掉返回事件的分发;
- 应用层:监听焦点移入移出,改变窗口focus属性,并处理返回事件;
解决方案:
当空隙处有返回事件产生时,系统层通过渲染层的接口,获取到当前焦点所在位置的layer名称,如果layer名称为空则断定为空隙处,直接做拦截处理,不再往应用层分发。
注:每个窗口、Activity在其Window中,都有设置其title属性,layer名称就是此title属性的值。
目录
1 前言
1.1 准备知识
1.2 问题概述
2 解决方案
3 代码部分
3.1 动态更新窗口焦点
3.2 窗口监听返回事件
3.3 判断焦点是否在窗口内部
3.4 窗口监听焦点移入/移出
4 注意事项
4.1 窗口范围
4.2 空隙处的返回事件处理
相关文章:
如何处理Android悬浮弹窗双击返回事件?
目录 1 前言 1.1 准备知识 1.2 问题概述 2 解决方案 3 代码部分 3.1 动态更新窗口焦点 3.2 窗口监听返回事件 3.3 判断焦点是否在窗口内部 3.4 窗口监听焦点移入/移出 4 注意事项 4.1 窗口范围 4.2 空隙处的返回事件处理 1 前言 1.1 准备知识 1)开发环…...
高可用篇_A Docker容器化技术_II Docker环境搭建和常见命令
原创作者:田超凡(程序员田宝宝) 版权所有,引用请注明原作者,严禁复制转载 Docker安装 Docker 要求 CentOS7 系统的内核版本在 3.10以上 ,查看本页面的前提条件来验证你的CentOS 版本是否支持 Docker 。 …...
Vue.js+SpringBoot开发食品生产管理系统
目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 加工厂管理模块2.2 客户管理模块2.3 食品管理模块2.4 生产销售订单管理模块2.5 系统管理模块2.6 其他管理模块 三、系统展示四、核心代码4.1 查询食品4.2 查询加工厂4.3 新增生产订单4.4 新增销售订单4.5 查询客户 五、…...
Python面试笔记
Python面试笔记 PythonQ. Python中可变数据类型与不可变数据类型,浅拷贝与深拷贝详解Q. 解释什么是lambda函数?它有什么好处?Q. 什么是装饰器?Q. 什么是Python的垃圾回收机制?Q. Python内置函数dir的用法?Q…...
springboot 查看和修改内置 tomcat 版本
解析Spring Boot父级依赖 去到项目的根pom文件中,找到parent依赖: <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>${springboot.version}…...
003——移植鸿蒙
目录 一、顶层Make分析 二、添加一个新的单板 2.1 Kconfig 2.2 Makefile 2.2.1 顶层Makefile 2.2.2 platform下的Makefile 2.2.3 platform下的bsp.mk文件 2.3 编译与调试 2.4 解决链接错误 三、内核启动流程的学习 3.1 韦东山老师总结的启动四步 3.2 启动文件分析…...
罗马数字转整数-力扣通过自己编译器编译
学会将力扣题目用自己自带的编译软件编译---纯自己想的本题解法 字符 数值 I 1 V 5 X 10 L 50 C 100 D 500 M 1000 例如, 罗马数字 2 写做 II ,即为两…...
深入解析JVM加载机制
一、背景 Java代码被编译器变成生成Class字节码,但字节码仅是一个特殊的二进制文件,无法直接使用。因此,都需要放到JVM系统中执行,将Class字节码文件放入到JVM的过程,简称类加载。 二、整体流程 三、阶段逻辑分析 3…...
python redis中blpop和lpop的区别
python redis中lpop()方法是获取并删除左边第一个对象。 def lpop(self,name: str,count: Optional[int] None,) -> Union[Awaitable[Union[str, List, None]], Union[str, List, None]]:"""Removes and returns the first elements of the list name.By de…...
第四百一十回
文章目录 1. 概念介绍2. 方法与细节2.1 获取方法2.2 使用细节 3. 示例代码4. 内容总结 我们在上一章回中介绍了"如何获取当前系统语言"相关的内容,本章回中将介绍如何获取时间戳.闲话休提,让我们一起Talk Flutter吧。 1. 概念介绍 我们在本章…...
程序员的README——编写可维护的代码(一)
用户行为不可预测,网络不可靠,事情总会出错。生产环境下的软件必须一直保持可用状态。 编写可维护的代码有助于你应对不可预见的情况,可维护的代码有内置的保护、诊断和控制。 切记通过安全和有弹性的编码实践进行防御式编程来保护你的系统&a…...
数据库管理-第160期 Oracle Vector DB AI-11(20240312)
数据库管理160期 2024-03-12 数据库管理-第160期 Oracle Vector DB & AI-11(20240312)1 向量的函数操作to_vector()将vector转换为标准值vector_norm()vector_dimension_count()vector_dimension_format() 2 将向量转换为字符串或CLOBvector_seriali…...
(C++进阶)boost库笔记
目录 1、boost::function 1.1 概述 1.2 boost包装器和C11包装器对比 1.2、代码示例 1、boost::function 1.1 概述 boost::function 是 Boost 库中提供的一个通用函数对象包装器,它可以存储指向任何可调用对象的指针,并且可以在任何时候通过 operat…...
MapReduce面试重点
文章目录 1. 简述MapReduce整个流程2. join原理 1. 简述MapReduce整个流程 数据划分(Input Splitting):开始时,输入数据被分割成逻辑上的小块,每个块被称为Input Split。 映射(Map):每个Input Split 由一个或多个Map任务处理&…...
C语言简单题(7)从主函数中输入10个等长字符串,用一个函数对他们排序,然后在主函数输出这10个已排好序的字符串
从主函数中输入10个等长字符串,用一个函数对他们排序,然后在主函数输出这10个已排好序的字符串 /* 从主函数中输入10个等长字符串,用一个函数对他们排序,然后在主函数输出这10个已排好序的字符串 */ #include<stdio.h> …...
光伏科普|太阳能光伏发电应用场景有哪些?
太阳能光伏发电的应用领域其实非常广泛,很多人会不相信,但在我们的日常生活中随处可见太阳能光伏产业,本文将详细介绍其应用场景有哪些。 一、工业领域厂房 太阳能光伏发电作为一种清洁、可再生的能源,安装在工业领域厂房&#…...
Go 构建高效的二叉搜索树联系簿
引言 树是一种重要的数据结构,而二叉搜索树(BST)则是树的一种常见形式。在本文中,我们将学习如何构建一个高效的二叉搜索树联系簿,以便快速插入、搜索和删除联系人信息。 介绍二叉搜索树 二叉搜索树是一种有序的二叉…...
基于YOLOv8/YOLOv7/YOLOv6/YOLOv5的交通信号灯识别系统(深度学习+UI界面+训练数据集+Python代码)
摘要:本研究详细介绍了一种采用深度学习技术的交通信号灯识别系统,该系统集成了最新的YOLOv8算法,并与YOLOv7、YOLOv6、YOLOv5等早期算法进行了性能评估对比。该系统能够在各种媒介——包括图像、视频文件、实时视频流及批量文件中——准确地…...
以太坊开发学习-solidity(三)函数类型
目录 函数类型 函数类型 solidity官方文档里把函数归到数值类型 函数类型是一种表示函数的类型。可以将一个函数赋值给另一个函数类型的变量, 也可以将一个函数作为参数进行传递,还能在函数调用中返回函数类型变量。 函数类型有两类:- 内部&…...
教你把公司吃干抹净、榨干带走
大家好: 衷心希望各位点赞。 您的问题请留在评论区,我会及时回答 正文 打工人一定要做到够自私,把公司的一切为我所用,你要知道闷头打工是没有出路的。聪明的人会以最快的速度榨干带走公司的一切资源、人脉、技能,为…...
3分钟彻底清理Windows右键菜单!ContextMenuManager让你的效率提升200%
3分钟彻底清理Windows右键菜单!ContextMenuManager让你的效率提升200% 【免费下载链接】ContextMenuManager 🖱️ 纯粹的Windows右键菜单管理程序 项目地址: https://gitcode.com/gh_mirrors/co/ContextMenuManager 你是不是也遇到过这种情况&…...
如何在浏览器中高效使用微信网页版?wechat-need-web完整实用指南
如何在浏览器中高效使用微信网页版?wechat-need-web完整实用指南 【免费下载链接】wechat-need-web 让微信网页版可用 / Allow the use of WeChat via webpage access 项目地址: https://gitcode.com/gh_mirrors/we/wechat-need-web 想要在浏览器中稳定使用微…...
[智能体-26]:ollama, 让模型的部署和提供服务(远程或本地)变得异常简单
极简一键部署,自动封装OpenAI 标准 API,本地 / 远程服务秒启用,无需复杂环境编译、配置端口、适配接口。核心亮点安装零门槛跨 Windows/Mac/Linux,一键安装包,无需 CUDA、Python 环境预处理。模型一键拉取运行bash运行…...
健身行业AI Agent部署失败率高达68%?(2024真实数据复盘与5步合规上线法)
更多请点击: https://intelliparadigm.com 第一章:健身行业AI Agent部署失败率高达68%?——2024真实数据复盘与5步合规上线法 2024年Q2《中国智能健身系统落地白皮书》抽样调研覆盖全国137家连锁健身房及SaaS服务商,结果显示&…...
FPGA加速机器学习在粒子物理触发系统中的应用与实战
1. 项目概述:当FPGA遇上机器学习,为粒子物理装上“火眼金睛” 在大型强子对撞机(LHC)的心脏地带,每秒发生着数亿次质子对撞。每一次对撞都可能产生希格斯玻色子、顶夸克,或是我们尚未知晓的新物理现象。然而…...
Arm Development Studio许可协议核心条款与合规指南
1. Arm Development Studio 终端用户许可协议解析作为一名长期从事嵌入式开发的工程师,我深知开发工具许可协议的重要性。Arm Development Studio 作为业界领先的嵌入式开发套件,其 EULA(终端用户许可协议)直接影响着我们的日常开…...
保姆级教程:在Ubuntu 22.04上从源码编译COLMAP 3.9(含6个常见Bug解决方案)
在Ubuntu 22.04上从源码编译COLMAP 3.9的终极避坑指南三维重建技术正在重塑数字世界的构建方式,而COLMAP作为开源领域的标杆工具,其强大的多视图几何算法让学术研究和工业应用都受益匪浅。但当你第一次尝试在Ubuntu系统上编译这个工具时,可能…...
明星数字人运营失效率高达68%?AI Agent驱动的粉丝交互系统,已帮3家MCN提升留存率217%
更多请点击: https://intelliparadigm.com 第一章:AI Agent娱乐行业应用的现状与挑战 近年来,AI Agent在娱乐行业的渗透持续加速,从智能剧本生成、虚拟偶像实时交互,到个性化内容推荐与跨平台用户行为建模,…...
电脑‘假关机’真烦人!深入聊聊Windows电源管理里的‘快速启动’到底是个啥
Windows快速启动技术揭秘:高效与兼容性的博弈深夜加班结束,你点击关机按钮准备休息,却发现显示器刚暗下去又突然亮起——这不是灵异事件,而是Windows的快速启动功能在"作祟"。这种介于关机和休眠之间的混合状态…...
Kubernetes多租户管理:实现资源隔离与安全的完整指南
Kubernetes多租户管理:实现资源隔离与安全的完整指南 引言 在企业环境中,多租户管理是Kubernetes的重要功能。通过多租户管理,可以实现不同团队或客户之间的资源隔离和安全控制。这对于共享Kubernetes集群的场景尤为重要。 作为一名资深的Dev…...
