配置无障碍服务
在 res/xml
目录下创建一个 accessibility_service_config.xml
文件,用于配置无障碍服务的相关信息,例如要监听的事件类型、反馈类型等。
<?xml version="1.0" encoding="utf-8"?>
<!-- 这行代码告诉电脑,这个文件是一个 XML 文件,用的是 1.0 版本,里面的文字是用 UTF - 8 这种编码方式写的。就好像告诉别人这本书是用什么规则写的一样。 -->
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
<!-- 这里开始定义一个无障碍服务,xmlns:android 就像是一个魔法咒语,告诉电脑接下来用到的一些特殊的词是和安卓系统有关的。 -->
android:accessibilityEventTypes="typeViewClicked|typeViewFocused"
<!-- 这行说的是,我们这个无障碍服务要去关注哪些事情。typeViewClicked 就是当屏幕上的东西被点击的时候,typeViewFocused 就是当屏幕上的东西被选中的时候。用 | 把它们连起来,意思是两个都要关注。 -->
android:accessibilityFeedbackType="feedbackGeneric"
<!-- 这行是说,当服务发现了上面说的那些事情后,要给出什么样的反馈。feedbackGeneric 就是一种通用的反馈方式。就好像你做对事情了,会得到一个通用的表扬一样。 -->
android:accessibilityFlags="flagDefault"
<!-- 这行设置了一些额外的小规则。flagDefault 就是用默认的那些规则。就像玩游戏用默认的游戏规则一样。 -->
android:canPerformGestures="true"
<!-- 这行表示这个服务能不能做出一些手势动作,比如点击、滑动等。设置成 true 就是可以做这些动作。就像你有一双可以做各种动作的小手一样。 -->
android:canRetrieveWindowContent="true"
<!-- 这行说的是这个服务能不能拿到屏幕上窗口里的内容。设置成 true 就是可以拿。就好像你可以看到窗户里面有什么东西一样。 -->
android:description="@string/accessibility_service_description"
<!-- 这行是给这个无障碍服务写一个简单的说明。@string/accessibility_service_description 是从别的地方拿过来的一段文字说明,就像给一个东西贴了一个标签,告诉别人这是什么。 -->
android:notificationTimeout="100"
<!-- 这行设置了一个时间,当有事情发生的时候,服务要在 100 毫秒内做出反应。就像你听到别人叫你,要在很短的时间内答应一样。 -->
android:packageNames="com.example.targetapp" />
<!-- 这行指定了这个无障碍服务只关注哪个应用程序。com.example.targetapp 就是那个应用的名字。就像你只关注某一个小朋友做的事情一样。 -->
在 AndroidManifest.xml
中注册服务
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapp">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<service
android:name=".MyAccessibilityService"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config" />
</service>
</application>
</manifest>
开始基础
// @Override 是一个特殊的标记,它告诉计算机,下面这个方法是要重写父类里的方法。就好像你要按照一个固定的模板来画画一样。
@Override
// 这是一个方法,名字叫 onAccessibilityEvent,当系统里有和无障碍相关的事情发生时,就会调用这个方法。event 就像是一个小信使,它带来了发生的事情的信息。
public void onAccessibilityEvent(AccessibilityEvent event) {
// 从这个小信使(event)那里拿到事件发生的源头,也就是哪个东西上面发生了这个事件。就像知道是哪个小朋友做了某件事情一样。把这个源头存到 source 这个小盒子里。
AccessibilityNodeInfo source = event.getSource();
// 检查 source 这个小盒子里是不是有东西。如果没有东西,那就说明没有找到事件的源头,就不用再往下做了。
if (source != null) {
// 这是一个注释,告诉我们下面要做的事情是查找一个写着“点击我”的东西。
// 查找要点击的视图
// 在这个事件源头里,找一找有没有写着“点击我”的东西。把找到的结果存到 targetNode 这个小盒子里。
AccessibilityNodeInfo targetNode = source.findAccessibilityNodeInfosByText("点击我");
// 检查 targetNode 这个小盒子里是不是有东西,并且找到的东西数量要大于 0。如果满足条件,说明找到了写着“点击我”的东西。
if (targetNode != null && targetNode.size() > 0) {
// 从找到的那些写着“点击我”的东西里,拿出第一个。把它存到 node 这个小盒子里。
AccessibilityNodeInfo node = targetNode.get(0);
// 检查这个拿出来的东西是不是可以被点击。就像检查一个按钮是不是能按一样。
if (node.isClickable()) {
// 如果这个东西可以被点击,那就让计算机模拟点击这个动作。就像你用手指去按按钮一样。
node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
}
}
}
}
- 未找到匹配元素:要是在
source
对应的界面范围内,没有任何元素的文本内容是 “点击我”,那么targetNodeList
会是一个空列表(即列表长度为 0)。 - 找到匹配元素:若找到了包含文本 “点击我” 的元素,
targetNodeList
就会包含这些元素对应的AccessibilityNodeInfo
对象。每个AccessibilityNodeInfo
对象都代表一个界面元素,并且包含了该元素的各种属性和信息,像元素的位置、大小、是否可点击等。
打印 node
的结果
当你尝试打印 node
时,通常会调用 node.toString()
方法。打印的内容会包含该界面元素的一些基本信息,示例如下:
AccessibilityNodeInfo[
packageName: com.example.app,
className: android.widget.Button,
text: 点击我,
contentDescription: null,
boundsInParent: Rect(0, 0 - 100, 50),
boundsInScreen: Rect(100, 200 - 200, 250),
childCount: 0,
enabled: true,
focusable: true,
focused: false,
clickable: true,
longClickable: false,
checked: false,
selected: false,
actions: [ACTION_CLICK]
]
找到输入框
// 引入安卓系统里和无障碍服务相关的工具包,这样我们就能使用里面的功能啦,就像打开一个装满工具的盒子。
import android.accessibilityservice.AccessibilityService;
// 引入安卓系统里和无障碍事件相关的工具包,方便我们处理各种无障碍事件,就像拿了一个专门处理事情的小本子。
import android.view.accessibility.AccessibilityEvent;
// 引入安卓系统里和无障碍节点信息相关的工具包,能让我们了解界面上各种元素的信息,就像有了一个查看元素信息的望远镜。
import android.view.accessibility.AccessibilityNodeInfo;
// 定义一个公共的类,名字叫 MyAccessibilityService,这个类继承自 AccessibilityService,就像盖房子用了一个特定的房子框架。
public class MyAccessibilityService extends AccessibilityService {
// 重写父类里的 onAccessibilityEvent 方法,当系统里有和无障碍相关的事件发生时,就会调用这个方法。
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
// 从事件里拿到事件发生的源头,也就是哪个界面元素上发生了这个事件,把它存到 source 这个小盒子里。
AccessibilityNodeInfo source = event.getSource();
// 检查 source 这个小盒子里是不是有东西,如果有东西,说明找到了事件源头。
if (source != null) {
// 这是一个注释,告诉我们下面要做的事情是查找一个提示文本为“请输入内容”的输入框。
// 查找输入框,假设输入框的提示文本为“请输入内容”
// 调用 findNodeByHintText 方法,在 source 里找提示文本是“请输入内容”的输入框,把找到的结果存到 inputNode 这个小盒子里。
AccessibilityNodeInfo inputNode = findNodeByHintText(source, "请输入内容");
// 检查 inputNode 这个小盒子里是不是有东西,如果有,说明找到了输入框。
if (inputNode != null) {
// 这是一个注释,告诉我们下面要做的事情是创建一个用来装要输入文本的包裹。
// 创建一个包含要输入文本的 Bundle
// 创建一个 Bundle 对象,它就像一个包裹,用来装我们要输入的文本。
android.os.Bundle arguments = new android.os.Bundle();
// 把要输入的文本“要输入的文本”放到这个包裹里,并且给它取个名字,名字是系统规定的一个特殊名字。
arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, "要输入的文本");
// 这是一个注释,告诉我们下面要做的事情是执行设置文本的操作。
// 执行设置文本的操作
// 让 inputNode 这个输入框执行设置文本的操作,把包裹里的文本设置进去。
inputNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments);
}
}
}
// 这是一个注释,告诉我们下面这个方法的作用是根据提示文本查找节点。
// 根据提示文本查找节点
// 定义一个私有的方法,名字叫 findNodeByHintText,这个方法接收两个参数,一个是根节点,一个是提示文本。
private AccessibilityNodeInfo findNodeByHintText(AccessibilityNodeInfo root, String hintText) {
// 检查根节点是不是为空,如果为空,说明没有东西可以找,就直接返回空。
if (root == null) {
return null;
}
// 检查根节点的提示文本是不是和我们要找的提示文本一样,如果一样,就把根节点返回。
if (hintText.equals(root.getHintText())) {
return root;
}
// 拿到根节点的子节点数量,就像知道一个大家庭里有几个小朋友。
int childCount = root.getChildCount();
// 用一个循环,一个一个地检查根节点的子节点。
for (int i = 0; i < childCount; i++) {
// 从根节点里拿出第 i 个子节点,存到 child 这个小盒子里。
AccessibilityNodeInfo child = root.getChild(i);
// 递归调用 findNodeByHintText 方法,在子节点里继续找符合提示文本的节点,把结果存到 result 这个小盒子里。
AccessibilityNodeInfo result = findNodeByHintText(child, hintText);
// 检查 result 这个小盒子里是不是有东西,如果有,说明找到了符合条件的节点,就把它返回。
if (result != null) {
return result;
}
}
// 如果上面都没有找到符合条件的节点,就返回空。
return null;
}
// 重写父类里的 onInterrupt 方法,当无障碍服务被中断时,就会调用这个方法。
@Override
public void onInterrupt() {
// 这是一个注释,告诉我们这个方法是在无障碍服务被中断时调用。
// 无障碍服务被中断时调用
}
}
获取输入框文本内容
import android.accessibilityservice.AccessibilityService;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
public class MyAccessibilityService extends AccessibilityService {
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
AccessibilityNodeInfo source = event.getSource();
if (source != null) {
// 查找输入框,假设输入框的提示文本为“请输入内容”
AccessibilityNodeInfo inputNode = findNodeByHintText(source, "请输入内容");
if (inputNode != null) {
// 检测输入框是否已经有内容
CharSequence inputText = inputNode.getText();
if (inputText != null && inputText.length() > 0) {
// 输入框已经有内容
System.out.println("输入框已经有内容:" + inputText);
} else {
// 输入框没有内容,执行输入操作
// 创建一个包含要输入文本的 Bundle
android.os.Bundle arguments = new android.os.Bundle();
arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, "要输入的文本");
// 执行设置文本的操作
inputNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments);
}
}
}
}
// 根据提示文本查找节点
private AccessibilityNodeInfo findNodeByHintText(AccessibilityNodeInfo root, String hintText) {
if (root == null) {
return null;
}
if (hintText.equals(root.getHintText())) {
return root;
}
int childCount = root.getChildCount();
for (int i = 0; i < childCount; i++) {
AccessibilityNodeInfo child = root.getChild(i);
AccessibilityNodeInfo result = findNodeByHintText(child, hintText);
if (result != null) {
return result;
}
}
return null;
}
@Override
public void onInterrupt() {
// 无障碍服务被中断时调用
}
}