业余时间了解了Android无障碍服务的一些有趣功能,比如微信自动抢红包、应用宝的一键安装功能等。大致原理是监听手机窗体内容变化,拿到对应的View,进行点击、长按等Touch操作,下面我们就借助 AccessibilityService 这个服务类实现模拟点击功能。
效果
一、创建 MyAccessibilityService
1、继承AccessibilityService,重写 onAccessibilityEvent
class MyAccessibilityService : AccessibilityService() {
override fun onAccessibilityEvent(event: AccessibilityEvent) {
//获取指定包名应用
val packageName = event.packageName
//只使用界面变化的监听,避免点击事件监听进入死循环
if(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED != event.eventType){
return
}
if ("com.yufs.accessibility" == packageName) {
LogUtils.e("Thread:${Thread.currentThread().name},event:${event}")
//找到对应node,开始点击
val nodeInfo = AsUtils.findNodeInfo(
this,
"com.yufs.accessibility:id/btn_click_node",
"节点模拟点击",
""
)
nodeInfo?.let {
thread {
LogUtils.e("找到节点,三秒后执行点击事件")
Thread.sleep(3000)
AsUtils.performClickNodeInfo(it)
}
}
}
}
override fun onInterrupt() {
LogUtils.e("onInterrupt==>")
}
/**
* 服务连接成功
*/
override fun onServiceConnected() {
super.onServiceConnected()
thread {
//便于设置完成后返回来看到显示效果
Thread.sleep(5000)
LogUtils.e("坐标点击:500,515")
AsUtils.click(this, 507f, 512f)
}
}
}
onAccessibilityEvent 是最重要的方法,所有界面的操作事件都会回调此方法,不限于当前应用,可以监听手机上其他应用的界面。例如界面变化时,我们打印AccessibilityEvent的信息,可以看到基本的包信息、类名等
2、 注册服务
<service
android:name=".service.MyAccessibilityService"
android:description="@string/description_in_xml"
android:exported="true"
android:enabled="true"
android:label="@string/app_name"
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_config" />
</service>
3、服务的配置文件 accessibility_config.xml
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackGeneric"
android:canPerformGestures="true"
android:canRetrieveWindowContent="true"
android:canRequestTouchExplorationMode="true"
android:description="@string/description_in_xml"
android:settingsActivity="com.yufs.accessibility.MainActivity"
android:notificationTimeout="100" />
<!--
accessibilityEventTypes:可以接收的事件类型,例如界面变化、点击等。typeAllMask 接收所有,根据实际情况选择合适的类型,减少电量的消耗
accessibilityFeedbackType:事件的反馈类型,暂时不知道用作啥
canPerformGestures:是否支持手势
canRetrieveWindowContent:是否允许读取窗口中的内容
canRequestTouchExplorationMode:在这种模式下,被触摸的项目会被大声说出,用户界面可以被激活通过手势探索
description:服务的描述文字
settingsActivity:开启服务界面显示一个设置按钮,可以返回应用指定界面
notificationTimeout:事件的发送间隔事件,单位毫秒
-->
二、实现点击
1、坐标点击:我们可以打开手机的指针位置,查看按钮的坐标
2、坐标点击代码
/**
* 坐标模拟点击:最低api24,即要求Android7.0以上
*/
fun click(accessibilityService: AccessibilityService, x: Float, y: Float) {
val builder = GestureDescription.Builder()
val path = Path()
path.moveTo(x, y)
path.lineTo(x, y)
builder.addStroke(GestureDescription.StrokeDescription(path, 0, 1))
val gesture = builder.build()
accessibilityService.dispatchGesture(
gesture,
object : AccessibilityService.GestureResultCallback() {
override fun onCancelled(gestureDescription: GestureDescription) {
super.onCancelled(gestureDescription)
}
override fun onCompleted(gestureDescription: GestureDescription) {
super.onCompleted(gestureDescription)
}
},
null
)
}
3、根据id查找View
获取应用中view的id可以通过Android SDK中monitor.bat查看,双击运行
拿到resouce id 就可以查找view
/**
* 查找节点信息
*
* @param id
* @param text
* @param contentDescription
* @return null表示未找到
*/
fun findNodeInfo(
service: AccessibilityService,
id: String,
text: String,
contentDescription: String
): AccessibilityNodeInfo? {
if (TextUtils.isEmpty(text) && TextUtils.isEmpty(contentDescription)) {
return null
}
val nodeInfo = service.rootInActiveWindow
if (nodeInfo != null) {
val list = nodeInfo.findAccessibilityNodeInfosByViewId(id)
for (n in list) {
val nodeInfoText =
if (TextUtils.isEmpty(n.text)) "" else n.text
.toString()
val nodeContentDescription =
if (TextUtils.isEmpty(n.contentDescription)) "" else n.contentDescription
.toString()
if (TextUtils.isEmpty(text)) {
if (contentDescription == nodeContentDescription) {
return n
}
} else {
if (text == nodeInfoText) {
return n
}
}
}
}
return null
}
4、View节点点击
/**
* 点击节点
* @return true表示点击成功
*/
fun performClickNodeInfo(nodeInfo: AccessibilityNodeInfo?): Boolean {
if (nodeInfo != null) {
if (nodeInfo.isClickable) {
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK)
return true
} else {
val parent = nodeInfo.parent
if (parent != null) {
val isParentClickSuccess = performClickNodeInfo(parent)
parent.recycle()
return isParentClickSuccess
}
}
}
return false
}
三、源代码
代码下载
四、总结
通过学习无障碍服务可以实现很多重复性操作,方便用户解决更多的问题,