kotlin Android AccessibilityService 无障碍入门

安卓的无障碍模式可以很好的进行自动化操作以帮助视障人士自动化完成一些任务。

无障碍可以做到,监听屏幕变化,朗读文本,定位以及操作控件等。

以下从配置到代码依次进行无障碍设置与教程。

一、配置 AndroidManifest.xml

无障碍是个服务,因此需要再 AndroidManifest.xml 进行声明等配置。包括申请权限,声明服务等

    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /><uses-permission android:name="android.permission.ACCESSIBILITY_SERVICE" /><application><serviceandroid:name="io.github.zimoyin.asdk.accessibility.AutoSdkAccessibilityService"android:exported="true"android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"><intent-filter><action android:name="android.accessibilityservice.AccessibilityService" /></intent-filter><meta-dataandroid:name="android.accessibilityservice"android:resource="@xml/accessibility_service_config" /></service></application>
  • android:name:无障碍服务类路径
  • meta-data/android:resource:无障碍配置,需要创建 res/xml/accessibility_service_config.xml 文件

二、无障碍配置

需要创建 res/xml/accessibility_service_config.xml 文件

<accessibility-servicexmlns:android="http://schemas.android.com/apk/res/android"android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged"android:accessibilityFeedbackType="feedbackSpoken"android:notificationTimeout="100"android:canPerformGestures="true"android:canRetrieveWindowContent="true"android:canRequestTouchExplorationMode="false"android:settingsActivity="true"android:accessibilityFlags="flagDefault|flagIncludeNotImportantViews|flagReportViewIds"android:description="@string/accessibility_service_description" />
  • accessibilityEventTypes: 监听事件类型,比如窗口滑动,弹窗,窗体变化,点击等事件监听,typeAllMask 则说监听全部事件,尽可能合理的配置事件以减少电量消耗,减少服务频繁唤醒
  • accessibilityFeedbackType: 回显给用户的方式(例如:配置TTS引擎,实现发音)
  • accessibilityFlags: 决定无障碍服务如何响应用户操作、事件监听范围以及对界面元素的访问权限
  • canPerformGestures:用于允许服务模拟用户的复杂手势操作 (如滑动、点击、长按等)(API 24新增)
  • description: 无障碍描述,这里需要在 res/value/string.xml下配置
  • notificationTimeout:响应事件间隔,单位 ms
  • canRetrieveWindowContent: 是否能读取窗口内容
  • settingsActivity: 允许用户在系统设置中通过点击你的无障碍服务名称,跳转到自定义的配置界面

三、无障碍描述配置

res/values/strings.xml

<resources><string name="accessibility_service_description">This is a accessibility service for AutoSDK.</string>
</resources>

四、代码

1. 打开系统配置页

        /*** 打开系统设置页面,跳转到辅助功能页面* @param context 上下文,可传入 Activity 或 Application*/fun openAccessibilitySettings(context: Context) {val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)intent.flags = Intent.FLAG_ACTIVITY_NEW_TASKcontext.startActivity(intent)}

2. 是否打开了无障碍配置

        /*** 检查当前辅助功能服务是否已启用*/fun isAccessibilityServiceEnabled(context: Context): Boolean {return getAccessibilityManager(context)?.isEnabled == true}/*** 获取 AccessibilityManager*/fun getAccessibilityManager(context: Context): AccessibilityManager? {return context.getSystemService(ACCESSIBILITY_SERVICE) as? AccessibilityManager}

3. 继承 AccessibilityService 并暴露实例对象

签名代码可以没有但是,AccessibilityService 是一定要继承的,并且类位置要与 AndroidManifest.xml 中声明的位置一致


class AutoSdkAccessibilityService : AccessibilityService() {init {instance = this}companion object {val LAST_ID: String = AutoSdkAccessibilityService::class.java.name/*** 当前辅助功能服务实例*/var instance: AutoSdkAccessibilityService? = nullprivate set/*** 检查 AndroidManifest.xml 是否存在 android.permission.SYSTEM_ALERT_WINDOW 权限*/fun hasSystemAlertWindowPermission(context: Context): Boolean {return isPermissionDeclared(context, Manifest.permission.SYSTEM_ALERT_WINDOW)}/*** 检查 AndroidManifest.xml 是否存在 android.permission.ACCESSIBILITY_SERVICE 权限*/fun hasAccessibilityPermission(context: Context): Boolean {return isPermissionDeclared(context, "android.permission.ACCESSIBILITY_SERVICE")}/*** 检查 AndroidManifest.xml 是否存在 android.permission.ACCESSIBILITY_SERVICE 权限*/@SuppressLint("QueryPermissionsNeeded")fun isAccessibilityServiceDeclared(context: Context): Boolean {val services = context.packageManager.queryIntentServices(Intent("android.accessibilityservice.AccessibilityService"),PackageManager.GET_META_DATA)for (serviceInfo in services) {if (serviceInfo.serviceInfo.packageName == context.packageName) {// 检查是否声明了 BIND_ACCESSIBILITY_SERVICE 权限if (serviceInfo.serviceInfo.permission != "android.permission.BIND_ACCESSIBILITY_SERVICE") {return false}// 检查是否声明了 meta-dataval metaData = serviceInfo.serviceInfo.metaDatareturn !(metaData == null || !metaData.containsKey("android.accessibilityservice"))}}return false}/*** 检查是否声明了指定权限*/private fun isPermissionDeclared(context: Context, permission: String): Boolean {return try {val packageInfo = context.packageManager.getPackageInfo(context.packageName,PackageManager.GET_PERMISSIONS)packageInfo.requestedPermissions?.contains(permission) == true} catch (e: Exception) {false}}/*** 检查当前辅助功能服务是否已启用*/fun isAccessibilityServiceEnabled(context: Context): Boolean {return getAccessibilityManager(context)?.isEnabled == true}/*** 获取 AccessibilityManager*/fun getAccessibilityManager(context: Context): AccessibilityManager? {return context.getSystemService(ACCESSIBILITY_SERVICE) as? AccessibilityManager}/*** 获取 AccessibilityServiceInfo*/fun getAccessibilityServiceInfo(context: Context): AccessibilityServiceInfo? {val accessibilityManager = getAccessibilityManager(context) ?: return nullval serviceInfo = accessibilityManager.installedAccessibilityServiceList.firstOrNull {it.id.endsWith(LAST_ID)}return serviceInfo}/*** 打开系统设置页面,跳转到辅助功能页面* @param context 上下文,可传入 Activity 或 Application*/fun openAccessibilitySettings(context: Context) {val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)intent.flags = Intent.FLAG_ACTIVITY_NEW_TASKcontext.startActivity(intent)}}/*** 当系统检测到 UI 变化(如窗口更新、控件点击)时才会触发*/override fun onAccessibilityEvent(event: AccessibilityEvent) {AccessibilityListener.send(event)}override fun onInterrupt() {// TODO}/*** 模拟点击* @param x x 坐标* @param y y 坐标*/fun clickAt(x: Float, y: Float) {val path = Path().apply {moveTo(x, y)lineTo(x, y)}val gestureDescription = GestureDescription.Builder().addStroke(GestureDescription.StrokeDescription(path, 0L, 100L)).build()dispatchGesture(gestureDescription, null, null)}/*** 模拟点击* @param path 模拟点击的路径* @param start 模拟点击的开始时间* @param end 模拟点击的结束时间*/fun clickAt(path:Path, start: Long, end: Long) {val gestureDescription = GestureDescription.Builder().addStroke(GestureDescription.StrokeDescription(path, start, end)).build()dispatchGesture(gestureDescription, null, null)}
}

获取根节点

AutoSdkAccessibilityService.instance?.rootInActiveWindow

遍历节点

fun AccessibilityNodeInfo.forEach(callback: (AccessibilityNodeInfo) -> Unit) {for (i in 0 until childCount) {val node = getChild(i)callback(node)node.forEach(callback)}
}

节点查找 filter

fun AccessibilityNodeInfo.filter(callback: (AccessibilityNodeInfo) -> Boolean): MutableList<AccessibilityNodeInfo> {val list = mutableListOf<AccessibilityNodeInfo>()forEach {if (callback(it)) {list.add(it)}}return list
}

点击节点


/*** 点击节点范围内的任意空间*/
fun AccessibilityNodeInfo?.click(service: AccessibilityService = requireNotNull(instance),minDuration: Long = 1L,maxDuration: Long = 200L
): Boolean {if (this == null) return falseval bounds = Rect().apply { this@click.getBoundsInScreen(this) }if (bounds.isEmpty) return false// 在控件边界内生成随机坐标val randomX = Random.nextInt(bounds.left, bounds.right)val randomY = Random.nextInt(bounds.top, bounds.bottom)val path = Path().apply {moveTo(randomX.toFloat(), randomY.toFloat())lineTo(randomX.toFloat(), randomY.toFloat())}val gesture = GestureDescription.Builder().addStroke(GestureDescription.StrokeDescription(path,0L,Random.nextLong(minDuration, maxDuration + 1))).build()return service.dispatchGesture(gesture,object : AccessibilityService.GestureResultCallback() {override fun onCancelled(gestureDescription: GestureDescription) {super.onCancelled(gestureDescription)}override fun onCompleted(gestureDescription: GestureDescription) {super.onCompleted(gestureDescription)}},null)
}/*** 点击节点* 注意:点击节点时,如果节点不可点击,则会返回false。* 一般情况下在控件上面都会有图标或者文本,如果匹配到了文本或者图标,非特殊情况下是不能被点击的,因此需要获取父或者子节点进行点击.* 推荐使用 [clickMatchNode]* @return 是否点击成功*/
fun AccessibilityNodeInfo?.clickNode(): Boolean {return this?.performAction(AccessibilityNodeInfo.ACTION_CLICK) == true
}/*** 点击节点* 注意:点击节点时,如果节点不可点击,则会查找父节点*/
fun AccessibilityNodeInfo?.clickMatchNode(): Boolean {if (this == null) return falsereturn if (isClickable) {performAction(AccessibilityNodeInfo.ACTION_CLICK)} else {parent.clickMatchNode()}
}/*** 长按节点* 注意:长按节点时,如果节点不可点击,则会返回false。* 一般情况下在控件上面都会有图标或者文本,如果匹配到了文本或者图标,非特殊情况下是不能被点击的,因此需要获取父或者子节点进行点击* 推荐使用 [longClickMatchNode]*/
fun AccessibilityNodeInfo?.longClickNode(): Boolean {return this?.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK) == true
}/*** 长按节点* 注意:长按节点时,如果节点不可点击,则会查找父节点*/
fun AccessibilityNodeInfo?.longClickMatchNode(): Boolean {if (this == null) return falsereturn if (isClickable) {performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK)} else {parent.longClickMatchNode()}
}

节点选择器


/*** 包装一个 AccessibilityNodeInfo 集合,提供链式条件过滤能力,仿照 Auto.js 的节点选择器风格。** @property node 待过滤的节点列表*/
class AccessibilityNodeInfoWrapper(val node: AccessibilityNodeInfo) {private val conditions = mutableListOf<(AccessibilityNodeInfo) -> Boolean>()/*** 筛选文本等于 [text] 的节点。*/fun text(text: String): AccessibilityNodeInfoWrapper {conditions += { it.text?.toString() == text }return this}/*** 筛选文本去除空格后等于 [text] 的节点。*/fun textTrim(): AccessibilityNodeInfoWrapper {conditions += { it.text?.toString()?.trim() == it.text?.toString() }return this}/*** 筛选文本包含 [substr] 的节点。*/fun textContains(substr: String): AccessibilityNodeInfoWrapper {conditions += { it.text?.toString()?.contains(substr) == true }return this}/*** 筛选文本匹配正则 [regex] 的节点。*/fun textMatches(regex: Regex): AccessibilityNodeInfoWrapper {conditions += { it.text?.toString()?.matches(regex) == true }return this}/*** 筛选类名等于 [className] 的节点。*/fun className(className: String): AccessibilityNodeInfoWrapper {conditions += { it.className.toString() == className }return this}/*** 筛选资源 ID 等于 [id] 的节点。*/fun id(id: String): AccessibilityNodeInfoWrapper {conditions += { it.viewIdResourceName == id }return this}/*** 筛选包名等于 [packageName] 的节点。*/fun pkg(packageName: String): AccessibilityNodeInfoWrapper {conditions += { it.packageName == packageName }return this}/*** 筛选 contentDescription 等于 [desc] 的节点。*/fun description(desc: String): AccessibilityNodeInfoWrapper {conditions += { it.contentDescription?.toString() == desc }return this}/*** 筛选节点可点击的。*/fun clickable(boolean: Boolean = true): AccessibilityNodeInfoWrapper {conditions += { it.isClickable == boolean}return this}/*** 筛选节点可见的(isVisibleToUser 为 true)。*/fun visible(): AccessibilityNodeInfoWrapper {conditions += { it.isVisibleToUser }return this}private fun conditionResult(info: AccessibilityNodeInfo): Boolean {for (condition in conditions) {if (!condition(info)) {return false}}return true}/*** 执行所有累积的条件过滤,返回符合条件的节点列表。* 调用完成后会清空已设置的条件,以便下一次重用。** @return 符合所有条件的节点列表*/fun done(): List<AccessibilityNodeInfo> = node.filter { info ->conditionResult(info)}.also {conditions.clear()}fun textStartsWith(string: String): AccessibilityNodeInfoWrapper {conditions += { it.text?.toString()?.startsWith(string) == true }return this}
}fun AccessibilityNodeInfo.selector(callback: AccessibilityNodeInfoWrapper.() -> Unit): List<AccessibilityNodeInfo> {return AccessibilityNodeInfoWrapper(this).apply { callback() }.done()
}fun AccessibilityNodeInfo.selector(): AccessibilityNodeInfoWrapper =AccessibilityNodeInfoWrapper(this)

事件分发

object AccessibilityListener {private val accessibilityListener = ConcurrentHashMap<UUID, (AccessibilityEvent) -> Unit>()fun onAccessibilityEvent(callback: (AccessibilityEvent) -> Unit) {val id = UUID.randomUUID()accessibilityListener[id] = callback}fun removeAccessibilityEvent(id: UUID) {accessibilityListener.remove(id)}fun send(event: AccessibilityEvent) {accessibilityListener.forEach {runCatching { it.value.invoke(event) }.onFailure {logger.error(it)}}}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/83722.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Vue篇】数据秘语:从watch源码看响应式宇宙的蝴蝶效应

目录 引言 一、watch侦听器&#xff08;监视器&#xff09; 1.作用&#xff1a; 2.语法&#xff1a; 3.侦听器代码准备 4. 配置项 5.总结 二、翻译案例-代码实现 1.需求 2.代码实现 三、综合案例——购物车案例 1. 需求 2. 代码 引言 &#x1f4ac; 欢迎讨论&#…

WPS中代码段的识别方法及JS宏实现

在WPS中&#xff0c;文档的基本结构可以通过对象模型来理解&#xff1a; &#xff08;1&#xff09;Document对象&#xff1a;表示整个文档 &#xff08;2&#xff09;Range对象&#xff1a;表示文档中的一段连续区域&#xff0c;可以是一个字符、一个句子或整个文档 &#…

el-tree结合el-tree-transfer实现穿梭框里展示树形数据

参考文章&#xff1a;我把他的弹框单拉出来一个独立文件作为组件方便使用&#xff0c;遇到一些问题记录一下。 testComponet.vue <template><div class"per_container"><div class"per_con_left"><div class"per_con_title&q…

Go 后端中双 token 的实现模板

下面是一个典型的 Go 后端双 Token 认证机制 实现模板&#xff0c;使用 Gin 框架 JWT Redis&#xff0c;结构清晰、可拓展&#xff0c;适合实战开发。 项目结构建议 /utils├── jwt.go // Access & Refresh token 的生成和解析├── claims.go // 从请求…

Typescript学习教程,从入门到精通,TypeScript 对象语法知识点及案例代码(7)

TypeScript 对象语法知识点及案例代码 TypeScript 是 JavaScript 的超集&#xff0c;提供了静态类型检查和其他增强功能。在 TypeScript 中&#xff0c;对象是面向对象编程&#xff08;OOP&#xff09;的基础。 一、对象概述 在 TypeScript 中&#xff0c;对象是属性的集合&a…

应用BERT-GCN跨模态情绪分析:贸易缓和与金价波动的AI归因

本文运用AI量化分析框架&#xff0c;结合市场情绪因子、宏观经济指标及技术面信号&#xff0c;对黄金与美元指数的联动关系进行解析&#xff0c;揭示本轮贵金属回调的深层驱动因素。 周三&#xff0c;现货黄金价格单日跌幅达2.1%&#xff0c;盘中触及3167.94美元/盎司关键价位&…

命令行登录 MySQL 报 Segmentation fault 故障解决

问题描述&#xff1a;对 mysql8.0.35 源码进行 make&#xff0c;由于一开始因为yum源问题少安装依赖库 库&#xff0c;在链接时遇到错误 undefined reference to&#xff0c;后来安装了相关依赖库&#xff0c;再次 make 成功。于是将 mysqld 启动&#xff0c;再用 mysql -u roo…

Axure设计数字乡村可视化大屏:构建乡村数据全景图

今天&#xff0c;让我们一同深入了解由Axure设计的数字乡村可视化大屏&#xff0c;看看它如何通过精心的布局和多样化的图表类型&#xff0c;将乡村的各类数据以直观、易懂的方式呈现出来&#xff0c;为乡村管理者提供有力的数据支持。 原型效果预览链接&#xff1a;Axure数字乡…

3D个人简历网站 4.小岛

1.模型素材 在Sketchfab上下载狐狸岛模型&#xff0c;然后转换为素材资源asset&#xff0c;嫌麻烦直接在网盘链接下载素材&#xff0c; Fox’s islandshttps://sketchfab.com/3d-models/foxs-islands-163b68e09fcc47618450150be7785907https://gltf.pmnd.rs/ 素材夸克网盘&a…

智能开发工具PhpStorm v2025.1——增强AI辅助编码功能

PhpStorm是一个轻量级且便捷的PHP IDE&#xff0c;其旨在提高用户效率&#xff0c;可深刻理解用户的编码&#xff0c;提供智能代码补全&#xff0c;快速导航以及即时错误检查。可随时帮助用户对其编码进行调整&#xff0c;运行单元测试或者提供可视化debug功能。 立即获取PhpS…

Spark 的运行模式(--master) 和 部署方式(--deploy-mode)

Spark 的 运行模式&#xff08;--master&#xff09; 和 部署方式&#xff08;--deploy-mode&#xff09;&#xff0c;两者的核心区别在于 资源调度范围 和 Driver 进程的位置。 一、核心概念对比 维度--master&#xff08;运行模式&#xff09;--deploy-mode&#xff08;部署…

sqli—labs第八关——布尔盲注

一&#xff1a;确定注入类型 按照我们之前的步骤来 输入 ?id1 and 11-- ?id1 and 12-- 界面正常 第二行界面异常空白 所以注入类型为单引号闭合型 二&#xff1a; 布尔盲注 1.判断是否使用条件 &#xff08;1&#xff09;&#xff1a;存在注入但不会直接显示查询结果 …

ARP 原理总结

&#x1f310; 一、ARP 原理总结 ARP&#xff08;Address Resolution Protocol&#xff09;是用于通过 IP 地址解析 MAC 地址的协议&#xff0c;工作在 链路层 与 网络层之间&#xff08;OSI 模型的第三层与第二层之间&#xff09;。 &#x1f501; ARP通信过程&#xff1a; …

SpringCloud——EureKa

目录 1.前言 1.微服务拆分及远程调用 3.EureKa注册中心 远程调用的问题 eureka原理 搭建EureKaServer 服务注册 服务发现 1.前言 分布式架构&#xff1a;根据业务功能对系统进行拆分&#xff0c;每个业务模块作为独立项目开发&#xff0c;称为服务。 优点&#xff1a; 降…

机顶盒刷机笔记

疑难杂症解决 hitool线刷网口不通tftp超时--》关闭防火墙cm201-2卡刷所有包提示失败abort install--》找个卡刷包只刷fastboot分区再卡刷就能通过了&#xff08;cm201救砖包 (M8273版子&#xff09;&#xff09; 刷机工具 海兔烧录工具HiTool-STB-5.3.12工具&#xff0c;需要…

Linux动静态库制作与原理

什么是库 库是写好的现有的&#xff0c;成熟的&#xff0c;可以复用的代码。现实中每个程序都要依赖很多基础的底层库&#xff0c;不可能每个人的代码都从零开始&#xff0c;因此库的存在意义非同寻常。 本质上来说库是一种可执行代码的二进制形式&#xff0c;可以被操作系统…

如何通过小智AI制作会说话的机器人玩具?

一、硬件准备与组装 1. 核心硬件选择 主控芯片&#xff1a;选择支持无线网络连接、音频处理和可编程接口的嵌入式开发板 音频模块&#xff1a;配备拾音麦克风与小型扬声器&#xff0c;确保语音输入/输出功能 显示模块&#xff1a;选择适配的交互显示屏用于可视化反馈 扩展模…

如何控制邮件发送频率避免打扰用户

一、用户行为 监测用户与邮件的互动数据&#xff0c;如打开率、点击率下滑或退订申请增多&#xff0c;可能是发送频率过高的警示信号。利用邮件营销平台的分析工具&#xff0c;识别这些指标的变动趋势&#xff0c;为调整提供依据。 二、行业特性与受众差异 不同行业用户对邮…

定积分的“偶倍奇零”性质及其使用条件

定积分的“偶倍奇零”性质是针对对称区间上的奇偶函数积分的重要简化方法。以下是其核心内容和应用要点&#xff1a; ​一、基本性质 ​偶函数&#xff08;偶倍&#xff09;​ 若 f(x) 在 [−a,a] 上为偶函数&#xff08;即 f(−x)f(x)&#xff09;&#xff0c;则&#xff1a; …

如何在 Windows 11 或 10 上安装 Fliqlo 时钟屏保

了解如何在 Windows 11 或 10 上安装 Fliqlo,为您的 PC 或笔记本电脑屏幕添加一个翻转时钟屏保以显示时间。 Fliqlo 是一款适用于 Windows 和 macOS 平台的免费时钟屏保。它也适用于移动设备,但仅限于 iPhone 和 iPad。Fliqlo 的主要功能是在用户不活动时在 PC 或笔记本电脑…