flutter_displaymode_鸿蒙适配与权限调研
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 暴露了以下典型能力:
- 读取支持的显示模式。
- 读取当前实际模式。
- 读取当前首选模式。
- 设置首选模式。
- 快速切换高刷新率或低刷新率。
原库的核心对象可以抽象为:
class DisplayMode {
final int id;
final int width;
final int height;
final double refreshRate;
final bool isAuto;
const DisplayMode({
required this.id,
required this.width,
required this.height,
required this.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 都可以实现:
enum DisplayModeCapability {
supported,
unsupported,
restricted,
unknown,
}
class DisplayModeResult {
final DisplayModeCapability capability;
final DisplayMode? requested;
final DisplayMode? active;
final String? message;
const DisplayModeResult({
required this.capability,
this.requested,
this.active,
this.message,
});
}
3.2 Platform channel 方法名
建议使用稳定、可扩展的方法名:
class HarmonyDisplayMode {
static const _channel = MethodChannel('flutter_displaymode');
static Future<List<DisplayMode>> get supported async {
final raw = await _channel.invokeMethod<List<dynamic>>('getSupportedModes');
return (raw ?? const [])
.map((item) => DisplayMode.fromMap(Map<String, dynamic>.from(item)))
.toList(growable: false);
}
static Future<DisplayMode?> get active async {
final raw = await _channel.invokeMethod<Map<dynamic, dynamic>>('getActiveMode');
if (raw == null) return null;
return DisplayMode.fromMap(Map<String, dynamic>.from(raw));
}
static Future<DisplayModeResult> setPreferred(DisplayMode mode) async {
final raw = await _channel.invokeMethod<Map<dynamic, dynamic>>(
'setPreferredMode',
mode.toMap(),
);
return DisplayModeResult.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.yaml
4.2 pubspec 声明
name: flutter_displaymode
description: Display mode preference bridge for Flutter and OpenHarmony.
version: 0.7.0-ohos.1
environment:
sdk: '>=3.0.0 <4.0.0'
flutter: '>=3.10.0'
flutter:
plugin:
platforms:
android:
package: dev.example.flutter_displaymode
pluginClass: FlutterDisplayModePlugin
ohos:
pluginClass: DisplayModePlugin
4.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';
export function registerPlugins(registrar: object): void {
DisplayModePlugin.registerWith(registrar);
}
5.2 MethodChannel 分发
export class DisplayModePlugin {
static registerWith(registrar: any): void {
const channel = registrar.createMethodChannel('flutter_displaymode');
channel.setMethodCallHandler(async (call: any) => {
switch (call.method) {
case 'getSupportedModes':
return this.getSupportedModes();
case 'getActiveMode':
return this.getActiveMode();
case 'setPreferredMode':
return this.setPreferredMode(call.arguments);
default:
throw new Error(`Method not implemented: ${call.method}`);
}
});
}
private static async getSupportedModes(): 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 {
final modes = await HarmonyDisplayMode.supported;
if (modes.isEmpty) return DisplayModeCapability.unsupported;
if (modes.length == 1 && modes.first.isAuto) {
return DisplayModeCapability.restricted;
}
return DisplayModeCapability.supported;
} on PlatformException catch (_) {
return DisplayModeCapability.unknown;
}
}
6.3 权限检查的现实边界
“权限检查”不能只读一个布尔值。对显示模式来说,至少要同时检查:
- 权限声明是否存在。
- API 是否在当前 SDK 暴露。
- 当前设备是否支持。
- 当前窗口是否满足调用条件。
- 系统是否接受请求。
七、请求刷新率偏好的实现策略
7.1 首选策略:公开窗口/渲染 API
如果目标 API 提供窗口级帧率范围或渲染帧率偏好,应优先使用该 API,而不是尝试修改全局 Display 设置。概念代码如下:
async function requestFrameRate(minRate: number, maxRate: number): Promise<object> {
const uiContext = getCurrentUIContext();
if (uiContext == null) {
return { capability: 'restricted', message: 'No active UI context' };
}
// 具体方法名以目标 API 版本的公开文档为准。
const accepted = await uiContext.requestFrameRateRange({ minRate, maxRate });
return {
capability: accepted ? 'supported' : 'restricted',
requested: { minRate, maxRate },
};
}
7.2 备用策略:仅调整 Flutter 渲染节奏
当系统不允许应用改变显示模式时,仍可通过 Flutter 的 SchedulerBinding、动画策略和资源降级来改善体验:
void configureRenderingPolicy(DisplayModeCapability capability) {
if (capability == DisplayModeCapability.restricted) {
// 业务侧降低动画复杂度,避免把系统限制误判为插件故障。
timeDilation = 1.0;
}
}
7.3 不建议的策略
- 通过 shell 命令修改系统设置。
- 通过隐藏 API 反射切换刷新率。
- 在没有官方权限定义时手写权限名。
- 把设备支持的刷新率写死为 60/90/120。
八、Dart 层兼容封装
8.1 保持原 API 名称
为了让已有业务平滑迁移,可以保留原库常用入口:
class FlutterDisplayMode {
static Future<List<DisplayMode>> get supported =>
HarmonyDisplayMode.supported;
static Future<DisplayMode?> get active =>
HarmonyDisplayMode.active;
static Future<DisplayModeResult> setPreferredMode(DisplayMode mode) =>
HarmonyDisplayMode.setPreferred(mode);
static Future<DisplayModeResult> setHighRefreshRate() async {
final modes = await supported;
final candidates = modes.where((m) => !m.isAuto).toList();
if (candidates.isEmpty) {
return const DisplayModeResult(
capability: DisplayModeCapability.restricted,
message: 'No selectable display mode',
);
}
candidates.sort((a, b) => b.refreshRate.compareTo(a.refreshRate));
return setPreferredMode(candidates.first);
}
}
8.2 页面初始化时机
原库建议在根 Widget 的 initState 中设置 preferred mode。鸿蒙版也应在页面获得有效窗口后调用,并避免在后台、Service 或无 UIContext 时调用。
class RootPageState extends State<RootPage> {
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_tryRequestHighRefreshRate();
});
}
Future<void> _tryRequestHighRefreshRate() async {
final result = await FlutterDisplayMode.setHighRefreshRate();
debugPrint('display mode result: ${result.capability}');
}
}
九、模式选择与降级规则
9.1 选择算法
DisplayMode? chooseMode(
List<DisplayMode> modes, {
required double targetRate,
}) {
final selectable = modes.where((mode) => !mode.isAuto).toList();
if (selectable.isEmpty) return null;
selectable.sort((a, b) {
final da = (a.refreshRate - targetRate).abs();
final db = (b.refreshRate - targetRate).abs();
return da.compareTo(db);
});
return selectable.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(await checkCapability(), DisplayModeCapability.restricted);
});
10.2 Platform channel 测试
testWidgets('setPreferredMode forwards mode map', (tester) async {
final calls = <MethodCall>[];
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(const MethodChannel('flutter_displaymode'), (call) async {
calls.add(call);
return {'capability': 'restricted', 'message': 'system policy'};
});
await FlutterDisplayMode.setPreferredMode(
const DisplayMode(id: 1, width: 1080, height: 2340, refreshRate: 90),
);
expect(calls.single.method, 'setPreferredMode');
});
10.3 真机验证清单
- 低刷新率设备。
- 高刷新率设备。
- LTPO 动态刷新设备。
- 横竖屏切换。
- 分屏和浮窗。
- 前后台切换。
- 系统省电模式。
- 温升或高负载场景。
十一、日志与可观测性
11.1 建议日志字段
function logModeDecision(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
更多推荐



所有评论(0)