automator 自动化操作-模块
automator 自动化操作-模块
这一页专门讲 ScriptX 里已经落地的自动化模块,也就是:
auto这一组无障碍自动化入口automator.takeScreenshot()/auto.takeScreenshot()selector()和一整套文本 / id / 类名 / 包名筛选器UiObject、UiObjectCollection、UiRectcurrentPackage()/currentActivity()
这里我会尽量按“第一次接触也能照着写”的方式来展开。不是只列一个方法名,而是把这些地方也讲清楚:
- 参数到底能传什么。
- 哪些值是固定可选值。
- 省略参数时会发生什么。
- 返回值失败时是
null、false还是空集合。 - 哪些地方会抛异常。
- 哪些写法看起来像能用,实际上容易踩坑。
这一页只覆盖当前 ScriptX 源码已经实现的接口,不按 Auto.js 全量目录去脑补不存在的 API。坐标点击、按压、滑动已经单独拆到 coordinates 自动化操作-坐标。
先记住这 12 条
- 这一整组能力的前提是无障碍服务可用;服务没起来时,很多 API 不是返回空,而是直接抛错或返回
false。 auto()是“尝试就绪”,auto.waitFor()才是“等到就绪再继续”。auto.setMode("fast")管的是窗口快照缓存策略,不是检索算法本身。auto.setSearchMode("normal" | "smart" | "fast")管的是查找流程:要不要先走快速搜索、找不到时要不要回退。auto.setFlags(...)当前源码里只会保存并返回字符串列表,还没有看到别的逻辑真正读取这组 flag。auto.setWindowFilter(fn)一旦启用,查找范围会从“当前活动窗口”切到“回调筛出来的窗口集合”,同时快速搜索通道会被关掉。selector()和它后面的链式条件不会原地修改旧对象,每次都是返回一个新的UiSelector。text()、desc()、className()、packageName()这种精确匹配方法,如果你不传参数,实际是在匹配“空值 / 没值”。textContains()、textStartsWith()、textEndsWith()这类方法如果你把参数漏掉了,底层会拿空字符串参与匹配,结果往往会非常宽。findOne()和waitFor()在当前实现里本质上是同一套等待逻辑;不传超时就是一直等,传无效超时通常会退成“立刻查一次”。UiObject.click()会先尝试无障碍节点动作,失败后再回退到按控件中心点点击;clickCenter()则是直接点中心,不走节点动作。UiObject.find(null)、UiObjectCollection.find(null)、selector().find()这种“空选择器”写法,等价于“不过滤,整棵子树全收”,很容易一次拿到很多节点。
auto(mode?)
auto 既是一个可调用入口,也是一个对象。
最常见的两种写法:
auto();
auto("fast");
参数
| 参数 | 类型 | 可填值 | 默认值 | 说明 |
|---|---|---|---|---|
mode | string | "normal"、"fast" | 不传时不改当前模式 | 先设置模式,再尝试让无障碍服务进入可用状态 |
返回值
boolean
真实行为
- 如果你传了
mode,它会先调用auto.setMode(mode)。 - 然后尝试让无障碍服务进入可用状态。
- 如果服务已经可用,返回
true。 - 如果服务正在准备但还没连上,可能返回
false。 - 如果系统明确判断“无障碍没启用”,当前实现可能会先拉起系统无障碍设置页,然后抛出异常。
什么时候用
- 你只想做一次“轻量就绪检查”。
- 你准备自己决定失败时是提示用户、退出脚本,还是继续别的逻辑。
示例
try {
if (!auto()) {
toast("无障碍还没就绪,稍后再试");
exit();
}
} catch (err) {
toastLog(`auto() 失败: ${err}`);
exit();
}
auto("fast");
const ok = text("允许").findOne(800);
if (ok) {
ok.click();
}
auto.waitFor()
这是更适合新手上手的入口。
auto.waitFor();
参数
无
返回值
boolean
真实行为
- 会一直等到无障碍服务真的可用。
- 如果系统判断服务没启用,源码会尝试拉起无障碍设置页。
- 等待期间每轮大约会 sleep
120ms再继续轮询。
和 auto() 的区别
| 写法 | 特点 |
|---|---|
auto() | 尝试就绪,不保证等到成功 |
auto.waitFor() | 阻塞等待,直到服务可用才继续 |
示例
toast("请确认无障碍已经开启");
auto.waitFor();
toast("无障碍已就绪");
auto.setMode(value)
auto.setMode("normal");
auto.setMode("fast");
参数
| 参数 | 类型 | 可填值 | 默认值 / 空值 | 说明 |
|---|---|---|---|---|
value | string | "normal"、"fast" | null、"" 都按 "normal" 处理 | 设置窗口快照缓存策略 |
返回值
string
返回标准化后的模式名,也就是:
"normal""fast"
两种模式的差别
| 值 | 行为 |
|---|---|
"normal" | 不保留快照缓存;切回该模式时会顺手清缓存 |
"fast" | 活动窗口和显式窗口的快照会短暂复用,当前源码缓存 TTL 大约是 450ms |
要注意什么
- 这个模式只影响快照缓存,不直接决定选择器是不是走快速搜索。
- 非法值会直接抛异常。
auto mode must be fast or normal
示例
auto.waitFor();
auto.setMode("fast");
for (let i = 0; i < 5; i++) {
const node = text("消息").findOnce();
if (node) {
log(node.bounds().toString());
}
}
auto.setMode("normal"); // 页面刚发生明显变化时,重新走全量抓取更稳
auto.setSearchMode(value)
auto.setSearchMode("normal");
auto.setSearchMode("smart");
auto.setSearchMode("fast");
参数
| 参数 | 类型 | 可填值 | 默认值 / 空值 | 说明 |
|---|---|---|---|---|
value | string | "normal"、"smart"、"fast" | null、"" 都按 "normal" 处理 | 设置选择器查找流程 |
返回值
string
返回标准化后的模式名:
"normal""smart""fast"
三种模式真正差在哪
| 值 | 行为 |
|---|---|
"normal" | 不走快速搜索,直接按常规树遍历查 |
"smart" | 先尝试快速搜索;如果没命中,再回退到常规遍历 |
"fast" | 只走快速搜索;快速搜索没命中就直接当成没找到 |
哪些情况会影响结果
smart 和 fast 并不是“单纯更快”,它们依赖当前选择器能不能被优化。大致上:
text / desc / id / className / packageName这几组字符串条件更容易被优化。algorithm("BFS")、filter(fn)、setWindowFilter(fn)这类写法会让快速路径受限,甚至完全关掉。fast模式可能因为不回退而漏掉节点,所以它适合你非常明确筛选条件、并且能接受“找不到就算没中”的场景。
额外行为
- 每次切换搜索模式都会自动清缓存。
- 非法值会抛异常。
auto search mode must be normal, smart or fast
示例
auto.waitFor();
auto.setSearchMode("smart");
const login = text("登录").findOne(1000);
if (login) {
login.click();
}
auto.setSearchMode("fast");
const one = id("title").findOnce();
log(one ? "hit" : "miss");
auto.setFlags(value)
auto.setFlags("demo");
auto.setFlags(["a", "b", "c"]);
auto.setFlags(null);
参数
| 传入值 | 当前行为 |
|---|---|
null / undefined | 清空并返回空数组 |
| 单个字符串 | 保存成单元素数组 |
| JS 数组 | 每项转字符串、去掉空白后保存 |
Java / Kotlin List | 同上 |
| 其他对象 | 直接 toString() 后保存成单元素数组 |
返回值
string[]
当前源码里的真实地位
这一点很重要:当前 setFlags() 只是把值存起来再返回。
在这份源码里,没有看到后续自动化查找逻辑去消费这组 flag。
所以你可以把它理解成:
- 预留配置口
- 你自己脚本里的元信息
- 后续扩展时可统一读取的一组标签
但不要把它当成“开了某个 flag 就一定会改变搜索行为”。
示例
const flags = auto.setFlags(["login-flow", "trace"]);
log(JSON.stringify(flags)); // ["login-flow","trace"]
auto.setFlags(null); // 清空
auto.clearCache()
auto.clearCache();
参数
无
返回值
true
会清什么
- 当前活动窗口缓存
- 按窗口 id 保存的显式窗口缓存
- 如果无障碍服务对象可用,还会通知服务侧清自动化缓存
什么时候该主动调
- 你刚完成了一次页面跳转
- 刚点击完按钮,界面结构已经明显变了
- 你在
fast模式下怀疑拿到的是旧快照
示例
const next = text("下一步").findOne(1000);
if (next && next.click()) {
auto.clearCache();
}
auto.setWindowFilter(fn)
auto.setWindowFilter(function (window) {
return window.active;
});
参数
| 参数 | 类型 | 说明 |
|---|---|---|
fn | function 或 null | 传函数时按窗口回调过滤;传 null 时清除过滤器 |
返回值
boolean
当前实现固定返回 true。
回调参数 window
回调会收到一个窗口信息对象,你最常会用到这些字段:
| 字段 | 类型 | 说明 |
|---|---|---|
window.id | number | 窗口 id |
window.title | string | 标题文本 |
window.summary | string | 概览描述 |
window.type | number | 原始窗口类型常量值 |
window.layer | number | 层级 |
window.active | boolean | 是否当前活动窗口 |
window.focused | boolean | 是否当前焦点窗口 |
window.packageName | string | null | 归属包名 |
window.bounds() | UiRect | null | 窗口屏幕区域 |
window.root() | UiObject | null | 这个窗口的根节点 |
window.typeName() | string | 类型名,如 TYPE_APPLICATION |
回调返回值怎样判定
底层不是只认原生布尔值。它会做一层布尔转换:
| 返回值 | 实际判定 |
|---|---|
true / 1 / "true" / "yes" / "on" | true |
false / 0 / "false" / "no" / "off" | false |
| 其他内容 | 按默认 false 处理 |
开启过滤器后会发生什么
selector.find*()的搜索根不再只看当前活动窗口。auto.root会变成“第一扇命中过滤器的窗口 root”。auto.windowRoots会变成“所有命中过滤器的窗口 root 数组”。- 快速搜索通道会被禁用,所以多窗口过滤通常更灵活,但也更慢。
清除过滤器
auto.setWindowFilter(null);
示例
auto.setWindowFilter(function (window) {
return window.packageName === currentPackage();
});
auto.setWindowFilter(function (window) {
return window.typeName() === "TYPE_INPUT_METHOD";
});
const imeRoot = auto.root;
if (imeRoot) {
log("当前输入法窗口 root 已拿到");
}
auto.service
这是一个高级入口。
返回值
SelectToSpeakService | null
怎么理解
- 有服务时,返回当前无障碍服务对象。
- 没服务时,返回
null。 - 这是宿主内部对象,不是给“第一次写脚本的人”优先依赖的稳定抽象。
更建议的用法
大多数脚本优先用:
auto.waitFor()selector().findOne(...)UiObject.click()
示例
if (!auto.service) {
toast("无障碍服务还没连上");
}
auto.windows
auto.windows 返回的是JS 数组,数组里的每一项都是窗口信息对象。
返回值
WindowInfo[]
能拿到什么
这一组最适合做:
- 多窗口调试
- 当前输入法窗口判断
- 只在某个包名窗口里查控件
- 看系统弹窗和应用主窗口是不是同时存在
示例
const windows = auto.windows;
for (let i = 0; i < windows.length; i++) {
const w = windows[i];
log(
`id=${w.id} type=${w.typeName()} active=${w.active} focused=${w.focused} pkg=${w.packageName}`
);
}
let ime = null;
for (let i = 0; i < auto.windows.length; i++) {
const candidate = auto.windows[i];
if (candidate.typeName() === "TYPE_INPUT_METHOD") {
ime = candidate;
break;
}
}
if (ime) {
log(`输入法窗口范围: ${ime.bounds()}`);
}
WindowInfo 常用方法
这是 auto.windows[i] 这类窗口对象身上最常用的 3 个方法。
bounds()
读取当前窗口的屏幕矩形。
bounds() 方法
- 返回
UiRect | null - 没有边界信息时返回
null
root()
拿这个窗口自己的根节点,后面可以继续按节点树做查找。
root() 方法
- 返回
UiObject | null - 可以直接从某个窗口对象跳到它自己的根节点
typeName()
把原始窗口类型数字转成更容易看懂的字符串。
typeName() 方法
把原始整型 type 转成人类可读名字。当前源码已经映射了这些值:
| 返回值 | 含义 |
|---|---|
TYPE_APPLICATION | 普通应用窗口 |
TYPE_INPUT_METHOD | 输入法窗口 |
TYPE_SYSTEM | 系统窗口 |
TYPE_ACCESSIBILITY_OVERLAY | 无障碍覆盖层 |
TYPE_SPLIT_SCREEN_DIVIDER | 分屏分隔条 |
| 其他值 | TYPE_数字 |
示例
let appWindow = null;
for (let i = 0; i < auto.windows.length; i++) {
const candidate = auto.windows[i];
if (candidate.typeName() === "TYPE_APPLICATION") {
appWindow = candidate;
break;
}
}
if (appWindow) {
const root = appWindow.root();
log(root ? root.toString() : "no root");
}
auto.root
返回值
UiObject | null
它指的到底是谁
- 没开
windowFilter时:当前自动化搜索范围里的第一个根节点,通常就是当前活动窗口根。 - 开了
windowFilter时:所有命中过滤器的窗口里,第一个窗口根节点。
它和 auto.rootInActiveWindow 的区别
| 属性 | 含义 |
|---|---|
auto.root | 当前“搜索范围”的第一个 root |
auto.rootInActiveWindow | 当前活动窗口 root,不理会过滤器 |
示例
const root = auto.root;
if (root) {
log(root.bounds().toString());
}
auto.rootInActiveWindow
返回值
UiObject | null
适合什么时候用
- 你临时开了
setWindowFilter(),但又想明确拿回“当前活动窗口”的根节点。 - 你在调试窗口过滤逻辑,想对比
auto.root和auto.rootInActiveWindow的差异。
示例
auto.setWindowFilter(function (window) {
return window.typeName() === "TYPE_INPUT_METHOD";
});
log(`filtered root = ${auto.root}`);
log(`active root = ${auto.rootInActiveWindow}`);
auto.windowRoots
返回值
UiObject[]
真实行为
- 没有窗口过滤器时,通常就是一个长度为
1的数组。 - 开了窗口过滤器时,会返回所有命中过滤器的窗口根节点数组。
示例
auto.setWindowFilter(function (window) {
return window.packageName === currentPackage();
});
const roots = auto.windowRoots;
for (let i = 0; i < roots.length; i++) {
log(`root[${i}] = ${roots[i]}`);
}
selector()
const s = selector();
返回值
UiSelector
怎么理解
它是一个“从空条件开始”的选择器构造器。后面每加一个条件,都会返回新的 UiSelector。
const base = selector();
const one = base.text("登录");
const two = one.clickable(true);
log(String(base)); // selector()
log(String(one)); // selector().text("登录")
log(String(two)); // selector().text("登录").clickable(true)
推荐心智模型
selector():先拿空白筛子.text(...)/.id(...):往筛子上继续加条件.findOne()/.find():真正开始查
选择器基础
这一组既有全局快捷函数,也有 UiSelector 方法。
text(value?)
按节点文本做精确匹配。
desc(value?)
按内容描述,也就是 contentDescription 做精确匹配。
id(value?)
按资源 id 匹配,同时会看完整 id 和短 id。
className(value?)
按节点类名精确匹配。
packageName(value?)
按节点所属包名精确匹配。
两种写法等价:
text("登录").findOne();
selector().text("登录").findOne();
共同规则
| API | 匹配字段 |
|---|---|
text(...) | 节点文本 |
desc(...) | 内容描述 / contentDescription |
id(...) | 资源 id,既会看完整 id,也会看最后一段短 id |
className(...) | 类名 |
packageName(...) | 包名 |
省略参数时会怎样
这点很容易忽略:
| 写法 | 实际含义 |
|---|---|
text() | 匹配“文本为空或没有文本”的节点 |
desc() | 匹配“描述为空或没有描述”的节点 |
id() | 匹配“没有可用 id 候选项”的节点 |
className() | 匹配类名为空的节点 |
packageName() | 匹配包名为空的节点 |
所以这组不传参数不是“匹配全部”,而是在做“空值精确匹配”。
id(...) 为什么和别的有点不一样
id(...) 不只比一种字符串。它会同时考虑:
- 完整 id,例如
com.demo:id/title - 短 id,例如
title
所以这两句都可能命中同一个节点:
id("title")
id("com.demo:id/title")
示例
const login = text("登录").findOne(1000);
if (login) {
login.click();
}
const tab = id("com.ss.android.ugc.aweme:id/home").findOnce()
|| id("home").findOnce();
包含匹配选择器
这一组是“包含匹配”。
textContains(value)
文本里只要包含指定片段就算命中。
descContains(value)
描述里只要包含指定片段就算命中。
idContains(value)
完整 id 或短 id 里只要包含指定片段就算命中。
classNameContains(value)
类名里只要包含指定片段就算命中。
packageNameContains(value)
包名里只要包含指定片段就算命中。
参数规则
- 参数会先转字符串。
- 如果你把参数漏掉了,很多 API 最终会拿
""去参与匹配。
最容易踩的坑
| 写法 | 风险 |
|---|---|
textContains() | 因为 "" 是任意字符串的子串,结果会非常宽 |
classNameContains() | 同理,几乎会匹配所有有类名的节点 |
packageNameContains() | 同理,范围会很大 |
idContains() | 会同时检查短 id 和完整 id |
所以这组最好都明确传值,不要省略。
示例
const tip = textContains("允许").findOnce();
const edit = classNameContains("EditText").findOne(800);
前缀匹配选择器
这一组是“前缀匹配”。
textStartsWith(value)
文本以前缀方式匹配。
descStartsWith(value)
描述以前缀方式匹配。
idStartsWith(value)
完整 id 和短 id 都会按前缀方式匹配。
classNameStartsWith(value)
类名前缀匹配。
packageNameStartsWith(value)
包名前缀匹配。
参数规则
- 支持任何能转成字符串的值。
idStartsWith(...)同样会同时检查短 id 和完整 id。- 省略参数时会退成空字符串前缀匹配,范围会很宽。
示例
const group = textStartsWith("群聊").findOnce();
const wxNode = packageNameStartsWith("com.tencent").findOne(1000);
后缀匹配选择器
这一组是“后缀匹配”。
textEndsWith(value)
文本以后缀方式匹配。
descEndsWith(value)
描述以后缀方式匹配。
idEndsWith(value)
完整 id 和短 id 都会按后缀方式匹配。
classNameEndsWith(value)
类名后缀匹配。
packageNameEndsWith(value)
包名后缀匹配。
参数规则
- 同样支持先转字符串。
idEndsWith("title")对短 id 和完整 id 都会比。- 省略参数时会退成空字符串后缀匹配,也会很宽。
示例
const settings = textEndsWith("设置").findOnce();
const icon = idEndsWith("avatar").findOne(1000);
正则匹配选择器
这是正则匹配组。
textMatches(pattern)
按正则匹配文本。
descMatches(pattern)
按正则匹配描述。
idMatches(pattern)
按正则匹配完整 id 或短 id。
classNameMatches(pattern)
按正则匹配类名。
packageNameMatches(pattern)
按正则匹配包名。
正则写法规则
底层当前是这样编译的:
- 如果你传的是普通字符串,比如
"登录|注册",它会直接Pattern.compile(...)。 - 如果你传的是
/.../flags这种形式,会把最外层/当作正则边界。 - 支持的 flags 只有:
ims
- 匹配时用的是
find(),不是matches(),也就是“只要命中一段就算成功”。
例子对比
textMatches("登录|注册")
textMatches("/登录|注册/")
textMatches("/login/i")
上面三种都会被当成正则,不是精确字符串比较。
空参数的风险
如果你写成:
textMatches()
那底层会编译一个空正则 "",几乎等于“到处都能匹配到”。
示例
const btn = textMatches("/^(登录|立即登录)$/").findOne(1200);
const anyWechat = packageNameMatches("/com\\.tencent\\..*/").findOnce();
布尔过滤器
这一组都是 UiSelector 的布尔过滤器。
clickable(value?)
只保留可点击节点。
longClickable(value?)
只保留可长按节点。
editable(value?)
只保留可编辑节点。
scrollable(value?)
只保留可滚动节点。
checkable(value?)
只保留可勾选节点。
checked(value?)
按“是否已勾选”筛节点。
selected(value?)
按“是否已选中”筛节点。
enabled(value?)
按“是否启用”筛节点。
focusable(value?)
按“是否可获得普通焦点”筛节点。
focused(value?)
按“当前是否已拿到普通焦点”筛节点。
visibleToUser(value?)
按“对用户是否可见”筛节点。
accessibilityFocused(value?)
按“是否已拿到无障碍焦点”筛节点。
contextClickable(value?)
按“是否支持上下文点击”筛节点。
dismissable(value?)
按“是否可关闭”筛节点。注意当前源码拼写就是 dismissable。
默认值规则
如果你省略参数,它们都会按 true 处理。
也就是说:
clickable()
clickable(true)
是一样的。
支持的布尔写法
底层会做安全布尔转换:
| 传入值 | 结果 |
|---|---|
true / false | 原样使用 |
1 / 0 | true / false |
"true" / "false" | true / false |
"yes" / "no" | true / false |
"on" / "off" | true / false |
| 其他无法识别的值 | 按默认值 true 处理 |
特别提醒
dismissable的拼写当前就是源码里的dismissable,不是dismissible。clickable("abc")不会报错,会退成clickable(true)。
示例
const send = text("发送")
.enabled()
.clickable()
.findOne(1000);
const unchecked = checkable(true).checked(false).find();
log(`count=${unchecked.size()}`);
数值过滤器
这是 UiSelector 的数值过滤器。
depth(value)
按节点深度筛选。这个值越大,通常说明节点在树里越靠下。
indexInParent(value)
按它在父节点里的索引筛选,第一项通常是 0。
drawingOrder(value)
按绘制顺序筛选。适合多个重叠兄弟节点时进一步缩范围。
row(value)
按节点所在行筛选,常见于表格、网格、宫格场景。
rowCount(value)
按总行数筛选,一般更适合筛容器节点。
rowSpan(value)
按跨行数筛选,用在表格类布局时更有意义。
column(value)
按节点所在列筛选。
columnCount(value)
按总列数筛选,一般更适合表格容器或网格容器。
columnSpan(value)
按跨列数筛选。
childCount(value)
按直接子节点数量筛选,常用于列表容器、卡片容器、面板节点。
参数规则
| 支持的值 | 说明 |
|---|---|
123 | 直接按数字用 |
"123" | 去空白后转整数 |
" 123 " | 仍然可用 |
"abc" | 抛异常 |
null / undefined | 抛异常 |
哪些字段缺省时常见什么值
| 字段 | 常见缺省值 |
|---|---|
row / column | -1 |
rowSpan / columnSpan | -1 |
rowCount / columnCount | 0 |
childCount | 0 |
所以这些条件很适合做“表格类 / 列表类节点”筛选。
示例
const firstChild = indexInParent(0).findOnce();
const gridItem = row(1).column(2).findOnce();
bounds(left, top, right, bottom)
这是边界完全相等匹配。
参数
4 个参数都必须能转成整数,否则直接抛异常:
bounds arguments must be numeric
适合场景
- 你已经知道目标节点的精确矩形
- 你在对比前后两次抓树,想锁定同一块区域
示例
const root = auto.rootInActiveWindow;
if (root) {
const rect = root.bounds();
const same = bounds(rect.left, rect.top, rect.right, rect.bottom).findOnce();
log(String(same));
}
boundsInside(left, top, right, bottom)
这是“节点边界必须完整落在给定区域内部”。
适合场景
- 只查屏幕上半区
- 只查某个弹窗内部
- 从一个大容器区域里继续筛子节点
示例
const topHalf = boundsInside(0, 0, device.width, Math.floor(device.height / 2)).find();
log(`topHalf=${topHalf.length}`);
boundsContains(x, y)
这是“节点矩形必须包含某个点”。
参数规则
x、y都要是数字或数字字符串- 否则抛异常
boundsContains(x, y) requires numeric coordinates
示例
const centerX = Math.floor(device.width / 2);
const centerY = Math.floor(device.height / 2);
const node = boundsContains(centerX, centerY).findOnce();
algorithm(value)
这是选择器遍历算法开关。
参数
| 值 | 含义 |
|---|---|
"DFS" | 深度优先 |
"BFS" | 广度优先 |
null / "" | 按 "DFS" |
大小写不敏感,所以 "dfs"、"bfs" 也能用。
为什么它不只是“学术参数”
它会影响两件事:
findOnce()/findOne()拿到的“第一个结果”是谁- 能不能走某些快速优化路径
当前源码里,BFS 会让部分优化失效,所以通常:
- 默认用
DFS - 只有你明确想按层级优先找第一个节点时,再切
BFS
示例
const node = selector()
.textContains("设置")
.algorithm("BFS")
.findOnce();
filter(fn)
这是自定义过滤器。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
fn | function | 回调参数是 UiObject,返回值会做布尔转换 |
非函数会直接抛异常:
filter(callback) requires a function
回调返回值规则
和 setWindowFilter() 一样,返回值会做布尔转换:
true/1/"yes"/"on"会当成真false/0/"no"/"off"会当成假- 其他值按默认
false
要注意什么
- 一旦用了
filter(fn),这个选择器就会被标记成“自定义过滤器选择器”。 - 这样通常就别指望它再走高阶快速搜索优化了。
- 所以建议先用普通字段条件把范围收窄,再加
filter(fn)。
示例
const matched = classNameContains("TextView")
.filter(function (node) {
const rect = node.bounds();
return node.text() && rect.width() > 200;
})
.find();
find()
这是 UiSelector 的“立即查全部”。
返回值
UiObjectCollection
行为
- 不等待
- 立刻返回当前找到的全部结果
- 一个都没有时,返回空集合,不是
null
示例
const items = textContains("消息").find();
log(items.length);
查找第一个结果
findOnce()
拿当前结果里的第一个节点,不等待。
findOnce(index)
拿当前结果里的第 index 个节点,不等待。
findOnce() 无参数
- 返回第一个匹配节点
- 找不到时返回
null
findOnce(index) 带索引
| 参数 | 类型 | 规则 |
|---|---|---|
index | number / 数字字符串 | 小于 0 或解析失败时返回 null |
示例
const first = textContains("评论").findOnce();
const third = textContains("评论").findOnce(2);
findOne(timeout?)
返回值
UiObject | null
超时规则
| 写法 | 行为 |
|---|---|
findOne() | 一直等,直到找到 |
findOne(1000) | 最多等 1000ms |
findOne("1000") | 也可以,会转成整数 |
findOne("abc") | 解析失败后按 0ms 处理,基本等于只查一次 |
当前实现每轮大约轮询一次后会 sleep 120ms。
示例
const login = text("登录").findOne(1500);
if (!login) {
toast("1.5 秒内没找到登录按钮");
}
untilFind(timeout?)
这是“等到至少有一个结果,再把整批结果拿回来”。
返回值
UiObjectCollection
行为
| 写法 | 行为 |
|---|---|
untilFind() | 一直等到结果非空 |
untilFind(1000) | 最多等 1000ms;超时后返回空集合 |
示例
const messages = textContains("消息").untilFind(2000);
if (messages.nonEmpty()) {
messages.each(function (item, index) {
log(index, item.text());
});
}
exists()
这是“立刻判断有没有”。
返回值
boolean
特点
- 不等待
- 不返回节点
- 就适合做分支判断
示例
if (text("允许").exists()) {
const allow = text("允许").findOnce();
if (allow) {
allow.click();
}
}
waitFor(timeout?)
当前实现里,它本质上和 findOne(timeout?) 一样,都是等待单个节点出现再返回。
返回值
UiObject | null
示例
const input = classNameContains("EditText").waitFor(1200);
if (input) {
input.setText("hello");
}
currentPackage()
这一组虽然也在 Global Functions 全局函数 里讲过,但自动化脚本里非常常用,这里再按自动化场景重讲一遍。
参数
无
返回值
string | null
依赖
依赖无障碍服务。服务没起来时会抛异常,而不是返回空串。
你真正拿到的是什么
它会尽量返回“当前前台应用包名”。
大多数时候你可以把它理解成“用户眼下正在看的 App 包名”。
但在下面这些瞬间,可能短暂拿到 null:
- 无障碍刚连上
- 页面正在快速切换
- 系统事件还没来得及更新缓存
示例
log(currentPackage()); // 例如 com.ss.android.ugc.aweme
if (currentPackage() !== "com.tencent.mm") {
toast("请先切到微信");
}
currentActivity()
参数
无
返回值
string | null
依赖
同样依赖无障碍服务。
它返回的不是 Activity 对象
它只是一个类名字符串。
而且当前实现是把组件串里 / 后面的部分截出来,所以你可能看到两种形式:
com.demo.MainActivity.MainActivity
如果你想把相对类名补成完整类名,可以这样写:
function normalizeActivityName() {
const pkg = currentPackage();
const act = currentActivity();
if (!pkg || !act) return act;
return act.startsWith(".") ? pkg + act : act;
}
示例
log(currentActivity());
const act = normalizeActivityName();
if (act === "com.ss.android.ugc.aweme.im.sdk.chat.ChatRoomActivity") {
log("已经在聊天页");
}
automator.takeScreenshot()
这是自动化模块里专门给无障碍脚本准备的截图入口。
它不是走常见的“申请录屏 / 截图权限”那套流程,而是直接调用无障碍服务自己的截图能力。所以它的前提不是截图权限,而是无障碍服务必须真的可用。
参数
无
返回值
Image
当前源码里实际返回的是内部包装过的 ScriptImage 对象。你在脚本里可以把它当成图片对象来用。
别名写法
这两个写法是同一个能力:
const image1 = automator.takeScreenshot();
const image2 = auto.takeScreenshot();
如果你平时脚本主要围绕 auto 来写,用 auto.takeScreenshot() 会更顺手;如果你想明确区分“自动化入口对象”和“无障碍状态对象”,也可以写 automator.takeScreenshot()。
前提条件
调用前你至少要满足这 3 条:
- 设备系统版本是 Android 11 及以上。
- 无障碍服务已经开启,并且 ScriptX 当前真的连上了这个服务。
- 不要在 1 秒内连续疯狂调用,否则系统会直接拒绝这次截图。
如果不满足第 1 条,当前实现会直接抛出:
automator.takeScreenshot() requires Android 11 or above
如果无障碍服务没起来,会抛出:
Accessibility service is not enabled
真实行为
按源码当前实现,它会这样工作:
- 先检查系统版本。
- 再确认无障碍服务已经处于运行状态。
- 然后调用无障碍服务的
takeScreenshot(...)。 - 截图目标固定是
Display.DEFAULT_DISPLAY,也就是默认显示器 / 主屏。 - 内部最多等待 8000ms。
- 成功后把结果转成软件位图,再包装成脚本里的
Image对象返回。
这里有两个很实用的理解:
- 它当前不接收参数,所以你不能手动传
displayId、区域、缩放比例之类的东西。 - 它返回的是“已经拿到内存里的图片对象”,不是文件路径;你后面要做的是继续处理这个对象,而不是去磁盘里找一张图。
这个 Image 对象能做什么
当前源码里,这个截图对象最常用的是下面这些方法:
| API | 返回值 | 说明 |
|---|---|---|
image.width() | number | 取图片宽度 |
image.height() | number | 取图片高度 |
image.getWidth() | number | width() 的别名 |
image.getHeight() | number | height() 的别名 |
image.pixel(x, y) | number | 读取指定坐标像素颜色 |
image.getPixel(x, y) | number | pixel(x, y) 的别名 |
image.recycle() | boolean | 回收图片,释放内存 |
image.isRecycled() | boolean | 判断图片是否已经回收 |
image.width()
读取截图宽度,返回像素值。
const image = automator.takeScreenshot();
log(`宽度: ${image.width()}`);
image.recycle();
image.height()
读取截图高度,返回像素值。
const image = automator.takeScreenshot();
log(`高度: ${image.height()}`);
image.recycle();
image.pixel(x, y)
读取某个坐标点的颜色值。
参数要求:
| 参数 | 类型 | 说明 |
|---|---|---|
x | number | 横坐标 |
y | number | 纵坐标 |
最常见的用途,是先取中心点颜色,或者检查某个固定按钮区域是不是变色了。
const image = automator.takeScreenshot();
const x = Math.floor(image.width() / 2);
const y = Math.floor(image.height() / 2);
const color = image.pixel(x, y);
log(`中心像素颜色: ${color}`);
image.recycle();
image.recycle()
用完截图后,建议主动调用一次,及时释放内存。
返回值:
- 第一次成功回收时返回
true - 如果这张图之前已经回收过了,再调一次会返回
false
const image = automator.takeScreenshot();
try {
log(image.width());
} finally {
image.recycle();
}
image.isRecycled()
检查图片是否已经被回收。
const image = automator.takeScreenshot();
log(image.isRecycled()); // false
image.recycle();
log(image.isRecycled()); // true
截图频率限制
这个限制不是 ScriptX 自己随便定的,而是 Android 无障碍截图能力本身就有限制。
当前源码已经把系统错误翻译成了更直白的提示:如果你调用太快,会抛出:
automator.takeScreenshot() failed: called too frequently, wait about 1 second
所以最稳的写法是:
- 需要连续截图时,间隔至少留 1000ms
- 实战里更建议留 1050ms ~ 1200ms
失败时会出现哪些提示
下面这些都是当前源码里已经明确写死的错误提示,不是猜的:
| 场景 | 典型报错 |
|---|---|
| 系统版本低于 Android 11 | automator.takeScreenshot() requires Android 11 or above |
| 无障碍服务没启用或没连上 | Accessibility service is not enabled |
| 截图等待超时 | automator.takeScreenshot() timed out |
| 系统回调成功但没有给出位图 | automator.takeScreenshot() did not return a bitmap |
| 系统内部错误 | automator.takeScreenshot() failed: internal error |
| 当前无障碍服务没有截图访问能力 | automator.takeScreenshot() failed: service has no screenshot access |
| 调用频率过快 | automator.takeScreenshot() failed: called too frequently, wait about 1 second |
| 默认显示器无效 | automator.takeScreenshot() failed: invalid display |
| 其他系统错误码 | automator.takeScreenshot() failed: errorCode=... |
最基础的写法
auto.waitFor();
const image = automator.takeScreenshot();
try {
log("截图成功");
log(`size=${image.width()}x${image.height()}`);
} finally {
image.recycle();
}
用 auto.takeScreenshot() 写
auto.waitFor();
const image = auto.takeScreenshot();
try {
log("通过 auto.takeScreenshot() 截图成功");
log(image.getWidth());
log(image.getHeight());
} finally {
image.recycle();
}
失败时自己接住异常
这是新手最该先掌握的一种写法,因为这个 API 不是“失败返回 null”,而是很多情况会直接抛异常。
auto.waitFor();
try {
const image = automator.takeScreenshot();
try {
log(`截图尺寸: ${image.width()} x ${image.height()}`);
} finally {
image.recycle();
}
} catch (err) {
toastLog(`截图失败: ${err}`);
}
连续截图时要主动留间隔
auto.waitFor();
try {
const first = automator.takeScreenshot();
try {
log(`first=${first.width()}x${first.height()}`);
} finally {
first.recycle();
}
sleep(1100);
const second = automator.takeScreenshot();
try {
log(`second=${second.width()}x${second.height()}`);
} finally {
second.recycle();
}
} catch (err) {
toastLog(`连续截图失败: ${err}`);
}
什么时候更适合用它
- 你已经在写无障碍自动化脚本,并且服务本来就要开着。
- 你只想快速拿一张当前界面的图做颜色判断或调试。
- 你不想再走额外的截图授权流程。
如果你的脚本本身就不依赖无障碍,或者设备系统版本低于 Android 11,那这条路就不适合你。
UiObjectCollection.length
这是集合上的只读长度属性。
返回值
number
额外说明
UiObjectCollection 不是普通 JS 数组,但它支持两种很顺手的访问方式:
const list = textContains("消息").find();
log(list.length);
log(list[0]);
集合状态
这是集合最常用的一组状态方法。
UiObjectCollection.size()
返回集合大小。
UiObjectCollection.empty()
判断集合是否为空。
UiObjectCollection.nonEmpty()
判断集合是否非空。
UiObjectCollection.isEmpty()
判断集合是否为空。语义上和 empty() 接近。
返回值
| API | 返回值 |
|---|---|
size() | 集合大小 |
empty() | 是否为空 |
nonEmpty() | 是否非空 |
isEmpty() | 是否为空 |
示例
const list = classNameContains("TextView").find();
if (list.nonEmpty()) {
log(`hit=${list.size()}`);
}
UiObjectCollection.get(index)
参数
| 参数 | 类型 | 规则 |
|---|---|---|
index | number / 数字字符串 | 小于 0 或越界时返回 null |
返回值
UiObject | null
示例
const list = textContains("消息").find();
const first = list.get(0);
const second = list.get("1");
集合内继续查找
这是“对集合里的每个元素都做一次子树检索”。
UiObjectCollection.find(selector)
把集合里每个节点的子树都扫一遍,并把所有命中结果收集回来。
UiObjectCollection.findOne(selector)
从集合里每个节点的子树里找第一个命中的节点。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
selector | UiSelector | 必传选择器;传 null 时会退成空选择器 |
一个很关键的细节
如果你传 null,底层会自动构造 selector(),也就是没有任何条件的空选择器。
这通常意味着:
collection.find(null):把每个元素整棵子树全扫出来collection.findOne(null):返回第一棵子树里遇到的第一个节点
所以实际脚本里,最好都显式传筛选条件。
返回值
| API | 返回值 |
|---|---|
find(selector) | UiObjectCollection |
findOne(selector) | UiObject | null |
示例
const cards = classNameContains("RecyclerView").find();
const avatar = cards.findOne(idEndsWith("avatar"));
UiObjectCollection.each(callback)
参数
| 参数 | 类型 | 说明 |
|---|---|---|
callback | function | 回调参数依次是 (item, index, collection) |
非函数会抛异常:
each(callback) requires a function
返回值
返回集合自身,方便链式调用。
示例
textContains("消息").find().each(function (item, index, collection) {
log(index, item.text(), `total=${collection.length}`);
});
UiObjectCollection.toArray()
返回值
普通 JS 数组 UiObject[]
什么时候有用
- 你想用原生数组方法
- 你想自己
map/filter - 你想传给只接受数组的辅助函数
示例
const array = textContains("消息").find().toArray();
array.forEach(function (item) {
log(item.text());
});
身份信息读取
这是最核心的一组“身份信息读取 API”。
UiObject.id()
读取短 id,例如 title。
UiObject.fullId()
读取完整 id,例如 com.demo:id/title。
UiObject.idInt()
读取整型资源 id,取不到时是 null。
UiObject.idHex()
把整型资源 id 以十六进制字符串形式返回。
UiObject.sourceNodeId()
读取无障碍节点自己的 sourceNodeId。
返回值说明
| API | 类型 | 说明 |
|---|---|---|
id() | string | null | 短 id,例如 title |
fullId() | string | null | 完整 id,例如 com.demo:id/title |
idInt() | number | null | 尝试解析出来的资源整型 id |
idHex() | string | null | idInt() 的十六进制形式,如 0x7f0a0123 |
sourceNodeId() | number | 无障碍 sourceNodeId,取不到时是 -1 |
怎么用最稳
const node = text("登录").findOnce();
if (node) {
log(JSON.stringify({
id: node.id(),
fullId: node.fullId(),
idInt: node.idInt(),
idHex: node.idHex(),
sourceNodeId: node.sourceNodeId()
}, null, 2));
}
文本与包名读取
UiObject.text()
读取节点文本。
UiObject.desc()
读取节点描述。
UiObject.className()
读取节点类名。
UiObject.packageName()
读取节点所属包名。
返回值
全部都是 string | null
典型用途
text():读按钮文案、列表项标题desc():读无文本图标按钮的描述className():做控件类型判断packageName():确认节点属于哪个 App / 哪个窗口
示例
const node = textContains("设置").findOnce();
if (node) {
log(node.text());
log(node.desc());
log(node.className());
log(node.packageName());
}
边界读取
这两个都会返回 UiRect。
UiObject.bounds()
读取屏幕坐标系下的矩形。
UiObject.boundsInParent()
读取相对父节点坐标系下的矩形。
差别
| API | 坐标系 |
|---|---|
bounds() | 屏幕坐标 |
boundsInParent() | 相对父节点坐标 |
示例
const node = text("登录").findOne(1000);
if (node) {
const screen = node.bounds();
const parent = node.boundsInParent();
log(`screen=${screen}`);
log(`parent=${parent}`);
}
UiRect 基础方法
这是 bounds() / boundsInParent() 返回的矩形对象可直接调用的方法。
UiRect.width()
读取矩形宽度。
UiRect.height()
读取矩形高度。
UiRect.centerX()
读取矩形中心点的 X。
UiRect.centerY()
读取矩形中心点的 Y。
UiRect.contains(x, y)
判断某个点是否落在矩形内部。
返回值
| API | 返回 |
|---|---|
width() | 宽度,最小不会小于 0 |
height() | 高度,最小不会小于 0 |
centerX() | 中心点 X |
centerY() | 中心点 Y |
contains(x, y) | 传入点是否落在矩形内;参数不合法时返回 false |
示例
const login = text("登录").findOne(1000);
if (login) {
const rect = login.bounds();
log(rect.width(), rect.height(), rect.centerX(), rect.centerY());
}
const root = auto.rootInActiveWindow;
if (root) {
const rect = root.bounds();
log(rect.contains(200, 300));
}
节点层级信息
UiObject.depth()
读取节点深度。
UiObject.indexInParent()
读取它在父节点里的位置索引。
UiObject.drawingOrder()
读取绘制顺序。
返回值
全部都是 number
怎么理解
| API | 说明 |
|---|---|
depth() | 节点深度 |
indexInParent() | 它在父节点里的索引 |
drawingOrder() | 绘制顺序 |
示例
const node = text("登录").findOne(1000);
log(node.depth(), node.indexInParent(), node.drawingOrder());
网格信息
这组特别适合表格、宫格、列表容器。
UiObject.row()
读取节点所在行。
UiObject.rowCount()
读取总行数。
UiObject.rowSpan()
读取跨行数。
UiObject.column()
读取节点所在列。
UiObject.columnCount()
读取总列数。
UiObject.columnSpan()
读取跨列数。
UiObject.childCount()
读取直接子节点数量。
返回值常见规则
| API | 没有相关信息时常见返回 |
|---|---|
row() / column() | -1 |
rowSpan() / columnSpan() | -1 |
rowCount() / columnCount() | 0 |
childCount() | 0 |
示例
const node = selector().classNameContains("RecyclerView").findOnce();
if (node) {
log(`childCount=${node.childCount()}`);
}
const cell = row(0).column(1).findOnce();
if (cell) {
log(cell.text());
}
节点状态读取
这是节点自身的布尔状态读取。
UiObject.clickable()
节点本身是不是可点击。
UiObject.longClickable()
节点本身是不是可长按。
UiObject.editable()
节点本身是不是可编辑。
UiObject.scrollable()
节点本身是不是可滚动。
UiObject.checkable()
节点本身是不是可勾选。
UiObject.checked()
节点当前是否已勾选。
UiObject.selected()
节点当前是否已选中。
UiObject.enabled()
节点当前是否已启用。
UiObject.password()
节点是不是密码输入框。
UiObject.focusable()
节点是否可获得普通焦点。
UiObject.focused()
节点当前是否已拿到普通焦点。
UiObject.visibleToUser()
节点当前是否对用户可见。
UiObject.accessibilityFocused()
节点当前是否已拿到无障碍焦点。
UiObject.contextClickable()
节点当前是否支持上下文点击。
UiObject.dismissable()
节点当前是否可关闭。注意这里仍然是源码拼写 dismissable。
返回值
全部都是 boolean
适合怎么用
先读状态,再决定动作,比盲点按钮稳很多:
const node = text("发送").findOnce();
if (node && node.enabled() && node.clickable()) {
node.click();
}
特别容易忽略的 4 个
| API | 含义 |
|---|---|
password() | 当前节点是不是密码输入框 |
visibleToUser() | 是否对用户可见 |
accessibilityFocused() | 是否拿到了无障碍焦点 |
contextClickable() | 是否支持上下文点击 |
树结构导航
这是树结构导航。
UiObject.parent()
拿父节点。
UiObject.child(index)
按索引拿某一个直接子节点。
UiObject.children()
拿全部直接子节点集合。
返回值
| API | 返回值 |
|---|---|
parent() | UiObject | null |
child(index) | UiObject | null |
children() | UiObjectCollection |
参数规则
child(index):
- 支持数字和数字字符串
- 小于
0、越界、解析失败时返回null
示例
const list = classNameContains("RecyclerView").findOnce();
if (list) {
const first = list.child(0);
const children = list.children();
log(children.length);
log(first ? first.text() : "no first child");
}
节点内查找
这是“只在当前节点子树里继续查”。
UiObject.find(selector)
在当前节点子树里查全部命中结果。
UiObject.findOne(selector)
在当前节点子树里查第一个命中的结果。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
selector | UiSelector | 传 null 时会退成空选择器 |
返回值
| API | 返回值 |
|---|---|
find(selector) | UiObjectCollection |
findOne(selector) | UiObject | null |
一个重要提醒
如果你写:
node.find(null)
那不是报错,而是“把当前节点整棵子树里所有节点都找出来”。
示例
const card = textContains("群聊").findOnce();
if (card) {
const avatar = card.findOne(idEndsWith("avatar"));
const labels = card.find(classNameContains("TextView"));
log(labels.length);
}
点击动作
这是节点动作里最常用的一组。
UiObject.click()
优先走节点点击动作,失败后再回退到中心点点击。
UiObject.clickCenter()
直接按屏幕坐标点击控件中心。
UiObject.longClick()
优先走节点长按动作,失败后再回退到中心长按。
返回值
全部都是 boolean
三者差别
| API | 真实行为 |
|---|---|
click() | 先尝试节点 ACTION_CLICK,失败后回退到“点控件中心” |
clickCenter() | 直接按屏幕坐标点控件中心 |
longClick() | 先尝试节点 ACTION_LONG_CLICK,失败后回退到中心长按 |
什么时候选哪个
- 普通按钮优先
click() - 目标节点明明能拿到,但节点动作老失败,可试
clickCenter() - 需要呼出上下文菜单、拖动前长按等场景用
longClick()
示例
const send = text("发送").findOne(1000);
if (send && !send.click()) {
send.clickCenter();
}
文本输入与选区
UiObject.setText(value)
给节点设置文本。
UiObject.setSelection(start, end)
设置输入框选区。
setText(value) 行为
| 规则 | 说明 |
|---|---|
| 任何值都会先转字符串 | 123 会变成 "123" |
null 会变成空字符串 | 等于尝试清空文本 |
| 返回值 | boolean |
setSelection(start, end) 行为
| 规则 | 说明 |
|---|---|
| 参数支持数字和数字字符串 | 会转整数 |
非法值会退成 -1 | 然后再尝试执行节点动作 |
| 返回值 | boolean |
示例
const input = classNameContains("EditText").findOne(1200);
if (input) {
input.setText("hello jsxhook");
input.setSelection(0, 5);
}
const input = classNameContains("EditText").findOne(1200);
if (input) {
input.setText(null); // 尝试清空
}
滚动动作
这是滚动动作组。
UiObject.scrollForward()
向前滚动,最常见于列表继续往下翻。
UiObject.scrollBackward()
向后滚动。
UiObject.scrollUp()
向上滚动。
UiObject.scrollDown()
向下滚动。
UiObject.scrollLeft()
向左滚动。
UiObject.scrollRight()
向右滚动。
返回值
全部都是 boolean
要注意什么
- 它们不会先替你判断
scrollable()。 - 节点不支持对应动作时,通常就是
false。 scrollUp/Down/Left/Right依赖对应 action id 是否可用。
示例
const list = selector().scrollable(true).findOnce();
if (list) {
list.scrollForward();
}
const panel = selector().scrollable(true).findOnce();
if (panel && !panel.scrollDown()) {
panel.scrollForward();
}
焦点动作
这是两套“焦点”动作:
UiObject.focus()
请求普通输入/控件焦点。
UiObject.clearFocus()
清除普通输入/控件焦点。
UiObject.accessibilityFocus()
请求无障碍焦点。
UiObject.clearAccessibilityFocus()
清除无障碍焦点。
| API | 焦点类型 |
|---|---|
focus() / clearFocus() | 普通输入 / 控件焦点 |
accessibilityFocus() / clearAccessibilityFocus() | 无障碍焦点 |
返回值
全部都是 boolean
示例
const input = classNameContains("EditText").findOne(1000);
if (input) {
input.focus();
input.accessibilityFocus();
}
选择与剪贴板
这是选择与剪贴板动作组。
UiObject.select()
执行选中动作。
UiObject.copy()
执行复制动作。
UiObject.cut()
执行剪切动作。
UiObject.paste()
执行粘贴动作。
返回值
全部都是 boolean
更适合的场景
- 文本可选控件
- 输入框
- 带原生编辑动作的宿主控件
示例
const input = classNameContains("EditText").findOne(1000);
if (input) {
input.setSelection(0, input.text() ? input.text().length : 0);
input.copy();
}
setClip("你好,世界");
const input = classNameContains("EditText").findOne(1000);
if (input) {
input.paste();
}
其他动作
这是剩下几组比较场景化的节点动作。
UiObject.contextClick()
执行上下文点击。
UiObject.show()
尽量把节点滚动到可见区域。
UiObject.dismiss()
执行关闭动作。
UiObject.expand()
执行展开动作。
UiObject.collapse()
执行折叠动作。
返回值
全部都是 boolean
大致适合的控件
| API | 更常见场景 |
|---|---|
contextClick() | 支持上下文点击的控件 |
show() | 让节点滚动到可见区域 |
dismiss() | 可关闭提示、弹层 |
expand() | 可展开条目、折叠面板 |
collapse() | 可折叠条目、展开面板的反向动作 |
示例
const node = descContains("更多").findOnce();
if (node) {
node.contextClick();
}
const item = textContains("高级设置").findOnce();
if (item) {
item.show();
}
实战建议
最后给你 6 条写自动化脚本时非常实用的经验:
先
auto.waitFor(),再查节点。
很多“为什么找不到”的问题,其实是服务还没真正连上。优先写稳定选择器,再考虑坐标。
文案、id、类名、包名、布尔状态先组合一遍,只有实在没有节点动作时,再下沉到坐标。搜索模式别乱设。
不确定时先用auto.setSearchMode("smart"),它比一上来就fast稳。界面刚变化后,怀疑结果发旧就
auto.clearCache()。筛选条件先宽后窄。
例如先classNameContains("TextView"),再textContains("设置"),最后必要时加filter(fn)。拿到
UiObject后先读状态。enabled()、clickable()、visibleToUser()往往比一上来直接click()更省排错时间。
