整点抢购倒计时倒计时悬浮卡片
整点抢购倒计时倒计时悬浮卡片/**
* ╔══════════════════════════════════════════════════════════╗
* ║ 案例 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();
页:
[1]