跳到主要内容

App 控件

操作 Android 原生 UI 控件

概述

App 控件模块用于查找和操作 Android 原生界面控件(Button、TextView、EditText、ListView 等)。

技术基础:

  • 基于 Android AccessibilityService
  • 通过 AccessibilityNodeInfo 获取控件树
  • 通过 performAction() 执行操作
详细文档

快速开始

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()