悬浮窗 - FloatPage
FloatPage 把一套与正式页相同的 page.json + page.js 挂到系统悬浮层上,适合运行状态 HUD、简易控制面板。任务脚本可用 setData 改里面的数据。
它和 悬浮球 FloatWindow 不是一回事:
悬浮球 FloatWindow | 悬浮窗 FloatPage | |
|---|---|---|
| 长什么样 | 一颗球 + 扇形菜单图标 | 完整 JSON 界面(组件、setData) |
| 工程文件 | deekeScript.json → floatWindow.menus | floats/<id>/page.json + page.js |
| 典型用途 | 开始 / 停止 / 跳过 | 进度、计数、开关、状态文案 |
最多同时显示 3 个悬浮窗。需要系统「显示在其他应用上层」权限(与悬浮球相同,简称悬浮窗权限)。
面板外壳默认:黑色 #000000,透明度用 opacity(0~1,默认 0.5),底色可用 background 改。内容区 page.json 的 style.background 请用透明色(如 #00000000),否则会盖住外壳;文字建议用浅色。
先看效果
左侧可切换并复制工程代码;右侧手机内可试多开(延时)、手柄开/关、拖动与 setData。
完整示例
目标:任务跑起来时弹出进度面板;脚本循环 setData;用户点「暂停」后任务下一轮退出;点「关闭」销毁面板。
1. 工程目录
floats/
status/
page.json
page.js
tasks/
floatPage.js不必写进 deekeScript.json 的 pages / tabBar。FloatPage.show('status') 会加载 floats/status/。
也可用完整路径:FloatPage.show('floats/status'),或临时挂正式页 FloatPage.show('pages/xxx')(一般仍建议放在 floats/)。
2. 权限
if (!Access.isFloatWindowsEnabled()) {
Dialogs.confirm('温馨提示', '请开启悬浮窗权限', function (ok) {
if (ok) {
Access.openFloatWindowsSetting();
}
});
return;
}3. floats/status/page.json
{
"style": {
"padding": 12,
"background": "#00000000",
"radius": 14
},
"body": [
{
"type": "row",
"style": { "gap": 8 },
"children": [
{
"type": "column",
"style": { "weight": 1, "gap": 2 },
"children": [
{
"type": "text",
"text": "{{text}}",
"style": { "fontSize": 16, "fontWeight": "bold", "color": "#FFFFFF" }
},
{
"type": "text",
"text": "{{hint}} · {{count}}/{{total}}",
"style": { "fontSize": 12, "color": "#CCFFFFFF" }
}
]
},
{
"type": "tag",
"text": "{{status}}",
"style": {
"background": "{{statusBg}}",
"color": "{{statusColor}}",
"radius": 999,
"paddingLeft": 10,
"paddingRight": 10,
"paddingTop": 4,
"paddingBottom": 4
}
}
]
},
{ "type": "space", "height": 8 },
{
"type": "progressBar",
"name": "progress",
"value": 0,
"min": 0,
"max": 100,
"unit": "%",
"color": "#4FD1C5",
"trackColor": "#2EFFFFFF",
"style": { "color": "#D9FFFFFF" }
},
{ "type": "space", "height": 10 },
{
"type": "row",
"style": { "gap": 8 },
"children": [
{
"type": "button",
"text": "暂停",
"size": "sm",
"onTap": "onTapPause",
"style": {
"weight": 1,
"background": "#33FFFFFF",
"color": "#FFFFFF",
"radius": 10
}
},
{
"type": "button",
"text": "关闭",
"size": "sm",
"onTap": "onTapClose",
"style": {
"weight": 1,
"background": "#80FFFFFF",
"color": "#111111",
"radius": 10
}
}
]
}
]
}4. floats/status/page.js
面板按钮不能直接停任务线程,通常用 Storage 打标,任务循环里读取。
var TOTAL = 30;
function panelState(partial) {
var count = partial.count != null ? (partial.count - 0) : 0;
var total = partial.total != null ? (partial.total - 0) : TOTAL;
if (total < 1) total = 1;
var progress = Math.floor((count * 100) / total);
var status = partial.status || '待命';
var running = status === '运行中';
var paused = status === '已暂停';
var done = status === '已结束';
return {
count: count,
total: total,
progress: progress,
text: partial.text || status,
hint: partial.hint || '顶部细条可拖动面板',
status: status,
statusBg: running ? '#3300C853' : paused ? '#33FFB300' : done ? '#334CAF50' : '#33FFFFFF',
statusColor: running ? '#A5D6A7' : paused ? '#FFE082' : done ? '#C8E6C9' : '#EEEEEE'
};
}
Page({
data: panelState({
count: 0,
text: '任务面板',
hint: '顶部细条可拖动面板',
status: '待命'
}),
onLoad: function () {
Storage.putBoolean('float_pause', false);
},
onClose: function () {
Storage.putBoolean('float_pause', true);
},
onTapPause: function () {
Storage.putBoolean('float_pause', true);
var cur = FloatPage.getData('status') || {};
this.setData(panelState({
count: cur.count,
total: cur.total,
text: '已暂停',
hint: '任务将在下一轮退出循环',
status: '已暂停'
}));
},
onTapClose: function () {
Storage.putBoolean('float_pause', true);
FloatPage.close('status');
}
});浮层里不要依赖 navigate / switchTab / 底栏 / 下拉刷新(这些对悬浮窗无效)。
5. tasks/floatPage.js
布尔标记请用 putBoolean / getBoolean。Storage.put(key, false) 会存成字符串 "false",在 JS 里非空串为真,if (Storage.get(key)) / !Storage.get(key) 会判错。
if (!Access.isFloatWindowsEnabled()) {
Access.openFloatWindowsSetting();
System.exit();
}
Storage.putBoolean('float_pause', false);
let total = 30;
let w = Math.floor(Device.width() * 0.8);
let ok = FloatPage.show('status', {
x: Math.floor((Device.width() - w) / 2),
y: 160,
width: w,
height: 500,
opacity: 0.5,
background: '#000000',
draggable: true,
focusable: false
});
if (!ok) {
console.log('打开失败:检查权限与 floats/status');
System.exit();
}
for (let i = 0; i < total; i++) {
if (Storage.getBoolean('float_pause')) {
break;
}
FloatPage.setData('status', {
count: i + 1,
total: total,
progress: Math.floor(((i + 1) * 100) / total),
text: '正在执行任务',
hint: '点「暂停」可中断',
status: '运行中',
statusBg: '#3300C853',
statusColor: '#A5D6A7'
});
System.sleep(1000);
}
FloatPage.close('status');
Engines.closeAll();这个示例覆盖了:
| 特性 | 怎么体现 |
|---|---|
| 系统悬浮层 | FloatPage.show 盖在其它 App 上 |
| JSON 界面 | page.json 里 text / tag / progressBar / button |
| 脚本推数据 | 任务循环 FloatPage.setData |
| 面板控任务 | 按钮写 Storage,任务读取后退出 |
| 可拖动 | 顶部细条(draggable: true / setDraggable) |
| 半透明外壳 | opacity + background(可用 setOpacity / setBackground) |
| 点击穿透 | touchable: false 或 setTouchable(id, false) |
| 读写位置尺寸 | getPosition / getSize |
| 多开 | 最多 3 个;可用不同 id + 同一 path;show 之间用 System.sleep 延时;高度建议错开以免叠成一块 |
也可在任务里这样多开并开关手柄:
let specs = [
{ id: 'm1', y: 80 },
{ id: 'm2', y: 300 },
{ id: 'm3', y: 520 }
];
for (let i = 0; i < specs.length; i++) {
FloatPage.show(specs[i].id, {
path: 'status',
x: 40,
y: specs[i].y,
width: Math.floor(Device.width() * 0.8),
height: 200,
opacity: 0.55,
draggable: true
});
System.sleep(800);
}
FloatPage.setDraggable('m1', false); // 关手柄
System.sleep(1000);
FloatPage.setDraggable('m1', true); // 开手柄API
FloatPage.show(id, options?)
返回: {boolean} 是否显示成功(无权限、目录不存在、超过 3 个时为 false)
| options | 说明 |
|---|---|
| x / y | 左上角坐标(px) |
| width / height | 宽高(屏幕像素 px);省略时默认宽约 80% 屏宽、高 500px |
| path | 加载的 floats 目录;省略则用 show 的 id。多开可共用:show('m1', { path: 'status' }) |
| opacity | 外壳透明度 0~1(仅外壳底色,不淡化文字)。默认 0.5 |
| background | 外壳底色,如 #000000、#006A65。默认黑 |
| draggable | 是否可拖,默认 true |
| focusable | 是否可获焦(输入框需要),默认 false |
| touchable | 是否接收点击,默认 true。false 时点击穿透 |
同一 id 再 show:已存在则更新位置/尺寸/外观并重新显示,不重复创建。
FloatPage.hide(id)
隐藏,实例与数据保留。再次 show 同一 id 可恢复。
FloatPage.close(id) / FloatPage.closeAll()
销毁悬浮窗:实例移除,面板内 data / 表单值全部清空。下次再 show 同一 id 会新建会话,从 page.js 的初始 data 开始。
整项任务被 FloatWindow.stopTask() 停止时也会 closeAll。
| hide | close | |
|---|---|---|
| 窗口 | 隐藏 | 销毁 |
| 数据 | 保留 | 清空 |
| 再 show | 恢复原实例 | 新建实例 |
FloatPage.exists(id) / FloatPage.isShowing(id)
exists:实例是否在(含已 hide)。isShowing:是否可见。
FloatPage.setData(id, data) / FloatPage.getData(id, selector?)
按 id 读写数据,与页内 this.setData 同一套绑定。
FloatPage.setPosition(id, x, y) / FloatPage.getPosition(id)
改 / 读位置。getPosition 返回 { x, y },不存在则 null。
FloatPage.setSize(id, width, height) / FloatPage.getSize(id)
改 / 读尺寸。getSize 返回 { width, height }。
FloatPage.setOpacity(id, opacity) / FloatPage.getOpacity(id)
改 / 读外壳透明度(0~1)。
FloatPage.setBackground(id, color) / FloatPage.getBackground(id)
改 / 读外壳底色(#RRGGBB)。透明度仍由 opacity 控制。
FloatPage.setTouchable(id, touchable) / FloatPage.isTouchable(id)
是否接收点击。false 时整窗点击穿透。
FloatPage.setDraggable(id, draggable) / FloatPage.isDraggable(id)
是否可通过顶部细条拖动。false 时隐藏手柄且不可拖;touchable: false 时拖拽也无法生效。
FloatPage.selfId()
在 floats/*/page.js 回调里返回当前实例 id(多开时与 path 可能不同)。非浮层返回 null。
