最近在排查一个挺典型的线上反馈:鸿蒙应用里通过 Web 组件加载的 H5 视频页面,视频本身播放正常,但只要点右下角的全屏按钮,画面要么纹丝不动,要么进去之后上下两条系统栏还挂在那边,看着就像“全屏失效”。这种问题在鸿蒙开发社区里出现频率不低,尤其是从 Android WebView 或小程序 WebView 迁移过来的团队,最容易在这上面卡住。
因为“全屏”听起来只是一个动作,实际上它是一条跨层的调用链:H5 页面先发起全屏请求,Web 组件确认放行并转发事件,窗口层再把画面扩展到系统栏后面。哪一环没接上,最终表现都是“全屏失效”。这篇把我在实际工程里从现象查到根因的完整过程写出来,包含 H5 侧的写法、ArkWeb 组件侧的事件监听、窗口层的沉浸式布局处理,以及一份可以直接抄的完整修复代码。
1. 先定位一遍:全屏请求在三个环节中的哪一环断了?
1.1 从现象做分层排除
遇到全屏失效,我第一件事不是改代码,而是先问自己:用户看到的“失效”到底是哪一种。不同的失效表现,对应的环节完全不一样。
- 点击全屏按钮后完全没反应,视频还停留在网页原始位置:大概率是 H5 侧没有真正发出全屏请求,或者 Web 组件把全屏请求拦下来了。
- 画面确实全屏了,但状态栏和导航栏还悬浮在顶层,屏幕上下各露一条:这是窗口层的沉浸式布局没有打开,视频虽然铺满了 Web 组件的可视区域,但应用窗口本身没有扩展到系统栏后面。
- 全屏一进入就黑屏,或者出现一块空白区域:这种情况往往跟容器层级有关。比如 Web 组件被塞在某个 XComponent 或 Stack 子节点里,全屏后的窗口层级覆盖关系错乱。
- 第一次能全屏,退出之后第二次就失效:大概率是某个事件只监听了一次,或者窗口状态切换后没有恢复组件尺寸,导致后续全屏请求被异常状态吞掉。
这个分层思路特别重要。我见过不少同学一上来就在 H5 页面里反复调requestFullscreen,调了半天没效果,其实问题根本不在 Web 层,而是在窗口层。反过来,也有同学直接在应用侧写窗口沉浸式布局,结果全屏事件压根没传到窗口层,一样白搭。
1.2 三层职责先理清:H5 页面、Web 组件、窗口
这三层各管一段:
- H5 页面负责“发起全屏”:无论是播放器控件上的全屏按钮,还是 JS 主动调用
video.requestFullscreen(),请求都产生于网页内部。 - Web 组件负责“转发全屏”:组件需要感知网页的全屏状态变化,通过
onFullScreenEnter/onFullScreenExit之类的回调告诉应用层。 - 窗口负责“呈现全屏”:应用拿到全屏事件后,调整窗口布局、隐藏系统栏或切换横竖屏,让视频真正铺满屏幕。
任何一个环节缺失,用户看到的就是“视频全屏失效”。所以排查顺序也应该是:先看 H5 侧有没有发请求,再看 Web 组件有没有收到事件,最后看窗口层有没有正确响应。
1.3 最小复现:把业务代码从问题里剥离出去
我排查这种问题,一定先做一个最小复现用例。写一个只包含一个<video>标签的 HTML 页面,用loadData或者loadUrl加载,然后在页面里放一个全屏按钮。这样做的目的很纯粹:把业务 JS、第三方播放器、复杂的页面结构全部剥掉,只看系统能力。
实践中我发现,很多“全屏失效”用最小复现根本复现不出来,那问题就基本锁定在业务 H5 侧;如果最小复现也一样失效,那就别折腾 H5 了,直接检查鸿蒙应用侧的配置和代码。
2. H5 侧最常见的自作主张:网页里的全屏和 App 里的全屏不是同一个概念
2.1 video 标签属性才是第一道暗坑
先看一个最简单的 H5 视频页面:
<video id="video" src="https://example.com/sample.mp4" controls></video>这种写法在普通浏览器里没问题,放到鸿蒙 Web 组件里,某些版本会出现一个非常迷惑的现象:视频一播放就直接进入“强制全屏”,播放器下面根本没有独立的全屏按钮,或者点了按钮反而退出全屏。用户反馈说“全屏按钮失效”,实际上是playsinline属性缺失导致的默认行为变化。
解决方案是在video标签上补上这几个属性:
<video id="video" src="https://example.com/sample.mp4" controls playsinline webkit-playsinline x5-playsinline ></video>playsinline的意思是“内联播放”,也就是视频在页面原本的位置播放,不自动跳转全屏。webkit-playsinline是 iOS WebView 的兼容写法,x5-playsinline是腾讯 X5 内核的兼容写法。在鸿蒙 ArkWeb 上,这几个属性不是必须全部加上,但加上之后能减少大量“行为不一致”导致的迷惑问题。
这里有一个很重要的经验:如果视频默认就在页面里内联播放,那么播放器自带的“全屏按钮”才会正常触发全屏流程。如果视频一开始就被强制全屏了,你再点全屏按钮,语义上就变成了“退出全屏”,用户感知自然是失效。
2.2 JS 触发全屏时,用户手势和兼容写法缺一不可
除了播放器自带按钮,很多 H5 页面会自定义全屏按钮,用 JS 去调requestFullscreen。代码通常长这样:
const video = document.getElementById('video'); document.getElementById('fullscreenBtn').addEventListener('click', () => { if (video.requestFullscreen) { video.requestFullscreen(); } else if (video.webkitRequestFullscreen) { video.webkitRequestFullscreen(); } });这看起来没问题,但有三个细节容易被忽略。
第一,requestFullscreen必须在用户手势的调用栈里执行。也就是说,不能在一个异步回调里隔了很久才调用,某些 WebView 会因此直接拒绝全屏请求。如果你在按钮 click 事件里先发了个网络请求,等数据回来再调requestFullscreen,很可能就失效。
第二,要判断当前是否已经处于全屏状态。正确写法是在全屏按钮的点击逻辑里先判断document.fullscreenElement:
if (document.fullscreenElement) { document.exitFullscreen(); } else { video.requestFullscreen(); }如果 H5 代码里没有这个判断,每次点击都调requestFullscreen,第二次之后就会被浏览器判定为无效操作,表现也是“第二次以后全屏失效”。
第三,如果视频不是直接放在顶层页面,而是放在<iframe>里,并且父页面没有给 iframe 加上allowfullscreen,那么 iframe 内的全屏请求会被静默拒绝。这也是非常典型的“H5 全屏失效”原因。检查一下视频页面所在的每个 iframe 标签,allowfullscreen和webkitallowfullscreen都要补上。
2.3 第三方播放器场景下的排查重点
现在很多项目用的是第三方 H5 播放器,比如 video.js、plyr、西瓜播放器等。这类播放器封装了全屏逻辑,内部可能自己维护了一套全屏状态。如果播放器全屏失效,我的建议是直接用播放器实例的 API 看它的状态,同时打开 DevTools 看控制台有没有全屏相关的报错。
比如 video.js 里,全屏失效经常跟playsinline配置有关。初始化时可以显式设置:
player = videojs('my-video', { playsinline: true, fullscreen: { options: { navigationUI: 'hide' } } });不同播放器配置项不同,但核心思路一致:让播放器知道它运行在嵌入式 WebView 环境里,不要自作主张去强制全屏,也不要依赖浏览器地址栏层面的全屏行为。
3. 鸿蒙 Web 组件全屏事件链:为什么事件没传上来
3.1 组件侧的正确监听姿势
如果 H5 侧确认没问题,下一步就是把 ArkWeb 组件侧的事件链路查一遍。在鸿蒙应用里,Web 组件加载 H5 页面后,网页发起全屏请求时,组件会触发onFullScreenEnter,退出全屏时触发onFullScreenExit。
示例代码大致是这样:
import { webview } from '@kit.ArkWeb'; import { window } from '@kit.AbilityKit'; @Entry @Component struct WebPage { private controller: webview.WebviewController = new webview.WebviewController(); private winClass: window.Window | null = null; build() { Stack() { Web({ src: 'https://example.com/video-page.html', controller: this.controller }) .javaScriptAccess(true) .domStorageAccess(true) .mediaAccess(true) .onFullScreenEnter(() => { hilog.info(0x0000, 'WebPage', 'FullScreenEnter'); // 在这里让窗口进入沉浸式全屏 }) .onFullScreenExit(() => { hilog.info(0x0000, 'WebPage', 'FullScreenExit'); // 在这里恢复窗口布局 }) } .width('100%') .height('100%') } aboutToAppear() { // 获取窗口实例,后续在全屏事件中使用 } }这里最重要的一点是:在onFullScreenEnter回调里,你要自己决定怎么把这个全屏“呈现”出来。ArkWeb 不会自动帮你把窗口扩展到系统栏,它只是告诉你“网页请求全屏了”,后续需要应用层配合。
3.2 事件不触发的配置误区
我排查过一个问题:H5 里点了全屏,onFullScreenEnter就是不触发。查了老半天,发现 Web 组件没有打开mediaAccess(true)和domStorageAccess(true)。虽然视频播放正常,但全屏请求属于另一种能力,媒体访问权限缺失时,部分系统版本不会把全屏事件转发给应用层。
所以基础属性的配置一定要给齐:
Web({ src: 'xxx', controller: this.controller }) .javaScriptAccess(true) .domStorageAccess(true) .mediaAccess(true) .fileAccess(true) .onlineImageAccess(true)至于有没有专门的fullScreenRequest({ enable: true })之类的属性,不同 API 版本略有差异。如果你的 SDK 版本里能找到这个配置项,务必把它打开;找不到的话,就用前面的事件监听方式。
另外还有一个小坑:如果你把 Web 组件包在某个自定义弹窗组件里,弹窗层级会拦截全屏事件。全屏按钮点击之后事件被外层弹窗消费掉了,onFullScreenEnter永远不会触发。这种情况我在实际项目里踩过,最后把视频 Web 组件挪出弹窗才解决。
3.3 打日志确认事件链路
在排查阶段,我习惯在三个位置打印日志:
- H5 侧:全屏按钮点击后,
document.fullscreenElement是否变化。 - Web 组件侧:
onFullScreenEnter/onFullScreenExit是否触发。 - 窗口侧:窗口布局全屏切换是否成功执行。
三个日志一比就能锁定断点位置。比如 H5 侧已经有fullscreenElement了,但组件侧回调没触发,那问题就在 Web 组件或者系统权限配置。组件侧回调触发了,窗口侧执行失败,那就去看窗口获取方式对不对。
生产环境下可以加一个按钮让用户上报,或者通过埋点采集fullScreenEnter的触发率。我自己的习惯是至少把这个事件触发的日志留在 hilog 里,线上问题先按时间点捞日志,比让用户一遍遍复现高效得多。
4. 窗口层才是最后一道关卡:沉浸式布局与屏幕方向
4.1 为什么全屏时系统栏还在
很多团队把问题定位到窗口层之后,都会遇到一个具体现象:视频全屏了,但状态栏和导航栏依然悬浮在最上面,画面像是被压缩到了中间区域。这在鸿蒙里非常常见,原因就是应用窗口默认没有开启沉浸式布局。
要让视频真正延伸到屏幕边缘,需要把窗口设置为全屏布局。核心 API 是setWindowLayoutFullScreen:
import { window } from '@kit.AbilityKit'; async function enterFullScreen(win: window.Window) { try { await win.setWindowLayoutFullScreen(true); } catch (err) { hilog.error(0x0000, 'FullScreen', 'setWindowLayoutFullScreen failed: %{public}s', JSON.stringify(err)); } } async function exitFullScreen(win: window.Window) { try { await win.setWindowLayoutFullScreen(false); } catch (err) { hilog.error(0x0000, 'FullScreen', 'restore window layout failed: %{public}s', JSON.stringify(err)); } }注意:setWindowLayoutFullScreen只是让应用内容扩展到系统栏后面,并不会主动隐藏系统栏。如果你希望状态栏和导航栏也消失,还需要配合setWindowSystemBarEnable来关闭系统栏:
async function hideSystemBars(win: window.Window) { try { await win.setWindowSystemBarEnable([]); } catch (err) { hilog.error(0x0000, 'FullScreen', 'hide system bars failed: %{public}s', JSON.stringify(err)); } }不过考虑到导航返回操作,一般不建议直接禁用所有系统栏,而是让视频画面扩展到系统栏后面,系统栏半透明悬浮即可。实际体验更好,也符合主流视频 App 的做法。
4.2 全屏进出时,窗口恢复的时机
只处理进入全屏是不够的。退出全屏时,必须把窗口布局同步恢复,否则会出现“退出全屏后页面顶到屏幕最上面,被状态栏盖住”的问题。
正确做法是让窗口状态的改变和全屏事件严格配对:
.onFullScreenEnter(() => { this.winClass?.setWindowLayoutFullScreen(true); }) .onFullScreenExit(() => { this.winClass?.setWindowLayoutFullScreen(false); })这中间有一个时序问题需要注意:onFullScreenExit触发时,网页已经开始退出全屏,但窗口布局切换是异步的。如果布局恢复太慢,用户会看到一瞬间的错位闪动。我比较推荐在全屏事件回调里提前一点执行窗口切换,或者在窗口布局切换期间加一层无操作遮罩,等切换完成后再移除。
4.3 横竖屏锁定造成的“假全屏”
还有一类“全屏失效”,画面确实放大到了整个屏幕,但方向完全不对。比如应用固定竖屏,视频全屏后只是竖着把画面拉伸,四周出现大黑边。用户感知是“这全屏是假的”。
如果需求允许横屏观看,需要在应用配置里声明屏幕方向支持。module.json5的abilities节点中可以配置orientation:
{ "module": { "abilities": [ { "name": "EntryAbility", "orientation": "auto_rotation" } ] } }如果不希望整个应用都支持横屏,只在 Web 组件全屏时临时切换到横屏,就需要在全屏事件里动态调用屏幕方向相关接口。这里我不展开讲全部实现,但可以提示一个坑:方向切换最好在全屏请求真正成功之后再触发,否则过早切横屏,网页可能会重新排版,导致全屏状态丢失。
5. 结合 ArkWeb 实际工程,给一份可以直接抄的完整修复代码
5.1 应用侧完整代码
我把前面几层的东西合到一起,给一个相对完整的 ArkTS 页面示例。假设你的 EntryAbility 在onWindowStageCreate阶段拿到了主窗口对象,可以通过 AppStorage 或者全局变量共享给页面。
// EntryAbility.ets import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit'; import { window } from '@kit.AbilityKit'; export default class EntryAbility extends UIAbility { onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) { // ... } onWindowStageCreate(windowStage: window.WindowStage): void { const mainWindow = windowStage.getMainWindowSync(); AppStorage.setOrCreate('mainWindow', mainWindow); windowStage.loadContent('pages/Index'); } }页面里这样用:
// Index.ets import { webview } from '@kit.ArkWeb'; import { window } from '@kit.AbilityKit'; import { hilog } from '@kit.PerformanceAnalysisKit'; @Entry @Component struct Index { private controller: webview.WebviewController = new webview.WebviewController(); private win: window.Window | undefined = AppStorage.get('mainWindow') as window.Window; build() { Stack() { Web({ src: 'https://example.com/video-page.html', controller: this.controller }) .javaScriptAccess(true) .domStorageAccess(true) .mediaAccess(true) .fileAccess(true) .onFullScreenEnter(() => { hilog.info(0x0000, 'WebFullScreen', 'fullScreenEnter'); this.win?.setWindowLayoutFullScreen(true); }) .onFullScreenExit(() => { hilog.info(0x0000, 'WebFullScreen', 'fullScreenExit'); this.win?.setWindowLayoutFullScreen(false); }) } .width('100%') .height('100%') } }这段代码的核心逻辑就是:网页全屏时,应用窗口跟着全屏;网页退出全屏时,应用窗口恢复。如果业务里还要隐藏状态栏,再在这个基础上调用setWindowSystemBarEnable([]),退出时恢复即可。
5.2 H5 侧补丁脚本,给“组件事件没触发”兜个底
如果你已经做到上面这一步,onFullScreenEnter事件也触发了,只是窗口恢复有延迟,那可以用一个 H5 侧脚本兜底,通过postMessage和 App 侧通信:
<script> document.addEventListener('fullscreenchange', function () { const isFullscreen = Boolean(document.fullscreenElement); if (window.ReactNativeWebView) { window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'fullscreenchange', isFullscreen: isFullscreen })); } }); </script>在鸿蒙侧,可以通过onMessage事件接收网页消息:
.onMessage((event) => { const msg = event.getNativeMessage(); try { const parsed = JSON.parse(msg); if (parsed.type === 'fullscreenchange') { // 二次兜底,确保窗口状态和网页全屏状态一致 } } catch (e) { // 非 JSON 消息忽略 } })这种双通道方案适合大型项目,避免组件事件因为极端场景漏发导致窗口状态不同步。
5.3 配置文件里别忘了沉浸式布局开关
除了窗口动态切换,module.json5里还有一件事常常被忽略:supportExtendToFullScreen。如果这个开关没开,某些版本的 ArkWeb 在网页请求全屏时会遇到额外限制。可以在abilities节点中加上:
{ "module": { "abilities": [ { "name": "EntryAbility", "supportExtendToFullScreen": true } ] } }这个字段的作用是让应用支持扩展到全屏显示,配合动态的setWindowLayoutFullScreen使用时,行为更稳定。不过不同 SDK 版本可能字段名有差异,如果你用的版本里报出“未知字段”,就只保留动态切换的方式。
6. 版本兼容与后续优化建议
6.1 不同 API 版本下的差异
鸿蒙的 Web 组件 API 在快速演进,onFullScreenEnter/onFullScreenExit在 API 10 以后比较明确。如果你还在用 API 9 甚至更早的版本,可能会发现事件名称不一致,或者事件回调参数格式不同。我处理这种兼容性的原则是:把全屏处理逻辑统一封装成一个方法,内部根据 SDK 版本做分支,避免业务页面里面到处都是if (canIUse('xxx'))。
另外,setWindowLayoutFullScreen在不同版本的行为也有细微差异。老版本上切换全屏布局时,窗口内容可能会短暂闪白,新版本基本平滑过渡。建议评估用户最低安装版本,如果版本足够新,就放心依赖这套 API;如果老版本占比高,就要考虑给老版本加一个全屏过渡动画,掩盖闪白问题。
6.2 全屏方向联动和 Web 组件尺寸恢复
视频全屏时,Web 组件自身尺寸不会自动变成整个屏幕。你在onFullScreenEnter里切换窗口布局后,组件仍然占据原来的布局位置。如果视频画面没有铺满,可以在这个回调里同步调整 Web 组件的高度和宽度,或者用一个透明度淡入的遮罩层配合。
退出全屏时同样要把 Web 组件尺寸恢复回去。实际操作中我遇到过:全屏切换后组件宽度计算错误,导致视频显示区域只剩一半。解决方式是给 Web 组件设置aspectRatio,或者在页面根容器上监听尺寸变化,全屏状态下强制更新 Web 组件的width和height。
6.3 后续排查工具清单
如果你按照上面的步骤还没解决,我建议整理一下排查资料,按这个清单继续深挖:
- 使用 DevTools 模式加载 H5 页面,确认全屏按钮点击后浏览器内部有没有
fullscreenchange事件。 - 检查 Web 组件是否存在容器嵌套,比如
List、Scroll、Grid内部是否限制了子组件尺寸。 - 检查窗口实例获取时机。如果窗口对象是在
windowStage.loadContent之前获取的,部分接口可能还没就绪,命令也不了。 - 开启 hilog 抓取,过滤关键字
fullScreen、Web、window,确认事件到底在哪一步丢失。
最后再分享一个我的排查小技巧:遇到全屏问题,不要一开始就盯着一层改代码,先花二十分钟把最小复现跑通,再一层层把业务代码加回去。哪一步开始出问题,问题就在哪一步。这个思路在 Web 组件相关的各类交互问题上都通用,不只是全屏这一件事。