控件定位属性详解 — id、text、desc 与特殊技巧
Android 控件树基础
Android 界面本质是一棵 ViewGroup → View 的层级树。AutoJs6 的无障碍服务能读取整棵树,每个 View 节点称为一个 UiObject。
Activity
└── ConstraintLayout (根)
├── TextView "标题" id: tv_title
├── EditText id: et_username
├── EditText id: et_password
└── LinearLayout (按钮组)
├── Button "登录" id: btn_login
└── Button "注册"
每个 UiObject 包含以下常用属性:
| 属性 | 类型 | 说明 |
|---|---|---|
| id | string | android:id,开发者设置的唯一标识 |
| text | string | 控件上显示的文字 |
| desc | string | content-description,无障碍描述 |
| className | string | 控件的类名(如 android.widget.Button) |
| packageName | string | 所属应用包名 |
| bounds | Rect | 屏幕坐标 {left, top, right, bottom} |
| childCount | number | 子控件数量 |
| clickable | boolean | 是否可点击 |
| enabled | boolean | 是否启用 |
| selected | boolean | 是否选中 |
| checked | boolean | 是否勾选 |
| focusable | boolean | 是否可聚焦 |
| visibleToUser | boolean | 是否对用户可见 |
各属性对比
id — 最推荐
优点:稳定、精确、不受语言和主题影响缺点:部分应用不给控件设 id(尤其是第三方 App、游戏、WebView 内控件)
// ✅ 最佳场景:开发者设了 id 的标准应用
let loginBtn = id("btn_login").findOne();
let usernameInput = id("et_username").findOne();
text — 次推荐
优点:人类可读、直观缺点:受系统语言影响、可能变化(如 "登录" → "登陆")、可能有多个同名控件
// ⚠️ 需要小心使用
let confirmBtn = text("确定").findOne();
// 如果界面上有多个"确定"按钮,会返回第一个
desc — 被低估的选项
优点:独立于 text,部分应用只设 desc 缺点:不是所有应用都设 desc
// 图标按钮通常有 desc 但没 text
let backBtn = desc("返回").findOne();
let settingsIcon = descContains("设置").findOne();
className + 位置 — 兜底方案
优点:总在缺点:太笼统,需要结合 bounds 或层级过滤
// 屏幕底部中间的 Button
let bottomBtn = className("Button")
.bounds(0, 1500, 1080, 1800) // 屏幕下方区域
.clickable(true)
.findOne();
WebView 内控件
部分应用使用 WebView 加载 H5 页面,此时无障碍服务看到的是一个整体的 WebView 控件,内部细节不可见。
处理方案:
- 用
className("android.webkit.WebView")定位 WebView - 用坐标点击代替控件选择器(
click(x, y)) - 或用图像识别(
images模块的找色找图)
// WebView 里的按钮可能无法用选择器定位
// 退而求其次用坐标
click(540, 1200);
动态列表控件
列表(ListView、RecyclerView、GridView)中的子项会动态复用,每次滚动后控件属性可能变化。
// ⚠️ 错误:固定索引可能失效
let firstItem = id("list").findOne().childAt(0); // 滚动后第一个可能变了
// ✅ 正确:每次滚动后重新查找
function scrollAndFind(targetText) {
for (let i = 0; i < 10; i++) {
let target = textContains(targetText).findOne(1000);
if (target) return target;
scrollDown();
}
return null;
}
特殊控件类型
CheckBox / Switch / ToggleButton
// 检查当前状态
let wifiSwitch = id("wifi_switch").findOne();
if (wifiSwitch.checked()) {
console.log("已开启");
} else {
wifiSwitch.click(); // 点击切换
}
EditText(输入框)
// 清空并输入内容
let input = id("search_input").findOne();
input.clear(); // 清空
input.setText("搜索词"); // 直接设置文本
// 或模拟输入
input.click(); // 先聚焦
input.input("搜索词"); // 逐个字符输入(更像人工)
ImageView(图片控件)
ImageView 通常没有 text 和 desc,需要结合 bounds 或上下文定位:
// ImageView 通常通过父子关系定位
let avatar = className("ImageView")
.bounds(0, 200, 300, 500) // 左侧头像区域
.findOne();
动态查找技巧
遍历子控件
let parent = id("list_container").findOne();
let children = parent.children(); // 获取所有子控件
children.forEach((child, index) => {
console.log(`${index}: text=${child.text()}, id=${child.id()}`);
});
向上找父控件
let item = text("某个文字").findOne();
let grandParent = item.parent().parent(); // 向上两级
let packageName = grandParent.packageName(); // 所属包名
找兄弟控件
let title = id("item_title").findOne();
let parent = title.parent();
let siblings = parent.children(); // 标题的所有兄弟
实战:多级筛选
// 场景:在列表中找到"价格低于 500 的商品项"
let listContainer = id("goods_list").findOne(5000);
if (listContainer) {
let items = listContainer.children();
items.forEach(item => {
let priceText = item.findOne(textMatches(/¥\d+/));
if (priceText) {
let price = parseInt(priceText.text().replace(/¥/, ""));
if (price < 500) {
item.click(); // 点击这个商品
}
}
});
}
避坑清单
| 坑 | 原因 | 解决 |
|---|---|---|
| 找控件找不到就崩溃 | findOne 超时返回 null | 先判空 if (btn) |
| 同属性多个控件 | 同名按钮 / 同类控件太多 | 加更多条件 / 用索引 |
| 文本随语言变化 | 多语言应用 | 优先用 id 或 desc |
| 列表滚动后控件失效 | RecyclerView 复用机制 | 每次滚动后重新查找 |
| WebView 内无法定位 | H5 渲染无原生控件 | 用坐标点击或图像识别 |