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 暴露了以下典型能力:

  1. 读取支持的显示模式。
  2. 读取当前实际模式。
  3. 读取当前首选模式。
  4. 设置首选模式。
  5. 快速切换高刷新率或低刷新率。

原库的核心对象可以抽象为:

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.ModeWindowManager 或厂商兼容逻辑;鸿蒙应用模型、窗口管理和权限模型不同。尤其是“显示模式”这个词可能同时指:

  • 屏幕物理分辨率。
  • 系统显示缩放比例。
  • 应用窗口刷新率偏好。
  • 渲染帧率或 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 方法返回值约定

字段类型含义
capabilityStringsupportedrestricted 等能力状态
requestedMap应用提交的目标模式
activeMap/null系统当前实际采用模式
messageString/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 检查顺序

  1. 判断当前平台是否为 OpenHarmony。
  2. 判断应用是否处于前台并拥有有效窗口。
  3. 查询插件实现是否存在。
  4. 查询显示 API 是否可用。
  5. 查询设备支持的模式。
  6. 提交偏好并回读实际模式。

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 真机验证清单

  1. 低刷新率设备。
  2. 高刷新率设备。
  3. LTPO 动态刷新设备。
  4. 横竖屏切换。
  5. 分屏和浮窗。
  6. 前后台切换。
  7. 系统省电模式。
  8. 温升或高负载场景。

十一、日志与可观测性

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 名称,并用至少三类真机完成验证后,再把骨架代码收敛成正式插件。

如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!


相关资源:

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐