更新记录
1.0.0(2026-10-16)
安卓悬浮窗
平台兼容性
uni-app(5.26)
| Vue2 |
Vue2插件版本 |
Vue3 |
Vue3插件版本 |
Chrome |
Safari |
app-vue |
app-vue插件版本 |
app-nvue |
app-nvue插件版本 |
Android |
Android插件版本 |
iOS |
鸿蒙 |
| √ |
1.0.0 |
√ |
1.0.0 |
× |
× |
√ |
1.0.0 |
√ |
1.0.0 |
10.0 |
1.0.0 |
× |
× |
| 微信小程序 |
支付宝小程序 |
抖音小程序 |
百度小程序 |
快手小程序 |
京东小程序 |
鸿蒙元服务 |
QQ小程序 |
飞书小程序 |
小红书小程序 |
快应用-华为 |
快应用-联盟 |
| × |
× |
× |
× |
× |
× |
× |
× |
× |
× |
× |
× |
uni-app x(5.26)
| Chrome |
Safari |
Android |
Android插件版本 |
iOS |
鸿蒙 |
微信小程序 |
| × |
× |
10.0 |
1.0.0 |
× |
× |
× |
其他
| 多语言 |
暗黑模式 |
宽屏模式 |
蒸汽模式 |
| × |
× |
× |
√ |
基于 UTS 的 Android 系统级悬浮窗插件。
- 使用
WindowManager + WebView 实现的系统级悬浮窗,支持长按拖拽、吸边、圆角、背景色
- 支持
html 与 url 二选一:只传 html 直接渲染内容;传 url 时加载页面,html(若传)追加到页面末尾
- HTML 内可通过
yaoFloatWindow.postMessage(data) 向原生回传消息(可多次触发)
- 支持
onMessage 独立注册消息回调,跨 show/update 持续生效,无需重复传
- 支持动态更新悬浮窗尺寸,可传
animated 参数控制是否动画过渡
将 yao-floating-window 目录放入项目的 uni_modules/ 下,HBuilderX 会自动识别。
仅支持 Android 端,不支持 iOS。
原生代码修改后必须重新制作自定义基座才能生效。
引入方式
import * as floatingWindow from '@/uni_modules/yao-floating-window'
API
| 方法 |
说明 |
floatingWindow.hasOverlayPermission() |
是否已授予悬浮窗权限(同步,返回 boolean) |
floatingWindow.requestOverlayPermission(options) |
申请悬浮窗权限(跳转系统设置页,返回时回调) |
floatingWindow.showFloatingWindow(options) |
显示悬浮窗 |
floatingWindow.updateFloatingWindow(options) |
更新悬浮窗内容/尺寸(悬浮窗显示中才有效) |
floatingWindow.hideFloatingWindow() |
隐藏(关闭)悬浮窗 |
floatingWindow.isFloatingWindowShowing() |
悬浮窗是否正在显示(同步,返回 boolean) |
floatingWindow.onMessage({ callback }) |
注册消息回调(只需一次,跨 show/update 持续生效) |
调用示例
<template>
<view class="page">
<view class="card">
<text>悬浮窗权限:{{ permission ? '已授予' : '未授予' }}</text>
<text>悬浮窗显示中:{{ showing ? '是' : '否' }}</text>
<button size="mini" @click="refreshStatus">刷新状态</button>
<button v-if="!permission" size="mini" type="warn" @click="requestPermission">申请悬浮窗权限</button>
</view>
<view class="card">
<text>方式一:自定义 HTML 代码(html 参数,吸边拖拽)</text>
<button type="primary" @click="showByHtml">显示悬浮窗(HTML 代码)</button>
</view>
<view class="card">
<text>方式二:HTML 文件(url 参数,支持 /static/、_www/ 相对路径或绝对路径)</text>
<button type="primary" @click="showByFile">显示悬浮窗(HTML 文件)</button>
</view>
<view class="card">
<button size="mini" @click="updateContent">更新内容</button>
<button size="mini" @click="updateSize">修改大小</button>
<button size="mini" type="warn" @click="hideWindow">隐藏悬浮窗</button>
</view>
<view class="card">
<text>来自悬浮窗的消息:{{ messages || '暂无' }}</text>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import * as floatingWindow from '@/uni_modules/yao-floating-window'
const permission = ref(false)
const showing = ref(false)
const messages = ref('')
// 注册消息回调(只需一次,跨 show/update 持续生效)
// callback 必须包在 options 对象里,@UTSJS.keepAlive 才能保活,避免被 GC 回收
floatingWindow.onMessage({
callback: (data) => {
messages.value += data + '\n'
try {
const obj = JSON.parse(data)
if (obj && obj.type === 'close') {
hideWindow()
}
} catch (e) {
// 非 JSON 消息,仅展示
}
}
})
function refreshStatus() {
permission.value = floatingWindow.hasOverlayPermission()
showing.value = floatingWindow.isFloatingWindowShowing()
}
function requestPermission() {
floatingWindow.requestOverlayPermission({
success: (res) => {
uni.showToast({ title: res.granted ? '授权成功' : '已返回设置页', icon: 'none' })
refreshStatus()
},
fail: (err) => {
uni.showToast({ title: err.errMsg, icon: 'none' })
refreshStatus()
}
})
}
function showByHtml() {
floatingWindow.showFloatingWindow({
html: `
<div style="background:#000000;padding:14px;color:#fff;font-family:sans-serif;">
<div style="font-size:15px;font-weight:bold;margin-bottom:8px;">自定义 HTML 悬浮窗</div>
<div id="data" style="font-size:12px;margin:8px 0;">等待 getData 数据…</div>
<button onclick="yaoFloatWindow.postMessage('hello from html code')">回传消息</button>
<script>
getData(function (data) {
var el = document.getElementById('data');
if (el) { el.textContent = 'data: ' + JSON.stringify(data); }
});
<\/script>
</div>
`,
css: 'html, body { padding: 0; margin: 0; }',
data: { msg: '来自 uni-app 的数据', time: Date.now() },
x: 16,
y: 160,
width: 240,
height: 160,
dragType: 3, // 1 不可拖拽,2 随意拖拽,3 吸边拖拽
transparent: true,
success: () => {
uni.showToast({ title: '悬浮窗已显示', icon: 'none' })
refreshStatus()
},
fail: (err) => {
uni.showToast({ title: err.errMsg, icon: 'none' })
refreshStatus()
}
})
}
function showByFile() {
floatingWindow.showFloatingWindow({
url: '/static/demo-float.html', // 也可用 _www/xxx、本地绝对路径或 http(s) 链接
('悬浮窗页面加载完成')`,
data: { source: 'file', msg: '页面加载时下发的数据' },
x: 16,
y: 160,
width: 180,
height: 300,
dragType: 3,
transparent: false,
background: '#E6202020', // transparent=false 时生效,格式 #RRGGBB / #AARRGGBB
success: () => {
uni.showToast({ title: '悬浮窗已显示', icon: 'none' })
refreshStatus()
},
fail: (err) => {
uni.showToast({ title: err.errMsg, icon: 'none' })
refreshStatus()
}
})
}
function updateContent() {
floatingWindow.updateFloatingWindow({
data: { message: '新数据', count: 42 },
success: () => uni.showToast({ title: '已推送新 data', icon: 'none' }),
fail: (err) => uni.showToast({ title: err.errMsg, icon: 'none' })
})
}
function updateSize() {
floatingWindow.updateFloatingWindow({
width: 300,
height: 200,
animated: true, // 平滑过渡(约 250ms 动画)
success: () => {},
fail: (err) => {}
})
}
function hideWindow() {
floatingWindow.hideFloatingWindow()
refreshStatus()
}
onShow(() => {
refreshStatus()
})
</script>
demo-float.html 示例
配合 showByFile 使用的 HTML 文件,放在项目的 static/demo-float.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { background: transparent; font-family: sans-serif; user-select: none; -webkit-user-select: none; }
.card { padding: 14px; color: #fff; }
.title { font-size: 15px; font-weight: bold; margin-bottom: 8px; }
.time { font-size: 26px; font-weight: bold; letter-spacing: 1px; margin-bottom: 8px; font-variant-numeric: tabular-nums; }
.data { font-size: 11px; color: rgba(255,255,255,0.75); margin-bottom: 12px; word-break: break-all; }
.row { display: flex; gap: 8px; }
button { flex: 1; border: none; border-radius: 8px; padding: 8px 0; font-size: 13px;
background: rgba(255,255,255,0.18); color: #fff; }
button:active { background: rgba(255,255,255,0.32); }
</style>
</head>
<body>
<div class="card">
<div class="title">HTML 文件悬浮窗</div>
<div class="time" id="time">--:--:--</div>
<div class="data" id="data">等待 getData 数据…</div>
<div class="row">
<button onclick="postHello()">回传消息</button>
<button onclick="postClose()">关闭悬浮窗</button>
</div>
</div>
<script>
// 接收 showFloatingWindow / updateFloatingWindow 传入的 data
function registerDataCallback() {
if (window.getData) {
window.getData(function(data) {
document.getElementById('data').innerText = JSON.stringify(data);
});
} else {
setTimeout(registerDataCallback, 50);
}
}
registerDataCallback();
// 实时时钟
setInterval(function () {
var el = document.getElementById('time');
if (el) {
el.textContent = new Date().toLocaleTimeString('zh-CN', { hour12: false });
}
}, 1000);
// 向原生发送消息(触发 onMessage 回调)
function postHello() {
yaoFloatWindow.postMessage(JSON.stringify({ type: 'hello', source: 'file', time: Date.now() }));
}
function postClose() {
yaoFloatWindow.postMessage(JSON.stringify({ type: 'close' }));
}
</script>
</body>
</html>
onMessage 注册说明
// 注册消息回调(只需调用一次,跨 show/update 持续生效)
floatingWindow.onMessage({
callback: (data) => {
// data 是 HTML 内 yaoFloatWindow.postMessage 传的字符串
console.log('收到消息:', data)
}
})
callback 必须包在 { callback: fn } 对象里传入,不能直接传函数。
UTS 的 @UTSJS.keepAlive 只对 options 对象里的回调属性生效,直接参数会被 GC 回收导致回调失效。
ShowOptions 参数
| 字段 |
类型 |
说明 |
url |
string |
页面地址,支持网络地址、/static/xxx.html、_www/...、本地绝对路径。与 html 二选一 |
html |
string |
HTML 内容字符串;与 url 同时传入时,追加到 url 页面内容之后 |
javaScript |
string |
页面加载完成后注入执行的 JS 脚本 |
css |
string |
注入到页面的 CSS 样式 |
data |
any |
传递给页面的数据(可 JSON 序列化),页面内通过 getData((data) => {...}) 接收 |
x / y |
number |
初始位置(dp),默认 24 / 300 |
width / height |
number |
宽高(dp),默认 600 / 300,传 0 表示撑满屏幕 |
toolbar |
boolean |
是否显示工具条(预留参数,当前版本未实现) |
dragType |
number |
拖动类型:1 不可拖拽,2 随意拖拽(默认),3 吸边拖拽 |
transparent |
boolean |
背景是否透明,默认 true |
background |
string |
transparent=false 时的背景色,格式 #RRGGBB / #AARRGGBB,默认 #E6202020 |
focusable |
boolean |
悬浮窗是否可聚焦(true 时 HTML 输入框可弹出键盘),默认 false |
success |
() => void |
成功回调 |
fail |
(err: FloatWindowFail) => void |
失败回调 |
complete |
(res: any) => void |
完成回调 |
UpdateOptions 参数
| 字段 |
类型 |
说明 |
url |
string |
新页面地址(与 html 二选一),含义同 ShowOptions.url |
html |
string |
新 HTML 内容;与 url 同时传入时追加到 url 页面之后 |
javaScript |
string |
重新注入的 JS 脚本 |
css |
string |
重新注入的 CSS 样式 |
data |
any |
重新传递给页面的数据(页面不重载时实时推送) |
width |
number |
新宽度(dp),传 0 表示撑满屏幕宽;不传则保持当前宽度 |
height |
number |
新高度(dp),传 0 表示撑满屏幕高;不传则保持当前高度 |
animated |
boolean |
是否以动画过渡方式更新宽高,默认 false(立即应用)。仅当新旧宽高均为像素值(非撑满)时才走动画,否则退化为立即应用 |
success |
() => void |
成功回调 |
fail |
(err: FloatWindowFail) => void |
失败回调 |
complete |
(res: any) => void |
完成回调 |
仅作用于当前显示中的悬浮窗,悬浮窗未显示时返回 9040104。
更新大小示例
// 立即更新为 400 x 500
floatingWindow.updateFloatingWindow({
width: 400,
height: 500
})
// 平滑过渡到 400 x 500(约 250ms 动画)
floatingWindow.updateFloatingWindow({
width: 400,
height: 500,
animated: true
})
// 只改宽度,高度保持不变
floatingWindow.updateFloatingWindow({
width: 300
})
// 撑满屏幕宽(撑满因无像素值,会忽略 animated 立即应用)
floatingWindow.updateFloatingWindow({
width: 0,
height: 600,
animated: true
})
页面端(HTML)通信
<script>
// 接收原生下发的 data
getData(function (data) {
console.log('收到原生数据:', data);
});
// 向原生发送消息(触发 onMessage)
yaoFloatWindow.postMessage('hello from html');
yaoFloatWindow.postMessage(JSON.stringify({ type: 'close' }));
</script>
错误码
| errCode |
说明 |
| 9040101 |
悬浮窗权限未授予,请先调用 requestOverlayPermission 授权 |
| 9040102 |
参数错误:html 与 file 无效或 HTML 文件不存在 |
| 9040103 |
悬浮窗操作失败 |
| 9040104 |
悬浮窗未显示,请先调用 showFloatingWindow |