YYPOST群发软件 发表于 昨天 06:54

整点抢购倒计时倒计时悬浮卡片

整点抢购倒计时倒计时悬浮卡片






/**
* ╔══════════════════════════════════════════════════════════╗
* ║   案例 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]
查看完整版本: 整点抢购倒计时倒计时悬浮卡片