我把这次适配的过程整理在这里,也欢迎大家到 CPF-Flutter 鸿蒙社区 一起交流 Flutter 适配中碰到的问题。

这次我基于社区已有的 flutter_platform_alert 继续往下做:我先从上游已发布的 0.9.1 出发,复用了社区提交 33a326430fd163d19b1d28047457cbbe1946a112 的基础鸿蒙适配,再顺手补上 HarmonyOS 7(API 26)单弹窗沉浸光感材质能力,同时保留原来的包名和调用入口。

我把这次修改发布在 个人源码分支,本文对应固定提交 5034fdca717f18136f3c0ba11342b45a4a403b68;Example 运行说明和示例页面源码也在同一提交中。

前言

我在查看 HarmonyOS 7(API 26)的“空间化:沉浸光感组件”文档时,注意到了下面这段官方演示:弹窗底板能透出背景色,按住胶囊按钮并移动时,触点附近会出现光晕,按钮也有轻微形变。我想试试怎样让 Flutter 页面调用鸿蒙原生弹窗时使用这种材质。

华为官方文档展示的HarmonyOS 7沉浸光感弹窗交互示例

图1 官方光感示例

我选择的场景是图像预览:用户调整亮度后准备离开,页面弹出确认框,询问是否放弃调整。确认框下方是彩色画布,可以直观看出不同材质的效果。

带着这个念头,我去查了常用的跨平台原生弹窗包 flutter_platform_alert。翻完源码我发现,鸿蒙社区其实已经把基础弹窗跑通了,只是还没接上 HarmonyOS 7 的这套新材质接口。于是,我就照着它原本的跨平台选项结构把这块能力补了起来。

一、使用场景与痛点

1.1 图像预览页的场景诉求

单弹窗系统材质的四类典型使用场景

图2 使用场景

我选择图像预览页,是因为确认框下方就是一整幅彩色画布,比较容易观察不同材质。系统默认弹窗在当前模拟器上已有轻微透色;不过,页面有时需要更明显的透色和触摸光晕,有时又希望使用更少背景干扰的确认框。

不过,长篇说明弹窗要优先保证文字可读,过多的背景色彩可能分散注意力。所以我希望按单次弹窗选择材质,让不同页面使用合适的策略。

常见情景示例场景原有表现与痛点适配后的优化体验
画布色彩丰富图像预览退出确认默认材质透色较轻,当前按压画面未见白色光晕请求 immersiveThick,在支持设备上呈现厚材质和触摸光晕
正文说明较长长段规则确认弹窗透入背景色彩可能干扰阅读指定 disabled,在支持的环境中为单框设置空材质
常规系统提示普通消息提醒框只需跟随系统默认外观,无需额外配置保持 systemDefault 默认行为与旧参数兼容
同页不同提示预览确认与结果提示同一页面内需要切换透光光晕与纯色弹窗按次在 OhosAlertOptions 传入不同策略

表1 常见场景

为了验证这套想法,我自己搭了一个图像预览页来跑完整流程,操作起来也就三步:

  1. 示例启动时预览亮度为 65%,我可以用底部滑块调整画面。
  2. 我选择一个材质策略,弹出“放弃预览调整?”确认框,观察背板和按钮按压反馈。
  3. 确认框把结果传回 Flutter:选择“继续编辑”保留当前亮度;选择“放弃调整”则恢复为 50%。

另外,为了让拿到代码的读者拉下来就能直接跑,我干脆用 CustomPaint 在本地画了一幅带暖橙色圆日和青绿色山峦的几何样图。我拖动滑块时画面明暗会跟着实时变化,但我全程没去读系统相册,这样连相册权限都不用额外申请。

1.2 查库评估与适配缘由

想明白页面要什么之后,我没有急着自己造新包,而是先去 pub.dev 摸了一圈底。打开 flutter_platform_alert 的主页时,我看到它由已认证发布者维护,Pub Points 拿满了 160/160,攒了 145 个 Likes,近 30 天下载量也超过了 1.17 万次,具体页面见图 3。

平时我们在多端项目里用它,主要是图它足够省心:调一下 showAlert 或 showCustomAlert,它就能帮我们拉起 Android、iOS、macOS、Windows 和 Linux 的系统原生对话框。只不过,上游发布的 0.9.1 版本里还没有内置 OpenHarmony 支持,这一点我一眼就能从 pub.dev 主页的平台标签上看出来。

flutter_platform_alert在pub.dev上的受欢迎程度与平台标签

图3 上游包主页

带着这个疑问,我转头去翻了 oh-Flutter 和 CPF-Flutter 社区仓库,很快就搜到了 oh-flutter/flutter_platform_alert。我对照提交记录把 ohos 目录通读了一遍,心里基本就有数了。

先说它已经帮我们铺好的路:社区提交 33a326430fd163d19b1d28047457cbbe1946a112 其实已经把基础的鸿蒙原生弹窗跑通了。插件会拿到当前前台窗口的 UIContext,调用 showAlertDialog 把确认框弹出来,再把用户点的按钮结果传回 Dart。既然这条跨端链路已经是现成的,我完全没必要另起炉灶重写一个包,直接在这上面补材质选项就够了。

再看它还差的那一步:这份社区代码当时还没有接上 HarmonyOS 7(API 26)的单弹窗沉浸光感材质。我点开 Dart 侧的 OhosAlertOptions,里面只有一个控制点击外部关闭的 cancelable 字段;再去翻原生侧的 FlutterPlatformAlertPlugin.ets,它在拼装 AlertDialogParamWithOptions 时也压根没碰过 systemMaterial。

回头我再去细读华为官方的《空间化:沉浸光感组件》文档才搞清楚,要想同时拿到弹窗背板透色、按钮微形变和触摸光晕,光靠组件自己设个枚举还不够,得让宿主工程和组件参数两边打配合:一方面,我们得在宿主配置里把 ohos.arkui.UIMaterial.state 打开;另一方面,插件在创建 ImmersiveMaterial 实例时还得配齐三项参数:

  • 用 style 指定材质的视觉厚薄样式。
  • 用 interactive 打开按压时的交互形变。
  • 用 lightEffect 配上触摸时的光晕反馈颜色。

原包缺的是按单次弹窗显式选择系统材质的入口,并不意味着系统默认弹窗一定不透色。在本轮模拟器里,我看到的默认确认框已经带有轻微透色;这次扩展让页面可以继续跟随默认,也可以请求空材质或厚沉浸材质。

确认了缺口之后,我其实还纠结过一阵接口该怎么设计:我是直接把 interactive、lightEffect 这些细碎字段全都摊开塞进 Dart 层的 OhosAlertOptions 里,还是往回收一收?

为此我专门去翻了包里现成的 IosAlertOptions、WindowsAlertOptions 和 MacosAlertOptions。对比下来我发现,flutter_platform_alert 一贯的路数,是只让业务调用方选“这个对话框要什么行为”,从来不逼着写 Flutter 页面的人去操心底层渲染器的颜色结构体和布尔开关。既然如此,我也别破坏整个包的手感了——我决定只在 OhosAlertOptions 里加一个 systemMaterial 枚举字段,把两边的活儿彻底分开:

  • 业务页面只管按场景挑策略,决定这一框是跟随默认、关掉材质,还是开厚光感。
  • 原生插件则在 immersiveThick 分支里自己把 style: uiMaterial.ImmersiveStyle.THICK、interactive: true 和 lightEffect: { color: Color.White } 组装到一块儿。

这样,我在页面里调用弹窗时只需选择策略,不必每次拼装鸿蒙侧的渲染参数。本轮验证集中在鸿蒙模拟器,其他平台没有逐端运行。

1.3 版本基线与代码分层

想到后面我还得把这份改动整理好提给社区审阅,如果把上游原来的代码、社区前辈写的鸿蒙底座和我自己加的补丁揉成一团,维护者看 diff 肯定头疼。所以在真正改代码之前,我先把这三层来源拆解清楚了:

  1. 最底层是上游发布基线。原作者在 2026 年 6 月 5 日发布了 0.9.1,对应社区仓里的同名 tag e6d960942a3f870247dc1ef71e0d73d8bd24f811,这一版里还没有 ohos 目录。

  2. 中间一层是社区已有的基础鸿蒙适配。我拉取并复用了 oh-flutter/flutter_platform_alert 里的提交 33a326430fd163d19b1d28047457cbbe1946a112(它的直接父提交正好就是 0.9.1 tag)。这块本来就是社区写好的成果,我在工程里原封不动保留了它的署名、MIT 许可证和回迁补丁,绝不算到我自己这次新增的功能里。

    顺带说明一下,社区主分支的变更记录目前还标着 Unreleased;我这里引用的是固定 commit 的源码,所以不会把它说成已经独立发版的稳定包。

  3. 最上面一层才是我这次写的 HarmonyOS 7 升级补丁。我特意把它拆成了两份互不耦合的最小补丁:第一步,我先拿本批 Flutter 3.41.9 实际导出的 getWindowStage 换掉旧版报错的 windowStageOf,先把编译跑通;第二步,我再叠加上单弹窗沉浸光感材质补丁 single-dialog-material.patch。动手前我也把该仓库的其余分支挨个翻了一遍,至少在我这次检索的范围内,还没有看到暴露单框 systemMaterial 的现成实现。

二、三种策略的效果

2.1 同一预览页的默认与 THICK 动图对照

我在 Mate X7(API 26)模拟器上录制了两段示例页面操作,分别展示 systemDefault 和 immersiveThick 的效果。两段都使用同一幅几何样图,但录制时的预览亮度并不完全一致:默认策略图中为 64%,THICK 策略图中为 65%。因此,这里主要对照材质和按压反馈,不能把色彩深浅当作严格同亮度测量。

图 4 和图 5 是示例应用内两种策略的展示,现有录制记录不足以将图 4 单独认定为原社区插件独立构建的运行结果。原社区代码缺少单框策略字段这一点,来自前面的源码核查,不靠动图证明。

默认策略下的确认框与按钮按压

图4 默认策略

THICK 策略下的确认框与触摸光晕

图5 THICK 策略

观察点systemDefaultimmersiveThick
当前录制亮度64%65%
确认框背板浅色底板,隐约透出画布色彩厚材质透出更多画布色彩
按住按钮普通浅灰按压背景可以看到随触点移动的白色光晕
选择“继续编辑”保留当时的 64%保留当时的 65%
选择“放弃调整”恢复为 50%恢复为 50%

表2 策略对照

从图中可以看到,两种策略都展示系统原生确认框,示例收到选择后再处理亮度。我要补的是同一个应用中按次请求不同材质的能力;系统默认外观会受宿主配置、设备与系统版本影响,不能一概写成“原包没有透色”。

2.2 三种单框策略的静态与按压细节

光看两段动图还不够细,我又在同一个示例宿主工程里,挨个传入 systemDefault、disabled 和 immersiveThick,专门看看按单次弹窗切换策略到底灵不灵。下面这四张静态图,都是我直接用 SDK 自带的 snapshot_display 命令从同一台模拟器里截出来的原图,没有做过任何后期修图。

系统默认策略下的图像预览确认框

图6 系统默认

图 6 是不传材质参数、使用 systemDefault 的画面。Dart 侧不会增加新的通道字段,原生侧也不设置 options.systemMaterial。本例宿主启用了 ohos.arkui.UIMaterial.state;在这台模拟器上,默认框能隐约透出画布色彩,并有一圈细亮边。这里没有做宿主开关启用与关闭的受控对照,因此我只记录这组配置下看到的效果。

显式指定disabled关闭单框材质的确认框

图7 关闭材质

图 7 显式传入了 disabled。在本轮设备和宿主配置下,原生侧为当前确认框设置 uiMaterial.Material.empty 后,背景呈现不透明的浅白色。长文本确认框可以考虑这一策略;但具体外观仍取决于设备能力和系统版本,不能保证所有环境的背景都完全不透明。

不过,只看弹窗静止时的截图,其实还不太容易看清按钮本身的变化,所以我又专门抓拍了手指按住按钮不放的那一瞬间。这里多交代一句,原包调用的鸿蒙 showAlertDialog 默认采用的是无底色纯文本按钮(ButtonStyleMode.TEXTUAL):平时我不按它的时候,按钮背景是全透明的;我手指按住哪一个,哪一个才会亮起胶囊背景。

我在同一个宿主里按住左边的“放弃调整”不松手,分别截下了两种策略的画面:

  • 在图 8 的 immersiveThick 模式下,我按住的左侧按钮亮起了半透明胶囊和一团明显的白色触点光晕;右边没被按住的“继续编辑”则继续透出底下画布的暖粉色。
  • 切到图 9 的 disabled 模式后,我按住左侧同一位置时,底下就只剩一层普通的浅灰色按压底色,白色光晕彻底消失了;右边没按住的区域透出来的也是不透明背板的纯白底色,所以看上去就是白底蓝字。

把这几张图放在一块儿对完,我基本就确认了:在当前这套环境里,哪怕我不去动宿主的全局开关,也能在每次弹窗时单独切出想要的背板材质和按钮光晕。

immersiveThick策略下按住按钮浮现白色触摸光晕

图8 触摸光晕

disabled策略下按住同一按钮仅显示普通灰色按压态

图9 禁用光晕

2.3 运行环境与关键日志

当然,上面看到的这些画面都是我在本轮指定的 SDK 和模拟器里跑出来的结果,肯定不能直接替所有真机打包票。我把自己这次实际用到的框架版本、模拟器模板和系统镜像都列在了表 3 里,大家要是想在本地复现,可以照着逐项核对:

项目本次实际环境
Flutter3.41.9+ohos-1.0.2
Dart3.11.5
DevEco Studio26.0.0.865
HarmonyOS SDK26.0.0.105 Release,API 26
插件来源上游 0.9.1 + 社区鸿蒙适配 33a3264 + 本文两层补丁
模拟器模板Mate X7,arm64,4 核、4 GB
显示配置2210×2416,density 500
镜像版本7.0.0.107
系统回读OpenHarmony-7.0.0.105
单元测试验证Dart 选项与协议测试 6 项通过;原生转译契约测试 17 项通过

表3 运行环境

在示例工程这边,宿主的 entry/src/main/module.json5 默认用 EntryAbility 作为启动入口。我在它的 metadata 数组里加上了 ohos.arkui.UIMaterial.state 并设为 enable,先把应用级的沉浸光感支持打开;除此之外,我没给示例申请相册、网络或存储等任何额外权限。

示例工程module.json5开启UIMaterial宿主配置

图10 宿主配置

抓运行日志的时候,我还碰到了一个小插曲:HiLog 把我较长的包名给截断了一截,我一开始直接拿完整包名去搜,死活没捞全。后来我索性先查出当前前台进程的真实 PID(31355),再配合我在代码里专门打上的 AlertMaterialExample 前缀去过滤。这么一来,我就在同一个进程里清清楚楚地对上了连续三次材质策略请求,还有每次点完按钮传回来的 negativeButton、positiveButton 回执。

同一进程连续触发三种材质弹窗与按钮回执的HiLog日志

图11 关键日志

三、原理与核心代码

3.1 扩展 Dart 平台选项

写 Dart 接口时,我保留了已有的弹窗方法和 PlatformAlertOptions 结构,只在鸿蒙配置类 OhosAlertOptions 中新增 systemMaterial。默认调用不增加新的通道字段;这次模拟器检查不代表所有旧设备和其他平台都已完成兼容验证。下面是 lib/src/alert_options/ohos_alert_options.dart 的完整代码:

/// Per-dialog system material policy on OpenHarmony API 26 or later.
///
/// Unsupported devices and older API levels keep the normal system dialog.
enum OhosAlertMaterialPolicy {
  /// Preserve the system default; this can already include immersive material.
  systemDefault,

  /// Disable system material for this dialog, without changing global settings.
  /// This does not guarantee an opaque background on every device.
  disabled,

  /// Request the system THICK immersive material where supported.
  immersiveThick,
}

/// Represents OpenHarmony-specific options for an alert dialog.
class OhosAlertOptions {
  /// Whether the dialog can be dismissed by tapping outside of it.
  final bool cancelable;

  /// Optional material policy for this dialog only.
  ///
  /// Requires an API 26 SDK to compile the native implementation. Runtime
  /// support is checked before requesting a non-default policy. This option
  /// does not override the user's global material configuration.
  final OhosAlertMaterialPolicy systemMaterial;

  /// Creates an instance of [OhosAlertOptions].
  const OhosAlertOptions({
    this.cancelable = true,
    this.systemMaterial = OhosAlertMaterialPolicy.systemDefault,
  });

  /// Converts these options to method channel arguments.
  Map<String, Object> toJson() => {
    'cancelable': cancelable,
    // 默认不新增通道字段,保留旧调用的参数与系统效果。
    if (systemMaterial != OhosAlertMaterialPolicy.systemDefault)
      'ohosSystemMaterial': systemMaterial.name,
  };
}

不传入新选项时,toJson() 只序列化原有的 cancelable;需要选择材质时,再发送 ohosSystemMaterial。这个约定使默认调用维持原来的参数形状,但运行兼容性仍受实际环境影响。

页面调用时只需要在“跟随默认、关掉材质、用厚沉浸光感”三项里挑一个,犯不着去引一堆鸿蒙特有的颜色和样式枚举。不过我又转念一想,光靠 Dart 层的枚举其实还防不住所有意外——万一有人绕过这个封装直接往通道里发字符串,或者 Dart 包跟原生侧版本没对上呢?

所以我到了鸿蒙原生代码里,还是得老老实实再做一道字符串校验。

3.2 原生材质解析与光感注入

顺着通道往下走,就到了鸿蒙侧的 ohos/src/main/ets/components/plugin/FlutterPlatformAlertPlugin.ets。

写这块逻辑时,我特意拆成了两个小私有方法:我让 materialPolicy 专门负责核对通道传过来的字符串合不合法,再让 applySystemMaterial 负责真正调 API 26 的 uiMaterial 去创建材质实例,并在 immersiveThick 分支里把厚毛玻璃、按压微形变和白色触摸光晕组装到一块儿。这样一旦哪一步没生效,我排查起来一眼就能分清是参数传错了,还是系统能力不支持。

下面是这两个方法的完整实现:

  private materialPolicy(call: MethodCall): string {
    const requested = String(call.argument('ohosSystemMaterial') ?? 'systemDefault');
    if (requested === 'systemDefault' || requested === 'disabled' || requested === 'immersiveThick') {
      return requested;
    }
    // 未知配置不阻断确认框,也不猜测成另一种显式材质。
    hilog.warn(0, TAG, 'Unknown dialog material policy; using system default');
    return 'systemDefault';
  }

  private applySystemMaterial(uiContext: UIContext, options: AlertDialogParamWithOptions, policy: string): void {
    if (policy === 'systemDefault') {
      return;
    }
    // 旧系统保留普通弹窗;编译仍需 API26 SDK,运行回退不是旧 SDK 编译支持。
    if (deviceInfo.sdkApiVersion < 26) {
      hilog.warn(0, TAG, 'Dialog material requires API 26; using system default');
      return;
    }
    try {
      let material: uiMaterial.Material | undefined = undefined;
      uiContext.runScopedTask((): void => {
        if (!uiMaterial.isImmersiveMaterialSupported()) {
          hilog.warn(0, TAG, 'Immersive material is unavailable; using system default');
          return;
        }
        if (policy === 'disabled') {
          material = uiMaterial.Material.empty;
        } else {
          material = new uiMaterial.ImmersiveMaterial({
            style: uiMaterial.ImmersiveStyle.THICK,
            interactive: true,
            lightEffect: { color: Color.White },
          });
        }
      });
      if (material !== undefined) {
        options.systemMaterial = material;
      }
    } catch (error) {
      // 材质查询失败只影响视觉选择,原生按钮与取消回执仍沿用原路径。
      const businessError = error as BusinessError;
      hilog.warn(0, TAG, `Dialog material fallback: ${businessError.code} ${businessError.message}`);
    }
  }

从源码上不难看出,我还考虑到了运行环境和窗口上下文异常的情况,否则最坏的场景是查个材质把整个确认框给弄崩了,那就得不偿失了。

我按下面的顺序检查运行条件:

  1. 通过 deviceInfo.sdkApiVersion < 26 判断系统 API 等级;不满足时保留默认参数。
  2. 进入当前窗口的 uiContext.runScopedTask,调用 uiMaterial.isImmersiveMaterialSupported() 检查材质能力;不支持时同样保留默认参数。

我还把点亮触摸光晕需要的参数全都拢在同一个分支里。在 immersiveThick 分支中,我实例化 uiMaterial.ImmersiveMaterial 时一口气传了 style: uiMaterial.ImmersiveStyle.THICK、interactive: true 和 lightEffect: { color: Color.White }。

等材质对象顺顺当当建好了,最后再往弹窗配置上挂。你可以看到,我先拿一个局部变量 material 在作用域闭包里接住结果,等 runScopedTask 跑完没出问题,再判断它是不是 undefined,有值才写进 options.systemMaterial。就算中途哪一步抛了异常,catch 块兜住之后照样保留系统普通确认框,不会耽误后面的按钮点击和回执返回。

3.3 接入原生弹窗方法

把上面两个材质辅助方法搞定实之后,就把它们接进了原有的 showDialog 主方法里。包里的 showAlert 和 showCustomAlert 最终都会走到这同一个私有方法。

下面是 showDialog 的完整函数;完整插件文件位于本次扩展源码中:

  private showDialog(
    title: string,
    message: string,
    buttons: Array<DialogButtonSpec>,
    cancelable: boolean,
    cancelResult: string,
    materialPolicy: string,
    result: MethodResult,
  ): void {
    const uiContext = this.getUIContext();
    if (uiContext === null) {
      result.error('NO_ACTIVITY', 'A foreground UIAbility is required to show an alert dialog.', null);
      return;
    }

    let callbackConsumed = false;
    const complete = (value: string): void => {
      if (callbackConsumed) {
        return;
      }
      callbackConsumed = true;
      result.success(value);
    };

    const dialogButtons: Array<AlertDialogButtonOptions> = buttons.map(
      (button: DialogButtonSpec): AlertDialogButtonOptions => {
        const option: AlertDialogButtonOptions = {
          value: button.label,
          primary: button.primary,
          action: (): void => complete(button.resultValue),
        };
        return option;
      },
    );

    try {
      const dialogOptions: AlertDialogParamWithOptions = {
        title: title,
        message: message,
        autoCancel: cancelable,
        cancel: (): void => complete(cancelResult),
        buttons: dialogButtons,
      };
      this.applySystemMaterial(uiContext, dialogOptions, materialPolicy);
      uiContext.showAlertDialog(dialogOptions);
    } catch (error) {
      const businessError = error as BusinessError;
      hilog.error(0, TAG, `Failed to show alert: ${businessError.code} ${businessError.message}`);
      result.error(String(businessError.code ?? 'ALERT_ERROR'), businessError.message ?? 'Failed to show alert.', null);
    }
  }

为了方便一眼看清整条链路的走向,我把这套调用和降级顺序画成了图 12。其实在调原生弹窗的时候,我们最需要把材质没生效和确认框根本没弹出来这两件事彻底分清,因为两边该怎么收场完全不一样:

原生弹窗调用流程:材质降级与展示错误分别处理

图12 调用流程

具体到代码里,我是这么分开对待的:

  • 要是连确认框都没能弹出来——比如当前拿不到前台 UIContext,或者调 showAlertDialog 时直接抛了系统异常,我就立刻通过 result.error 把错误抛回给 Dart。毕竟要是这时候我不报错,页面说不定还误以为用户已经点过确认、直接把刚调好的画面给重置了,那肯定不行。
  • 反过来,如果只是当前机器不支持沉浸材质,弹窗本身明明还能用,那我就让插件退一步,照常弹出系统普通确认框让用户做选择。

3.4 在 Flutter 页面中调用

示例页面把亮度保存在自己的 State 中,插件负责展示确认框和返回选择。下面是实际 Example 中的完整 _show 与 _label 方法,我在片段中补了中文注释;它们使用页面已有的 _busy、_brightness 和 _receipt 状态。完整可运行页面和宿主配置保存在扩展源码的 example 中。

  Future<void> _show(OhosAlertMaterialPolicy policy) async {
    // 上一次确认框仍在等待时,不重复发起。
    if (_busy) return;
    setState(() {
      _busy = true;
      _receipt = '${_label(policy)}:等待原生弹窗选择';
    });
    debugPrint('AlertMaterialExample request policy=${policy.name}');
    try {
      final result = await FlutterPlatformAlert.showCustomAlert(
        windowTitle: '放弃预览调整?',
        text: '当前亮度调整尚未保留。选择继续编辑可返回预览;放弃后恢复为 50% 亮度。示例不会读取或删除文件。',
        positiveButtonTitle: '放弃调整',
        negativeButtonTitle: '继续编辑',
        options: PlatformAlertOptions(
          // 本次策略只作用于当前鸿蒙弹窗。
          ohos: OhosAlertOptions(systemMaterial: policy),
        ),
      );
      debugPrint(
        'AlertMaterialExample result policy=${policy.name} button=${result.name}',
      );
      if (!mounted) return;
      setState(() {
        if (result == CustomButton.positiveButton) {
          // 用户确认放弃后,恢复到示例约定的 50%。
          _brightness = .5;
          _receipt = '${_label(policy)}:已恢复样图亮度';
        } else {
          _receipt = '${_label(policy)}:继续编辑,保留当前调整';
        }
      });
    } catch (error) {
      debugPrint('AlertMaterialExample error policy=${policy.name} $error');
      if (mounted) {
        setState(() => _receipt = '弹窗未完成:$error');
      }
    } finally {
      // 成功或失败都恢复操作状态。
      if (mounted) setState(() => _busy = false);
    }
  }

  String _label(OhosAlertMaterialPolicy policy) {
    switch (policy) {
      case OhosAlertMaterialPolicy.systemDefault:
        return '系统默认';
      case OhosAlertMaterialPolicy.disabled:
        return '关闭此框材质';
      case OhosAlertMaterialPolicy.immersiveThick:
        return '系统 THICK';
    }
  }

截图中早期示例把 50% 写成“初始亮度”,这里已将提示修正为“恢复为 50% 亮度”。它是示例约定的重置值,启动值仍为 65%,本次只澄清文案,没有改变处理逻辑。

我用 _busy 防止弹窗尚未处理完时重复发起请求。只有 positiveButton 会把亮度重设为 0.5;选择继续编辑或发生异常时,页面保留当前亮度。示例启动值为 0.65,具体保留值还要看用户是否移动了滑块。

四、踩坑与边界条件

4.1 宿主开关与单框策略的双层边界

最初接入时,我只设置了 new uiMaterial.ImmersiveMaterial({ style: uiMaterial.ImmersiveStyle.THICK })。模拟器中的背板出现了半透明效果,但按住按钮滑动时没有看到光晕。

对照官方开启规则和本轮模拟器结果,我把需要检查的配置分成宿主模块开关和组件实例参数两部分。表 4 还记录了构建时遇到的路径与 SDK 问题:

排查现象真实原因正确处理方式
传了 immersiveThick,但按压按钮没有触摸光晕宿主 module.json5 未声明开启 UIMaterial.state,或组件未配 lightEffect宿主 metadata 声明 "ohos.arkui.UIMaterial.state": "enable",插件内部一并配上 interactive 与 lightEffect
开启宿主 UIMaterial.state 后,部分弹窗不想透色发光系统默认外观可能包含材质效果请求 OhosAlertMaterialPolicy.disabled;支持时注入 Material.empty
直接在含中文路径的目录编译 HAP 报 00306003当前版本 Hvigor 工具链对工程路径字符集有限制我在构建前先把工程复制到纯英文 ASCII 路径下,再执行 flutter build hap
用旧版 HarmonyOS SDK 编译插件报错找不到 uiMaterial@ohos.arkui.uiMaterial 声明自 API 26 引入编译环境需使用 HarmonyOS SDK API 26;代码中 sdkApiVersion < 26 仅做运行时降级保护

表4 排查要点

4.2 原生光感材质与自绘模糊的区别

Flutter 可以通过 Dialog 和 BackdropFilter 绘制模糊背景。本例中,底下的几何样图由 Flutter 绘制,确认框则由鸿蒙 ArkUI 创建。模拟器中请求 ImmersiveMaterial 后,我观察到以下效果:

  • 背板不仅有模糊,还带着材质特有的透色层次。
  • 弹窗外沿有系统渲染的细微亮边,我按住胶囊按钮时按钮还会有轻微的膨胀形变。
  • 手指滑动时,lightEffect 会在触点周围实时渲染出流动的白色光晕。

单独的高斯模糊只负责背景模糊;若用自绘方式重现边缘和触摸反馈,还需要另外实现相应交互。这里我选择让系统原生组件处理这些效果。

示例使用的是 ArkUI 原生确认框,具体的字号缩放、深浅色和无障碍表现仍需在目标设备上单独验证;本轮截图不能证明这些场景都已覆盖。

4.3 已测范围与未验证条件

本轮的运行结果来自表 3 所列的 HarmonyOS 7 / API 26 模拟器:我检查了三种材质策略、按钮返回以及示例页面的亮度变化。自动化部分包括 6 项 Dart 测试和 17 项原生转译契约检查;后者运行在主机,系统 API 使用替身,不等同于在设备上执行了 17 项测试。

旧版本系统加载含 API 26 静态导入的产物是否成功,当前没有设备验证。代码里的 sdkApiVersion < 26 分支只是运行时保护,不能单独证明模块在低版本上一定能装载。实体设备上的光感强度、字号与无障碍表现也未在本轮核验,接入时应按目标设备验证。

五、打包交付与复用建议

5.1 整理并发布可复用的第三方 Package

我已把扩展包和示例放到文首的个人源码仓库,本文以固定提交 5034fdca717f18136f3c0ba11342b45a4a403b68 为准。社区仓库提供了基础鸿蒙适配;本次新增的单框策略和 Example 可以在个人仓库中查看。工程包含以下内容:

  • 第一块是插件本体:包括公开的 Dart 入口和 OhosAlertOptions(放在 lib/ 下)、HarmonyOS 7 材质解析与降级逻辑(放在 ohos/ 下),还有原包自带的其他平台代码与 MIT LICENSE 许可文件。
  • 第二块是来源说明与分层补丁:我把社区原有的基础鸿蒙适配补丁、本轮适配 Flutter 3.41.9 窗口接口的 framework-3419-compat.patch,以及这次核心的单弹窗材质补丁 single-dialog-material.patch 全都分开存好了。后面不管是维护者审代码,还是我往上流或 oh-Flutter 提 PR,一眼就能看清哪一行是谁改的。
  • 第三块是示例与自动化测试:example/lib/material_policy_example.dart 是本轮插件示例页面;test/ohos_material_options_test.dart 和 native_material_contract_test.cjs 分别检查 Dart 选项和原生分发契约。图 1 来自华为官方文档,并非该示例运行画面。

5.2 宿主接入配置与构建验收步骤

在自己的项目里,我可以先在 pubspec.yaml 中使用固定提交接入扩展包,避免分支后续更新影响复现:

dependencies:
  flutter_platform_alert:
    git:
      url: https://atomgit.com/abaoge/flutter_platform_alert.git
      ref: 5034fdca717f18136f3c0ba11342b45a4a403b68

执行 flutter pub get 后,业务页面就能使用本次扩展的 Dart 接口。想运行本文 Example 的话,我会先把源码拉到纯英文路径,再切到相同提交:

git clone https://atomgit.com/abaoge/flutter_platform_alert.git
cd flutter_platform_alert
git checkout 5034fdca717f18136f3c0ba11342b45a4a403b68

Example 使用相对路径依赖当前源码;下文构建命令中的 -t lib/material_policy_example.dart 用于选择本文页面。若需要运行仓库中的三种自动采集入口,可在 example/ 目录按需执行 bash build_material.sh systemDefault、bash build_material.sh disabled 或 bash build_material.sh immersiveThick。

如果大家想在自己的 Flutter 鸿蒙项目里接上这套单弹窗材质,照着我下面这三步走就行,当然前提是你的编译和运行环境得满足前面列出来的 API 26 与材质支持条件。

第一步:先打开宿主模块的光感元数据开关。

我在示例宿主的 ohos/entry/src/main/module.json5 里加了一段 metadata 配置。你往自己业务工程里接的时候,同样可以在对应的 ability 或 module 节点下把 ohos.arkui.UIMaterial.state 设为 enable:

"metadata": [
  {
    "name": "ohos.arkui.UIMaterial.state",
    "value": "enable"
  }
]

第二步:在业务页面里按弹窗用途挑材质。

  • 碰到普通的系统提醒框,你照旧调原来的 FlutterPlatformAlert.showAlert 或 showCustomAlert 就好,不用多传参数。
  • 到了图像预览、海报编辑这类底下铺着丰富色彩的页面,想让确认框透出背景并在按压按钮时亮起光晕,就像我一样传 OhosAlertMaterialPolicy.immersiveThick。
  • 反过来要是碰上文字一大段的规则说明或严肃确认框,怕背景透色干扰阅读,直接传一个 OhosAlertMaterialPolicy.disabled 就能单独把这一框的透色关掉。

第三步:放到纯英文路径下跑测试和打 HAP 包。

本轮工具链在中文目录构建时返回了表 4 中的错误。我把工程复制到 ASCII 路径,配置 Flutter 3.41.9+ohos 和 DevEco Studio(API 26 SDK)环境变量,再从包根目录执行:

flutter pub get
flutter test test/ohos_material_options_test.dart
flutter analyze lib test
cd example
flutter pub get
flutter build hap --debug --no-codesign -t lib/material_policy_example.dart

打出无签名的 debug HAP 包之后,我直接把它装进 API 26 模拟器里跑验收。你如果想要在本地复现的时候,也可以按我平时检查的这四步过一遍:

  1. 先挨个点一遍页面底部的“系统默认”“关闭此框材质”和“系统 THICK”三个按钮,肉眼比对一下确认框背板的透色差别。
  2. 在开启 THICK 的弹窗上按住底部按钮左右来回滑动(注意别滑出按钮边界),看看有没有一团白色光晕跟着指尖流动。
  3. 将滑块调到 65%,选择“继续编辑”,核对亮度是否保留。
  4. 再次弹框并选择“放弃调整”,确认样图亮度恢复到示例约定的 50%。

这几步可以复现本轮模拟器的材质与按钮检查。结果只对应当前系统和设备;不支持材质的环境会走回退路径,其他环境还需要单独核验。

小结

回过头看,我做这次适配的起因其实特别简单,就是看中了 HarmonyOS 7 官方演示里那个跟着手指走的光晕按钮,想把它搬进自己的 Flutter 图像预览页里。一路查下来,我发现社区已经在 flutter_platform_alert 里帮我们把鸿蒙原生弹窗的底子搭好了,真正缺的只是 API 26 新出的这层 uiMaterial 配置。所以我决定沿用原包,只在 Dart 侧通过 OhosAlertMaterialPolicy 给页面留了一个干净的策略开关,把厚材质、按压形变和白色光晕的具体参数全收拢在原生插件里组装。

参考资料:

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐