|
|
屏幕控件扫描仪穷人版布局分析器
- /**
- * 案例33:屏幕控件扫描仪穷人版布局分析器
- *
- * 题材来源:写自动化脚本最烦的就是"不知道控件在哪",
- * "布局分析"是收费+电脑端功能,这个脚本让手机
- * 直接跑一遍,当前屏幕所有可点控件的文字、id、中心坐标全列出来,
- * 复制坐标就能写 clickPoint(x,y)。
- * 原理:auto.getXml() 返回当前窗口的无障碍节点树 XML(案例31锤死可用),
- * 正则扒出每个 <node> 标签的 text / resource-id / class / bounds /
- * clickable 属性,bounds 算中心点,过滤掉纯装饰节点。
- * 零新 API:只用 auto.getXml() + printl + 正则,纯 ES5。
- *
- * 用法:把脚本挂后台,切到任何 App 界面(登录页、设置页、商品页...),
- * 运行 → 日志里就是这张屏幕的"控件地图"。
- * 想只看某个按钮:过滤关键字 填 "登录" 之类的词。
- */
- // ========== 配置区 ==========
- var 过滤关键字 = ""; // 空=列全部可点控件;填词=只显示名字含这个词的
- var 最多列出 = 40; // 防止刷屏,超过就省略
- var 只列可点击 = true; // false=连有文字的节点一起列(有些文字不可点但值得知道)
- var 输出间隔毫秒 = 300; // 有节奏输出:清单每行之间的停顿,0=瞬间刷完
- // ========== 属性扒取 ==========
- function 扒属性(标签, 属性名) {
- var m = 标签.match(new RegExp(属性名 + '="([^"]*)"'));
- return m == null ? "" : m[1];
- }
- // bounds="[110,1140][970,1228]" → {x,y 中心点, w,h 宽高}
- function 解析bounds(串) {
- var m = 串.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);
- if (m == null) return null;
- var 左 = parseInt(m[1], 10), 上 = parseInt(m[2], 10);
- var 右 = parseInt(m[3], 10), 下 = parseInt(m[4], 10);
- return {
- x: Math.round((左 + 右) / 2),
- y: Math.round((上 + 下) / 2),
- w: 右 - 左,
- h: 下 - 上
- };
- }
- // 控件"人话名":text > content-desc > resource-id 尾段 > class 尾段
- function 取名字(标签) {
- var 文 = 扒属性(标签, "text");
- if (文 !== "") return 文;
- var desc = 扒属性(标签, "content-desc");
- if (desc !== "") return desc;
- var rid = 扒属性(标签, "resource-id");
- if (rid !== "") return "#" + rid.split("/").pop();
- var cls = 扒属性(标签, "class");
- return cls === "" ? "(无名)" : cls.split(".").pop();
- }
- // ========== 主流程 ==========
- function main() {
- printl("=== 案例33 屏幕控件扫描仪 启动 ===");
- var xml;
- try {
- xml = "" + auto.getXml();
- } catch (e) {
- printl("❌ getXml 失败(无障碍没开?): " + e);
- return;
- }
- if (xml.length < 10) { printl("❌ XML 是空的,当前界面可能不允许读取"); return; }
- // 前台包名 + 屏幕尺寸(根节点 bounds)
- var 包 = xml.match(/package="([^"]+)"/);
- 包 = 包 == null ? "未知" : 包[1];
- var 首 = xml.match(/<node[^>]*>/);
- var 根 = 首 == null ? null : 解析bounds(扒属性(首[0], "bounds"));
- printl("📱 前台: " + 包 + " | 屏幕约 " + (根 ? 根.w + "x" + 根.h : "?") + " | XML " + xml.length + " 字符");
- // 逐节点解析
- var 标签表 = xml.match(/<node[^>]*>/g) || [];
- var 清单 = [];
- for (var i = 0; i < 标签表.length; i++) {
- var 标签 = 标签表[i];
- if (扒属性(标签, "visible-to-user") === "false") continue; // 看不见的跳过
- var 可点 = 扒属性(标签, "clickable") === "true";
- var 有文 = 扒属性(标签, "text") !== "" || 扒属性(标签, "content-desc") !== "";
- if (只列可点击 && !可点 && !有文) continue;
- if (!可点 && !有文) continue; // 纯装饰节点不要
- var 名 = 取名字(标签);
- if (过滤关键字 !== "" && 名.indexOf(过滤关键字) < 0) continue;
- var b = 解析bounds(扒属性(标签, "bounds"));
- if (b == null) continue;
- 清单.push({ 名: 名, 类: 扒属性(标签, "class").split(".").pop(), 可点: 可点, b: b });
- }
- printl("🔍 节点共 " + 标签表.length + " 个,入选 " + 清单.length + " 个" +
- (过滤关键字 !== "" ? "(关键字:" + 过滤关键字 + ")" : "") + " ==========");
- sleep(600); // 报完统计,喘口气再逐行上清单
- var 上限 = 清单.length < 最多列出 ? 清单.length : 最多列出;
- for (var j = 0; j < 上限; j++) {
- var c = 清单[j];
- var 显名 = c.名.length > 20 ? c.名.slice(0, 20) + "…" : c.名;
- printl(" " + (j + 1) + ". [" + 显名 + "] " + c.类 +
- (c.可点 ? " 可点" : " 仅文字") +
- " → clickPoint(" + c.b.x + ", " + c.b.y + ") 尺寸" + c.b.w + "x" + c.b.h);
- if (输出间隔毫秒 > 0) sleep(输出间隔毫秒);
- }
- if (清单.length > 上限) printl(" ...其余 " + (清单.length - 上限) + " 个省略(调 最多列出)");
- sleep(600); // 清单完,收尾提示也留个节拍
- printl("💡 用法:看中的控件,把行尾坐标抄进 clickPoint(x,y) 就能点;");
- printl(" 文字类控件也可以直接 auto.findText("" + (清单.length > 0 ? 清单[0].名 : "某文字") + "") 找节点再 clickNode;");
- printl(" 常驻玩法:每 2 秒扫一次 + 对比清单,界面出现新按钮(弹窗/广告)立刻报警。");
- printl("=== 案例33 收工 ===");
- }
- main();
复制代码
|
|