视图搜索与操作示例

Posted by Vove on November 2, 2019

何时用到视图操作

  1. 当第三方应用提供Api时,可以很方便调用api 来完成指令。例如地图App(高德/百度地图)调用Api方法:导航指令源码
  2. 当第三方应用无提供api时,可能需要脚本来模拟手势操作界面,并且目前VAssistant包含很多视图操作完成的指令。本文来详细介绍V-Assist 视图搜索/操作Api的使用方法。

视图搜索

我们看到的屏幕上内容,包括文字、图片和按钮等,这里统称为视图节点(ViewNode)。在操作界面时,需要先找到目标节点,查找需要指定节点特征,比如可以根据节点id、节点文字和视图节点描述(desc)来搜索。

视图操作三步法

指定搜索条件 ===> 搜索 ===得到结果===> 操作视图

  • 普通点击:text('目标文本').await().tryClick()
  • 设置文本:id('editview').findFirst().text = '123'

idtext来筛选视图;awaitfindFirst来进行搜索;tryClicktext = '123'进行视图操作。

另外,三步法中的搜索可以省略,省略后的结果:text('目标文本').tryClick()在搜索条件后直接跟随操作代码,会默认等待搜索2s。

准备

  • VAssistant 版本需要 1.9.8.4+
  • 你可以使用Visual Studio Code配合vassist-debugger插件来远程调试(推荐),或者使用App自带的脚本编辑器来调试脚本。

vassist-debugger插件使用教程

例子引入

使用酷安更新全部应用为例

模拟步骤:打开酷安 -> 点击主页右上角应用管理 -> 点击'全部更新'
脚本:

1
2
3
4
5
6
-- 打开酷安
system.openAppByPkg('com.coolapk.market')
-- 点击主页右上角应用管理
id('menu_badge_icon').tryClick()
-- 点击 全部更新  
text('全部更新').tryClick()

可以看到脚本中使用了id('menu_badge_icon')text('全部更新')来确定目标视图。
text内容很好确定,那么id从何而来?

这里提供两种方式获取视图节点信息。

1. 使用V-Assist-Debugge插件

打开酷安首页。

然后点击下图图标输出布局。

可以得到类似下文内容(为了篇幅,省略了部分内容):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|-0 {class: FrameLayout, bounds: Rect(0, 0 - 720, 1280), childCount: 2}  
  |-0 {class: LinearLayout, bounds: Rect(0, 0 - 720, 1280), childCount: 1}  
    |-0 {class: FrameLayout, bounds: Rect(0, 0 - 720, 1280), childCount: 1}  
             ......
                  |-1 {class: FrameLayout, bounds: Rect(95, 42 - 556, 123), childCount: 1}  
                    |-0 {class: FrameLayout, bounds: Rect(95, 42 - 556, 123), childCount: 1}  
                      |-0 {class: LinearLayout, bounds: Rect(95, 57 - 556, 108), childCount: 2, Clickable}  
                        |-0 {class: ImageView, bounds: Rect(109, 65 - 143, 99), childCount: 0}  
                        |-1 {class: TextView, text: 万象息屏, bounds: Rect(157, 66 - 556, 98), childCount: 0}  
                  |-2 {class: FrameLayout, bounds: Rect(556, 42 - 720, 123), childCount: 1}  
                    |-0 {class: FrameLayout, bounds: Rect(556, 42 - 720, 123), childCount: 1}  
                      |-0 {class: LinearLayout, bounds: Rect(570, 48 - 706, 116), childCount: 2}  
                        |-0 {class: RelativeLayout, bounds: Rect(570, 48 - 638, 116), childCount: 3, Clickable}  
                          |-0 {class: ImageView, id: menu_badge_icon, bounds: Rect(583, 61 - 624, 102), childCount: 0}  
                          |-1 {class: View, bounds: Rect(583, 61 - 624, 102), childCount: 0}  
                          |-2 {class: TextView, id: menu_badge, text: 1, bounds: Rect(600, 69 - 607, 86), childCount: 0}  
                        |-1 {class: RelativeLayout, bounds: Rect(638, 48 - 706, 116), childCount: 2, Clickable}  
                          |-0 {class: ImageView, id: menu_badge_icon, bounds: Rect(651, 61 - 692, 102), childCount: 0}  
                          |-1 {class: View, bounds: Rect(651, 61 - 692, 102), childCount: 0}  
                |-1 {class: FrameLayout, bounds: Rect(0, 123 - 720, 186), childCount: 1}  
                  |-0 {class: FrameLayout, bounds: Rect(0, 123 - 720, 186), childCount: 2}  
                     ........ 
                            |-0 {class: TextView, text: 上榜, bounds: Rect(0, 128 - 17, 165), childCount: 0}  
                        |-3 {class: ActionBar$Tab, bounds: Rect(37, 123 - 192, 184), childCount: 1, Clickable}  
                          |-0 {class: LinearLayout, id: linear_view, bounds: Rect(37, 128 - 192, 165), childCount: 1}  
                            |-0 {class: TextView, text: #双十一#, bounds: Rect(57, 128 - 172, 165), childCount: 0}  
                        |-4 {class: ActionBar$Tab, bounds: Rect(192, 123 - 286, 184), childCount: 1, Clickable}  
                          |-0 {class: LinearLayout, id: linear_view, bounds: Rect(192, 128 - 286, 165), childCount: 1}  
                            |-0 {class: TextView, text: 话题, bounds: Rect(212, 128 - 266, 165), childCount: 0}  
                        |-5 {class: ActionBar$Tab, bounds: Rect(281, 123 - 385, 184), childCount: 1, Clickable}  
                          |-0 {class: LinearLayout, id: linear_view, bounds: Rect(281, 126 - 385, 167), childCount: 1}  
                            |-0 {class: TextView, text: 酷图, bounds: Rect(303, 126 - 363, 167), childCount: 0}  
                .........
{com.coolapk.market, com.coolapk.market.view.main.MainActivity}  
主线程执行完毕

上面内容是布局层次关系,可以找到menu_badge_icon的菜单元素。想找到目标节点并不容易,赶紧来看方法二。

2. 使用其他App分析布局

这里推荐AutoJs开发者工具。下面以Auto.js为例。

  1. 在Auto.js主页菜单开启悬浮窗
  2. 点击悬浮按钮,出来的第三个按钮即为布局分析功能,选择屏幕上的节点即可查看对应信息。

视图匹配函数(第一步)

函数 说明
id(id) 指定视图id
text(texts) 文本匹配模式:相同文本,不区分大小写\n同equalsText
equalsText(texts) 文本匹配模式:相同文本,不区分大小写
similaryText(texts) 根据文本相似度 > 0.75(中文转为拼音后的比较)
containsText(texts) 文本匹配模式:包含文本,不区分大小写
matchesText(regex) 文本匹配模式:正则模式
desc(descs) 匹配desc
containsDesc(descs) 包含desc
type(types) 匹配控件的className
editable() 匹配可编辑控件
scrollable() 匹配可滑动
  • 它们之间可链式调用来限制多个条件,如:id('xxx').text('目标文本')
  • 指定多个文本:lua: text('aaa','bbbbb') ; js: text(['aaa','bbbbb'])
  • 所有文本节点:type('textview')
  • 根据描述搜索:desc('描述')
  • 可滚动的视图:scrollable()

搜索函数(第二步)

在确定限制条件后,即可开始搜索。搜索分为立即不等待和等待两种方法。

1
2
3
4
5
all_node = id('xxx').text('目标文本').find() -- 搜索并返回所有满足的视图节点
first = id('xxx').text('目标文本').findFirst() -- 搜索并返回第一个满足条件的节点
id('xxx').await() -- 等待指定节点出现(默认超时时间30s)

id('xxx').waitHide() -- 等待符合条件的视图消失,常用于等待加载视图消失(加载完成)
函数 说明
findFirst() 立即搜索,返回找到第一个,可能失败
find() 搜索所有符合条件,返回Array
waitFor() 无限等待,直到搜索到,返回ViewNode
waitFor(m) 等待最长m毫秒,超时失败返回空
await() 同waitFor()
waitHide() 等待消失 常用于加载View的消失,参数:([waitMs: Int])\n(可选)waitMs:等待时间,最长30s, 返回Boolean: false:超时; true:该ViewNode消失

视图操作(第三步)

在搜索到目标节点后可以进行的操作:

1
2
app_icon = id('menu_badge_icon').await() -- 等待出现
app_icon.tryClick() -- 点击

因为搜索可以省略,所以可以简写为id('menu_badge_icon').tryClick()

函数 说明
tryClick() 尝试点击”
globalClick() 使用全局函数click进行点击操作,如点击网页控件\n需要高级无障碍服务
swipe(dx, dy, delay) 以此Node中心滑动到相对(dx,dy)的地方
tryLongClick() 长按
longClick() 长按
doubleClick() 双击
setText(text) 设置文本,一般只能用于可编辑控件
trySetText(text) 设置文本
getChilds() 获取下级所有Node,返回Array
getParent() 获取父级Node,返回ViewNode?
getBounds() 获取边界范围
getCenterPoint() 获取中心点坐标Point(x,y)(相对于本机屏幕)
getText() 获取Node包含的文本
select() 选择
trySelect() 选择
focus() 获得焦点
appendText() 追加文本,适用于纯文本输入框

扩展

  • 输入框模拟回车事件

在某些场景比如App内搜索,比如酷安搜索:输入框右侧有搜索图标,输入文字后可点击进行搜索;而网易云搜索框不存在搜索图标,很难执行类似输入法回车(类似Enter)的命令。
为此VAssistant内置了输入法,并提供api来使用。

函数 说明
init() 设置VAssist内置输入法(无需显式调用)
restore() 恢复原输入法,指令结束后会自动恢复,也可显式调用,在合适时机恢复。
sendKey(keyCode) 发送按键,keyCode 见系统函数sendKey说明
sendKeys(keys) 发送一组按键
selectedText 编辑框选择的文本
input(text)” to “输入
sendDefaultEditorAction() 发送默认输入框能响应的EditorAction事件
actionSearch() 发送IME_ACTION_SEARCH
actionGo() 发送IME_ACTION_GO
actionDone() 发送IME_ACTION_DONE
sendEnter() 发送回车键
delete() 删除键
select(start, end) 选择文本

这里网易云搜索那一部分代码:

1
2
3
4
-- 进入搜索页
id("search_src_text").text = '歌曲名'
-- 无法获取弹框视图,使用发送按键
input.sendKey(66)  -- 或 input.sendDefaultEditorAction()
  • 等待进入App内

在界面跳转时需要等待进入后面页面再执行后面代码,可使用waitForApp(pkg, activityName)

例:等待进入酷安应用管理

1
2
3
4
5
6
7
8
-- 打开酷安
system.openAppByPkg('com.coolapk.market')
-- 点击主页右上角应用管理
id('menu_badge_icon').tryClick()
-- 等待进入应用管理
waitForApp('com.coolapk.market','AppManagerActivity')
-- 点击 全部更新  
text('全部更新').tryClick()

新建指令完整流程

  1. 自定义指令 - 正则
  2. 自定义指令 - 测试
  3. 指令开源项目