更新记录

1.0.0(2026-03-06)

  • 新增 Android UTS 悬浮窗插件 dh-float-window
  • 支持 Float / FloatBridge 双向 JSON 协议通信
  • 支持生命周期、窗口控制、内容控制、事件广播、结构化函数调用
  • 支持 WebView -> Uni 请求响应模型(request/response)

平台兼容性

uni-app(4.0)

Vue2 Vue3 Chrome Safari app-vue app-nvue Android Android插件版本 iOS 鸿蒙
- - - - - - 5.0 1.0.0 × -
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
- - - - - - - - - - - -

其他

多语言 暗黑模式 宽屏模式

UniApp Android 悬浮窗插件(应用级浮窗 / 悬浮球 / WebView通信)

Android UTS 悬浮窗插件,提供 WebView 悬浮窗创建、窗口控制、WebView与UniApp双向通信、函数调用、多拖拽模式,适用于工具类、助手类、悬浮控制面板等场景。

  • 平台: Android
  • 语言: UTS + JavaScript
  • 推荐入口: core/float.api.js
  • 底层入口: getFloatManager()

目录

  1. 插件定位
  2. 能力特性
  3. 坐标与尺寸统一规范
  4. 安装与目录结构
  5. Quick Start
  6. 选择哪套 API
  7. UniApp API(高层 Float)
  8. UniApp API(底层 getFloatManager)
  9. HTML API(FloatBridge)
  10. DragMode 说明
  11. Safeguard 说明
  12. 完整示例
  13. 常见问题
  14. 类型定义

插件定位

dh-float-window 面向 UniApp Android 场景,核心目标是把悬浮窗业务从“事件拼接 + 回调地狱”升级为“结构化协议 + Promise 风格 + 自动保障”。

插件包含两层能力:

层级 入口 适用场景
高层封装 @/uni_modules/dh-float-window/core/float.api.js 业务开发(推荐)
底层原生 getFloatManager() 插件扩展、性能优化、协议定制

能力特性

特性 说明
多窗口 同时创建多个窗口,按 id 管理
双向通信 UniApp 与 HTML 双向 emit/request
函数调用 UniApp 直接 call HTML 全局方法
请求响应 HTML request 到 UniApp,支持 Promise
窗口控制 show/hide/destroy/setRect/setDragMode
触摸策略 focusable/touchThrough 细粒度控制
失联保障 内置心跳检测、自动恢复、失败兜底关闭
透明背景 WebView 支持透明背景与圆角小组件样式

坐标与尺寸统一规范

插件第一原则:

  • x / y / width / height 输入单位统一为 CSS 逻辑像素(css px)
  • 插件内部负责换算到 Android 原生窗口需要的物理像素
  • 插件不内置业务设计稿规则(如 750/rpx/rem

这条规则的直接含义:

  1. 你在 UniApp 里传 width: 360,语义是 360 css px
  2. HTML 在标准 viewport(initial-scale=1)下写 width: 360px 时,尺寸可对齐
  3. 如果你的项目使用 750 设计稿,请在业务调用侧自行换算后再传给插件

业务侧换算示例(仅项目侧,不属于插件规则):

function fromDesign750(value) {
  const info = uni.getWindowInfo();
  return Math.round((info.windowWidth / 750) * value);
}

await Float.create({
  id: 'demo',
  x: fromDesign750(50),
  y: fromDesign750(400),
  width: fromDesign750(630),
  height: fromDesign750(460),
  url: htmlUrl('float-demo.html'),
});

建议:同一页面避免多套缩放体系叠加(如窗口尺寸用 750 缩放,同时 HTML 内再混用 rem 根缩放与固定 px),否则会出现视觉尺寸偏差。


安装与目录结构

插件位于:src/uni_modules/dh-float-window

路径 说明
utssdk/app-android/index.uts Android 原生实现
utssdk/interface.uts UTS 类型接口
utssdk/index.uts UTS 导出入口
index.d.ts 底层 TS 类型
core/float.api.js 高层 Promise API
core/float.api.d.ts 高层 TS 类型

Quick Start

1. UniApp 侧创建悬浮窗

import Float from '@/uni_modules/dh-float-window/core/float.api.js';

function htmlUrl(file) {
  // #ifdef APP-PLUS
  return 'file://' + plus.io.convertLocalFileSystemURL(`_www/static/html/example/${file}`);
  // #endif
  return '';
}

async function openFloat() {
  const granted = await Float.ensurePermission();
  if (!granted) {
    return;
  }

  await Float.create({
    id: 'demo_main',
    url: htmlUrl('float-demo.html'),
    x: 24,
    y: 180,
    width: 760,
    height: 560,
    dragMode: 2,
    focusable: true,
    touchThrough: false,
    backgroundColor: '#00000000',
    dataJson: JSON.stringify({ from: 'quick-start' }),
  });
}

2. HTML 侧读取初始化数据并发消息

<script>
  function onReady() {
    const data = window.FloatBridge.getInitData();
    console.log('initData', data);

    window.FloatBridge.emit('ping', { ts: Date.now(), from: 'html' });
  }

  if (window.FloatBridge) {
    onReady();
  } else {
    window.addEventListener('float-bridge-ready', onReady, { once: true });
  }
</script>

3. UniApp 侧监听并响应 HTML 请求

Float.on('demo_main', 'ping', (payload) => {
  console.log('ping from html', payload);
});

Float.onRequest('demo_main', 'getTime', () => {
  return { now: Date.now() };
});

选择哪套 API

推荐结论

生产业务优先使用 core/float.api.js,仅在需要更底层控制时使用 getFloatManager()

评估对比

维度 core/float.api.js getFloatManager()
使用难度
返回值风格 Promise boolean / callback
消息协议 已封装 需手工 JSON
请求响应 内置 onRequest 需手工 respondRequest
失联保障 内置 Safeguard 需自行实现
适用场景 业务开发 框架扩展、调优

UniApp API(高层 Float)

入口:@/uni_modules/dh-float-window/core/float.api.js

import Float from '@/uni_modules/dh-float-window/core/float.api.js';

生命周期与窗口控制

方法 参数 返回 说明
canDrawOverlays() - boolean 检查悬浮窗权限
openPermissionSettings() - boolean 拉起系统设置
ensurePermission() - Promise<boolean> 检查悬浮窗权限,不足时拉起系统设置
create(options) FloatCreateOptions Promise<void> 创建窗口并自动启动 Safeguard
show(id) string Promise<void> 显示窗口
hide(id) string Promise<void> 隐藏窗口并暂停保活计时
destroy(id) string Promise<void> 销毁窗口并清理状态
setRect(id, rect, animate?) id, {x,y,width,height}, boolean Promise<void> 修改位置和尺寸
setDragMode(id, mode) id, number Promise<void> 设置拖拽模式
setZIndex(id, zIndex) id, number Promise<void> 设置层级
setFocusable(id, enabled) id, boolean Promise<void> 设置可聚焦
setTouchThrough(id, enabled) id, boolean Promise<void> 设置触摸穿透
setAppScope(id, scope) id, string Promise<void> 业务作用域标记
loadUrl(id, url) id, string Promise<void> 加载 URL
loadHtml(id, html) id, string Promise<void> 加载 HTML 字符串
reload(id) id Promise<void> 刷新页面

消息与调用

方法 参数 返回 说明
emit(id, type, payload?) id, string, object Promise<void> 向指定窗口发送事件
broadcast(type, payload?) string, object Promise<void> 广播到所有窗口
call(id, options) { method, args?, needResult? } Promise<any> 调用 HTML 全局方法
on(id, type, cb) id, type, callback void 监听窗口消息
off(id, type, cb?) id, type, callback? void 取消监听
onRequest(id, type, handler) id, type, handler void 注册 HTML request 处理器
offRequest(id, type) id, type void 取消 request 处理

Safeguard

方法 参数 返回 说明
enableSafeguard(id, options?) id, FloatGuardOptions void 开启/更新保障策略
disableSafeguard(id) id void 关闭保障
runSafeguardCheck(id) id Promise<void> 手动执行一次心跳检查

高频示例

const id = 'demo_main';

await Float.create({
  id,
  url: htmlUrl('float-demo.html'),
  x: 30,
  y: 180,
  width: 760,
  height: 560,
  dragMode: 2,
  focusable: true,
  touchThrough: false,
  dataJson: JSON.stringify({ userId: 'u1001' }),
});

Float.on(id, 'search', (payload) => {
  console.log('search keyword =>', payload?.keyword);
});

Float.onRequest(id, 'sum', (payload) => {
  return { sum: Number(payload?.a || 0) + Number(payload?.b || 0) };
});

const ret = await Float.call(id, {
  method: 'demoAdd',
  args: [7, 8],
  needResult: true,
});
console.log('demoAdd result =>', ret);

UniApp API(底层 getFloatManager)

入口:@/uni_modules/dh-float-window

import { getFloatManager } from '@/uni_modules/dh-float-window';
const manager = getFloatManager();

API 表

方法 返回 说明
canDrawOverlays() boolean 是否有悬浮窗权限
openOverlayPermissionSettings() void 打开系统悬浮窗授权页
create(options) boolean 创建窗口
show(id) boolean 显示窗口
hide(id) boolean 隐藏窗口
destroy(id) boolean 销毁窗口
setRect(id, rect, animate) boolean 位置尺寸
setDragMode(id, mode) boolean 拖拽模式
setZIndex(id, zIndex) boolean 层级
setFocusable(id, enabled) boolean 焦点
setTouchThrough(id, enabled) boolean 穿透
setAppScope(id, scope) boolean 作用域
loadUrl(id, url) boolean 加载 URL
loadHtml(id, html) boolean 加载 HTML
reload(id) boolean 刷新
emit(id, type, payloadJson) boolean 发送消息
broadcast(type, payloadJson) boolean 广播消息
call(id, options) boolean 调用 HTML 方法
respondRequest(id, requestId, ok, payloadJson) boolean 响应 request
onMessage(callback) void 全量消息监听
offMessage() void 清除消息监听

底层示例

const id = 'native_demo';

if (!manager.canDrawOverlays()) {
  manager.openOverlayPermissionSettings();
}

const ok = manager.create({
  id,
  url: htmlUrl('float-demo.html'),
  x: 40,
  y: 220,
  width: 680,
  height: 420,
  dragMode: 2,
  focusable: true,
  touchThrough: false,
  dataJson: JSON.stringify({ from: 'manager-demo' }),
});

if (!ok) {
  throw new Error('create failed');
}

manager.onMessage((messageJson) => {
  const message = JSON.parse(messageJson);
  console.log('[onMessage]', message.type, message.source, message.payload);

  if (message.type === 'request:getTime') {
    const requestId = message?.payload?.requestId;
    manager.respondRequest(message.source, requestId, true, JSON.stringify({ now: Date.now() }));
  }
});

HTML API(FloatBridge)

在悬浮窗页面加载完成后,插件会注入 window.FloatBridge

API 表

方法 参数 返回 说明
getWindowId() - string 当前窗口 ID
getInitData() - any 读取 create.dataJson
emit(type, payload, target?) string, any, string? void 发事件到 UniApp
request(type, payload, target?) string, any, string? Promise<any> 请求 UniApp 并等待响应
close() - void 关闭当前窗口
hide() - void 隐藏窗口
show() - void 显示窗口
resize(width, height) number, number void 调整窗口尺寸
move(x, y) number, number void 移动窗口
setDragMode(mode) number void 设置拖拽模式(0不可拖拽,1自由拖拽,2吸边拖拽)
backToApp(data?) object? void 回到主应用

HTML 示例

<script>
  function ready() {
    const id = window.FloatBridge.getWindowId();
    const initData = window.FloatBridge.getInitData();

    console.log('windowId =>', id);
    console.log('initData =>', initData);

    window.FloatBridge.emit('ping', { from: 'html', ts: Date.now() });

    window.FloatBridge.request('sum', { a: 11, b: 29 })
      .then((res) => {
        console.log('sum result =>', res);
      })
      .catch((err) => {
        console.error('sum error =>', err);
      });
  }

  if (window.FloatBridge) {
    ready();
  } else {
    window.addEventListener('float-bridge-ready', ready, { once: true });
  }
</script>

DragMode 说明

dragMode 决定窗口是否可拖拽以及拖拽行为。

名称 行为
0 none 不可拖拽
1 free 自由拖拽
2 edge 拖拽后自动吸附左右边缘

推荐实践

场景 建议 dragMode
大型主面板 2
小型工具卡片 1
Toast/纯展示层 0

示例

await Float.setDragMode('demo_main', 2);
<script>
  // HTML 内切换拖拽模式
  window.FloatBridge.setDragMode(1);
</script>

Safeguard 说明

Safeguard 是悬浮窗失联保护机制,默认在 Float.create() 后自动开启。

机制流程:

  1. UniApp 定时向窗口发心跳 __float_heartbeat__
  2. HTML 侧自动回应 __float_heartbeat_ack__
  3. 超时累计达到阈值后,按恢复链路执行
  4. 失败时可选择自动关闭窗口,避免假死

恢复链路:

  1. reload
  2. hide + show
  3. destroy + recreate
  4. 关闭窗口(closeOnFail = true

默认配置(来自源码):

字段 默认值 说明
enabled true 是否启用
intervalMs 12000 心跳间隔
timeoutMs 8000 单次超时
maxMissed 2 连续失败阈值
closeOnFail true 恢复失败是否关闭

配置示例

Float.enableSafeguard('demo_main', {
  intervalMs: 10000,
  timeoutMs: 6000,
  maxMissed: 2,
  closeOnFail: true,
});

保障事件示例

Float.on('system', 'guard-warning', (payload) => {
  console.warn('guard warning =>', payload);
});

Float.on('system', 'guard-failed', (payload) => {
  console.error('guard failed =>', payload);
});

完整示例

UniApp 页面(节选)

源码参考:src/pages/float-demo/index.vue

import Float from '@/uni_modules/dh-float-window/core/float.api.js';

const MAIN_ID = 'float_demo_main';

function htmlUrl(file) {
  return 'file://' + plus.io.convertLocalFileSystemURL(`_www/static/html/example/${file}`);
}

async function runDemo() {
  const granted = await Float.ensurePermission();
  if (!granted) return;

  await Float.create({
    id: MAIN_ID,
    url: htmlUrl('float-demo.html'),
    x: 24,
    y: 180,
    width: 760,
    height: 560,
    dragMode: 2,
    focusable: true,
    touchThrough: false,
    backgroundColor: '#00000000',
    dataJson: JSON.stringify({ from: 'demo-page' }),
  });

  Float.on(MAIN_ID, 'ping', (payload) => {
    console.log('ping =>', payload);
  });

  Float.onRequest(MAIN_ID, 'sum', (p) => ({
    sum: Number(p?.a || 0) + Number(p?.b || 0),
  }));
}

HTML 页面(节选)

源码参考:src/static/html/example/float-demo.html

<script>
  function demoAdd(a, b) {
    return Number(a || 0) + Number(b || 0);
  }

  async function bootstrap() {
    const initData = window.FloatBridge.getInitData();
    console.log('initData =>', initData);

    window.FloatBridge.emit('ping', { msg: 'hello from web' });

    const time = await window.FloatBridge.request('getTime', { from: 'float-demo' });
    console.log('getTime =>', time);
  }

  if (window.FloatBridge) {
    bootstrap();
  } else {
    window.addEventListener('float-bridge-ready', bootstrap, { once: true });
  }
</script>

常见问题

1. 设置同样 width/height/x/y,新插件看起来更小

通常是业务侧出现了重复缩放(例如:调用前做过 750 换算,HTML 内又叠加一层 rem 缩放并混用 px)。

插件规则固定为 css px,请统一业务侧坐标系,避免多重缩放叠加。

2. HTML 里 FloatBridge 偶发未定义

建议等待 float-bridge-ready 事件后再调用。

3. 如何避免“悬浮窗卡死但还在屏幕上”

启用 Safeguard 并监听 guard-warning / guard-failed,必要时提示用户重开窗口。

4. 请求超时怎么处理

Float.call(...needResult: true)FloatBridge.request(...) 都应在业务层加重试或降级提示。


类型定义

  • 底层类型:src/uni_modules/dh-float-window/index.d.ts
  • 高层类型:src/uni_modules/dh-float-window/core/float.api.d.ts

建议 TS 项目优先使用高层类型与高层 API。

隐私、权限声明

1. 本插件需要申请的系统权限列表:

android.permission.SYSTEM_ALERT_WINDOW

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

插件在悬浮窗中加载 WebView 内容,支持双向消息通信

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

暂无用户评论。