更新记录

1.0.2(2026-10-22)

发版

1.0.1(2026-10-22)

修复:Android 上 openFloatWindow 返回成功但悬浮窗看不见

  • z-order:uni-app x 的页面根视图在 onLoad 之后才挂到 decorView,后加的子 View 会盖住 悬浮窗。现在挂载后立即 bringToFront(),并在 0/120/600ms 各补一次,同时监听 decorView 布局变化持续纠正(close() 时移除监听)。
  • 面板不再全透明:WebView 背景由 TRANSPARENT 改为白色(视频内容用黑色)。 之前页面未加载/加载失败时,整个窗口等于隐形。
  • 尺寸按密度换算:sizePreset / size / ballSize / panelSize / edgePadding 语义是 dp, 之前当 px 下发,3x 屏上只有 1/3 大小(medium 仅 116×87dp)。现按 viewport.density 换算。
  • 诊断增强:getFloatWindowDiagnostics 新增 windowOnTop(是否在 decorView 最上层)、 windowFrame(原生 view 真实宽高/可见性)、density,可直接定位「挂上了但看不见」。

平台兼容性

uni-app(5.13)

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

uni-app x(5.13)

Chrome Safari Android iOS 鸿蒙 微信小程序
- - 5.0 12 √ -

其他

多语言 暗黑模式 宽屏模式 蒸汽模式
× × × √

ai-hzh 悬浮窗与画中画

面向 uni-app / uni-app x 的悬浮窗、系统画中画、H5 ↔ App 双向通信 UTS 插件。 一套 API 覆盖 Android / iOS / HarmonyOS,支持应用内悬浮、Android 跨应用悬浮、球-面板切换、 拖拽吸附、位置记忆、远程 H5 通信与视频画中画。

  • 插件 ID:ai-hzh
  • 版本:1.0.1
  • 要求:HBuilderX 5.13+ / uni-app x 5.13+,必须制作自定义基座(含原生能力)

界面预览

App 内原生播放

iOS Android HarmonyOS

画中画浮在手机桌面

iOS Android HarmonyOS

iOS 截图为真机(模拟器 isPictureInPictureSupported() 恒为 false,PiP 只能真机验证); Android 为 Android 15 模拟器,HarmonyOS 为 DevEco 模拟器。


1. 平台能力矩阵

能力 Android iOS HarmonyOS
应用内悬浮(inApp) ✅ ✅ ✅
跨应用悬浮(overlay) ✅ 需系统权限 ❌ 自动降级为 inApp ❌ 自动降级为 inApp
球 / 面板切换 ✅ ✅ ✅
拖拽 / 边缘吸附 / 位置记忆 ✅ ✅ ✅
尺寸预设 / 自定义尺寸 ✅ ✅ ✅
网页内容(url) ✅ ✅ ✅
本地资源(localAsset) ✅ ✅ ✅
本地文件(localFile) ❌ ❌ ✅(仅画中画)
H5 ↔ App 双向通信 ✅ ✅ ✅
网页摄像头 / 麦克风 ✅ ✅ ❌
系统画中画 ✅ Android 8.0+ ✅ ✅
画中画播放/暂停/跳转 ✅ ✅ ✅
宿主唤回(bringHostAppToFront) ✅ ❌ ❌

不支持的能力不会静默失败,会走 fail 并返回对应错误码(见第 6 节)。


2. 安装

  1. 把 uni_modules/ai-hzh 整个目录放进项目(不要改目录名,不要只拷 utssdk 里的文件)。
  2. HBuilderX 中「运行 → 运行到手机或模拟器 → 制作自定义调试基座」,安装后运行。
  3. 只从插件根目录导入 API:
import { openFloatWindow, closeFloatWindow } from '@/uni_modules/ai-hzh'

3. 快速开始

import {
  AIHZHError,
  FloatWindowCapabilities,
  FloatWindowState,
  H5MessagePayload,
  getCapabilities,
  openFloatWindow,
  closeFloatWindow,
  onH5Message
} from '@/uni_modules/ai-hzh'

getCapabilities({
  success: (capabilities: FloatWindowCapabilities): void => {
    if (!capabilities.inApp) {
      console.log('当前平台不支持悬浮窗', capabilities.restrictedReason)
      return
    }
    openFloatWindow({
      mode: 'inApp',
      content: { type: 'url', value: 'https://example.com/panel' },
      sizePreset: 'medium',
      dragEnabled: true,
      edgeSnap: true,
      rememberPosition: true,
      success: (state: FloatWindowState): void => {
        console.log('已打开', state.position, state.size)
      },
      fail: (err: AIHZHError): void => {
        console.log('打开失败', err.errCode, err.errMsg)
      }
    })
  }
})

// 事件 API 收的是 { listener } 对象,不是裸回调
onH5Message({
  listener: (message: H5MessagePayload): void => {
    console.log('H5 发来消息', message.event, message.data)
  }
})

.uvue 里必须给回调参数写显式类型。UTS 不会从 options 对象反推 lambda 参数类型, 省略注解会报 Cannot infer type for this parameter。

Android 跨应用悬浮

import { checkOverlayPermission, openOverlayPermissionSettings, openFloatWindow } from '@/uni_modules/ai-hzh'

checkOverlayPermission({
  success(permission) {
    if (!permission.granted) {
      if (permission.canOpenSettings) openOverlayPermissionSettings()
      return
    }
    openFloatWindow({
      mode: 'overlay',
      focusMode: 'passthrough',
      content: { type: 'url', value: 'https://example.com/tool' },
      sizePreset: 'small',
      edgeSnap: true,
      rememberPosition: true
    })
  }
})

授权后必须回到 App 重新 checkOverlayPermission 确认 granted === true,再打开 overlay。

画中画

import { enterPictureInPicture, pausePictureInPicture, seekPictureInPicture } from '@/uni_modules/ai-hzh'

enterPictureInPicture({
  content: {
    type: 'url',
    // 测试视频:42.7MB,支持 Range,可直接用于进度拖动
    value: 'https://mp-e8fb124d-4949-40ed-9db8-fb30109550f4.cdn.bspapp.com/1.mp4',
    isVideoStream: true
  },
  startPositionMs: 2000,
  loop: false,
  success(state) {
    console.log('已进入画中画', state.playing, state.durationMs)
  }
})

H5 ↔ App

H5 侧(页面由本插件悬浮窗承载时):

<script>
  // bridgeName 在 content 里声明,例如 'AiHzhBridge'
  window.AiHzhBridge.postMessage(JSON.stringify({
    event: 'submitOrder',
    data: { orderNo: 'DEMO-001' },
    requestId: 'req-h5-001'
  }))

  window.addEventListener('AiHzhBridgeMessage', function (event) {
    console.log('H5 收到 App 消息', event.detail)
  })
</script>

App 侧:

import { sendMessageToH5 } from '@/uni_modules/ai-hzh'

sendMessageToH5({
  event: 'appGreeting',
  data: { text: '你好,H5' },
  requestId: 'req-app-001',
  success(res) {
    console.log('已投递', res.bytes, 'bytes →', res.targetOrigin)
  }
})

安全约束(三端一致,UTS 侧统一校验)

  • 远程桥 / 媒体采集必须 https,且 content.allowedOrigins 必须声明精确 Origin(如 https://example.com,不带路径/通配符)。
  • 消息必须含非空 event,序列化后 ≤ 256KB。
  • 当前页面 Origin 不在白名单内时:sendMessageToH5 返回 9015012,H5 发来的消息被丢弃。
  • bridgeName 必须是合法 JS 标识符,长度 ≤ 64。

测试资源

用途 地址
画中画 / 悬浮窗内视频 https://mp-e8fb124d-4949-40ed-9db8-fb30109550f4.cdn.bspapp.com/1.mp4
H5 桥示例页 uni_modules/ai-hzh/static/bridge-demo.html

该视频为 MP4(42.7MB,Accept-Ranges: bytes),适合验证起播位置、进度拖动与循环播放。 示例工程 pages/index/index.uvue 里的 videoUrl 用的就是它,直接改这一处即可换片源。

4. API

所有异步 API 都支持 success / fail / complete。

能力与权限

API 说明
getCapabilities(options) 读取能力矩阵
getCapabilitiesSync() 同步读取能力矩阵
checkOverlayPermission(options) 查询 Android 悬浮权限
openOverlayPermissionSettings(options?) 跳转系统授权页
bringHostAppToFront(options?) Android overlay 唤回宿主

生命周期

API 说明
openFloatWindow(options) 打开,成功返回 FloatWindowState
updateFloatWindow(options) 动态更新(只传要改的字段)
closeFloatWindow(options?) 关闭并释放
showFloatWindow / hideFloatWindow 显隐
bringFloatWindowToFront 应用内提升层级

openFloatWindow 主要参数:

参数 类型 默认 说明
mode inApp / overlay / pip Android overlay,其余 inApp 悬浮模式
focusMode passthrough / interactive overlay 默认 passthrough Android 焦点模式
content FloatWindowContent 必填 页面或媒体内容
position {x, y} 右下角 初始坐标
size / sizePreset {width,height} / small…fullscreen medium 尺寸
displayMode ball / panel 无 球或面板
ballSize / panelSize {width,height} 56×56 / 320×240 球/面板尺寸
dragEnabled / edgeSnap / rememberPosition boolean true / false / false 拖拽行为
keepInScreen / edgePadding boolean / number true / 12 安全区修正

单位约定

  • size / sizePreset / ballSize / panelSize / edgePadding → dp(iOS pt、HarmonyOS vp 同值), 插件内部按屏幕密度换算成物理像素再下发原生。
  • position 与 getFloatWindowPosition 的返回值 → 物理 px(原样读写,便于位置记忆与回填)。

之前版本把预设尺寸当 px 用,在 3x 屏上只有 1/3 大小,看起来像「没显示」;1.0.1 起已修正。

位置 / 尺寸 / 内容

setFloatWindowPosition、getFloatWindowPosition、setFloatWindowSize、 setFloatWindowSizePreset、setFloatWindowDragEnabled、setFloatWindowContent、 setFloatWindowDisplayMode、toggleFloatWindowDisplayMode、 setMediaPlaybackRequiresUserGesture(仅 Android 生效)。

状态与诊断

getFloatWindowState、getFloatWindowDiagnostics(含 Origin、桥状态、权限、监听器数量)。

事件

事件 载荷
onMove / offMove {x, y, width, height, dragging}
onClick / offClick {x, y}
onContentEvent / offContentEvent {type, url, code, message, progress},type 含 pageBegin/pageEnd/pageError/progress 与 pip*
onDisplayModeChange / offDisplayModeChange {displayMode, size, position}
onH5Message / offH5Message {event, data, requestId, sourceOrigin, isMainFrame, timestamp}

offXxx() 一律清空该事件的全部监听(不区分是否传 listener)——iOS 的 Swift 闭包不是 Equatable,无法按引用精确移除单个回调,为保持三端行为一致统一为「清空」语义。 页面卸载时务必调用;同一事件重复 onXxx 会叠加多个监听,需要替换请先 off。

画中画

enterPictureInPicture、exitPictureInPicture、playPictureInPicture、 pausePictureInPicture、seekPictureInPicture、getPictureInPicturePlaybackState、 preparePictureInPicture(打开原生播放器并等待进入画中画)、 cancelPictureInPicturePreparation(放弃已准备的播放器;鸿蒙等价于 exit)。

兼容别名

open / setConfig / close / show / hide / toFront / setPosition / getPosition / setSize / setDragEnable / setShowPattern / setContent(新项目建议用完整名)。


5. 权限配置

Android

插件 utssdk/app-android/AndroidManifest.xml 已声明:

SYSTEM_ALERT_WINDOW / INTERNET / CAMERA / RECORD_AUDIO / MODIFY_AUDIO_SETTINGS /
FOREGROUND_SERVICE / WAKE_LOCK

SYSTEM_ALERT_WINDOW 属于特殊权限,必须走 openOverlayPermissionSettings 让用户手动授予。 网页采集摄像头/麦克风前,宿主需先拿到系统运行时权限(插件在 onPermissionRequest 时会尝试申请, 但 uni-app 的 Activity 无法回调结果,建议宿主自行先申请一次)。

iOS

最低系统版本 iOS 15.0(utssdk/app-ios/config.json 的 deploymentTarget)。 当前 Xcode 26 只接受 ≥15.0 的部署目标,设成 13.0 会直接编译失败。

插件 Info.plist 已追加 UIBackgroundModes: audio、NSCameraUsageDescription、 NSMicrophoneUsageDescription、NSPhotoLibraryUsageDescription。

HarmonyOS

远程网页需要 ohos.permission.INTERNET(uni-app x 默认已声明)。


6. 错误码

错误码 含义
9015001 平台或能力不支持
9015002 参数或内容不合法
9015003 Android 跨应用悬浮权限未授予
9015004 悬浮窗尚未打开
9015005 操作状态冲突(重复打开)
9015006 平台不允许该模式
9015007 媒体来源/格式/控制不支持
9015008 原生系统操作失败
9015009 操作忙
9015010 内容容器或消息桥不可用
9015011 可信 Web 配置不合法
9015012 页面未获得桥接授权
9015013 H5 消息桥尚未就绪
9015014 H5 消息格式无效
9015015 H5 消息超过 256KB
9015016 App 向 H5 派发失败

7. 目录结构

uni_modules/ai-hzh
├── package.json
├── readme.md / changelog.md
├── static/bridge-demo.html          # H5 ↔ App 通信示例
└── utssdk
    ├── interface.uts                # 对外契约(类型 + 函数签名)
    ├── unierror.uts                 # 错误码表
    ├── index.uts                    # Web / 小程序兜底(统一返回 9015001)
    ├── shared
    │   ├── core.uts                 # 纯逻辑:校验 / 几何 / 桥安全(可单测)
    │   └── bridge.uts               # 回调投递 + 事件分发
    ├── app-android
    │   ├── index.uts                # 桥接层
    │   ├── AIHZHNative.kt           # 门面 + 状态机 + 权限
    │   ├── AIHZHFloatWindow.kt      # WindowManager 悬浮窗 + WebView + 拖拽
    │   ├── AIHZHPipActivity.kt      # 画中画宿主(ExoPlayer,支持 MP4/HLS)
    │   ├── config.json / AndroidManifest.xml
    ├── app-ios
    │   ├── index.uts
    │   ├── AIHZHNative.swift        # 门面 + 状态机
    │   ├── AIHZHFloatWindow.swift   # 独立 UIWindow + WKWebView + 手势
    │   ├── AIHZHPip.swift           # AVPlayerLayer + AVPictureInPictureController
    │   └── config.json / Info.plist
    └── app-harmony
        ├── index.uts                # OverlayManager + 状态机
        └── FloatOverlay.ets         # ArkUI 浮层声明式 UI

8. 已知限制

  1. HarmonyOS 画中画依赖系统 PiP 开关(PiPWindow.isPiPEnabled()):设备未开启时 capabilities.pip 为 false,enterPictureInPicture 返回 9015007;悬浮窗与 H5 桥不受影响。
  2. HarmonyOS 网页采集不支持(webMediaCapture: false)。
  3. iOS 不支持跨应用悬浮,mode: 'overlay' 自动降级为 inApp(checkOverlayPermission 返回 granted: false)。
  4. 画中画跳转:Android 在 PiP 窗口内仅提供播放/暂停与关闭按钮,seekPictureInPicture 需 App 在前台;iOS 通过 MPRemoteCommandCenter 支持进度拖动。
  5. localFile 仅鸿蒙画中画使用,且限制在应用 files/cache/temp 目录内的普通 MP4。
  6. localAsset 在 Android 上依次探测 assets/<path>、assets/www/<path>、assets/apps/<path>, 不同 HBuilderX 版本静态资源落盘位置不同;探测失败会抛 pageError。

隐私、权限声明

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

Android:SYSTEM_ALERT_WINDOW(仅跨应用悬浮需要)、INTERNET、CAMERA、RECORD_AUDIO(仅网页采集需要);iOS:相机/麦克风/相册隐私描述、后台音频(画中画);HarmonyOS:INTERNET。

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

插件不采集、不上传任何数据;悬浮窗内网页的 Cookie/本地存储由系统 WebView 按站点隔离,卸载应用即清除。

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

无广告。

暂无用户评论。