HarmonyOS 7 实战 09:闪控窗开发,悬浮交互与生命周期
有了画中画,还能少挨用户骂
大家平时看视频或者开视频会议,突然来个微信消息,你是退出全屏去回,还是忍着?HarmonyOS 上的“闪控窗”(应用内悬浮窗/小窗)就是为了解决这个痛点。让用户在应用内可以“一心二用”,边看视频边记笔记,或者边听语音边刷评论。
听起来很酷,但很多同学真去实现的时候,直接卡在了第一步:“窗口怎么浮不起来?”或者“浮起来了,但滑一下卡得要死,或者拖拽时直接闪退”。
别慌,闪控窗没那么玄乎,但确实有几个必须踩过的坑。这篇就把它的创建、交互、生命周期管理掰开揉碎了讲清楚。
一、别搞混了:系统悬浮窗和应用内悬浮窗
首先得澄清一个概念。很多同学一上来就搜“HarmonyOS 悬浮窗权限”,结果找到了 ohos.permission.SYSTEM_FLOAT_WINDOW。这个权限是给“全局悬浮窗”用的,比如你在打游戏,微信弹个小窗出来。这个权限是 system_core 级别,普通应用根本申请不到。
我们大部分应用需要的,是应用内悬浮窗(闪控窗)。也就是说,用户只能在你的应用里看到这个小窗,退出应用它就消失了。这种不需要申请系统级权限,用 window 模块就能实现。
先看核心 API:
typescript
import window from '@ohos.window';
async function createFloatWindow(context: Context) {
const windowStage = await window.getLastWindow(context);
// 创建子窗口
const floatWindow = await windowStage.createSubWindow('myFloatWindow');
await floatWindow.resize(600, 400); // 宽高
await floatWindow.moveWindowTo(100, 200); // 位置
await floatWindow.setWindowMode(window.WindowMode.FLOATING);
await floatWindow.loadContent('pages/FloatContent');
floatWindow.showWindow();
}
这里最核心的是 createSubWindow。它会在当前应用内创建一个子窗口,loadContent 加载你的小窗页面内容。WindowMode.FLOATING 表示这是一个浮动的窗口,用户可以拖拽它。
二、拖拽与缩放:别自己写手势,会累死
很多同学以为窗口拖拽要自己写 onTouch 手势,计算 offsetX、offsetY,然后再去 moveWindowTo。这完全是费力不讨好,而且极易导致卡顿和位置跳跃。
实际上,window 模块自带窗口拖拽能力。你只需要在配置里开启,用户按住窗口顶部就可以拖拽了。在子窗口的页面里,设置标题栏区域:
typescript
@Entry
@Component
struct FloatContent {
build() {
Column() {
// 顶部拖拽区域
Row() {
Text('小窗标题')
.fontColor(Color.White)
Blank()
Image($r('app.media.close'))
.onClick(() => {
// 关闭窗口的逻辑
})
}
.width('100%')
.height(40)
.backgroundColor('#333333')
.gesture(
PanGesture()
.onActionUpdate((event: GestureEvent) => {
// 这里可以配合系统拖拽,或者自己写
})
)
// 内容区
Text('视频播放区')
.layoutWeight(1)
}
.width('100%')
.height('100%')
}
}
坑来了:系统级的窗口拖拽是有限制的。 在 HarmonyOS 7 中,子窗口的拖拽通常需要和 window.on('windowEvent') 配合。如果发现自己写拖拽很卡,建议直接用系统默认的窗口装饰条(标题栏)来实现拖拽,不要自己接手势,除非你要做极其复杂的交互(比如吸附边缘、半隐隐藏)。
三、交互:点击穿透与手势冲突
小窗浮在上面,用户点击小窗区域,事件应该被小窗消费;点击小窗以外的区域,应该穿透到下层页面。
这听起来很简单,但实际开发中,最容易出现的问题是:底下的页面被误触,或者小窗无法接收点击。
在 ArkUI 中,窗口默认是会拦截事件流的。如果你的小窗页面里有全屏的透明区域,一定要设置 .hitTestBehavior(HitTestMode.Transparent),让事件穿透过去。
typescript
Stack() {
// 透明背景层
Row()
.width('100%')
.height('100%')
.hitTestBehavior(HitTestMode.Transparent)
// 实际内容
Column() {
Text('内容')
}
}
另外,如果你在小窗里播放视频,用户拖拽小窗和拖动视频进度条可能会冲突。这时候就要区分手势:长按拖拽窗口,短按拖动进度条。别问我怎么知道的,测试小姐姐当时差点把我按在地上摩擦。
四、生命周期:最容易被忽略的“生死”问题
小窗的生命周期和主窗口完全不同步。主窗口在后台时,小窗不一定在后台;主窗口销毁时,小窗必须销毁,否则内存直接泄漏。
在 Ability 的 onWindowStageDestroy 里,一定要清理子窗口:
typescript
export default class EntryAbility extends UIAbility {
private floatWindow: window.Window | null = null;
async onWindowStageCreate(windowStage: window.WindowStage) {
// ...
}
onWindowStageDestroy() {
if (this.floatWindow) {
this.floatWindow.destroyWindow();
this.floatWindow = null;
}
}
}
还有一个非常致命的坑:用户把应用切到后台,再切回来,小窗不见了。
原因是子窗口在应用切到后台时被系统回收了,或者被隐藏了。你需要监听应用的前后台状态,手动恢复小窗:
typescript
this.context.eventHub.on('windowStageEvent', (event: window.WindowStageEventType) => {
if (event === window.WindowStageEventType.RESUMED) {
// 应用回到前台,重新显示小窗
if (this.floatWindow) {
this.floatWindow.showWindow();
}
}
});
五、常见坑合集
坑一:小窗里的 TextInput 无法输入。
有些同学在闪控窗里放了输入框,结果键盘弹不出来。这是因为子窗口和主窗口的焦点管理是隔离的。在弹窗输入时,需要把焦点显式地传递给子窗口,或者干脆不要在小窗里放复杂输入框。
坑二:频繁 resize 和 moveWindowTo 导致卡顿。
不要在手势的 onActionUpdate 里每一帧都调用 moveWindowTo,那样 UI 线程会炸。正确做法是用节流(Throttle)或者用系统的拖拽 API。如果非要自己写,至少加个 setTimeout 或者 requestAnimationFrame 限制频率。
坑三:小窗内容加载慢,白屏。
loadContent 是异步的。如果你在创建窗口后立刻调用 showWindow,页面可能还没渲染完,就会看到白屏。最好在 loadContent 的回调里再 showWindow。
六、总结
闪控窗(应用内悬浮窗)的核心在于搞清楚 SubWindow 的创建、拖拽和生命周期。记住三点:
-
普通应用只能做应用内悬浮窗,别去申请 SYSTEM_FLOAT_WINDOW。
-
交互区域要区分清楚,用
hitTestBehavior控制点击穿透。 -
主窗口销毁时,一定要 destroy 子窗口,并在应用切回前台时主动
showWindow。
更多推荐


所有评论(0)