无障碍服务(AccessibilityService)

使用步骤

  1. 实现一个继承自 AccessibilityService 的服务类。
  2. 设定配置信息,以便系统知道该辅助模式的一些基本信息,例如监听那些事件。
  3. 在清单文件(AndroidManifest.xml)中,注册此服务。
  4. 在系统设置中,找到“无障碍”,并开启此服务。

1 继承 AccessibilityService

  1. public abstract void onAccessibilityEvent(AccessibilityEvent event);//监听的事件发生回调
  2. public abstract void onInterrupt();//系统事件被打断回调

2 配置辅助模式

配置 AccessibilityService 有两种方式,

  • 通过 xml 配置文件
<accessibility-service
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackAllMask"
    android:accessibilityFlags="flagReportViewIds"
    android:canRetrieveWindowContent="true"
    android:packageNames="com.forwarding.wechat"
    android:description="@string/accessbility_desc"
    android:notificationTimeout="100" />

accessibilityEventTypes:监听的事件类型,例如:typeAllMask 表示全部事件,而 typeViewClicked 表示只监听点击事件。
accessibilityFeedbackType:监听事件的反馈模式。
canRetrieveWindowContent:是否允许获取视图层级的访问权,如果它被设置为 false,node.getSource() 方法会调用失败。
accessibilityFlags:指定 Flag,一般用于指定根据 Node 获取 View ID 的权限。
packageNames:开启监听的应用包名,可以指定多个包名,通过逗号“,”分割,不设置此属性标识全局监听。
description:辅助功能的描述,它会显示在系统设置的“无障碍”中的描述信息中。
notificationTimeout:响应的毫秒数。

更多配置参数:https://developer.android.com/reference/android/accessibilityservice/AccessibilityService

  • 通过 Java 代码中动态配置。
    重写 AccessibilityService 的 onServiceConnected() 方法(不推荐)

3 清单文件中注册服务

<service android:label="无障碍名"
    android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
    android:name=".WeForwardServer">
    <intent-filter>
        <action android:name="android.accessibilityservice.AccessibilityService"/>
    </intent-filter>
    <meta-data
        android:name="android.accessibilityservice"
        android:resource="@xml/accessibility_config"/>
</service>

4 开启辅助模式

默认为关闭状态,打开它的时候,你会收到一个警告弹窗,说明当前你正在开启一个无障碍的服务

编写逻辑代码

1 判断事件

  • eventType 判断事件类型(通过 eventType 来判断事件的类型,我们可以利用 getEventType() 方法获取到它。)
  • packageName 判断事件发生的 App(通过 getPackageName() 方法,判断出事件发生在那个 App 里的。)
  • className 判断当前发生事件的是那个类(通过 getClassName() 判断当前发生事件的是那个类)
  • text 判断当前事件触发源上的 Text(通过 getText() 获取当前事件源的 text 属性,可能是 TextView 的 Text,也可能是 Activity 的 Label 属性)

2 找到待控制的关键节点(Node)

通常我们是使用辅助模式去操作页面上的某个元素,那这一步,就是为了找到它。

在辅助模式下,页面上的每个元素,其实都是一个个 AccessibilityNodeInfo 节点,它是一个类似树形的结构,其内和我们真实 App 内的布局层级是一致的,但是并不能将它单纯的理解成一个 ViewTree。

既然是树形结构,我们首先要获取到根节点的 NodeInfo,可以通过以下两个方式获取:

event.getSource()
getRootInActiveWindow()

这两个方法都会返回一个 AccessibilityNodeInfo 对象。getSource() 是AccessibilityEvent 的方法,它可用的前提是前面配置 android:canRetrieveWindowContent 的时候,被设置为 True。所以我推荐使用 getRootInActiveWindow() 方法来获取。这两个方法还是略微有些差异,有兴趣可以打断点看看信息,但是大多数情况下,对我们使用者来说是一致的。

获得根节点的 AccessibilityNodeInfo 之后,就可以通过它找到我们想操作的关键节点,在 AccessibilityNodeInfo 中,提供了以下两个方法来找到关键节点。

findAccessibilityNodeInfosByViewId(String viewId)
findAccessibilityNodeInfosByText(String text)

一个是依赖 ViewId,另外一个是依赖 Text 信息。

使用 ViewId 查找关键节点是稳妥的方案,而使用 Text 去查找,可能会找不到。

无论通过哪种方式查找 关键节点 ,都是存在能找到多个 NodeInfo 的可能的,所以这两个方法干脆的都返回了一个 List ,所以需要我们通过其他条件再过滤一遍,通常就是通过 Text 信息过滤。

var mNodeInfo = rootInActiveWindow
var listItem = mNodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/lp")
for (item in listItem) {
    if (item.text.toString().equals("承香墨影")){
        nodeClick(item)
    }
}

如果是使用 findXxxByText() 的方法的话,还需要注意它实际上不是通过类似 == 或者 equals() 的方法来查找子节点的,而是通过类似 contain() 的方式,所以只要节点的 text 属性包含查找的内容,都会被找到,这个我们额外还需要增加判断条件。

如果这些方法都试过,还是找不到关键节点,可以通过遍历的方式查找。

AccessibilityNodeInfo 既然是一个树状结构,也提供了我们遍历树的方法。

getParent():查找父节点。
getChild():返回子节点。
getChildCount():当前节点的子节点个数。
通过 getChild() 和 getChildCount() 两个方法,我们是可以对整个 ViewNodeTree 进行遍历,来找到我们关注的关键节点,这是一个最后的方案,并不推荐使用。

3 触发事件

辅助模式一般都是帮助我们响应一些事件,而这些事件大体上,可以分为两类。

  • 全局系统事件。
    对于全局系统事件,其实我们并不需要第二步找到的关键节点。AccessibilityService 提供了一个 performGlobalAction() 方法,我们可以通过该方法,操作一些全局的系统事件,例如:模拟返回键点击、模拟 HOME 键点击、锁屏等等。
// 返回键
performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK);
// HOME键
performGlobalAction(AccessibilityService.GLOBAL_ACTION_HOME);

除了全局系统事件之外,通常我们就是想操作第二步拿到的关键节点。

在 AccessibilityNodeInfo 中,提供了一个 performAction() 的方法,可以通过该方法,对关键节点传递一个我们需要的事件。

这些事件都被定义在 AccessibilityNodeInfo 中,以 ACTION_ 为前缀定义。例如:ACTION_CLICK 是一个点击事件,ACTION_SET_TEXT 设置一个输入。

这里仅介绍一些比较常见的操作,更多的操作也是类似的使用方式。

  • View 事件。
    找到关键节点之后,就可以发送 AccessibilityNodeInfo.ACTION_CLICK 模拟对这个 View 的点击操作。
  1. EditText 输入文字
    对 EditText 输入文字,最少需要两个参数,关键节点和输入的文字。这就需要用到 performAction() 的另外一个重载方法,它允许额外在传递一个 Bundle 来指定参数。
private fun nodeSetText(node : AccessibilityNodeInfo?,text:String){
    var argument = Bundle()
    argument.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,text)
    node?.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT,argument)
}

所有支持定义的额外参数,都被定义在 AccessibilityNodeInfo 中,并以 ACTION_ARGUMENT_ 为前缀定义。
3. ListView 的滚动
AccessibilityNodeInfo 其实只能操作当前屏幕下可见的 节点,所以碰上 ListView 或者 RecycleView 这种列表,就需要对它进行滚动。

滚动的事件有两种:

  • ACTION_SCROLL_FORWARD
  • ACTION_SCROLL_BACKWARD
private fun nodeScrollList(node : AccessibilityNodeInfo?){
    node?.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)
}

一个前进一个后退,足够使用了。

4 回收资源

nodeInfo.recycle();
  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值