查看: 89|回复: 3

Flutter鸿蒙视频播放器SystemChrome系统栏控制踩坑与最佳

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
去年在Flutter鸿蒙项目里做视频播放器时,以为全屏播放就是藏状态栏和导航栏那么简单,结果在鸿蒙真机上踩了一串坑:退出全屏后状态栏图标颜色不对、横竖屏切换布局错乱、不同页面方向锁定需求不同。折腾几天才把 SystemChrome 这套 API 的门道摸清。下面把实战中的场景、踩坑和最终代码组织方式分享出来,顺便对比鸿蒙 ArkTS 的原生窗口控制 API。

一、SystemChrome 能做什么
Flutter 的 SystemChrome 位于 package:flutter/services.dart,通过 platform channel 与原生平台通信,控制系统栏、屏幕方向等系统级 UI 元素。核心用到的三个静态方法:setPreferredOrientations(屏幕方向)、setEnabledSystemUIMode(系统栏显示模式)、setSystemUIOverlayStyle(系统栏样式)。注意这些方法都是全局生效的,调一次整个应用都受影响。

二、屏幕方向锁定:允许列表而非设置方向
视频播放页需要支持横竖屏切换,但首页和列表页必须锁竖屏。一开始只在首页 initState 里调 setPreferredOrientations,发现播放页 pop 回首页后方向没恢复。原因:setPreferredOrientations 传入的是允许的方向列表(白名单),全局生效。正确做法是在每个需要控制方向的页面主动设置。

竖屏锁定最好同时传入 portraitUp 和 portraitDown,避免用户躺着时方向错乱。横屏也一样,landscapeLeft 和 landscapeRight 都写上。
  1. void _setOrientation(Orientation orientation) {
  2.   if (orientation == Orientation.portrait) {
  3.     SystemChrome.setPreferredOrientations([
  4.       DeviceOrientation.portraitUp,
  5.       DeviceOrientation.portraitDown,
  6.     ]);
  7.   } else if (orientation == Orientation.landscape) {
  8.     SystemChrome.setPreferredOrientations([
  9.       DeviceOrientation.landscapeLeft,
  10.       DeviceOrientation.landscapeRight,
  11.     ]);
  12.   } else {
  13.     SystemChrome.setPreferredOrientations(DeviceOrientation.values);
  14.   }
  15. }
复制代码

三、全屏模式选 immersiveSticky
视频播放时需隐藏状态栏和导航栏。SystemChrome 提供几种 SystemUiMode:edgeToEdge(内容延伸到系统栏区域但系统栏仍可见)、leanBack(点击屏幕才恢复系统栏,且不会自动再隐藏)、immersive(滑动显示系统栏,不会自动隐藏)、immersiveSticky(滑动显示系统栏,几秒后自动隐藏)。

视频播放器推荐 immersiveSticky,用户从边缘滑动调出系统栏调节亮度/音量,松手后自动藏回,体验最自然。
  1. void _toggleFullScreen() {
  2.   if (_isFullScreen) {
  3.     SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
  4.   } else {
  5.     SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
  6.   }
  7. }
复制代码

注意:在鸿蒙某些版本上,从底部上滑呼出导航栏后,应用会收到 didChangeSystemUIOverlays 回调,可通过 WidgetsBindingObserver 监听并做 UI 调整(如暂停字幕或显示进度条)。

四、状态栏样式适配深色模式
深色模式下状态栏图标应为浅色(白色),浅色模式下应为深色(黑色),否则图标与背景混在一起。SystemUiOverlayStyle 包含 statusBarColor(背景色)、statusBarIconBrightness(图标亮度)、systemNavigationBarColor 和 systemNavigationBarIconBrightness(导航栏样式)等。
  1. void _applyStatusBarStyle() {
  2.   SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
  3.     statusBarColor: _statusBarColor,
  4.     statusBarIconBrightness: _useLightIcons ? Brightness.light : Brightness.dark,
  5.     systemNavigationBarColor: _isFullScreen ? Colors.black : Colors.white,
  6.     systemNavigationBarIconBrightness: _isFullScreen ? Brightness.light : Brightness.dark,
  7.   ));
  8. }
复制代码

建议在 MaterialApp 的 builder 里根据当前主题亮度统一调用,避免每个页面重复写。
  1. MaterialApp(
  2.   builder: (context, child) {
  3.     final brightness = Theme.of(context).brightness;
  4.     SystemBarManager.applyTheme(brightness);
  5.     return child!;
  6.   },
  7. )
复制代码

五、导航栏显示控制
全屏阅读器等场景可能需要隐藏底部导航栏。用 manual 模式加空 overlays 列表可隐藏所有系统 UI,但用户从底部上滑时导航栏仍会重新出现,应用无法阻止。
  1. void _toggleNavigationBar() {
  2.   if (_systemNavVisible) {
  3.     SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
  4.   } else {
  5.     SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
  6.   }
  7. }
复制代码

也可灵活控制只隐藏状态栏或导航栏:overlays 传 [SystemUiOverlay.top] 或 [SystemUiOverlay.bottom]。

六、与鸿蒙 ArkTS 原生 API 对比
鸿蒙 ArkTS 通过 window 模块(WindowStage/UIAbilityContext)直接操作窗口对象,API 更底层。例如:
  1. import { window } from '@kit.ArkUI';
  2. let windowClass = window.getLastWindow(getContext(this));
  3. windowClass.setWindowLayoutFullScreen(true);
  4. windowClass.setStatusBarProperties({ statusBarColor: '#00000000', statusBarContentColor: '#FFFFFF' });
  5. windowClass.setPreferredOrientation(window.Orientation.PORTRAIT);
  6. windowClass.setSpecificSystemBarEnabled('navigation', false); // 鸿蒙特有,可单独禁用手势导航栏
复制代码

主要区别:
- Flutter 是声明式封装,跨平台统一但无法覆盖平台特有功能(如鸿蒙的 setSpecificSystemBarEnabled)。
- 鸿蒙 API 更灵活,但页面生命周期切换需手动管理(onPageShow/onPageHide)。
- 方向控制:Flutter 用允许列表(白名单),鸿蒙用固定方向,Flutter 更合理。
- 生命周期绑定:两边都需自行处理,半斤八两。

七、踩过的坑
1. setPreferredOrientations 全局影响:pop 回首页方向不恢复。解决:在 dispose 中恢复方向,或首页每次 initState 主动设置。
2. restoreSystemUIOverlays 的“栈”式行为容易混乱,建议弃用,直接在 dispose 里明确恢复已知状态。
3. immersiveSticky 与 leanBack 区别:leanBack 点击屏幕恢复系统栏且不自动隐藏,immersiveSticky 滑动临时显示并自动隐藏。视频播放用 immersiveSticky 更合适。
4. 状态栏颜色和图标亮度需同时考虑,否则图标不可见。
5. 设置 statusBarColor: Colors.transparent 后,内容延伸到状态栏区域,必须用 SafeArea 包裹 AppBar 避免标题遮挡。
6. 前后台切换导致样式丢失:在 WidgetsBindingObserver 的 didChangeAppLifecycleState 监听 resumed 状态,重新应用样式。
7. 鸿蒙真机特殊表现:immersiveSticky 底部上滑会触发“智慧多窗”而非临时显示导航栏,这是系统 ROM 行为,应用层无法修改,暂时接受。

八、项目代码组织
建议封装一个 SystemBarService 类,统一管理所有 SystemChrome 调用:
  1. class SystemBarService {
  2.   static void initialize() { /* 应用启动时初始化 */ }
  3.   static void enterVideoFullscreen() { /* 进入视频全屏 */ }
  4.   static void exitFullscreen() { /* 退出全屏 */ }
  5.   static void applyTheme(Brightness brightness) { /* 根据主题应用样式 */ }
  6. }
复制代码

在 main.dart 调 initialize,MaterialApp builder 调 applyTheme,各页面调 enterVideoFullscreen/exitFullscreen。所有操作集中管理,便于排查。

九、验证提示
务必在真机上测试系统栏控制,模拟器行为与真机差异大(特别是鸿蒙)。验证环境:Flutter + HarmonyOS 6.0 + nova。
回复

使用道具 举报

发表于 2 小时前 | 显示全部楼层

Re: Flutter鸿蒙视频播放器SystemChrome系统栏控制踩坑与最佳

楼主的总结非常有价值,我也是在Flutter + 鸿蒙上折腾视频播放器时被SystemChrome坑过几次。特别是方向锁定的全局生效,一开始想用initState设置离开时恢复,结果发现pop回上个页面时状态根本没变,后来学乖了直接在路由观察器里根据当前路由动态调。 另外提一个可能大家也会遇到的点:在鸿蒙上,`immersiveSticky` 有时自动隐藏的延迟会比Android慢(尤其是HarmonyOS 3.x早期版本),用户滑动后要等好几秒才缩回去,体验很割裂。后来我换成监听 `SystemChrome.systemUIChange` 事件,配合定时器手动缩回,才稳定下来。不知道楼主有没有类似的调优经验? 还有状态栏颜色那部分,如果视频播放器背景是渐变的(比如从黑过渡到亮色),单纯在builder里根据主题亮度设置一次可能不够,最好能在播放页面内监听背景色变化动态调整icon亮度,否则全屏时某些帧背景变浅会看不清图标。不过好在鸿蒙的setSystemUIOverlayStyle支持实时调用,性能开销也不大。
回复 支持 反对

使用道具 举报

发表于 2 小时前 | 显示全部楼层

Re: Flutter鸿蒙视频播放器SystemChrome系统栏控制踩坑与最佳

感谢分享!最近也在做Flutter鸿蒙的视频播放器,你提到的“pop回首页方向没恢复”我遇到过,一开始以为是被其他页面污染了,后来才意识到是全局白名单的问题。按照你说的在每个页面主动设置,确实稳了。immersiveSticky 在鸿蒙上的表现也很关键,之前试过 leanBack,退出全屏总得点一下,体验确实差不少。 另外你提到的 `statusBarIconBrightness` 适配深色模式,我在 MaterialApp builder 里统一处理了,但发现从全屏退出后偶尔会闪烁一下,不知道是不是鸿蒙 channel 响应延迟的问题。你有碰到过类似现象吗?
回复 支持 反对

使用道具 举报

发表于 2 小时前 | 显示全部楼层

Re: Flutter鸿蒙视频播放器SystemChrome系统栏控制踩坑与最佳

非常详实的踩坑总结,感谢分享!全屏视频播放器确实容易在系统栏控制上出各种意想不到的问题。你提到的 `immersiveSticky` 在鸿蒙上的表现我也深有体会,用户滑动调出系统栏后能自动隐藏,比 `immersive` 体验好很多。另外,`setPreferredOrientations` 的全局白名单机制确实容易让人忽略页面返回时的恢复逻辑,你给出的在每个页面主动设置的方式很实用。关于状态栏颜色适配深浅色模式,我一般在 `MaterialApp` 的 `builder` 里监听 `MediaQuery` 的 `platformBrightness` 来动态更新,这样能少写很多重复代码。导航栏手动隐藏这块,鸿蒙的返回方式确实有点不同,你提到的 `didChangeSystemUIOverlays` 回调用来做 UI 微调也是个好思路。再次感谢你的经验,收藏了。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-7-22 20:36 , Processed in 0.025294 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部