|
|
整点抢购倒计时倒计时悬浮卡片
- /**
- * ╔══════════════════════════════════════════════════════════╗
- * ║ 案例 7:整点抢购倒计时倒计时悬浮卡片
- * ║ 知识点:高精度定时(分段等待 + 提前量)、 ║
- * ║ 倒计时悬浮卡片、uiPost 线程调度、 ║
- * ║ 安全点击封装(touch/无障碍降级)、 ║
- * ║ 命中误差统计、event.onStopBtnEvent ║
- * ╚══════════════════════════════════════════════════════════╝
- *
- * 目标:全网最经典的"抢购/抢茅台/抢优惠券"套路——
- * 1. 屏幕上挂一个倒计时大字卡片
- * 2. 粗等 → 细等 → 自旋,三段式逼近目标时间
- * 3. 提前 LEAD_MS 毫秒出手(补偿点击本身的耗时)
- * 4. 到点连点 3 下,统计实际命中误差
- * 难度:★★★☆☆
- * 运行:无障碍服务即可;USE_DEMO=true 不用改时间直接看效果
- */
- // ==================== 第 0 步:配置区(改这里)====================
- var USE_DEMO = true; // true:10 秒后开抢(演示用);false:按下面的整点时间
- var DEMO_SECONDS = 10; // 演示模式:多少秒后开抢
- var BUY_HOUR = 10, BUY_MIN = 0, BUY_SEC = 0; // 真实模式:每天 10:00:00 抢
- var LEAD_MS = 80; // 提前量:点击动作本身约耗时 60~100ms,提前出手才准
- var CLICK_TIMES = 3; // 到点连点几下(抢购页面经常要点多次才进)
- var DEMO_KEY_FEEDBACK = true; // 出手后模拟按一次 HOME 键,让演示肉眼可见(真实用设 false)
- // ==================== 顶部:全局变量声明 ====================
- var gWin = null; // 悬浮窗对象
- var gTvTitle = null; // 标题(目标时刻)
- var gTvCount = null; // 倒计时大字
- var gTvStatus = null; // 状态行
- var gTvResult = null; // 结果行(命中误差)
- var gTargetTs = 0; // 目标时间戳(毫秒)
- var gStruck = false; // 是否已出手
- var gStopTick = false; // 倒计时线程退出标志
- // ==================== 调色板 ====================
- var C_BG = "#CC0B1220"; // 卡片底(深蓝黑)
- var C_HEAD = "#F43F5E"; // 标题栏(玫红,抢购氛围)
- var C_PANEL = "#0F172A"; // 面板
- var C_TEXT = "#E2E8F0"; // 正文
- var C_OK = "#4ADE80"; // 成功(绿)
- var C_WARN = "#FBBF24"; // 提醒(黄)
- var C_ERR = "#F87171"; // 失败(红)
- var C_DIM = "#64748B"; // 暗灰
- // ==================== 工具函数(先定义,后使用)====================
- function doSleep(ms) {
- try { java.lang.Thread.sleep(ms); } catch (e) {}
- }
- function now() { return new Date().getTime(); }
- function pad2(n) { return (n < 10 ? "0" : "") + n; }
- // 时间戳 → "10:00:00.000"(毫秒也要,抢购看得清)
- function formatMs(ts) {
- var d = new Date(ts);
- return pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + ":" +
- pad2(d.getSeconds()) + "." +
- (d.getMilliseconds() < 100 ? (d.getMilliseconds() < 10 ? "00" : "0") : "") +
- d.getMilliseconds();
- }
- // 安全点击:touch.tap(无障碍)优先,失败兜底 auto.clickPoint
- function safeClick(x, y) {
- try { touch.tap(x, y); }
- catch (e) { auto.clickPoint(x, y); }
- }
- // ==================== UI 线程调度(案例6 的教训,直接复用)====================
- // 从脚本线程 setText 不会触发重新测量,大字倒计时会卡住不刷新,必须 post 到 UI 线程
- function uiPost(view, fn) {
- if (view && view.post) {
- try { view.post(fn); return; } catch (e) {}
- }
- try { fn(); } catch (e2) {}
- }
- function setCountdown(text, color) {
- var c = color;
- uiPost(gTvCount, function () {
- try {
- gTvCount.setText(text);
- gTvCount.setTextColor(android.graphics.Color.parseColor(c));
- } catch (e) {}
- });
- }
- function setStatus(text, color) {
- var shown = "⏳ " + text, c = color;
- uiPost(gTvStatus, function () {
- try {
- gTvStatus.setText(shown);
- gTvStatus.setTextColor(android.graphics.Color.parseColor(c));
- } catch (e) {}
- });
- }
- function setResult(text, color) {
- var shown = text, c = color;
- uiPost(gTvResult, function () {
- try {
- gTvResult.setText(shown);
- gTvResult.setTextColor(android.graphics.Color.parseColor(c));
- } catch (e) {}
- });
- }
- // ==================== 第 1 步:创建倒计时卡片 ====================
- function initFloatWindow() {
- var sw = screen.getScreenWidth();
- var sh = screen.getScreenHeight();
- var w = Math.floor(sw * 0.62); // 宽 62%
- var h = Math.floor(sh * 0.20); // 高 20%
- var x = Math.floor((sw - w) / 2); // 水平居中
- var y = Math.floor(sh * 0.30); // 距顶部 30%
- // 结构:标题栏 → 大字倒计时 → 状态行 → 结果行
- var xml =
- '<LinearLayout orientation="vertical" w="' + w + '" h="' + h +
- '" background="' + C_BG + '" padding="4">' +
- '<LinearLayout orientation="horizontal" w="fill_parent" h="wrap_content" ' +
- 'background="' + C_HEAD + '" padding="6">' +
- ' <TextView text="⚡ 整点抢购" textColor="#FFFFFF" textSize="13" textStyle="bold"/>' +
- ' <TextView id="target" text="" textColor="#FFE4E6" textSize="11" ' +
- 'w="fill_parent" gravity="right"/>' +
- '</LinearLayout>' +
- '<LinearLayout orientation="vertical" w="fill_parent" h="fill_parent" ' +
- 'background="' + C_PANEL + '" padding="8">' +
- ' <TextView id="countdown" text="00:00" textColor="' + C_WARN + '" ' +
- 'textSize="34" textStyle="bold" gravity="center" w="fill_parent"/>' +
- ' <TextView id="statusLine" text="⏳ 准备中..." textColor="' + C_DIM + '" ' +
- 'textSize="12" gravity="center" w="fill_parent" padding="4"/>' +
- ' <TextView id="resultLine" text="" textColor="' + C_OK + '" ' +
- 'textSize="12" gravity="center" w="fill_parent"/>' +
- '</LinearLayout>' +
- '</LinearLayout>';
- try {
- gWin = new floatUI();
- gWin.loadXML(xml);
- gWin.setPosition(x, y); // loadXML 成功后再调用
- gTvTitle = gWin.findViewById("target");
- gTvCount = gWin.findViewById("countdown");
- gTvStatus = gWin.findViewById("statusLine");
- gTvResult = gWin.findViewById("resultLine");
- printl("[卡片] 创建成功 pos=(" + x + "," + y + ") size=" + w + "x" + h);
- return true;
- } catch (e) {
- printl("[卡片] 创建失败: " + e);
- return false;
- }
- }
- // ==================== 第 2 步:算目标时间 ====================
- // 演示模式:现在 + N 秒;真实模式:今天的整点,已过则顺延到明天
- function calcTargetTime() {
- if (USE_DEMO) {
- uiPost(gTvTitle, function () { try { gTvTitle.setText("演示模式"); } catch (e) {} });
- return now() + DEMO_SECONDS * 1000;
- }
- var t = new Date();
- t.setHours(BUY_HOUR, BUY_MIN, BUY_SEC, 0);
- if (t.getTime() <= now()) t.setDate(t.getDate() + 1); // 过点了就等明天
- var label = BUY_HOUR + ":" + pad2(BUY_MIN) + ":" + pad2(BUY_SEC);
- uiPost(gTvTitle, function () {
- try { gTvTitle.setText("目标 " + label); } catch (e) {}
- });
- return t.getTime();
- }
- // ==================== 第 3 步:倒计时线程 ====================
- // 每 100ms 刷一次大字,直到出手为止(gStopTick 控制退出)
- function startCountdownThread() {
- new thread().runJsCode(function () {
- while (!gStopTick) {
- if (gStruck) break;
- var remain = gTargetTs - now();
- if (remain <= 0) {
- setCountdown("抢!", C_ERR);
- break;
- }
- // 剩 1 秒内显示毫秒,最后阶段更有紧张感
- if (remain < 1000) {
- setCountdown(remain + "ms", C_ERR);
- } else {
- var s = Math.ceil(remain / 1000);
- setCountdown(pad2(Math.floor(s / 60)) + ":" + pad2(s % 60), C_WARN);
- }
- doSleep(100);
- }
- }, "倒计时线程");
- printl("[线程] 倒计时线程已启动");
- }
- // ==================== 第 4 步:三段式高精度等待 ====================
- // 粗等:每次睡 100ms,到目标前 500ms 停(避免睡过头)
- // 细等:每次睡 10ms,到目标前 LEAD_MS 停
- // 自旋:busy-wait 精确到 1ms,保证出手时刻精准
- // 为什么要提前 LEAD_MS:从"判断到点"到"点击落到屏幕"本身要几十毫秒,
- // 提前出手,点击动作才正好压在目标时刻上
- function preciseWait(targetTs) {
- // 段 1:粗等
- while (targetTs - now() > 500) {
- doSleep(Math.min(100, targetTs - now() - 500));
- }
- // 段 2:细等
- while (targetTs - now() > LEAD_MS + 20) {
- doSleep(10);
- }
- // 段 3:自旋到出手时刻
- var fireAt = targetTs - LEAD_MS;
- while (now() < fireAt) {
- // busy-wait,不睡,精确咬住毫秒
- }
- }
- // ==================== 第 5 步:出手(到点连点)====================
- function strike() {
- var sw = screen.getScreenWidth();
- var sh = screen.getScreenHeight();
- // 演示:点屏幕中央;真实场景改成抢购按钮的坐标(用识图/找字定位)
- var cx = Math.floor(sw * 0.5);
- var cy = Math.floor(sh * 0.5);
- gStruck = true;
- setStatus("出手!", C_ERR);
- var fireTs = now();
- for (var i = 0; i < CLICK_TIMES; i++) {
- safeClick(cx + rand(-6, 6), cy + rand(-6, 6)); // 随机偏移防检测
- doSleep(40); // 连点间隔 40ms,太快会被系统吞掉
- }
- // 误差 = 实际出手 - 目标(负数 = 提前,正数 = 迟到)
- var error = fireTs - gTargetTs;
- var line = "✅ 出手于 " + formatMs(fireTs) + " 误差 " +
- (error >= 0 ? "+" : "") + error + "ms(提前量 " + LEAD_MS + "ms)";
- setResult(line, Math.abs(error) <= 30 ? C_OK : C_WARN);
- setStatus("抢购完成,3 秒后自动收起", C_OK);
- printl(line);
- // ---- 演示反馈:出手动作要肉眼可见 ----
- // toast 提示出手时刻和误差(放在点击之后,不能占用出手时机的时间)
- try { toast.show("⚡ 已出手!连点 " + CLICK_TIMES + " 次,误差 " +
- (error >= 0 ? "+" : "") + error + "ms"); } catch (e) {}
- // 模拟按键反馈:出手后按一次 HOME 键,屏幕会明显回到桌面
- if (DEMO_KEY_FEEDBACK) {
- doSleep(300);
- try {
- auto.home();
- printl("[反馈] 已模拟按 HOME 键");
- } catch (e) { printl("[反馈] 按键模拟失败: " + e); }
- }
- }
- function rand(a, b) { return a + Math.floor(Math.random() * (b - a + 1)); }
- // ==================== 第 6 步:优雅停止 ====================
- // 清理动作放前面,runTime.stopAllRuning() 必须放最后
- function cleanup() {
- gStopTick = true;
- setStatus("已手动停止", C_DIM);
- doSleep(300);
- try { if (gWin) { gWin.close(); gWin = null; } } catch (e) {}
- printl("[清理] 倒计时卡片已关闭");
- runTime.stopAllRuning(); // 最后才停所有线程
- stop();
- }
- event.onStopBtnEvent(function () {
- cleanup();
- });
- // ==================== 第 7 步:主流程 ====================
- function runMain() {
- printl("╔══════════════════════════════════════════╗");
- printl("║ 案例7:整点抢购倒计时(精准定时) ║");
- printl("╚══════════════════════════════════════════╝");
- if (!initFloatWindow()) {
- printl("悬浮卡片创建失败,脚本退出");
- return;
- }
- gTargetTs = calcTargetTime();
- printl("[目标] " + formatMs(gTargetTs) + "(" +
- (gTargetTs - now()) + "ms 后)");
- setStatus("等待开抢,提前量 " + LEAD_MS + "ms", C_WARN);
- try {
- toast.show("⚡ 倒计时开始:" + Math.round((gTargetTs - now()) / 1000) +
- " 秒后出手,注意看悬浮卡片");
- } catch (e) {}
- startCountdownThread();
- doSleep(500);
- // 三段式逼近目标时刻
- preciseWait(gTargetTs);
- // 到点出手
- strike();
- // 让观众看清结果,再收尾
- doSleep(3000);
- cleanup();
- }
- runMain();
复制代码
|
|