上一篇介绍了的这个系列的背景【玩转Android无障碍】之序言接下来就开始一步一步实现吧
工欲善其事必先利其器
- 接触Android无障碍(
AccessibilityService
)功能开发首先遇到的问题就是如何获取页面上的元素信息,只有拿到元素唯一标识信息(比如元素id或者text)才能通过调用系统的findAccessibilityNodeInfosByViewId()
和findAccessibilityNodeInfosByText()
去获取这个元素,然后对这个元素进行相关操作,比如常见的点击某个元素、获取某个元素的内容、滑动一下屏幕等类似的操作,通过这些操作就能组合成一条完整的业务流程。
如何获取元素
-
常规获取页面元素的方式是使用Android Studio的
LayoutInspector
进行查看页面元素信息,但是这种方式有一个致命的问题就是APP要开启debug模式才可以获取到页面元素,所以要想分析其他市场上的APP是行不通的(不考虑模拟器或者root机),当然在历史的长河中还存在过Android Device Monitor
和UI Automator Viewer
,这两个工具也可以获取到节点元素信息,不过基本上算是被淘汰了,这里就不展开讲了,感兴趣的可以网上搜搜有很多现成的资料 -
本文要介绍的是直接通过代码获取当前屏幕窗口的根节点,然后一层一层的遍历,最后打印出自己需要的相对重要的节点信息即可,有了这么一套工具之后就可以对你想处理的页面可以很方便的获取页面元素,让自己只专注于具体的业务流程开发就可以了,可以节约大部分查找页面元素的时间。
具体实现
实现思路
AccessibilityService
类中有个getRootInActiveWindow()
方法,他可以获取到当前活动窗口的根节点,返回的是AccessibilityNodeInfo
对象,然后可以拿到它的childCount
,通过循环遍历就可以查找到所有的子节点了,节点包含的信息很多,我们一般需要是className
、text
、viewIdResourceName
、description
、isClickable
、isScrollable
、isEditable
这些基础数据,为了方便打印出我们需要的数据,首先定义一个包装类NodeWrapper
,重写它的toString()
方法,这样就可以按照既定的格式输出一些我们需要的信息了
kotlin
data class NodeWrapper(
var className: String,
var text: String? = null,
var id: String? = null,
var description: String? = null,
var isClickable: Boolean = false,
var isScrollable: Boolean = false,
var isEditable: Boolean = false,
var nodeInfo: AccessibilityNodeInfo? = null
) {
override fun toString() = "className = $className → text = $text → id = $id → description = $description → isClickable = $isClickable → isScrollable = $isScrollable → isEditable = $isEditable"
}
代码设计
- 我们把这个打印节点信息的方法定义为
printNodeInfo()
,因为rootInActiveWindow()
返回是AccessibilityNodeInfo
对象,所以为了方便调用我们定义一个AccessibilityNodeInfo
类的扩展方法,大致内容如下:
kotlin
fun AccessibilityNodeInfo?.printNodeInfo() {
val node = this ?: return
//选择我们需要的信息包装起来,便于打印
val nodeWrapper = NodeWrapper(
className = node.className.default(),
text = node.text.default(),
id = node.viewIdResourceName.default(),
description = node.contentDescription.default(),
isClickable = node.isClickable,
isScrollable = node.isScrollable,
isEditable = node.isEditable,
nodeInfo = node
)
//打印我们需要的信息
Log.d("printNodeInfo", nodeWrapper.toString())
val size = node.childCount
if (size > 0) {
//通过递归调用去打印子节点的信息
for (index in 0 until size) {
node.getChild(index).printNodeInfo()
}
}
}
如何使用
- 首先需要定义一个继承
AccessibilityService
类的子类TestAccessibilityService
类,重写onCreate()
方法,在里边进行初始化赋值给我们定义的全局变量testAccessibilityService = this
kotlin
companion object {
var testAccessibilityService: AccessibilityService? = null
}
override fun onCreate() {
super.onCreate()
testAccessibilityService = this
}
override fun onDestroy() {
testAccessibilityService = null
super.onDestroy()
}
- 然后在需要的地方就可以调用
testAccessibilityService?.rootInActiveWindow?.printNodeInfo()
进行打印节点信息了。先来看下效果吧
ini
className = android.widget.FrameLayout → text = → id = → description = → clickable = false → scrollable = false → editable = false
className = android.widget.LinearLayout → text = → id = → description = → clickable = false → scrollable = false → editable = false
className = android.widget.FrameLayout → text = → id = → description = → clickable = false → scrollable = false → editable = false
className = android.widget.LinearLayout → text = → id = com.android.wechat.tools:id/action_bar_root → description = → clickable = false → scrollable = false → editable = false
className = android.widget.FrameLayout → text = → id = android:id/content → description = → clickable = false → scrollable = false → editable = false
className = android.view.ViewGroup → text = → id = → description = → clickable = false → scrollable = false → editable = false
className = android.widget.LinearLayout → text = → id = → description = → clickable = false → scrollable = false → editable = false
className = android.view.ViewGroup → text = → id = com.android.wechat.tools:id/toolbar → description = → clickable = false → scrollable = false → editable = false
className = android.widget.TextView → text = 微信自动化工具 → id = → description = → clickable = false → scrollable = false → editable = false
className = androidx.appcompat.widget.LinearLayoutCompat → text = → id = → description = → clickable = false → scrollable = false → editable = false
className = android.view.ViewGroup → text = → id = → description = → clickable = false → scrollable = false → editable = false
className = android.widget.FrameLayout → text = → id = com.android.wechat.tools:id/nav_host_fragment_content_main → description = → clickable = false → scrollable = false → editable = false
className = android.view.ViewGroup → text = → id = → description = → clickable = false → scrollable = false → editable = false
className = android.widget.Button → text = 无障碍服务已开启 → id = com.android.wechat.tools:id/btn_open_service → description = → clickable = true → scrollable = false → editable = false
className = android.widget.Button → text = 页面元素检测工具 → id = com.android.wechat.tools:id/btn_print_node → description = → clickable = true → scrollable = false → editable = false
className = android.widget.Button → text = 获取微信好友列表 → id = com.android.wechat.tools:id/btn_get_friend_list → description = → clickable = true → scrollable = false → editable = false
className = android.widget.Button → text = 一键检测《通过假转账方式》 → id = com.android.wechat.tools:id/btn_check → description = → clickable = true → scrollable = false → editable = false
className = android.widget.Button → text = 一键检测《通过拉群方式》 → id = com.android.wechat.tools:id/btn_check_by_group → description = → clickable = true → scrollable = false → editable = false
className = android.widget.Button → text = 微信自动抢红包 → id = com.android.wechat.tools:id/btn_wx_auto_hb → description = → clickable = true → scrollable = false → editable = false
className = android.widget.Button → text = 微信自动回复消息 → id = com.android.wechat.tools:id/btn_wx_auto_reply → description = → clickable = true → scrollable = false → editable = false
className = android.widget.TextView → text = → id = com.android.wechat.tools:id/tv_task_des → description = → clickable = false → scrollable = false → editable = false
className = androidx.recyclerview.widget.RecyclerView → text = → id = com.android.wechat.tools:id/recycler_view → description = → clickable = false → scrollable = false → editable = false
className = android.view.View → text = → id = android:id/statusBarBackground → description = → clickable = false → scrollable = false → editable = false
- 可以看到当前窗口的元素都已经被详细的打印出来了,不过这样打印出来看起来怪怪的,总感觉是哪里有问题,看了半天发现,输出的节点都是同一级的,看不出来父子元素的关系,这样就会出现我们想要查看某个元素的父节点的时候就特别费劲,如果页面简单还好,稍微复杂点的页面能把人看懵逼。那么该怎么去展示父子节点的关系呐,突然想到我们gradle中有个
dependencies
task,这个task可以打印出出我们所依赖库中的具体依赖关系,也就是我们需要的父子关系,我们可以先看一下它输出的是什么样的。
lua
+--- androidx.core:core-ktx:1.7.0
| +--- org.jetbrains.kotlin:kotlin-stdlib:1.5.31 -> 1.7.10 (*)
| +--- androidx.annotation:annotation:1.1.0 -> 1.3.0
| \--- androidx.core:core:1.7.0
| +--- androidx.annotation:annotation:1.2.0 -> 1.3.0
| +--- androidx.annotation:annotation-experimental:1.1.0
| +--- androidx.lifecycle:lifecycle-runtime:2.3.1
| | +--- androidx.lifecycle:lifecycle-common:2.3.1
| | | \--- androidx.annotation:annotation:1.1.0 -> 1.3.0
| | +--- androidx.arch.core:core-common:2.1.0
| | | \--- androidx.annotation:annotation:1.1.0 -> 1.3.0
| | \--- androidx.annotation:annotation:1.1.0 -> 1.3.0
| \--- androidx.versionedparcelable:versionedparcelable:1.1.1
| +--- androidx.annotation:annotation:1.1.0 -> 1.3.0
| \--- androidx.collection:collection:1.0.0 -> 1.1.0
| \--- androidx.annotation:annotation:1.1.0 -> 1.3.0
可以清晰的看出层级关系,这不这个是我们需要的吗,先来分析一下他的结构欧,根节点+---
开始标记,每个子节点前加个|
区分,然后加几个空格让他出现父子关系的样子,如果子节点是最后一个再加个\---
表示当前是层级的最后一个节点,以此类推就出现上边展示的输出格式了,接下来看看具体代码吧
ini
fun AccessibilityNodeInfo?.printNodeInfo(prefix: String = "", isLast: Boolean = false) {
val node = this ?: return
val nodeWrapper = NodeWrapper(
text = node.text.default(),
id = node.viewIdResourceName.default(),
className = node.className.default(),
description = node.contentDescription.default(),
isClickable = node.isClickable,
isScrollable = node.isScrollable,
isEditable = node.isEditable,
nodeInfo = node
)
val marker = if (isLast) """\--- """ else "+--- "
val currentPrefix = "$prefix$marker"
Log.d("printNodeInfo", currentPrefix + nodeWrapper.toString())
val size = node.childCount
if (size > 0) {
val childPrefix = prefix + if (isLast) " " else "| "
val lastChildIndex = size - 1
for (index in 0 until size) {
val isLastChild = index == lastChildIndex
node.getChild(index).printNodeInfo(childPrefix, isLastChild)
}
}
}
代码不复杂,只是在原来代码的基础上多了几个前缀字符的标记而已,现在再打印一下同一个页面信息看看效果吧
ini
+--- className = android.widget.FrameLayout → text = → id = → description = → isClickable = false → isScrollable = false → isEditable = false
| +--- className = android.widget.LinearLayout → text = → id = → description = → isClickable = false → isScrollable = false → isEditable = false
| | \--- className = android.widget.FrameLayout → text = → id = → description = → isClickable = false → isScrollable = false → isEditable = false
| | \--- className = android.widget.LinearLayout → text = → id = com.android.wechat.tools:id/action_bar_root → description = → isClickable = false → isScrollable = false → isEditable = false
| | \--- className = android.widget.FrameLayout → text = → id = android:id/content → description = → isClickable = false → isScrollable = false → isEditable = false
| | \--- className = android.view.ViewGroup → text = → id = → description = → isClickable = false → isScrollable = false → isEditable = false
| | +--- className = android.widget.LinearLayout → text = → id = → description = → isClickable = false → isScrollable = false → isEditable = false
| | | \--- className = android.view.ViewGroup → text = → id = com.android.wechat.tools:id/toolbar → description = → isClickable = false → isScrollable = false → isEditable = false
| | | +--- className = android.widget.TextView → text = 微信自动化工具 → id = → description = → isClickable = false → isScrollable = false → isEditable = false
| | | \--- className = androidx.appcompat.widget.LinearLayoutCompat → text = → id = → description = → isClickable = false → isScrollable = false → isEditable = false
| | \--- className = android.view.ViewGroup → text = → id = → description = → isClickable = false → isScrollable = false → isEditable = false
| | \--- className = android.widget.FrameLayout → text = → id = com.android.wechat.tools:id/nav_host_fragment_content_main → description = → isClickable = false → isScrollable = false → isEditable = false
| | \--- className = android.view.ViewGroup → text = → id = → description = → isClickable = false → isScrollable = false → isEditable = false
| | +--- className = android.widget.Button → text = 无障碍服务已开启 → id = com.android.wechat.tools:id/btn_open_service → description = → isClickable = true → isScrollable = false → isEditable = false
| | +--- className = android.widget.Button → text = 页面元素检测工具 → id = com.android.wechat.tools:id/btn_print_node → description = → isClickable = true → isScrollable = false → isEditable = false
| | +--- className = android.widget.Button → text = 获取微信好友列表 → id = com.android.wechat.tools:id/btn_get_friend_list → description = → isClickable = true → isScrollable = false → isEditable = false
| | +--- className = android.widget.Button → text = 一键检测《通过假转账方式》 → id = com.android.wechat.tools:id/btn_check → description = → isClickable = true → isScrollable = false → isEditable = false
| | +--- className = android.widget.Button → text = 一键检测《通过拉群方式》 → id = com.android.wechat.tools:id/btn_check_by_group → description = → isClickable = true → isScrollable = false → isEditable = false
| | +--- className = android.widget.Button → text = 微信自动抢红包 → id = com.android.wechat.tools:id/btn_wx_auto_hb → description = → isClickable = true → isScrollable = false → isEditable = false
| | +--- className = android.widget.Button → text = 微信自动回复消息 → id = com.android.wechat.tools:id/btn_wx_auto_reply → description = → isClickable = true → isScrollable = false → isEditable = false
| | +--- className = android.widget.TextView → text = → id = com.android.wechat.tools:id/tv_task_des → description = → isClickable = false → isScrollable = false → isEditable = false
| | \--- className = androidx.recyclerview.widget.RecyclerView → text = → id = com.android.wechat.tools:id/recycler_view → description = → isClickable = false → isScrollable = false → isEditable = false
| \--- className = android.view.View → text = → id = android:id/statusBarBackground → description = → isClickable = false → isScrollable = false → isEditable = false
哈哈,是不是瞬间好看多了,各个父子关系可以清晰的看出来,同级元素页可以一眼看出来了
- 因为
getRootInActiveWindow()
是在AccessibilityService
类中才有的方法,每次调用需要写一些重复性的代码,所有我们再定义一个AccessibilityService
类的扩展方法,以后直接调用xxx.printNodeInfo()
就可以了
kotlin
fun AccessibilityService?.printNodeInfo() {
this ?: return
rootInActiveWindow.printNodeInfo()
}
拓展
- 虽然打印的代码部分完成了,这个时候又引出一个问题,比如我们想在微信里边看某个页面的节点信息怎么办呐,总不能每次都编译一遍代码去执行打印节点的方法吧,我们期望的是在任何页面都可以随时随地的打印,如何在微信页面触发我们的
printNodeInfo()
方法呐,自然而然的想到用悬浮窗去实现了,悬浮窗可以浮在任何页面之上,我们只需点一下悬浮窗,然它获取AccessibilityService
对象并调一下printNodeInfo()
方法就可以了,关于Android悬浮窗这里就不做过多讲解了,因为不是本文的重点,在网上找了优秀的开源库直接使用就可以了。我们看一下微信的页面信息吧
ini
+--- className = android.widget.FrameLayout → text = → id = → description = → isClickable = false → isScrollable = false → isEditable = false
| \--- className = android.widget.FrameLayout → text = → id = com.tencent.mm:id/fkh → description = → isClickable = false → isScrollable = false → isEditable = false
| \--- className = android.widget.RelativeLayout → text = → id = com.tencent.mm:id/gv5 → description = → isClickable = false → isScrollable = false → isEditable = false
| \--- className = android.widget.LinearLayout → text = → id = com.tencent.mm:id/gv4 → description = → isClickable = false → isScrollable = false → isEditable = false
| +--- className = android.widget.RelativeLayout → text = → id = → description = → isClickable = false → isScrollable = false → isEditable = false
| | +--- className = android.widget.ListView → text = → id = android:id/list → description = → isClickable = false → isScrollable = false → isEditable = false
| | | +--- className = android.widget.LinearLayout → text = → id = com.tencent.mm:id/l49 → description = → isClickable = true → isScrollable = false → isEditable = false
| | | | +--- className = android.widget.ImageView → text = → id = → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | +--- className = android.widget.TextView → text = 微信 → id = com.tencent.mm:id/b7 → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | \--- className = android.widget.TextView → text = Version 8.0.40 → id = com.tencent.mm:id/b6 → description = → isClickable = true → isScrollable = false → isEditable = false
| | | +--- className = android.widget.LinearLayout → text = → id = com.tencent.mm:id/iwg → description = → isClickable = true → isScrollable = false → isEditable = false
| | | | +--- className = android.view.View → text = → id = com.tencent.mm:id/krm → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | \--- className = android.widget.LinearLayout → text = → id = → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | +--- className = android.widget.LinearLayout → text = → id = com.tencent.mm:id/br8 → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | | \--- className = android.widget.LinearLayout → text = → id = → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | | \--- className = android.widget.LinearLayout → text = → id = com.tencent.mm:id/gv6 → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | | \--- className = android.widget.LinearLayout → text = → id = com.tencent.mm:id/kp3 → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | | +--- className = android.widget.LinearLayout → text = → id = → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | | | \--- className = android.widget.LinearLayout → text = → id = → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | | | \--- className = android.widget.TextView → text = 功能介绍 → id = android:id/title → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | | \--- className = android.widget.LinearLayout → text = → id = → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | \--- className = android.widget.LinearLayout → text = → id = com.tencent.mm:id/its → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | \--- className = android.widget.ImageView → text = → id = com.tencent.mm:id/isy → description = → isClickable = false → isScrollable = false → isEditable = false
| | | +--- className = android.widget.LinearLayout → text = → id = com.tencent.mm:id/iwg → description = → isClickable = true → isScrollable = false → isEditable = false
| | | | \--- className = android.widget.LinearLayout → text = → id = → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | +--- className = android.widget.LinearLayout → text = → id = com.tencent.mm:id/br8 → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | | \--- className = android.widget.LinearLayout → text = → id = com.tencent.mm:id/kp3 → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | | \--- className = android.widget.LinearLayout → text = → id = → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | | \--- className = android.widget.LinearLayout → text = → id = → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | | \--- className = android.widget.TextView → text = 投诉 → id = android:id/title → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | \--- className = android.widget.LinearLayout → text = → id = com.tencent.mm:id/its → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | \--- className = android.widget.ImageView → text = → id = com.tencent.mm:id/isy → description = → isClickable = false → isScrollable = false → isEditable = false
| | | +--- className = android.widget.LinearLayout → text = → id = com.tencent.mm:id/iwg → description = → isClickable = true → isScrollable = false → isEditable = false
| | | | \--- className = android.widget.LinearLayout → text = → id = → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | +--- className = android.widget.LinearLayout → text = → id = com.tencent.mm:id/br8 → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | | \--- className = android.widget.LinearLayout → text = → id = → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | | \--- className = android.widget.LinearLayout → text = → id = com.tencent.mm:id/gv6 → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | | \--- className = android.widget.LinearLayout → text = → id = com.tencent.mm:id/kp3 → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | | +--- className = android.widget.LinearLayout → text = → id = → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | | | \--- className = android.widget.LinearLayout → text = → id = → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | | | \--- className = android.widget.TextView → text = 检查新版本 → id = android:id/title → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | | \--- className = android.widget.LinearLayout → text = → id = → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | \--- className = android.widget.LinearLayout → text = → id = com.tencent.mm:id/its → description = → isClickable = false → isScrollable = false → isEditable = false
| | | | \--- className = android.widget.ImageView → text = → id = com.tencent.mm:id/isy → description = → isClickable = false → isScrollable = false → isEditable = false
| | | \--- className = android.widget.TextView → text = → id = android:id/title → description = → isClickable = true → isScrollable = false → isEditable = false
| | \--- className = android.view.View → text = → id = com.tencent.mm:id/e5c → description = → isClickable = false → isScrollable = false → isEditable = false
| \--- className = android.widget.FrameLayout → text = → id = com.tencent.mm:id/i1f → description = → isClickable = false → isScrollable = false → isEditable = false
| \--- className = android.widget.LinearLayout → text = → id = → description = → isClickable = false → isScrollable = false → isEditable = false
| +--- className = android.widget.TextView → text = 《软件许可及服务协议》 → id = com.tencent.mm:id/khc → description = → isClickable = true → isScrollable = false → isEditable = false
| +--- className = android.widget.LinearLayout → text = → id = → description = → isClickable = false → isScrollable = false → isEditable = false
| | +--- className = android.widget.TextView → text = 《隐私保护指引摘要》 → id = com.tencent.mm:id/khb → description = → isClickable = true → isScrollable = false → isEditable = false
| | \--- className = android.widget.TextView → text = 《隐私保护指引》 → id = com.tencent.mm:id/kha → description = → isClickable = true → isScrollable = false → isEditable = false
| +--- className = android.widget.TextView → text = 客服电话:400 670 0700 → id = com.tencent.mm:id/exh → description = → isClickable = false → isScrollable = false → isEditable = false
| \--- className = android.widget.TextView → text = 腾讯公司 版权所有
最后
-
这样我们的元素检测小工具就算全部写完了,以后想看某个页面查看节点信息就可以直接打开我们的APP,开启无障碍服务后,启动悬浮窗,打开指定APP某个页面,点一下悬浮窗就可以在控制台看到美化后的具体的节点信息了。
-
后续如何有需求我们可以把打印的节点信息直接在APP中查看,但是看出节点这种事情还是在PC端大屏上看着才舒服,所以暂时就不展示在手机端喽
-
刀已磨好,接下来就开始在微信中实操吧
预告
下一篇让我们期待一下【玩转Android无障碍】之小试牛刀,通过一个简单的例子带大家实操一下