九.无障碍服务AccessibilityService实现自动化
前言
在上一节中,我们对AccessibilityService
已经做了一个详细的讲解。需要了解的同学可跳转:
Android无障碍服务AccessibilityService详解
利用AccessibilityService
可实现界面的点击
,输入
和滑动
等。那么这样的话,我们就可以用AccessibilityService
来对其他app做简单控制。在日常开发中,我们有时开发了一个app
急需做功能测试,有了AccessibilityService
,我们便可以对一款app
做些简单的自动化测试了。这里我封装了一个无障碍服务
的帮助类——AccessibilityHelper
,主要用于快速实现界面的点击
,输入
和滑动
等操作,为咱们的自动化测试提供快捷方式,这样我们在写自动化测试脚本时,就能只专注测试流程的编写了。
今天涉及知识有:
- 为啥封装一个AccessibilityHelper
- AccessibilityHelper方法简介
- 如何最大限度的让界面某个操作生效
- AccessibilityHelper在自定义AccessibilityService中的简单使用
- 效果图和项目结构图
- AccessibilityHelper源码
先来波效果图
效果图.gif
一. 为啥封装一个AccessibilityHelper
为啥封装一个AccessibilityHelper
?多数对于AccessibilityService
介绍的文章均停留在Android无障碍服务AccessibilityService详解文章的深度,至于如何找到界面的控件,如何实现界面该按钮的点击
,输入
和滑动
等均是绝口不提的,我在研究这玩意的时候,倒是浪费了不少时间。在网上查找,看官网文档,还是有很多不解,于是在一个开发群中请教一位大佬,人家只回复了寥寥数字:这还不简单
,遂接着问具体如何实现呢,给我的回答是两字: 鸡儿
。但是从对AccessibilityService
基础的了解,到如何找到界面控件和实现该控件的具体操作(点击
,输入
和滑动
等)仍存在一段距离,于是AccessibilityHelper
应运而生。AccessibilityHelper
可以帮我们很快的定位到界面上某个控件,也能帮助我们很快的实现该控件的操作(点击
,输入
和滑动
等),让你不再烦恼如何才能找到一个界面上的某个控件,如何实现点击,滑动等,而是专注于业务流程(先执行点击跳到xxx
界面,再滑动该界面...).这便是AccessibilityHelper
存在的意义。既然如此,那么就让我们来看看AccessibilityHelper
有哪些主要方法吧。
二. AccessibilityHelper方法简介
AccessibilityHelper
作为一个单例工具类,具备以下主要方法:
/**点击动作**/
public boolean performClick(AccessibilityNodeInfo targetInfo)
/**点击返回键**/
public boolean clickBackKey(AccessibilityService service)
/**点击Home键**/
public boolean clickHomeKey(AccessibilityService service)
/**点击最近任务**/
public boolean clickLastTaskKey(AccessibilityService service)
/**点击通知栏**/
public boolean clickNotificationKey(AccessibilityService service)
/***
* 根据控件显示内容text找到的控件是否存在
*
* 界面中可能出现多个控件显示同样的内容,则根据text获取的控件不止一个
* 这时,则需要控件id做辅助筛选,当无viewId做筛选条件时,默认取找到第一个含内容的view返回
*
* @param service AccessibilityService对象
* @param text 视图文字
* @param viewId 参数是 com.xxx.xxx:id/tv_main,必须要带上包名
* 当 viewId为null时,默认取找到第一个含内容的view作为查找的返回结果
*
* @return true:该控件存在 false:该控件不存在
*/
public boolean isExistViewByText(AccessibilityService service, String text,String viewId)
/***
* 根据控件ViewId找到的控件是否存在
*
* 当界面中是一个列表的时候,根据viewId查找可能会得到一个控件列表
* 而所要寻找的不一定是默认的第一项,这时则需要文字即text辅助查找
*
* @param service AccessibilityService对象
* @param viewId 参数是 com.xxx.xxx:id/tv_main,必须要带上包名
* @param text 控件上显示的内容,当text为null时,默认取根据id获取到的列表的第一个
*
* @return true:该控件存在 false:该控件不存在
*/
public boolean isExistViewById(AccessibilityService service, String viewId,String text)
/***
* 根据 EditText中的内容找到 EditText 对象,并改变EditText中的内容
*
* @param service AccessibilityService对象
* @param viewText EditText原来显示的内容
* @param viewId 参数是 com.xxx.xxx:id/tv_main,必须要带上包名
* 当 viewId为null时,默认取找到第一个含内容的view作为查找的返回结果
* @param message EditText中要设置的内容
* @return
*/
public boolean changeInputByViewText(AccessibilityService service, String viewText,String viewId,String message)
/***
* 根据 EditText中的ViewId找到 EditText 对象,并改变EditText中的内容
*
* @param service AccessibilityService对象
* @param viewId 参数是 com.xxx.xxx:id/tv_main,必须要带上包名
* @param viewText 控件上显示的内容,当text为null时,默认取根据id获取到的列表的第一个
* @param message EditText中要设置的内容
* @return
*/
public boolean changeInputByViewId(AccessibilityService service, String viewId,String viewText,String message)
/***
* 根据控件上显示的文字找到该控件,并执行点击事件
*
* 注:若该控件不可点击,则找其父控件甚至父级的父级...来执行点击
*
* 当界面中是一个列表的时候,根据viewId查找可能会得到一个控件列表
* 而所啊哟寻找的不一定是默认的第一项,这时则需要文字即text辅助查找
*
* @param service AccessibilityService对象
* @param text 控件上显示的内容
* @param viewId 参数是 com.xxx.xxx:id/tv_main,必须要带上包名
* 当 viewId为null时,默认取找到第一个含内容的view作为查找的返回结果
* @return
*/
public boolean performClickByText(AccessibilityService service, String text,String viewId)
/***
* 根据控件ViewId找到该控件,并执行点击事件
*
* 注:若该控件不可点击,则找其父控件甚至父级的父级...来执行点击
*
* 当界面中是一个列表的时候,根据viewId查找可能会得到一个控件列表
* 而所啊哟寻找的不一定是默认的第一项,这时则需要文字即text辅助查找
*
* @param service AccessibilityService对象
* @param viewId 参数是 com.xxx.xxx:id/tv_main,必须要带上包名
* @param text 控件上显示的内容,当text为null时,默认取根据id获取到的列表的第一个
*
* @return
*/
public boolean performClickById(AccessibilityService service, String viewId,String text)
/***
* 手势滑动
*
* 注: 开始滑动时间与滑动延长时间参考值如下:
* startTime=100L,
* duration=500L
*
* @param service AccessibilityService对象
* @param startX 起始 x 坐标
* @param startY 起始 Y 坐标
* @param endX 结束 X 坐标
* @param endY 结束 Y 坐标
* @param startTime 滑动开始时间(单位毫秒)
* @param duration 滑动持续时间(单位毫秒)
* @param callback 监听
* @return
*/
public boolean performGestureSliding(AccessibilityService service,
float startX,
float startY,
float endX,
float endY,
long startTime,
long duration,
AccessibilityService.GestureResultCallback callback)
/***
* 手势执行点击事件
*
* 注: 开始点击时间与点击延长时间参考值如下:
* startTime=50L,
* duration=500L
*
* @param service AccessibilityService对象
* @param x 点击屏幕的 x 坐标
* @param y 点击屏幕的 y 坐标
* @param startTime 滑动开始时间(单位毫秒)
* @param duration 滑动持续时间(单位毫秒)
* @param callback 监听
* @return
*/
public boolean performClickByGesture(AccessibilityService service,
float x, float y,
long startTime, long duration,
AccessibilityService.GestureResultCallback callback)
/***
* 线程休眠时间
*
* @param miao:double类型, 单位秒
*/
public void waitTime(double miao)
三. 如何最大限度的让界面某个操作生效
归根结底是要准确的找到界面上该控件,然后进行操作事件。由于一般app界面ui
设计的复杂性,可能一个界面有很多控件,列表,弹窗等。这使得要准确寻找到界面上某个控件异常困难,控件找不到,就更别谈操作了。
那么为了最大程度的找到某个特定控件,我们可以遵循以下规则(以点击动作为例):
- 能找到该控件的id的话,尽量用
performClickById(AccessibilityService service, String viewId,String text)
方法,若该控件有内容的话,也将内容作为参数传到该方法中 - 当控件没有
viewId
时,采用performClickByText(AccessibilityService service, String text,String viewId)
实现点击动作 - 当要找的控件既没
viewId
又没内容
时,采用performClickByGesture(AccessibilityService service, float x, float y, long startTime, long duration, AccessibilityService.GestureResultCallback callback)
实现点击。
如何打开要测试的app?
请参考Android实现打开第三方app
如何找到界面控件的viewId
,内容
或界面坐标
等信息?
请参考Android上DDMS的简单使用
四.AccessibilityHelper在自定义AccessibilityService中的简单使用
以本项目打开另一个项目kotlinTest
,并点击该项目中的测试按钮,使之显示文字为例
下面贴出AccessibilityHelper
在自定义无障碍服务MyService
中的使用代码:
/**
* Title:无障碍服务
*
* description:
* 这个服务是不需要你在activity里去开启的,属于系统级别辅助服务 需要在设置里去手动开启 和我们平常app里
* 经常使用的service 是有很大不同的 非常特殊
* 你可以在 \sdk\samples\android-23\legacy\ApiDemos 这样的目录下 找到这个工程 这个工程下面有一个accessibility
* 包 里面有关于这个服务的demo 当然他们那个demo 非常复杂,但是信息量很大,有兴趣深入研究的同学可以多看demo
* 我这里只实现最基本的功能 且没有做冗余和异常处理,只包含基础功能,不能作为实际业务上线!
*
* autor:pei
* created on 2021/4/7
*/
@RequiresApi(api = Build.VERSION_CODES.N)
public class MyService extends AccessibilityService {
public static final String TEST="com.kotlintest";
public static MyService mService;
private static int mCount=0;
/***
* AccessibilityService 这个服务可以关联很多属性,这些属性 一般可以通过代码在这个方法里进行设置,
* 我这里偷懒 把这些设置属性的流程用xml 写好 放在manifest里,如果你们要使用的时候需要区分版本号
* 做兼容,在老的版本里是无法通过xml进行引用的 只能在这个方法里手写那些属性 一定要注意.
* 同时你的业务如果很复杂比如需要初始化广播啊之类的工作 都可以在这个方法里写。
*/
@Override
protected void onServiceConnected() {
super.onServiceConnected();
LogUtil.i("====建立服务链接====");
}
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
LogUtil.i("====启动Event====");
//屏幕尺寸
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
int x = displayMetrics.widthPixels;
int y = displayMetrics.heightPixels;
LogUtil.i("======屏幕尺寸:x="+x+" y="+y);
mService=this;
if (event.getPackageName() == null) {
LogUtil.e("=======event.getPackageName()为空====");
return;
}
String packageName = event.getPackageName().toString();
if(StringUtil.isNotEmpty(packageName)){
switch (packageName) {
case TEST://测试demo
LogUtil.i("========测试demo=========");
doTask();
break;
default:
break;
}
}
}
@Override
public void onInterrupt() {
LogUtil.i("====无障碍服务要结束了====");
mService=null;
}
/**服务是否启动**/
public static boolean isStart(){
return mService!=null;
}
private void doTask(){
//延时一秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
LogUtil.i("=========mCount="+mCount);
switch (mCount) {
case 1://点击按钮
//点击测试按钮
AccessibilityHelper.getInstance().performClickById(mService,"com.kotlintest:id/mBtnTest",null);
break;
default:
break;
}
mCount++;
}
}
五.效果图和项目结构图
效果图.gif项目结构图
image.png
六. AccessibilityHelper源码
AccessibilityHelper
源码如下: