AI工具 码道 推荐: https://developer.huaweicloud.com/codeartsco.html?source=dmzntgwatomgit1&sourcead=dmzntgwatomgiths
欢迎加入 CPF-Flutter 鸿蒙社区: https://atomgit.com/CPF-Flutter
本文配套仓库: https://atomgit.com/oh-flutter/holding-harmony
摘要:本文介绍 Flutter 触觉反馈插件 Gaimon 在 OpenHarmony 平台上的适配与使用。文章从 Gaimon 的核心功能出发,讲解如何使用 FVM 管理鸿蒙 Flutter SDK、创建 OpenHarmony 平台目录、添加 Gaimon 依赖并配置震动权限,最后通过 DevEco Studio 在真实设备上运行验证,帮助开发者快速将触觉反馈能力接入鸿蒙应用。
关键词:Flutter、Gaimon、OpenHarmony、触觉反馈、鸿蒙适配
前言
在 Flutter 应用开发中,按钮点击、表单提交、操作成功、风险提醒和错误提示,除了通过文字、颜色或弹窗向用户传递信息之外,还可以通过触觉反馈让用户更快感知当前状态。合理的触觉反馈能够让操作结果更加明确,也能提升移动应用的交互体验。尤其是在用户没有持续注视屏幕,或者应用需要快速反馈时,震动是一种直接而有效的提示方式。
Gaimon 是一个用于实现触觉反馈的 Flutter 三方库。它将不同平台上的触觉能力封装成统一的 Dart API,开发者只需要在 Flutter 代码中调用 `Gaimon.selection()`、`Gaimon.success()` 或 `Gaimon.error()` 等方法,就可以为应用增加对应的反馈效果,不需要分别编写 Android、iOS 和 OpenHarmony 的原生代码。对于需要同时支持多个移动平台的项目来说,这种统一的调用方式可以减少平台判断和重复开发,让业务代码更加清晰。
除了常见的预置反馈之外,Gaimon 还支持不同强度的触觉效果、成功/警告/错误等状态反馈,以及基于 AHAP 文件和自定义波形的复杂触觉模式。开发者可以根据具体业务选择合适的效果:列表选择可以使用轻微的 Selection 反馈,提交成功可以使用 Success 反馈,危险操作可以使用 Warning 反馈,提交失败可以使用 Error 反馈;如果预置效果不能满足需求,还可以通过 `.ahap` 文件或 `timings`、`amplitudes` 参数构造更有节奏感的自定义震动。不过,原始 Gaimon 插件主要面向 Android 和 iOS 平台,OpenHarmony 项目无法直接使用。为了让 Flutter 应用能够在鸿蒙设备上复用这套触觉反馈能力,本文对 Gaimon 进行了 OpenHarmony 平台适配,在尽量保持原有 Dart 公共 API 不变的前提下,补充鸿蒙侧的插件实现,并完成设备能力检测、预置反馈、自定义模式、波形播放和停止播放等功能的验证。
本文将从一个实际的 Flutter 示例项目开始,介绍如何使用 FVM 管理适配鸿蒙的 Flutter SDK,创建 OpenHarmony 平台目录,添加 Gaimon 三方库依赖,配置鸿蒙震动权限,并通过 DevEco Studio 在真实设备上运行验证。读者可以按照文章步骤完成环境配置,也可以直接参考完整演示页面,将 Gaimon 接入到自己的按钮、表单、列表和业务状态处理中。
一、Gaimon 鸿蒙适配效果展示
1.1 能力概览
Gaimon 是一个 Flutter 触觉反馈插件,原始版本主要支持 Android 和 iOS。经过 OpenHarmony 适配后,Flutter 应用可以继续使用原有 Dart API,在鸿蒙设备上完成触觉反馈。本文示例按 OpenHarmony API 12 及以上环境进行验证,API 12 以下版本不纳入本文讨论范围。
支持以下功能:
- 设备触觉能力检测:通过 Gaimon.canSupportsHaptic 判断当前设备是否支持触觉反馈。
- 预置触觉反馈:提供 Selection、Light、Medium、Heavy、Rigid 和 Soft 六种基础效果。
- 状态反馈:提供 Success、Warning 和 Error 三种业务状态效果。
- AHAP 自定义触觉模式:通过 .ahap 文件描述有节奏的触觉效果。
- 自定义震动波形:通过 timings、amplitudes 和 repeat 组合播放波形。
- 停止当前震动:通过 Gaimon.stop() 取消正在播放的反馈。
本文介绍如何使用 Flutter 插件 Gaimon 。
1.2 三方平台支持矩阵对比
下表横向对比 Gaimon 在 Android、iOS 和 OpenHarmony 三个平台上的功能支持情况,便于开发者评估适配后的能力边界:
| 功能 | Android | iOS | OpenHarmony | 差异说明 |
|---|---|---|---|---|
| 设备能力检测 | 支持 | 支持 | 支持 | 三个平台均可通过Gaimon.canSupportsHaptic检测设备是否支持触觉反馈。 |
| 预置反馈 | 支持 | 支持 | 支持 | Android 与 iOS 原生支持selection、light、medium、heavy、rigid、soft六种反馈;OpenHarmony 端经适配后同样支持。 |
| 状态反馈 | 支持 | 支持 | 支持 | 三个平台均支持success、warning、error三种业务状态反馈。 |
| AHAP 自定义模式 | 支持 | 支持 | 部分支持 | Android 与 iOS 可完整解析并播放 AHAP 文件;OpenHarmony 端通过Gaimon.patternFromData播放,复杂节奏的还原程度以当前适配实现为准。 |
| 自定义波形 | 支持 | 支持 | 部分支持 | 三个平台均支持timings、amplitudes参数构造波形;OpenHarmony 端按适配后的分段逻辑播放,repeat的具体播放次数以当前适配实现为准。 |
| 停止播放 | 支持 | 支持 | 支持 | 三个平台均可通过Gaimon.stop()取消正在播放的触觉反馈。 |
二、使用FVM创建项目
2.1 用FVM切换SDK
使用fvm flutter create gaimon_demo新建一个名为gaimon_demo的文件夹。
如果使用的是 OHOS Flutter SDK,项目还应包含或能够生成ohos/目录,但是我这里没有,因为没使用适配了鸿蒙的 Flutter SDK。
cd /d E:\gaimon_demo(进入 gaimon_demo 这个目录) fvm use 3.41.10-ohos-1.0.1 --force(使用 fvm 切换 3.41.10 版本的 Flutter SDK) flutter version(查看 Flutter SDK 的版本) fvm flutter create --platforms=ohos .(创建 ohos 鸿蒙平台)执行完看到已经创建了鸿蒙的目录。
三、添加gaimon的依赖
3.1 配置pubspec.yaml依赖
我这里使用的是 VSCode,使用 Android Studio 也是可以的。在pubspec.yaml添加下方依赖:
gaimon: git: url: https://atomgit.com/Deng666/fluttertpc_gaimon.git ref: 1.5.0-ohos-1.0.0使用fvm flutter pub get同步依赖
3.2 完整演示页面
在 lib/main.dart 中添加这一行代码:
import 'package:gaimon/gaimon.dart';把main.dart的计数器案例删除了,代码如下:
import 'package:flutter/material.dart'; import 'package:gaimon/gaimon.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( home: Scaffold(), ); } }代码讲解:
- 设备触觉能力检测。应用启动后可以通过 `Gaimon.canSupportsHaptic` 检测当前设备是否支持触觉反馈,并根据检测结果决定是否启用页面中的播放按钮。这样可以避免在不支持触觉反馈的设备上直接调用接口,也能让使用者清楚地看到当前插件是否已经完成注册。
- 预置触觉反馈。插件提供 `selection`、`light`、`medium`、`heavy`、`rigid` 和 `soft` 六种基础反馈,分别对应选择操作、轻度反馈、中度反馈、重度反馈、刚性反馈和柔和反馈。开发者可以根据交互的重要程度选择合适的反馈,例如列表选择使用 `selection`,普通操作使用 `light` 或 `medium`,重要确认使用 `heavy` 或 `rigid`。
- 业务状态反馈。插件还提供 `success`、`warning` 和 `error` 三种状态反馈,适合放在表单提交、保存数据、删除确认、风险提醒和操作失败等业务流程中。这样用户不仅能通过页面文字看到操作结果,还能通过触觉获得即时反馈,尤其适合移动端和鸿蒙设备上的高频交互场景。
- AHAP 自定义触觉模式。应用可以把 `.ahap` 文件作为 Flutter 资源加载,再通过 `Gaimon.patternFromData` 播放自定义触觉效果。本示例准备了 Heartbeat、Rumble、Gravel 和 Inflate 四种模式,用来展示心跳、持续震动、颗粒感震动和逐渐增强等不同效果。相较于单次预置反馈,AHAP 更适合表达有节奏、有变化的复杂触觉图案。
- 手动构造震动波形。开发者也可以直接传入 `timings`、`amplitudes` 和 `repeat` 参数调用 `Gaimon.patternFromWaveForm`,自行组合等待、震动、停顿和再次震动等阶段。本示例包含单次成功波形、双脉冲波形和重复波形,便于理解震动时间与强度之间的关系。OpenHarmony 端会按照适配后的分段逻辑播放波形,`repeat` 的具体播放次数以当前适配实现为准。
- 停止当前反馈。通过 `Gaimon.stop()` 可以取消正在播放的触觉反馈,适合页面退出、任务取消或用户主动停止播放等场景。这个接口能够和自定义波形、重复播放配合使用,让业务层拥有更完整的播放控制能力。
import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:gaimon/gaimon.dart'; void main() { runApp(const GaimonDemoApp()); } class GaimonDemoApp extends StatelessWidget { const GaimonDemoApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, title: 'Gaimon Demo', theme: ThemeData( colorScheme: ColorScheme.fromSeed( seedColor: Colors.teal, ), useMaterial3: true, ), home: const GaimonPage(), ); } } class GaimonPage extends StatefulWidget { const GaimonPage({super.key}); @override State<GaimonPage> createState() => _GaimonPageState(); } class _GaimonPageState extends State<GaimonPage> { bool? _supported; bool _loading = false; String _lastAction = '暂无操作'; bool get _enabled => _supported == true && !_loading; @override void initState() { super.initState(); _checkSupport(); } Future<void> _checkSupport() async { setState(() { _supported = null; _lastAction = '正在检测设备是否支持触觉反馈...'; }); try { final supported = await Gaimon.canSupportsHaptic; if (!mounted) return; setState(() { _supported = supported; _lastAction = supported ? '设备支持触觉反馈' : '设备不支持触觉反馈'; }); } catch (_) { if (!mounted) return; setState(() { _supported = false; _lastAction = '检测失败,请确认插件是否注册'; }); } } void _run(String name, VoidCallback action) { if (!_enabled) return; setState(() { _loading = true; _lastAction = '正在执行:$name'; }); try { // Gaimon 的这些方法返回 void,不需要 await。 action(); if (!mounted) return; setState(() { _loading = false; _lastAction = '$name 执行完成'; }); } catch (_) { if (!mounted) return; setState(() { _loading = false; _lastAction = '$name 执行失败'; }); } } Future<void> _playAhap(String name, String asset) async { if (!_enabled) return; setState(() { _loading = true; _lastAction = '正在播放:$name'; }); try { final data = await rootBundle.loadString(asset); Gaimon.patternFromData(data); if (!mounted) return; setState(() { _loading = false; _lastAction = '$name 播放完成'; }); } catch (_) { if (!mounted) return; setState(() { _loading = false; _lastAction = '$name 播放失败'; }); } } void _playWaveform( String name, List<int> timings, List<int> amplitudes, bool repeat, ) { _run( name, () => Gaimon.patternFromWaveForm( timings, amplitudes, repeat, ), ); } void _stop() { Gaimon.stop(); setState(() { _loading = false; _lastAction = '已停止当前震动'; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Gaimon 触觉反馈演示'), actions: [ IconButton( tooltip: '重新检测设备能力', onPressed: _loading ? null : _checkSupport, icon: const Icon(Icons.refresh), ), ], ), body: ListView( padding: const EdgeInsets.all(16), children: [ _buildStatusCard(), const SizedBox(height: 20), _buildSectionTitle('一、设备能力检测'), _buildInfoCard( 'Gaimon.canSupportsHaptic', '检测当前设备是否支持触觉反馈。', ), const SizedBox(height: 20), _buildSectionTitle('二、预置触觉反馈'), _buildPreset( 'Selection', '轻微的选择反馈,适合列表选择、按钮点击。', 'Gaimon.selection()', Icons.touch_app, Gaimon.selection, ), _buildPreset( 'Light', '轻度震动,适合轻量级操作提示。', 'Gaimon.light()', Icons.blur_on, Gaimon.light, ), _buildPreset( 'Medium', '中等强度震动,适合普通操作反馈。', 'Gaimon.medium()', Icons.radio_button_checked, Gaimon.medium, ), _buildPreset( 'Heavy', '较强震动,适合重要操作或强提醒。', 'Gaimon.heavy()', Icons.vibration, Gaimon.heavy, ), _buildPreset( 'Rigid', '刚性感较强的反馈,触感更加明确。', 'Gaimon.rigid()', Icons.crop_square, Gaimon.rigid, ), _buildPreset( 'Soft', '柔和的反馈,适合不希望过于明显的提示。', 'Gaimon.soft()', Icons.circle_outlined, Gaimon.soft, ), _buildPreset( 'Success', '成功反馈,例如提交成功、保存成功。', 'Gaimon.success()', Icons.check_circle_outline, Gaimon.success, ), _buildPreset( 'Warning', '警告反馈,例如删除前提醒、风险提示。', 'Gaimon.warning()', Icons.warning_amber, Gaimon.warning, ), _buildPreset( 'Error', '错误反馈,例如提交失败、操作失败。', 'Gaimon.error()', Icons.error_outline, Gaimon.error, ), const SizedBox(height: 20), _buildSectionTitle('三、AHAP 自定义模式'), _buildAhap( 'Heartbeat', '心跳效果,读取 heartbeats.ahap 播放。', 'assets/haptics/heartbeats.ahap', ), _buildAhap( 'Rumble', '持续震动效果,读取 rumble.ahap 播放。', 'assets/haptics/rumble.ahap', ), _buildAhap( 'Gravel', '颗粒感震动效果,读取 gravel.ahap 播放。', 'assets/haptics/gravel.ahap', ), _buildAhap( 'Inflate', '逐渐增强的震动效果,读取 inflate.ahap 播放。', 'assets/haptics/inflate.ahap', ), const SizedBox(height: 20), _buildSectionTitle('四、手动构造波形'), _buildWaveform( '单次成功波形', '先等待,再轻微震动,最后强震动。', [0, 55, 55, 53], [0, 178, 0, 255], false, ), _buildWaveform( '双脉冲波形', '连续产生两次震动。', [0, 40, 80, 40, 80], [0, 255, 0, 180, 0], false, ), _buildWaveform( '重复波形', '重复播放当前波形。鸿蒙适配中会播放有限次数。', [0, 35, 45, 35], [0, 200, 0, 160], true, ), const SizedBox(height: 20), Card( child: ListTile( leading: const Icon(Icons.stop_circle_outlined), title: const Text('停止当前震动'), subtitle: const Text('调用 Gaimon.stop() 停止正在播放的反馈。'), trailing: FilledButton( onPressed: _supported == true ? _stop : null, child: const Text('停止'), ), ), ), ], ), ); } Widget _buildStatusCard() { final checking = _supported == null; final supported = _supported == true; final title = checking ? '正在检测设备能力' : supported ? '设备支持触觉反馈' : '设备不支持触觉反馈'; return Card( color: supported ? Colors.green.shade50 : null, child: ListTile( leading: Icon( checking ? Icons.sync : supported ? Icons.check_circle : Icons.info_outline, color: supported ? Colors.green : null, ), title: Text(title), subtitle: Text('最近操作:$_lastAction'), ), ); } Widget _buildSectionTitle(String title) { return Padding( padding: const EdgeInsets.only(bottom: 8), child: Text( title, style: const TextStyle( fontSize: 19, fontWeight: FontWeight.bold, ), ), ); } Widget _buildInfoCard(String api, String description) { return Card( child: ListTile( leading: const Icon(Icons.info_outline), title: Text(api), subtitle: Text(description), ), ); } Widget _buildPreset( String name, String description, String api, IconData icon, VoidCallback action, ) { return Card( child: ListTile( leading: Icon(icon), title: Text(name), subtitle: Text('$description\n$api'), isThreeLine: true, trailing: FilledButton( onPressed: _enabled ? () => _run(name, action) : null, child: const Text('播放'), ), ), ); } Widget _buildAhap( String name, String description, String asset, ) { return Card( child: ListTile( leading: const Icon(Icons.audio_file), title: Text(name), subtitle: Text('$description\npatternFromData()'), isThreeLine: true, trailing: FilledButton( onPressed: _enabled ? () => _playAhap(name, asset) : null, child: const Text('播放'), ), ), ); } Widget _buildWaveform( String name, String description, List<int> timings, List<int> amplitudes, bool repeat, ) { return Card( child: ListTile( leading: const Icon(Icons.waves), title: Text(name), subtitle: Text( '$description\n' 'timings: $timings\n' 'amplitudes: $amplitudes\n' 'repeat: $repeat', ), isThreeLine: true, trailing: FilledButton( onPressed: _enabled ? () => _playWaveform( name, timings, amplitudes, repeat, ) : null, child: const Text('播放'), ), ), ); } }本文的示例页面会把以上能力集中到一个界面中,每个按钮都对应一个实际的 Gaimon API。用户可以先查看设备能力检测结果,再依次体验预置反馈、AHAP 模式和手动波形,最后使用停止按钮结束当前反馈。这样既能直观看到适配后的运行效果,也能作为后续接入真实业务的参考页面。
四、运行项目
4.1 配置权限
使用 DevEco Studio 打开我们的gaimon_demo这个项目,选择 ohos 目录,并且在ohos/entry/src/main/module.json5加上震动权限,点击右上角的运行。Gaimon 的触觉反馈核心权限是 ohos.permission.VIBRATE,网络权限不是触觉反馈本身必需的权限。
"requestPermissions": [ {"name": "ohos.permission.INTERNET"}, { "name": "ohos.permission.VIBRATE", "reason": "$string:vibrate_reason", "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } } ]4.2 真机运行与效果验证
运行出来的效果如下:
五、适配原理与踩坑记录
本次适配的目标不是重新设计一套鸿蒙专用 API,而是在尽量保持 Gaimon 原有 Dart 调用方式不变的前提下,补充 OpenHarmony 原生实现。业务层仍然只需要调用 `Gaimon.selection()`、`Gaimon.success()`、`Gaimon.patternFromData()` 等方法,平台差异由插件内部处理。
适配原理。Dart 层通过名为 `gaimon` 的 `MethodChannel` 与原生侧通信。在 `lib/gaimon.dart` 中,OpenHarmony 会走原生触觉实现,避免把不同强度的反馈都降级成 Flutter 通用的 `HapticFeedback`。鸿蒙侧的实现位于 `ohos/src/main/ets/components/plugin/GaimonPlugin.ets`,通过 `@kit.SensorServiceKit` 的 `vibrator` 调用系统震动能力,并对预置反馈、状态反馈、波形播放和停止操作进行分发。
API 12 的实现方式。** API 12 不使用 API 18 才提供的 `VibratorPatternBuilder`,否则项目在最低 API 12 的兼容配置下会产生编译或运行兼容问题。因此,适配代码采用基础 preset/time 接口:将波形拆成多个时间片,通过定时器依次开始和停止每一段震动。AHAP 文件也不是在鸿蒙端直接交给 iOS 的 Core Haptics 播放,而是先由 Dart 层的 `ahapToWaveform` 转换为 `timings` 和 `amplitudes`,再交给鸿蒙侧分段执行。
这次适配中实际遇到的注意点:
- - API 12 不能依赖 API 18 的高级波形构造接口,所以最终采用基础接口加定时分段的兼容方案。这会让复杂 AHAP 图案变成近似效果,锐度、攻击、衰减和音频事件不能与 iOS 完全一致。Dart 层的波形强度沿用 Android 常见的 `0–255` 范围,鸿蒙侧需要换算成 `0–100`。强度为 `0` 的片段只表示停顿,不能当作一次有效震动处理。
- repeat=true` 在当前鸿蒙实现中固定循环 3 次,并不是无限循环。这样可以避免 API 12 环境下出现无法停止的持续震动;需要更复杂的循环策略时,应由业务层控制播放次数。
- 新的播放请求或调用 `Gaimon.stop()` 时,不仅要调用系统的 `stopVibration()`,还要取消之前尚未执行的定时任务。否则旧波形的定时回调可能在页面切换后继续触发。当前实现通过定时器列表和播放代数标记处理了这个问题。 - 预置效果在部分设备上可能不支持。实现中优先尝试 preset,调用失败后回退到按时间震动;设备关闭系统触感或硬件不支持时,仍可能没有明显效果。
- - `ohos.permission.VIBRATE` 必须配置在宿主应用的 `ohos/entry/src/main/module.json5` 中,并在资源文件中提供 `vibrate_reason`。插件自身的 HAR 模块不会替宿主应用申请权限。
- -插件注册器应由适配鸿蒙的 Flutter SDK 在执行 `fvm flutter pub get` 后生成,不要手动修改 `GeneratedPluginRegistrant.ets`。如果生成文件中没有 `GaimonPlugin`,优先检查是否误用了普通 Flutter SDK版本说明。 仓库中的 `1.5.0-ohos-1.0.0` 是较早的 OpenHarmony 适配标签,不包含后续 API 12 最低版本适配;本文使用的 `main` 分支包含 API 12 适配提交。若后续为 API 12 适配创建新的稳定标签,应将依赖中的 `ref` 替换为对应标签。
本次适配最终保留了 Gaimon 原有的 Dart API,业务代码不需要针对鸿蒙再写一套调用逻辑。实际使用时,基础反馈和状态反馈可以直接接入按钮、列表和表单;AHAP 与自定义波形则应在目标鸿蒙设备上进行真机验证,因为它们在 API 12 上属于基于基础震动接口的近似实现。
六、总结
本文围绕 Flutter 触觉反馈插件 Gaimon 的 OpenHarmony 适配展开,完整走通了从环境准备到真机验证的流程。关键步骤可以概括为:使用 FVM 管理适配鸿蒙的 Flutter SDK,通过fvm flutter create --platforms=ohos .生成 OpenHarmony 平台目录,在 pubspec.yaml 中引入 Gaimon 的鸿蒙适配版本依赖,并在ohos/entry/src/main/module.json5中配置震动权限,最后使用 DevEco Studio 在真实设备上运行验证。
从功能支持情况来看,设备能力检测、预置反馈、状态反馈和停止播放等核心能力在 OpenHarmony 上均已完整支持;AHAP 自定义模式和自定义波形目前为部分支持,复杂节奏的还原程度以及repeat的具体播放次数仍以当前适配实现为准。整体上,Gaimon 的鸿蒙适配已经能够覆盖大多数常见触觉反馈场景,开发者可以直接将其接入按钮、表单、列表和业务状态处理中。
使用过程中需要注意以下几点:一是务必使用适配了鸿蒙的 Flutter SDK,否则项目无法生成ohos/目录;二是不要遗漏震动权限配置,否则真机运行时无法触发触觉反馈;三是对于 AHAP 文件和自定义波形,建议在目标设备上提前验证实际效果,避免复杂节奏或重复播放与预期存在差异。
后续可以从两个方向继续优化:一方面完善 AHAP 解析能力,提升复杂触觉图案在 OpenHarmony 上的还原度;另一方面支持更多自定义参数,例如更精细的强度控制、更灵活的重复策略以及更丰富的波形组合方式,让鸿蒙端的触觉反馈能力与 Android、iOS 进一步对齐。