Android自动化之AccessibilityService

本文介绍了Android无障碍服务的使用,包括如何创建 AccessibilityService,监听和处理各种事件,如窗口状态变化、内容变化和通知。通过此类服务,可以实现辅助功能、自动化脚本或爬虫工具。文章提供了详细的代码示例,展示了如何点击、滑动和执行手势操作,并提及了焦点管理和连续手势支持。此外,还强调了无障碍服务的便捷设置和应用场景。
摘要由CSDN通过智能技术生成

功能介绍

无障碍服务是一种应用,可提供界面增强功能,来协助残障用户或可能暂时无法与设备进行全面互动的用户完成操作。例如,正在开车、照顾孩子或参加喧闹聚会的用户可能需要其他或替代的界面反馈方式。

AccessibilityService: 类

当AccessibilityEvent事件被启动后AccessibilityService 会接收回调函数运行于后台,这些事件指的是在用户接口间的状态转换,比如,焦点变化,按钮被点击等。

简单来说,通过继承于此类并且实现它的抽象方法,我们可以在service中接收到页面变动、页面组件变动和应用通知等消息,并做出响应的操作,并且可以通过该类获取到的视图节点做一些点击、滑动等操作,而且在android8.0之后支持连续手势。

借助此功能可以实现某某App的某某辅助、某某脚本、某某爬虫工具,官 方 wài guà 最 为 致 命。

快速开始

详细请看官方文档

1. 创建无障碍服务

创建一个扩展 AccessibilityService 的类

package com.example.android.apis.accessibility;

import android.accessibilityservice.AccessibilityService;
import android.view.accessibility.AccessibilityEvent;

public class MyAccessibilityService extends AccessibilityService {
    @Override
        public void onAccessibilityEvent(AccessibilityEvent event) {
        int eventType = event.getEventType();//事件类型
        CharSequence packageName = event.getPackageName();//触发事件的包名
        if (packageName == null) return;
        String packageNameString = packageName.toString();
        // 要过滤的包名
        if (!packageNameString.startsWith("com.tencent")) return;
        switch (eventType) {
            case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: {
                // 页面发生变化
                AccessibilityNodeInfo rootNode = getRootInActiveWindow();
                if (rootNode == null) return;
                curWin = event.getClassName().toString();
                Log.d(TAG, "Activity切换,当前窗体:" + curWin);
                break;
            }
            case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: {
                // 组件发生变化
                String className = event.getClassName().toString();
                Log.d(TAG, "Activity内容改变事件:" + className.toString());
                AccessibilityNodeInfo rootNode = getRootInActiveWindow();
                if (rootNode == null) return;
            }
            case AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED: {
                // 应用通知
                List<CharSequence> texts = event.getText();
                if (!texts.isEmpty()) {
                    for (CharSequence text : texts) {
                        String content = text.toString();
                        Log.d(TAG, "收到通知:" + content);
                    }
                }
                break;
            }
        }
    }

    @Override
    public void onInterrupt() {
    }
    
    @Override
    protected void onServiceConnected() {
        super.onServiceConnected();
        Toast.makeText(this, "xxx辅助功能已开启", Toast.LENGTH_SHORT).show();
    }
}

2. 清单声明、权限以及无障碍物服务配置

AndroidManifest.xml<application />中声明

<service android:name=".MyAccessibilityService"
        android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
        android:label="@string/accessibility_service_label">
      <intent-filter>
        <action android:name="android.accessibilityservice.AccessibilityService" />
      </intent-filter>
      <meta-data
        android:name="android.accessibilityservice"
        android:resource="@xml/robot_accessibility" />
</service>

其中@xml/robot_accessibility是无障碍服务的配置

在res中新建文件夹xml,在xml中创建robot_accessibility.xml
在这里插入图片描述

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackAllMask"
    android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows|flagIncludeNotImportantViews|flagRequestEnhancedWebAccessibility"
    android:canPerformGestures="true"
    android:canRetrieveWindowContent="true"
    android:notificationTimeout="300" />

打开辅助功能,运行应用,查看日志

使用手册

  1. 能有单独的音量设置
  2. 有快速开启关闭的快捷键(Android 8.0以上在虚拟导航栏有快捷设置)
  3. 指纹手势
  4. 多语言文字转语音
  5. 为用户执行操作 (更改输入焦点和选择、滚动列表、转到主屏幕、按“返回”按钮,以及打开通知屏幕和最近用过的应用列表等,所有可见元素都能由无障碍服务选择)
    // 封装一个点击节点的方法 (NodeID为自定义枚举类,getId得到的是字符串,可以直接用字符串)
    private boolean clickView(NodeID nodeId, int index) {
        AccessibilityNodeInfo rootNode = getRootInActiveWindow();
        if (rootNode == null) {
            List<AccessibilityWindowInfo> windows = getWindows();
            if (windows.size() != 0) {
                Log.d(TAG, "windows: " + windows);
                rootNode = windows.get(0).getRoot();
            }
            if (rootNode == null) {
                Log.d(TAG, "获取不到rootNode");
                return false;
            }
        }
        List<AccessibilityNodeInfo> nodes = rootNode.findAccessibilityNodeInfosByViewId(nodeId.getId());
        int size = nodes.size();
        if (size != 0) {
            AccessibilityNodeInfo node = nodes.get(index);
            Log.d(TAG, nodeId + " 共找到" + size + "个控件, 点击第" + (index + 1) + "个," + node);
            node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
            return true;
        }
        Log.d(TAG, nodeId + " 点击失败");
        return false;
    }
    // 当一些控件没有id,可以通过getChild或者getParent的方式拿到 返回为一个AccessibilityNodeInfo实例,此后可以调用方法进行点击、滑动或者获取它在屏幕中的位置
    private AccessibilityNodeInfo getViewChild(NodeID nodeId, int index, int childIndex) {
        AccessibilityNodeInfo rootNode = getRootInActiveWindow();
        if (rootNode == null) {
            List<AccessibilityWindowInfo> windows = getWindows();
            if (windows.size() != 0) {
                Log.d(TAG, "windows: " + windows);
                rootNode = windows.get(0).getRoot();
            }
            if (rootNode == null) {
                Log.d(TAG, "获取不到rootNode");
                return null;
            }
        }
        List<AccessibilityNodeInfo> nodes = rootNode.findAccessibilityNodeInfosByViewId(nodeId.getId());
        int size = nodes.size();
        if (size != 0) {
            AccessibilityNodeInfo node = nodes.get(index);
            Log.d(TAG, nodeId + " 共找到" + size + "个控件");
            AccessibilityNodeInfo nodeChild = node.getChild(childIndex);
            return nodeChild;
        }
        Log.d(TAG, nodeId + " childIndex: " + childIndex + " 点击失败");
        return null;
    }
  1. 监听手势
  2. 连续手势 (Android8.0以上)
    // 一个通过手势模拟点击的例子 rect为控件的位置 可以通过AccessibilityNodeInfo实例的getBoundsInScreen方法获取
    public void clickGesture(Rect rect) {
        Path path = new Path();
        path.moveTo(rect.centerX(), rect.centerY());
        GestureDescription.StrokeDescription strokeDescription;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            strokeDescription = new GestureDescription.StrokeDescription(path, 10, 10);
            GestureDescription.Builder gestureDescription = new GestureDescription.Builder().addStroke(strokeDescription);
            this.dispatchGesture(gestureDescription.build(), null, null);
        }
    }
    // 通过手势在x轴上滑动的例子 注意的距离可能需要通过dpi计算
    public void horizontalSlideGesture(Rect rect, int distanceX) {
        // 有需要请认真阅读Path类说明
        Path path = new Path();
        path.moveTo(rect.centerX(), rect.centerY());
        path.lineTo((float) (rect.centerX() + distanceX), rect.centerY());
        GestureDescription.StrokeDescription strokeDescription;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            strokeDescription = new GestureDescription.StrokeDescription(path, 0, 1500);
            GestureDescription.Builder gestureDescription = new GestureDescription.Builder().addStroke(strokeDescription);
            this.dispatchGesture(gestureDescription.build(), null, null);
        }
    }
  1. 使用焦点类型

无障碍服务可以使用 AccessibilityNodeInfo.findFocus() 方法来确定哪个界面元素具有输入焦点或无障碍功能焦点。您还可以使用 focusSearch() 方法搜索可通过输入焦点选择的元素。最后,无障碍服务可以使用 performAction(AccessibilityNodeInfo.ACTION_SET_ACCESSIBILITY_FOCUS) 方法设置无障碍功能焦点。

后记

本文仅个人记录使用,还有一些常用封装方法,仅供参考,并非最优解。
实际使用请通读官方文档

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

happy_plus

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值