Skip to content

悬浮窗 - FloatPage

FloatPage 把一套与正式页相同的 page.json + page.js 挂到系统悬浮层上,适合运行状态 HUD、简易控制面板。任务脚本可用 setData 改里面的数据。

它和 悬浮球 FloatWindow 不是一回事

悬浮球 FloatWindow悬浮窗 FloatPage
长什么样一颗球 + 扇形菜单图标完整 JSON 界面(组件、setData
工程文件deekeScript.jsonfloatWindow.menusfloats/<id>/page.json + page.js
典型用途开始 / 停止 / 跳过进度、计数、开关、状态文案

最多同时显示 3 个悬浮窗。需要系统「显示在其他应用上层」权限(与悬浮球相同,简称悬浮窗权限)。

面板外壳默认:黑色 #000000,透明度用 opacity(0~1,默认 0.5),底色可用 background 改。内容区 page.jsonstyle.background 请用透明色(如 #00000000),否则会盖住外壳;文字建议用浅色。

先看效果

左侧可切换并复制工程代码;右侧手机内可试多开(延时)、手柄开/关、拖动与 setData。

完整示例

目标:任务跑起来时弹出进度面板;脚本循环 setData;用户点「暂停」后任务下一轮退出;点「关闭」销毁面板。

1. 工程目录

text
floats/
  status/
    page.json
    page.js
tasks/
  floatPage.js

不必写进 deekeScript.jsonpages / tabBarFloatPage.show('status') 会加载 floats/status/

也可用完整路径:FloatPage.show('floats/status'),或临时挂正式页 FloatPage.show('pages/xxx')(一般仍建议放在 floats/)。

2. 权限

javascript
if (!Access.isFloatWindowsEnabled()) {
  Dialogs.confirm('温馨提示', '请开启悬浮窗权限', function (ok) {
    if (ok) {
      Access.openFloatWindowsSetting();
    }
  });
  return;
}

3. floats/status/page.json

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 打标,任务循环里读取。

javascript
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 / getBooleanStorage.put(key, false) 会存成字符串 "false",在 JS 里非空串为真,if (Storage.get(key)) / !Storage.get(key) 会判错。

javascript
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: falsesetTouchable(id, false)
读写位置尺寸getPosition / getSize
多开最多 3 个;可用不同 id + 同一 pathshow 之间用 System.sleep 延时;高度建议错开以免叠成一块

也可在任务里这样多开并开关手柄:

javascript
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是否接收点击,默认 truefalse点击穿透

同一 idshow:已存在则更新位置/尺寸/外观并重新显示,不重复创建。

FloatPage.hide(id)

隐藏,实例与数据保留。再次 show 同一 id 可恢复。

FloatPage.close(id) / FloatPage.closeAll()

销毁悬浮窗:实例移除,面板内 data / 表单值全部清空。下次再 show 同一 id 会新建会话,从 page.js 的初始 data 开始。
整项任务被 FloatWindow.stopTask() 停止时也会 closeAll

hideclose
窗口隐藏销毁
数据保留清空
再 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

注意

  • 先开悬浮窗权限,再 show
  • hide 保数据;close 销毁并清空数据。
  • 不要与 FloatWindow(悬浮球菜单)搞混。
  • 相关:悬浮球权限 Access页面结构