Flutter Display Mode 适配 OpenHarmony:先确认三方应用能否设置显示模式
前言
flutter_displaymode是一个面向 Android 的 Flutter 插件,用于读取设备支持的显示模式,并设置应用希望使用的分辨率与刷新率。pub.dev 当前页面显示的版本为0.7.0,平台标注为 Android;其 README 同时提醒,系统仍可能根据内部策略拒绝或调整应用请求。参见官方包页面:https://pub.dev/packages/flutter_displaymode。
本文的目标不是把 Android 的实现机械搬到鸿蒙,而是先回答一个更重要的问题:普通三方应用在 OpenHarmony/HarmonyOS 上,是否拥有把整机显示模式切换为指定分辨率或刷新率的权限?
先给结论:截至本文调研时,公开资料中没有看到面向普通三方应用、可稳定保证全局切换屏幕分辨率/刷新率的通用授权。OpenHarmony 的部分显示管理接口属于系统 API;应用侧更现实的方案是申请窗口或渲染帧率偏好,由系统和设备策略决定是否采用。文章中的代码因此以“能力探测、偏好请求、实际结果回读、失败降级”为核心。
重要结论:不要在鸿蒙适配版中承诺“调用一次 API 就一定切到 120 Hz”。普通应用最多表达偏好,最终结果仍由系统、设备面板、功耗策略和窗口状态决定。
图 1:本文采用“Flutter API 保持兼容、鸿蒙侧能力探测、系统策略兜底”的适配思路。发布时建议替换为项目实机截图或架构图。
一、原库能力与适配目标
1.1 原库解决什么问题
flutter_displaymode暴露了以下典型能力:
- 读取支持的显示模式。
- 读取当前实际模式。
- 读取当前首选模式。
- 设置首选模式。
- 快速切换高刷新率或低刷新率。
原库的核心对象可以抽象为:
classDisplayMode{finalint id;finalint width;finalint height;finaldouble refreshRate;finalbool isAuto;constDisplayMode({requiredthis.id,requiredthis.width,requiredthis.height,requiredthis.refreshRate,this.isAuto=false,});}1.2 鸿蒙版适配的目标
鸿蒙版建议保持 Dart 层调用习惯,减少业务代码分支:
| 目标 | Android 原行为 | OpenHarmony 建议行为 |
|---|---|---|
| 读取模式 | 返回系统支持列表 | 返回公开 API 能探测到的候选列表 |
| 设置模式 | 设置 preferred mode | 提交窗口/渲染偏好,不承诺强制切换 |
| 读取实际模式 | 查询 active mode | 查询系统回报或返回 unknown |
| 不支持设备 | 抛出 PlatformException | 返回能力状态并安全降级 |
| 后台调用 | 通常 noActivity | 明确要求前台窗口和有效 UIContext |
1.3 为什么不能照搬 Android
Android 实现通常依赖Display.Mode、WindowManager或厂商兼容逻辑;鸿蒙应用模型、窗口管理和权限模型不同。尤其是“显示模式”这个词可能同时指:
- 屏幕物理分辨率。
- 系统显示缩放比例。
- 应用窗口刷新率偏好。
- 渲染帧率或 VSync 频率。
- LTPO 面板的动态刷新策略。
如果不先拆分概念,插件很容易把“渲染帧率请求成功”误报成“系统刷新率已经切换”。
二、三方应用权限调研结论
2.1 公开资料能确认什么
OpenHarmony 文档中存在ohos.display等显示相关模块,部分页面明确标注为 System API。官方文档入口:https://gitee.com/openharmony/docs。OpenHarmony API 参考总入口:https://docs.openharmony.cn/pages/v5.0/
公开资料还可以确认三点:
- 应用权限由 Access Token 体系管理。
- 系统 API 与普通应用可用 API 不是同一个集合。
- 即使设备支持多刷新率,系统也可能基于功耗、温度、场景和窗口状态进行调度。
2.2 是否存在一个“设置显示模式”权限
目前不建议在普通三方应用的module.json5中虚构类似以下权限:
{"name":"ohos.permission.SET_DISPLAY_MODE"}原因很简单:没有找到可核验的公开权限定义,添加不存在的权限不会自动获得能力,反而会造成审核和维护风险。权限名必须以目标 SDK 对应的官方权限清单为准:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/permissions-guidelines-V5。
审核建议:如果能力需要系统签名、特权应用或厂商白名单,应在插件文档中明确写出,不要把它包装成普通应用权限。
2.3 结论分层
| 能力层级 | 普通三方应用可行性 | 适配策略 |
|---|---|---|
| 读取屏幕尺寸、密度 | 通常可行 | 使用公开设备/窗口 API |
| 读取应用窗口信息 | 通常可行 | 绑定当前窗口上下文 |
| 请求应用帧率偏好 | 取决于 API 和设备 | 能力探测后调用 |
| 强制整机刷新率 | 通常不可保证 | 返回 unsupported 或 best-effort |
| 修改系统分辨率 | 不应假设可行 | 仅系统应用/厂商能力考虑 |
| 修改全局显示缩放 | 不应假设可行 | 引导用户到系统设置,若产品允许 |
2.4 调研后的产品表述
推荐对外写成:
鸿蒙版支持在设备和系统允许时提交应用显示/帧率偏好,并提供实际结果读取和自动降级;不保证修改系统全局显示设置。
不推荐写成:
鸿蒙版可以强制打开 120 Hz。
三、API 映射设计
3.1 Dart 公共接口
先定义与平台无关的接口,Android、OpenHarmony、iOS 都可以实现:
enumDisplayModeCapability{supported,unsupported,restricted,unknown,}classDisplayModeResult{finalDisplayModeCapabilitycapability;finalDisplayMode?requested;finalDisplayMode?active;finalString?message;constDisplayModeResult({requiredthis.capability,this.requested,this.active,this.message,});}3.2 Platform channel 方法名
建议使用稳定、可扩展的方法名:
classHarmonyDisplayMode{staticconst_channel=MethodChannel('flutter_displaymode');staticFuture<List<DisplayMode>>getsupportedasync{finalraw=await_channel.invokeMethod<List<dynamic>>('getSupportedModes');return(raw??const[]).map((item)=>DisplayMode.fromMap(Map<String,dynamic>.from(item))).toList(growable:false);}staticFuture<DisplayMode?>getactiveasync{finalraw=await_channel.invokeMethod<Map<dynamic,dynamic>>('getActiveMode');if(raw==null)returnnull;returnDisplayMode.fromMap(Map<String,dynamic>.from(raw));}staticFuture<DisplayModeResult>setPreferred(DisplayModemode)async{finalraw=await_channel.invokeMethod<Map<dynamic,dynamic>>('setPreferredMode',mode.toMap(),);returnDisplayModeResult.fromMap(raw??const{});}}3.3 方法返回值约定
| 字段 | 类型 | 含义 |
|---|---|---|
capability | String | supported、restricted等能力状态 |
requested | Map | 应用提交的目标模式 |
active | Map/null | 系统当前实际采用模式 |
message | String/null | 调试或降级原因 |
这种设计比单纯返回true/false更适合鸿蒙,因为请求成功和实际采用可能是两个结果。
四、Flutter 插件目录改造
4.1 推荐目录
flutter_displaymode/ ├─ lib/ │ └─ flutter_displaymode.dart ├─ android/ │ └─ src/main/kotlin/... ├─ ohos/ │ ├─ index.ets │ ├─ package.json5 │ └─ src/main/ets/ │ ├─ DisplayModePlugin.ets │ └─ DisplayModeMapper.ets ├─ example/ │ └─ ohos/ └─ pubspec.yaml4.2 pubspec 声明
name:flutter_displaymodedescription:Display mode preference bridge for Flutter and OpenHarmony.version:0.7.0-ohos.1environment:sdk:'>=3.0.0 <4.0.0'flutter:'>=3.10.0'flutter:plugin:platforms:android:package:dev.example.flutter_displaymodepluginClass:FlutterDisplayModePluginohos:pluginClass:DisplayModePlugin4.3 ohos/package.json5
{"modelVersion":"5.0.0","name":"flutter_displaymode_ohos","version":"0.7.0-ohos.1","description":"OpenHarmony implementation for flutter_displaymode","main":"index.ets","license":"MIT"}五、鸿蒙侧插件骨架
5.1 插件入口
下面代码是适配骨架,具体注册接口应以所使用 Flutter OpenHarmony embedding 版本为准:
import{DisplayModePlugin}from'./src/main/ets/DisplayModePlugin';exportfunctionregisterPlugins(registrar:object):void{DisplayModePlugin.registerWith(registrar);}5.2 MethodChannel 分发
exportclassDisplayModePlugin{staticregisterWith(registrar:any):void{constchannel=registrar.createMethodChannel('flutter_displaymode');channel.setMethodCallHandler(async(call:any)=>{switch(call.method){case'getSupportedModes':returnthis.getSupportedModes();case'getActiveMode':returnthis.getActiveMode();case'setPreferredMode':returnthis.setPreferredMode(call.arguments);default:thrownewError(`Method not implemented:${call.method}`);}});}privatestaticasyncgetSupportedModes():Promise<object[]>{return[{id:0,width:0,height:0,refreshRate:0,isAuto:true}];}}5.3 为什么初版返回 auto
在尚未确认公开 API 和设备支持矩阵前,返回auto是比伪造 60/90/120 Hz 更安全的行为。业务层可以据此隐藏强制切换按钮,或者显示“由系统自动调度”。
六、能力探测与权限检查
6.1 检查顺序
- 判断当前平台是否为 OpenHarmony。
- 判断应用是否处于前台并拥有有效窗口。
- 查询插件实现是否存在。
- 查询显示 API 是否可用。
- 查询设备支持的模式。
- 提交偏好并回读实际模式。
6.2 能力状态示例
Future<DisplayModeCapability>checkCapability()async{try{finalmodes=awaitHarmonyDisplayMode.supported;if(modes.isEmpty)returnDisplayModeCapability.unsupported;if(modes.length==1&&modes.first.isAuto){returnDisplayModeCapability.restricted;}returnDisplayModeCapability.supported;}onPlatformExceptioncatch(_){returnDisplayModeCapability.unknown;}}6.3 权限检查的现实边界
“权限检查”不能只读一个布尔值。对显示模式来说,至少要同时检查:
- 权限声明是否存在。
- API 是否在当前 SDK 暴露。
- 当前设备是否支持。
- 当前窗口是否满足调用条件。
- 系统是否接受请求。
七、请求刷新率偏好的实现策略
7.1 首选策略:公开窗口/渲染 API
如果目标 API 提供窗口级帧率范围或渲染帧率偏好,应优先使用该 API,而不是尝试修改全局 Display 设置。概念代码如下:
asyncfunctionrequestFrameRate(minRate:number,maxRate:number):Promise<object>{constuiContext=getCurrentUIContext();if(uiContext==null){return{capability:'restricted',message:'No active UI context'};}// 具体方法名以目标 API 版本的公开文档为准。constaccepted=awaituiContext.requestFrameRateRange({minRate,maxRate});return{capability:accepted?'supported':'restricted',requested:{minRate,maxRate},};}7.2 备用策略:仅调整 Flutter 渲染节奏
当系统不允许应用改变显示模式时,仍可通过 Flutter 的SchedulerBinding、动画策略和资源降级来改善体验:
voidconfigureRenderingPolicy(DisplayModeCapabilitycapability){if(capability==DisplayModeCapability.restricted){// 业务侧降低动画复杂度,避免把系统限制误判为插件故障。timeDilation=1.0;}}7.3 不建议的策略
- 通过 shell 命令修改系统设置。
- 通过隐藏 API 反射切换刷新率。
- 在没有官方权限定义时手写权限名。
- 把设备支持的刷新率写死为 60/90/120。
八、Dart 层兼容封装
8.1 保持原 API 名称
为了让已有业务平滑迁移,可以保留原库常用入口:
classFlutterDisplayMode{staticFuture<List<DisplayMode>>getsupported=>HarmonyDisplayMode.supported;staticFuture<DisplayMode?>getactive=>HarmonyDisplayMode.active;staticFuture<DisplayModeResult>setPreferredMode(DisplayModemode)=>HarmonyDisplayMode.setPreferred(mode);staticFuture<DisplayModeResult>setHighRefreshRate()async{finalmodes=awaitsupported;finalcandidates=modes.where((m)=>!m.isAuto).toList();if(candidates.isEmpty){returnconstDisplayModeResult(capability:DisplayModeCapability.restricted,message:'No selectable display mode',);}candidates.sort((a,b)=>b.refreshRate.compareTo(a.refreshRate));returnsetPreferredMode(candidates.first);}}8.2 页面初始化时机
原库建议在根 Widget 的initState中设置 preferred mode。鸿蒙版也应在页面获得有效窗口后调用,并避免在后台、Service 或无 UIContext 时调用。
classRootPageStateextendsState<RootPage>{@overridevoidinitState(){super.initState();WidgetsBinding.instance.addPostFrameCallback((_){_tryRequestHighRefreshRate();});}Future<void>_tryRequestHighRefreshRate()async{finalresult=awaitFlutterDisplayMode.setHighRefreshRate();debugPrint('display mode result:${result.capability}');}}九、模式选择与降级规则
9.1 选择算法
DisplayMode?chooseMode(List<DisplayMode>modes,{required double targetRate,}){finalselectable=modes.where((mode)=>!mode.isAuto).toList();if(selectable.isEmpty)returnnull;selectable.sort((a,b){finalda=(a.refreshRate-targetRate).abs();finaldb=(b.refreshRate-targetRate).abs();returnda.compareTo(db);});returnselectable.first;}9.2 降级矩阵
| 场景 | 返回状态 | UI 行为 |
|---|---|---|
| 没有公开接口 | unsupported | 隐藏切换入口 |
| 有接口但权限受限 | restricted | 显示系统托管提示 |
| 有候选但请求未采用 | supported+ active 不一致 | 展示实际模式 |
| 后台调用 | restricted | 延迟到前台回调 |
| API 版本不匹配 | unknown | 记录日志并保持 auto |
十、测试方案
10.1 单元测试
test('auto mode is treated as restricted',()async{fakeModes=const[DisplayMode(id:0,width:0,height:0,refreshRate:0,isAuto:true),];expect(awaitcheckCapability(),DisplayModeCapability.restricted);});10.2 Platform channel 测试
testWidgets('setPreferredMode forwards mode map',(tester)async{finalcalls=<MethodCall>[];TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(constMethodChannel('flutter_displaymode'),(call)async{calls.add(call);return{'capability':'restricted','message':'system policy'};});awaitFlutterDisplayMode.setPreferredMode(constDisplayMode(id:1,width:1080,height:2340,refreshRate:90),);expect(calls.single.method,'setPreferredMode');});10.3 真机验证清单
- 低刷新率设备。
- 高刷新率设备。
- LTPO 动态刷新设备。
- 横竖屏切换。
- 分屏和浮窗。
- 前后台切换。
- 系统省电模式。
- 温升或高负载场景。
十一、日志与可观测性
11.1 建议日志字段
functionlogModeDecision(event:string,payload:object):void{console.info('[flutter_displaymode]',JSON.stringify({event,timestamp:Date.now(),...payload,}));}建议记录设备型号、系统 API 版本、候选模式、请求模式、实际模式和失败原因,但不要记录用户隐私数据。
11.2 关键指标
- 请求成功率。
- 请求后 active 与 preferred 的一致率。
- restricted 占比。
- 页面首帧耗时变化。
- 高刷新率下的掉帧率与功耗。
十二、常见问题与优化建议
12.1 为什么拿到了 120 Hz 仍然只有 60 FPS
显示刷新率、应用渲染帧率和实际可见帧率不是一回事。Flutter 页面如果存在昂贵布局、图片解码或同步 I/O,即使系统允许高刷新率,也可能无法稳定输出 120 FPS。
12.2 为什么设置成功但 active 没变化
这是预期可能性之一。原库 README 已说明 preferred mode 只是偏好,系统可以基于内部策略不切换。鸿蒙适配必须把 active 回读作为最终结果。
12.3 是否要申请系统权限
只有在官方文档明确给出权限名、保护级别和申请方式时才申请。若接口被标为 System API,普通三方应用不应通过改配置绕过限制。
12.4 是否应该保留 Android 实现
应该。跨平台插件应按平台拆分实现,Dart 公共 API 保持一致;Android 继续使用原逻辑,OpenHarmony 使用独立实现和能力探测。
12.5 如何避免 API 版本漂移
environment:flutter:'>=3.10.0'sdk:'>=3.0.0 <4.0.0'同时在 CI 中固定 DevEco Studio、SDK 和 Flutter OpenHarmony embedding 版本,并在发布说明中列出已验证 API 级别。
总结
把flutter_displaymode适配到鸿蒙,第一步不是寻找一个看似相近的系统权限,而是确认普通三方应用的能力边界。当前更稳妥的结论是:不把全局显示模式切换当作普通应用必得能力,而是实现“公开 API 探测、窗口/渲染偏好请求、实际模式回读、失败降级”。
这样设计可以保留 Flutter 业务层的使用习惯,也能适应不同 OpenHarmony 版本、设备面板和系统策略。下一步应在目标 DevEco/Flutter embedding 版本上确认具体公开 API 名称,并用至少三类真机完成验证后,再把骨架代码收敛成正式插件。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!
相关资源:
- OpenHarmony 适配文档:https://docs.openharmony.cn/
- Flutter 插件开发指南:https://docs.flutter.dev/packages-and-plugins/developing-packages
flutter_displaymode原始包:https://pub.dev/packages/flutter_displaymode