|
|
资讯缩略图:
资讯发布日期:2026-09-14
资讯发布简介:悬浮窗控制台floatUI 可拖动悬浮窗 + 按钮控制
资讯关键词:悬浮窗控制台floatUI 可拖动悬浮窗 + 按钮控制
资讯所属分类:IT资讯
联系:
① 本信息收集于网络,如有不对的地方欢迎联系我纠正! ② 本信息免费收录,不存在价格的问题! ③ 如果您的网站也想这样出现在这里,请您加好友情链接,我当天会审核通过!
④友情链接关键字:软件网站分类目录 网址:http://www.postbbs.com/
资讯详细描述
 | |  |  | 悬浮窗控制台floatUI 可拖动悬浮窗 + 按钮控制
- /* =============================================================================
- AIWROK 技巧项目 04 —— 悬浮窗控制台(floatUI 可拖动悬浮窗 + 按钮控制)
- 作用
- ────
- 一个能拖动、能点按钮的悬浮小窗,飘在屏幕最上层:
- ▶启动 / ⏸暂停 / ▶恢复 / ⏹停止 / 收起展开 / ✖关闭,
- 后台异步跑多线程任务,窗口实时显示状态。
- 这个项目演示的特殊技巧
- ──────────────────────
- 1. floatUI 悬浮窗:loadXML 加载布局、findViewById 拿控件、setPosition 定位
- 2. 拖动手势:给标题栏挂 OnTouchListener,用 MotionEvent 的 ACTION_DOWN/MOVE/UP
- + getRawX/getRawY 算位移
- ⚠ 真机实测坑:直接写 new View.OnTouchListener(...) 会打印"当前环境不支持拖拽",
- 因为短类名 View / MotionEvent 并不是全局可用的。
- 正确写法:先 importClass(android.view.View) / importClass(android.view.MotionEvent),
- 或用全类名 android.view.View.OnTouchListener、android.view.MotionEvent.ACTION_MOVE。
- 本脚本两者都做了(resolveView / resolveMotionEvent),拖不动时还能"点标题栏换位"兜底。
- 3. 按钮回调:控件.setOnClickListener(function(){...})
- 4. 折叠:View.GONE / View.VISIBLE 控制主体显隐(环境不支持时会自动降级)
- 5. 关闭:ui.close(),同时把线程停掉,不留后台野线程
- 6. 点击去重:真机上一次点击可能被派发多次(日志里刷 3 条"⏹ 已停止"就是这个原因),
- 所以命令要 ① 300ms 同命令去重 ② 池内方法自身幂等,双保险。
- 注意
- ────
- 本项目的悬浮窗就是"控制台"本身,不是自动弹出来刷屏的日志窗。
- 脚本里没有额外弹日志窗,全部日志走 printl 输出到 IDE。
- ============================================================================= */
- new floatUI().hideSystemFloat();//隐藏aiwork系统自带的悬浮图标
- var CFG = {
- workerCount: 3,
- taskCount: 15, // 每轮任务数(重启时自动重新装填)
- posX: 40, posY: 220, // 悬浮窗初始位置
- cmdDebounce: 300 // 同一命令 300ms 内的重复点击直接忽略
- };
- // ============================== Java 类导入(拖拽必需) ==============================
- // 坑:直接写 new View.OnTouchListener(...) 会报"不支持拖拽",
- // 因为短类名 View / MotionEvent 并不是全局可用的,必须先 importClass。
- try { importClass(android.view.View); } catch (e) {}
- try { importClass(android.view.MotionEvent); } catch (e) {}
- /** 取 View 类:短名 → 全类名 → null */
- function resolveView() {
- if (typeof View !== 'undefined' && View) return View;
- try { if (android && android.view && android.view.View) return android.view.View; } catch (e) {}
- return null;
- }
- /** 取 MotionEvent 类:短名 → 全类名 → null */
- function resolveMotionEvent() {
- if (typeof MotionEvent !== 'undefined' && MotionEvent) return MotionEvent;
- try { if (android && android.view && android.view.MotionEvent) return android.view.MotionEvent; } catch (e) {}
- return null;
- }
- var ui = null; // floatUI 实例
- var ctrls = {}; // 控件引用
- var screenW = 1080, screenH = 1920;
- var dragLastX = 0, dragLastY = 0, dragging = false;
- var expanded = true;
- var winX = CFG.posX, winY = CFG.posY;
- var dragEnabled = false; // 拖拽是否真的绑上了
- var lastCmd = { name: '', t: 0 };
- // ============================== 工具 ==============================
- function log(m) { try { printl(m); } catch (e) {} }
- function sleepMs(ms) { try { java.lang.Thread.sleep(ms); } catch (e) {} }
- function nowMs() { return java.lang.System.currentTimeMillis(); }
- function getScreenSize() {
- try {
- var m = context.getResources().getDisplayMetrics();
- if (m && m.widthPixels) { screenW = m.widthPixels; screenH = m.heightPixels; }
- } catch (e) {
- try { screenW = screen.getScreenWidth(); screenH = screen.getScreenHeight(); } catch (e2) {}
- }
- log('屏幕尺寸: ' + screenW + ' x ' + screenH);
- }
- // ============================== 后台任务池(精简版) ==============================
- function MiniPool(workerCount) {
- var workerN = workerCount || 3;
- var workers = [], queue = [], lock = false;
- var running = false, paused = false, accepting = true;
- var active = 0, total = 0, done = 0, failed = 0;
- var exited = 0; // 已自然退出的线程数
- function section(fn) {
- var g = 0;
- while (lock && g < 3000) { sleepMs(2); g++; }
- lock = true;
- try { return fn(); } finally { lock = false; }
- }
- function take() {
- var t = null;
- section(function () { if (queue.length > 0) { t = queue.shift(); active++; } });
- return t;
- }
- function loop(idx) {
- while (running) {
- if (paused) { sleepMs(120); continue; }
- var task = take();
- if (task == null) {
- if (!accepting && queue.length === 0) break;
- sleepMs(60);
- continue;
- }
- var ok = true, err = '';
- try { task.fn('T' + idx); } catch (e) { ok = false; err = String(e); }
- section(function () { active--; if (ok) done++; else failed++; });
- consoleLog((ok ? '✔ ' : '✘ ') + 'T' + idx + ' ' + task.name + (ok ? '' : ' ' + err));
- }
- // 关键:线程自然跑完退出时,要把池子状态归位;
- // 否则 running 一直是 true,任务跑完后再点【启动】会被当成"已在运行"而空转。
- section(function () {
- exited++;
- if (exited >= workerN) { running = false; paused = false; accepting = true; }
- });
- }
- this.add = function (name, fn) { section(function () { queue.push({ name: name, fn: fn }); total++; }); return this; };
- /** 本轮是否已跑完 */
- this.isFinished = function () { return total > 0 && (done + failed) >= total; };
- /** 清空队列与计数(只能在没有线程运行时调用) */
- this.reset = function () {
- if (running || active > 0) return false;
- section(function () {
- queue = []; total = 0; done = 0; failed = 0; exited = 0; accepting = true; paused = false;
- });
- return true;
- };
- // 所有控制方法都要幂等:重复点击只提示、不重复执行、不重复打日志
- this.start = function () {
- if (running) { consoleLog('· 已经在运行了'); return false; }
- if (queue.length === 0) { consoleLog('· 队列是空的,先装任务'); return false; }
- running = true; paused = false; accepting = true; exited = 0;
- for (var i = 0; i < workerN; i++) {
- var th = new thread();
- workers.push(th);
- (function (n) { th.runJsCode(function () { loop(n); }, '悬浮台线程' + n); })(i + 1);
- sleepMs(140);
- }
- consoleLog('▶ 已启动 ' + workerN + ' 个线程');
- return true;
- };
- this.pause = function () {
- if (!running) { consoleLog('· 没在运行,不用暂停'); return false; }
- if (paused) { consoleLog('· 已经是暂停状态'); return false; }
- paused = true;
- consoleLog('⏸ 已暂停');
- return true;
- };
- this.resume = function () {
- if (!paused) { consoleLog('· 当前不是暂停状态'); return false; }
- paused = false;
- consoleLog('▶ 已恢复');
- return true;
- };
- this.stop = function () {
- // 幂等:确实没东西在跑就什么都不做("⏹ 已停止"刷 3 条就是这么来的)
- var inf = this.info();
- if (!inf.running && inf.alive === 0 && inf.active === 0) {
- consoleLog('· 已经停止了');
- return false;
- }
- accepting = false; running = false; paused = false;
- for (var i = 0; i < workers.length; i++) {
- try { if (workers[i].isAlive()) workers[i].stop(); } catch (e) {}
- }
- exited = 0;
- consoleLog('⏹ 已停止');
- return true;
- };
- this.info = function () {
- var a = 0;
- for (var i = 0; i < workers.length; i++) { try { if (workers[i].isAlive()) a++; } catch (e) {} }
- return { total: total, done: done, failed: failed, queue: queue.length, active: active, alive: a, paused: paused, running: running };
- };
- }
- var pool = null;
- // ============================== 窗口内的状态显示 ==============================
- var consoleLines = [];
- function consoleLog(text) {
- var n = new Date();
- var t = ('0' + n.getHours()).slice(-2) + ':' + ('0' + n.getMinutes()).slice(-2) + ':' + ('0' + n.getSeconds()).slice(-2);
- consoleLines.unshift('[' + t + '] ' + text);
- if (consoleLines.length > 4) consoleLines = consoleLines.slice(0, 4);
- log(text);
- // 注意:这里不直接刷控件。控件更新统一放到主线程心跳里做,
- // 避免工作线程直接操作 UI 触发 Android 的 "only the original thread can touch its views" 异常。
- }
- function renderConsole() {
- try {
- if (ctrls.log) ctrls.log.setText(consoleLines.join('\n'));
- } catch (e) {}
- }
- function refreshStatus() {
- if (!pool) return;
- var i = pool.info();
- var pct = i.total > 0 ? Math.floor((i.done + i.failed) * 100 / i.total) : 0;
- var txt = '进度 ' + pct + '% ✔' + i.done + ' ✘' + i.failed +
- ' 队列' + i.queue + ' 线程' + i.alive;
- try { if (ctrls.status) ctrls.status.setText(txt); } catch (e) {}
- }
- // ============================== 浮动窗口 ==============================
- function buildXml() {
- return '' +
- '<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" ' +
- ' android:id="box" android:layout_width="230dp" android:layout_height="wrap_content" ' +
- ' android:orientation="vertical" android:background="#E6111A2B" android:padding="6dp">' +
- ' <LinearLayout android:id="title" android:layout_width="match_parent" ' +
- ' android:layout_height="34dp" android:orientation="horizontal" android:gravity="center_vertical" ' +
- ' android:background="#1F2A44">' +
- ' <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" ' +
- ' android:text="✥" android:textColor="#8FA3C8" android:textSize="15sp" android:paddingLeft="6dp" android:paddingRight="6dp"/>' +
- ' <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" ' +
- ' android:text="AIWROK 控制台" android:textColor="#FFFFFF" android:textSize="13sp"/>' +
- ' <Button android:id="btnFold" android:layout_width="34dp" android:layout_height="28dp" ' +
- ' android:text="-" android:textColor="#FFFFFF" android:textSize="12sp" android:background="#334155" android:padding="0dp"/>' +
- ' <Button android:id="btnClose" android:layout_width="34dp" android:layout_height="28dp" ' +
- ' android:text="✕" android:textColor="#FFFFFF" android:textSize="12sp" android:background="#EF4444" android:padding="0dp"/>' +
- ' </LinearLayout>' +
- ' <LinearLayout android:id="body" android:layout_width="match_parent" ' +
- ' android:layout_height="wrap_content" android:orientation="vertical" android:paddingTop="6dp">' +
- ' <TextView android:id="status" android:layout_width="match_parent" android:layout_height="wrap_content" ' +
- ' android:text="进度 0% ✔0 ✘0 队列0 线程0" android:textColor="#60A5FA" android:textSize="11sp" android:paddingBottom="6dp"/>' +
- ' <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" ' +
- ' android:orientation="horizontal">' +
- ' <Button android:id="btnStart" android:layout_width="0dp" android:layout_weight="1" ' +
- ' android:layout_height="36dp" android:text="▶ 启动" android:textColor="#FFFFFF" android:textSize="12sp" android:background="#16A34A" android:padding="0dp"/>' +
- ' <Button android:id="btnPause" android:layout_width="0dp" android:layout_weight="1" ' +
- ' android:layout_height="36dp" android:text="⏸ 暂停" android:textColor="#FFFFFF" android:textSize="12sp" android:background="#D97706" android:padding="0dp"/>' +
- ' </LinearLayout>' +
- ' <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" ' +
- ' android:orientation="horizontal" android:paddingTop="4dp">' +
- ' <Button android:id="btnResume" android:layout_width="0dp" android:layout_weight="1" ' +
- ' android:layout_height="36dp" android:text="▶ 恢复" android:textColor="#FFFFFF" android:textSize="12sp" android:background="#2563EB" android:padding="0dp"/>' +
- ' <Button android:id="btnStop" android:layout_width="0dp" android:layout_weight="1" ' +
- ' android:layout_height="36dp" android:text="⏹ 停止" android:textColor="#FFFFFF" android:textSize="12sp" android:background="#B91C1C" android:padding="0dp"/>' +
- ' </LinearLayout>' +
- ' <TextView android:id="log" android:layout_width="match_parent" android:layout_height="wrap_content" ' +
- ' android:text="" android:textColor="#86EFAC" android:textSize="10sp" android:paddingTop="6dp"/>' +
- ' </LinearLayout>' +
- '</LinearLayout>';
- }
- function moveTo(x, y) {
- if (x < 0) x = 0;
- if (y < 0) y = 0;
- if (x > screenW - 120) x = screenW - 120;
- if (y > screenH - 80) y = screenH - 80;
- winX = x; winY = y;
- try { ui.setPosition(x, y); } catch (e) {}
- }
- /** 绑定拖拽。设备实测:短类名 View/MotionEvent 不存在 → 必须用 importClass 后的类 */
- function setupDrag() {
- var V = resolveView();
- var M = resolveMotionEvent();
- if (!V || !M || !ctrls.title || !ctrls.title.setOnTouchListener) {
- log('⚠ 拖拽不可用(缺 ' + (V ? '' : 'View ') + (M ? '' : 'MotionEvent') + '),改用【点标题栏换位】');
- bindTitleTapToMove();
- return false;
- }
- try {
- ctrls.title.setOnTouchListener(new V.OnTouchListener({
- onTouch: function (view, event) {
- try {
- switch (event.getAction()) {
- case M.ACTION_DOWN:
- dragging = true;
- dragLastX = event.getRawX();
- dragLastY = event.getRawY();
- break;
- case M.ACTION_MOVE:
- if (dragging) {
- var dx = event.getRawX() - dragLastX;
- var dy = event.getRawY() - dragLastY;
- moveTo(winX + dx, winY + dy);
- dragLastX = event.getRawX();
- dragLastY = event.getRawY();
- }
- break;
- case M.ACTION_UP:
- dragging = false;
- break;
- }
- return true;
- } catch (e2) { return false; }
- }
- }));
- dragEnabled = true;
- log('✔ 拖拽已绑定:按住标题栏可移动窗口');
- return true;
- } catch (e) {
- log('⚠ 拖拽绑定失败: ' + e);
- bindTitleTapToMove();
- return false;
- }
- }
- /** 拖拽不可用时的兜底:点一下标题栏,在几个预设位置间循环 */
- var presetIdx = -1;
- function bindTitleTapToMove() {
- if (!ctrls.title || !ctrls.title.setOnClickListener) return;
- try {
- ctrls.title.setOnClickListener(function () {
- var presets = [
- [20, 160], [Math.floor(screenW * 0.5) - 115, 160],
- [screenW - 250, 160], [screenW - 250, Math.floor(screenH * 0.5)],
- [20, Math.floor(screenH * 0.6)]
- ];
- presetIdx = (presetIdx + 1) % presets.length;
- moveTo(presets[presetIdx][0], presets[presetIdx][1]);
- log('↔ 窗口已换位到第 ' + (presetIdx + 1) + ' 个位置');
- });
- } catch (e) {}
- }
- function toggleFold() {
- expanded = !expanded;
- var V = resolveView();
- try {
- if (V && V.GONE !== undefined && ctrls.body && ctrls.body.setVisibility) {
- ctrls.body.setVisibility(expanded ? V.VISIBLE : V.GONE);
- log(expanded ? '🔼 已展开' : '🔽 已收起');
- return true;
- }
- } catch (e) {}
- expanded = true;
- log('⚠ 当前环境不支持折叠,窗口保持展开');
- return false;
- }
- function closeConsole() {
- log('✖ 关闭控制台');
- try { pool.stop(); } catch (e) {}
- try { ui.close(); } catch (e) { log('关闭悬浮窗失败: ' + e); }
- alive = false;
- }
- // ============================== 命令分发(去重 + 立即刷界面) ==============================
- // 实测发现:一次点击可能被派发多次(3 条一模一样的"已停止"就是这么来的)。
- // 双保险:① 同一命令 300ms 内只认第一次;② MiniPool 内部各方法本身幂等。
- function onceCmd(name) {
- var t = nowMs();
- if (name === lastCmd.name && t - lastCmd.t < CFG.cmdDebounce) return false;
- lastCmd = { name: name, t: t };
- return true;
- }
- /** 按钮回调跑在 UI 线程,这里直接刷控件是安全的,点完立刻能看到变化 */
- function afterCmd() {
- refreshStatus();
- renderConsole();
- }
- function cmdStart() {
- if (!onceCmd('start')) return;
- if (pool.isFinished()) { // 上一轮已跑完 → 自动重新装填,避免"重启空跑"
- pool.reset();
- refillTasks();
- consoleLog('🔄 已重新装填 ' + CFG.taskCount + ' 个任务');
- }
- pool.start();
- afterCmd();
- }
- function cmdPause() { if (!onceCmd('pause')) return; pool.pause(); afterCmd(); }
- function cmdResume() { if (!onceCmd('resume')) return; pool.resume(); afterCmd(); }
- function cmdStop() { if (!onceCmd('stop')) return; pool.stop(); afterCmd(); }
- function cmdFold() { if (!onceCmd('fold')) return; toggleFold(); afterCmd(); }
- function cmdClose() { if (!onceCmd('close')) return; closeConsole(); }
- // ============================== 界面装配 ==============================
- var alive = true;
- function createConsole() {
- getScreenSize();
- ui = new floatUI();
- ui.loadXML(buildXml());
- ctrls.title = ui.findViewById('title');
- ctrls.body = ui.findViewById('body');
- ctrls.status = ui.findViewById('status');
- ctrls.log = ui.findViewById('log');
- ctrls.btnStart = ui.findViewById('btnStart');
- ctrls.btnPause = ui.findViewById('btnPause');
- ctrls.btnResume = ui.findViewById('btnResume');
- ctrls.btnStop = ui.findViewById('btnStop');
- ctrls.btnFold = ui.findViewById('btnFold');
- ctrls.btnClose = ui.findViewById('btnClose');
- if (!ctrls.btnStart) { log('✘ 悬浮窗控件未取到,设备可能不支持 floatUI'); return false; }
- // 按钮事件(统一走 cmd* 分发器:带去重 + 幂等 + 立即刷界面)
- ctrls.btnStart.setOnClickListener(function () { try { cmdStart(); } catch (e) { log('启动失败: ' + e); } });
- ctrls.btnPause.setOnClickListener(function () { try { cmdPause(); } catch (e) {} });
- ctrls.btnResume.setOnClickListener(function () { try { cmdResume(); } catch (e) {} });
- ctrls.btnStop.setOnClickListener(function () { try { cmdStop(); } catch (e) {} });
- ctrls.btnFold.setOnClickListener(function () { try { cmdFold(); } catch (e) {} });
- ctrls.btnClose.setOnClickListener(function () { try { cmdClose(); } catch (e) {} });
- setupDrag();
- moveTo(CFG.posX, CFG.posY);
- consoleLog('控制台就绪,点【▶ 启动】跑任务');
- return true;
- }
- // ============================== 任务与心跳 ==============================
- function buildTask(idx) {
- return function (tag) {
- sleepMs(rand.randNumber(400, 1200));
- if (idx % 8 === 0) throw new Error('第' + idx + '个任务异常');
- };
- }
- /** 装填一轮任务 */
- function refillTasks() {
- for (var i = 1; i <= CFG.taskCount; i++) pool.add('任务' + i, buildTask(i));
- }
- /** 心跳(跑在主线程):刷新窗口状态 + 给主线程保活 */
- function heartbeat() {
- if (!alive) { log('心跳结束,主线程退出'); return; }
- refreshStatus();
- renderConsole();
- runTime.setTimeout(heartbeat, 1000);
- }
- // ============================== 入口 ==============================
- function main() {
- log('════════════════════════════════════════════');
- log(' 悬浮窗控制台(floatUI)');
- log('════════════════════════════════════════════');
- pool = new MiniPool(CFG.workerCount);
- refillTasks();
- if (!createConsole()) return;
- heartbeat();
- log(dragEnabled ? '👉 按住标题栏拖动窗口' : '👉 点标题栏可循环换位');
- log(' 【▶ 启动】跑任务 【⏸ 暂停】/【▶ 恢复】 【⏹ 停止】 【-】收起 【✕】关闭');
- log(' 任务跑完后再点【▶ 启动】会自动重新装填一轮');
- }
- main();
复制代码
| |  | |  |
|
unto日志分级落盘配置持久化任务调度nextnocontent
|