App 控件
操作 Android 原生 UI 控件
概述
App 控件模块用于查找和操作 Android 原生界面控件(Button、TextView、EditText、ListView 等)。
技术基础:
- 基于 Android AccessibilityService
- 通过 AccessibilityNodeInfo 获取控件树
- 通过 performAction() 执行操作
详细文档
- Query 查询器:详见 App Query
- Element 控件对象:详见 App Element
快速开始
local node = require("new_node")
-- 方式1:标准写法(推荐,可获取错误信息)
local btn = node.app():text("登录"):get()
if btn then
local ok, err = btn:click()
if not ok then print("点击失败:", err) end
end
-- 方式2:快捷写法(tryClick 自动处理 nil)
local ok = node.app():text("登录"):tryClick()
-- 输入文本
local ok = node.app():id("com.example:id/input"):tryInput("hello world")
-- 获取控件信息
local btn = node.app():text("确定"):get()
if btn then
print("文本:", btn.text)
print("位置:", btn.bounds.x, btn.bounds.y)
print("中心:", btn.bounds.centerX, btn.bounds.centerY)
end
API 总览
入口方法
| 方法 | 返回值 | 说明 |
|---|---|---|
node.app() | Query | 创建 App 查询器(默认模式 0) |
node.app(mode) | Query | 指定模式:0=全部, 1=过滤系统控件, 2=仅可见 |
Query 约束方法
所有约束方法返回 Query 对象,支持链式调用。
| 方法 | 说明 |
|---|---|
.id(id) | 控件 ID |
.idContains(text) | ID 包含 |
.idStartsWith(text) | ID 前缀 |
.idEndsWith(text) | ID 后缀 |
.idMatches(pattern) | ID Lua 模式匹配 |
.text(text) | 文本完全匹配 |
.textContains(text) | 文本包含 |
.textStartsWith(text) | 文本前缀 |
.textEndsWith(text) | 文本后缀 |
.textMatches(pattern) | 文本 Lua 模式匹配 |
.desc(desc) | 描述完全匹配 |
.descContains(desc) | 描述包含 |
.descStartsWith(desc) | 描述前缀 |
.descEndsWith(desc) | 描述后缀 |
.descMatches(pattern) | 描述 Lua 模式匹配 |
.hint(text) | 提示文本 |
.hintContains(text) | 提示文本包含 |
.hintStartsWith(text) | 提示文本前缀 |
.hintEndsWith(text) | 提示文本后缀 |
.hintMatches(pattern) | 提示文本 Lua 模式匹配 |
.type(type) | 控件类型 |
.pkg(package) | 包名匹配 |
Query 布尔约束
| 方法 | 说明 |
|---|---|
.clickable(bool) | 可点击,默认 true |
.longClickable(bool) | 可长按 |
.scrollable(bool) | 可滚动 |
.enabled(bool) | 可用,默认 true |
.editable(bool) | 可编辑 |
.focusable(bool) | 可聚焦 |
.focused(bool) | 焦点状态 |
.checkable(bool) | 可选中 |
.checked(bool) | 选中状态 |
.selected(bool) | 选择状态 |
.visible(bool) | 可见状态 |
Query 数值约束
| 方法 | 说明 |
|---|---|
.index(n) | 结果索引(1 开始,负数从末尾) |
.depth(n) | 控件深度 |
.childCount(n) | 子控件数量 |
Query 配置方法
| 方法 | 说明 |
|---|---|
.timeout(ms) | 等待超时(默认 0,最大 60000ms) |
.interval(ms) | 轮询间隔(默认 100ms) |
.limit(n) | 限制 all() 返回数量 |
Query 终结方法
| 方法 | 返回值 | 说明 |
|---|---|---|
.get() | Element/nil | 获取第一个匹配 |
.all() | table | 获取所有匹配(空时返回 {}) |
.exists() | boolean | 是否存在 |
.count() | number | 匹配数量 |
.waitUntilGone(ms) | boolean | 等待控件消失 |
.debug() | string | 打印查询条件(调试用) |
Query 快捷方法
| 方法 | 返回值 | 说明 |
|---|---|---|
.tryClick() | boolean | 找到则点击 |
.tryLongClick() | boolean | 找到则长按 |
.tryInput(text) | boolean | 找到则输入 |
.tryClear() | boolean | 找到则清空 |
.tryFocus() | boolean | 找到则聚焦 |
Element 动作方法
动作方法在 Element 上执行,返回 boolean, string?(成功, 错误信息)。
| 方法 | 说明 |
|---|---|
:click() | 点击 |
:longClick() | 长按 |
:input(text) | 输入文本(会清空原有) |
:append(text) | 追加文本 |
:clear() | 清空文本 |
:focus() | 获取焦点 |
:blur() | 清除焦点 |
:scrollUp() | 向上滚动 |
:scrollDown() | 向下滚动 |
:scrollLeft() | 向左滚动 |
:scrollRight() | 向右滚动 |
:scrollIntoView() | 滚动使自身可见 |
:select() | 选中 |
:deselect() | 取消选中 |
:expand() | 展开 |
:collapse() | 折叠 |
错误处理示例:
local ok, err = elem:click()
if not ok then
print("点击失败:", err) -- "element is stale" / "action not supported"
end
使用示例
基础查找
local node = require("new_node")
-- 通过文本查找
local btn = node.app():text("确定"):get()
if btn then btn:click() end
-- 通过 ID 查找
local input = node.app():id("com.example:id/search"):get()
if input then input:input("关键词") end
-- 通过类型查找所有
local images = node.app():type("ImageView"):all()
if images then
for i, img in ipairs(images) do
print(i, img.bounds)
end
end
-- 组合条件
local item = node.app()
:type("TextView")
:textContains("价格")
:clickable()
:get()
等待控件
-- 等待控件出现(最多等 5 秒)
local btn = node.app():text("加载完成"):timeout(5000):get()
if btn then
print("找到了")
else
print("超时未找到")
end
-- 检查是否存在
if node.app():text("登录成功"):timeout(3000):exists() then
print("登录成功")
end
控件操作
-- 点击
local btn = node.app():text("登录"):get()
if btn then btn:click() end
-- 长按
local item = node.app():text("删除"):get()
if item then item:longClick() end
-- 输入文本
local input = node.app():id("search"):get()
if input then
input:clear() -- 先清空
input:input("hello") -- 再输入
end
-- 滚动列表
local list = node.app():type("ListView"):get()
if list then
list:scrollDown()
end
遍历控件
-- 获取所有匹配控件(all() 返回空表,不是 nil)
local items = node.app():type("TextView"):all()
for i, item in ipairs(items) do
print(i, item.text)
end
-- 获取子控件
local container = node.app():id("list"):get()
if container then
local children = container:children()
for _, child in ipairs(children) do
print(child.text)
end
end
关系查询
local label = node.app():text("商品名称"):get()
if label then
-- 获取父控件
local card = label:parent()
-- 获取兄弟控件
local price = label:next()
-- 获取第 2 个子控件
local child = card:child(2)
-- 在父控件中查找
local btn = card:query():text("购买"):get()
if btn then btn:click() end
end
最佳实践
1. 优先使用稳定的定位方式
-- ✅ 优先使用 ID(最稳定)
local btn = node.app():id("com.example:id/btn_login"):get()
-- ✅ 其次使用 desc(无障碍描述)
local btn = node.app():desc("登录按钮"):get()
-- ⚠️ text 可能随语言变化
local btn = node.app():text("Login"):get()
2. 添加足够的约束条件
-- ❌ 约束太少,可能匹配到多个
local btn = node.app():type("Button"):get()
-- ✅ 添加更多约束
local btn = node.app():type("Button"):text("确定"):clickable():get()
3. 使用等待机制
-- ❌ 页面可能还没加载完
local btn = node.app():text("开始"):get()
-- ✅ 等待控件出现
local btn = node.app():text("开始"):timeout(3000):get()
4. 检查返回值
-- ✘ 不检查可能 nil 报错
node.app():text("确定"):get():click()
-- ✔ 方式1:检查返回值
local btn = node.app():text("确定"):get()
if btn then
local ok, err = btn:click()
if not ok then print("点击失败:", err) end
else
print("控件未找到")
end
-- ✔ 方式2:用快捷方法
node.app():text("确定"):tryClick()