React Native for OpenHarmony 实战:react-native-pulsar 触觉反馈库鸿蒙化适配指南

这篇记录把 react-native-pulsar 适配到开源鸿蒙的完整过程,代码在 HarmonyOS 7.0.0(26) 模拟器上跑过,文中代码、日志、截图均来自实际适配过程

适配结论速览

  • 适配方法:19 个 TurboModule 方法,12 个直接映射,4 个核心映射,4 个按平台能力边界降级
  • 预设覆盖:197 个全量建表,46 个 System 家族精确标定,151 个命名语义近似
  • 依赖处理:移除 react-native-worklets 依赖,高层 hook 在鸿蒙可用
  • 验证方式:模拟器跑通 JS → TurboModule → vibrator 服务全链路,hilog 可证
  • 产出物:example HAP、独立适配库 HAR、中英双版使用文档

一、为什么选这个库

RN OHOS 适配清单中 pulsar 长期处于待适配状态,热度 8.29,月下载 42 万+,属于高频库

机器人遥操场景需要触觉通道:操作员在平板上遥操,碰撞预警与动作确认依赖触觉传递。iOS 有 UIImpactFeedbackGenerator,Android 有 VibrationEffect,鸿蒙此前无对应能力

pulsar 分两层:预设覆盖常见反馈,PatternComposer 支持自定义模式。离散点适合档位切换提示,连续包络适合遥操力反馈

二、动手前先画映射表

适配前先将 19 个 TurboModule 方法与 @ohos.vibrator 的能力逐项对照。映射关系确定后,实现即为机械劳动

pulsar 方法组数量鸿蒙 vibrator 对应能力结论
Pulsar 控制组(play/enable/stop/support…)12startVibration + stopVibration + 状态管理直接映射
PatternComposer(parsePattern/play/stop/release)4VibratorPatternBuilder(API 18+)核心映射
RealtimeComposer(set/stop/isActive/playDiscrete)4公开 API 没有实时振幅流通道降级处理

VibratorPatternBuilder 自鸿蒙 API 18 开始提供 addContinuousEvent(time, duration, {intensity, frequency}) 和 addTransientEvent(time, {intensity, frequency}),intensity 与 frequency 均为 0-100,与 pulsar 的 (time, amplitude, frequency) 近似一一对应

适配调用链如下,后续章节按此顺序展开

在这里插入图片描述

三、工程结构

参照 CPF-RN 社区已适配库的结构,目录如下

react-native-pulsar/
├── src/specs/v1/NativeRNPulsar.ts   # 重建的 TurboModule 规范,原包只发 .d.ts
├── harmony/pulsar/                   # 鸿蒙 HAR 模块
│   ├── index.ets                     # 包入口,导出 PulsarPackage
│   ├── ts.ets                        # 类型入口,app 侧 import 用
│   └── src/main/
│       ├── cpp/                      # C++ 胶水层
│       │   ├── CMakeLists.txt
│       │   ├── PulsarPackage.h       # 继承 codegen 生成的 BaseReactNativePulsarPackage
│       │   └── generated/            # codegen-lib-harmony 生成
│       └── ets/
│           ├── PulsarPackage.ets     # RNOHPackage 子类
│           └── PulsarTurboModule.ts  # TurboModule 实现主体
└── package.json                      # harmony.autolinking 声明

两个注意点

一、TurboModule 规范需自行重建。pulsar 的 npm 包只发布编译产物 lib 与类型声明 lib/typescript/src/NativeRNPulsar.d.ts,而 RNOH codegen 需要 .ts 源文件。按 .d.ts 重建 19 个方法签名后,再用库级 codegen 生成胶水层

npx react-native codegen-lib-harmony --no-safety-check \
  --npm-package-name react-native-pulsar \
  --cpp-output-path ./harmony/pulsar/src/main/cpp/generated \
  --ets-output-path ./harmony/pulsar/src/main/ets/generated \
  --turbo-modules-spec-paths ./src/specs/v1/NativeRNPulsar.ts

必须使用库级 codegen-lib-harmony。使用 app 级生成的 RNOHGeneratedPackage 是通用类,与宿主 app 的同名类链接时冲突(redefinition);改用库级后生成带库名前缀的 BaseReactNativePulsarPackage,冲突消失

二、package.json 的 harmony 字段需按社区约定声明,app 侧自动链接才会识别

"harmony": {
  "alias": "react-native-pulsar",
  "autolinking": {
    "etsPackageClassName": "PulsarPackage",
    "cppPackageClassName": "PulsarPackage",
    "cmakeLibraryTargetName": "rnoh_pulsar",
    "ohPackageName": "@react-native-ohos/react-native-pulsar"
  }
}

四、核心实现,三层映射

4.1 预设,197 个全量标定

pulsar 预设分两类,System* 家族 46 个,impactLight/Medium/Heavy、notificationSuccess/Warning/Error 这些语义很明确,我按触觉设计惯例做了标定表,时长强度频率三个维度都给

命名预设 151 个,在原生侧是 pulsar 引擎合成的,鸿蒙没有等价引擎,我的做法也是全量建表,按名字语义给时长和强度,Alarm 给 500ms 强震,Chime 给 100ms 中震,Feather 这种就给 15ms 轻触,197 个预设全部建表。未命中时回落中等 impact 并打 warn 日志,不静默失败,便于定位

在这里插入图片描述

全量表在模拟器上验过命中,点 Buzz 按钮,hilog 中 vibrator 服务收到的时长为 120ms,与标定表一致

10-26 10:13:54.099 E C02701/.../VibratorServiceClient: in Vibrate Vibrate time failed, ret:-1, time:120, usage:7

4.2 PatternComposer,离散直映射加连续包络分段

这是核心算法,pulsar 的 Pattern 分两块,discretePattern 是离散点带 time/amplitude/frequency,continuousPattern 是振幅和频率两条折线

离散点直接映射 addTransientEvent,连续折线的处理是把相邻点切成段,段内取左端点强度,频率取中点插值

private buildVibrationEvents(pattern: Pattern) {
  const events = [];
  // 离散模式 → 瞬态事件,直映射
  for (const point of pattern.discretePattern ?? []) {
    events.push({
      time: Math.round(point.time),
      duration: 0,
      intensity: this.clamp100(point.amplitude),
      frequency: this.clamp100(point.frequency),
    });
  }
  // 连续包络 → 按时间轴切段
  const ampPoints = pattern.continuousPattern?.amplitude ?? [];
  const freqPoints = pattern.continuousPattern?.frequency ?? [];
  for (let i = 0; i < ampPoints.length - 1; i++) {
    const t0 = Math.round(ampPoints[i].time);
    const t1 = Math.round(ampPoints[i + 1].time);
    const duration = t1 - t0;
    if (duration <= 0) continue;
    const midTime = (t0 + t1) / 2;
    events.push({
      time: t0,
      duration: Math.min(duration, 5000), // OHOS 单段上限 5000ms
      intensity: this.clamp100(ampPoints[i].value),
      frequency: freqPoints.length > 0 ? this.clamp100(this.sampleAt(freqPoints, midTime)) : 50,
    });
  }
  events.sort((a, b) => a.time - b.time); // OHOS 要求事件升序
  return events;
}

三个实现细节:amplitude/frequency 在 pulsar 侧为 0-1、OHOS 侧为 0-100,clamp100 做量纲兼容;连续事件单段上限 5000ms,超长包络截断;事件数组必须按时间升序,乱序会被 vibrator 服务拒绝

4.3 RealtimeComposer,按平台能力边界处理

RealtimeComposer 的 4 个方法在鸿蒙公开 API 中无对应能力:@ohos.vibrator 没有流式设置当前振幅的通道。这属于平台能力差异。处理方式如下

  • RealtimeComposer_isActive() 恒返回 false
  • RealtimeComposer_set() 打 warn 说明不支持
  • RealtimeComposer_playDiscrete(amplitude, frequency) 降级成一次瞬态振动
  • Pulsar_hapticSupport() 返回 2 也就是 STANDARD_SUPPORT 而不是 3,让 JS 侧能感知到真实能力等级

五、集成进宿主应用

适配库最终落到宿主 app。载体为 RoboDeck 鸿蒙跨平台机器人调试台,库未上中心仓,采用社区标准的手动链接方式

app 的 build-profile.json5 声明模块,模块名取 module.json5 里的短名,不是包名

{
  "name": "pulsar",
  "srcPath": "../../../lib-analysis/pulsar-ohos/harmony/pulsar"
}

C++ 侧在 app 的 CMakeLists.txt 里 add_subdirectory 再链接

add_subdirectory("${OH_MODULES_DIR}/@react-native-ohos/react-native-pulsar/src/main/cpp" ./pulsar)
target_link_libraries(rnoh_app PUBLIC rnoh_pulsar)

然后 C++ 和 ArkTS 双侧注册,RNOH 是双包机制,缺一个都不行

// cpp/PackageProvider.cpp
packages.push_back(std::make_shared<PulsarPackage>(ctx));
// ets/PackageProvider.ets
import {PulsarPackage} from '@react-native-ohos/react-native-pulsar/ts';
export function getRNOHPackages(ctx: RNPackageContext): RNOHPackage[] {
  return [...createRNOHPackagesAutolinking(ctx), new PulsarPackage(ctx)];
}

另一个易错点:适配模块自身的 build-profile.json5 不要配置 externalNativeOptions,为模块配置 native 构建后,hvigor 会以模块为单位单独执行 CMake,此时拿不到完整 RNOH 上下文导致编译失败。正确做法是 C++ 完全由 app 构建时编译,模块只管 ArkTS

六、跑起来验证

在 HarmonyOS 7.0.0(26) 模拟器运行集成后的 RoboDeck,点击预设与自定义模式按钮,UI 日志与原生层日志一致

在这里插入图片描述
hilog 显示两条链路均完整:预设链路到达 VibratorServiceClient 的 Vibrate,PatternComposer 链路到达 PlayPattern,两者最终返回同一设备层错误 14600101
在这里插入图片描述14600101 是模拟器无振动硬件导致的设备层错误。JS → TurboModule → vibrator API 的调用链已完整验证,真机可正常执行振动,效果层待真机复验

除宿主集成外,源码仓提供独立 example 工程(RN 0.84.1 + RNOH 0.84.3,自动链接),产物为 HAP 与适配库 HAR

cd example/harmony
RNOH_C_API_ARCH=1 ./hvigorw assembleApp --no-daemon      # HAP
./hvigorw assembleHar -p module=pulsar --no-daemon       # 独立 pulsar.har

注意:未安装 @rnoh/hvigor-plugin 时,hvigor 的 BuildJS 任务为空跑,修改 App.tsx 后打包仍是旧 bundle(表现为界面不刷新),需手动生成 bundle

cd example
npx react-native bundle-harmony --dev false --entry-file index.js \
  --bundle-output harmony/entry/src/main/resources/rawfile/bundle.harmony.js

构建日志与产物如下

在这里插入图片描述
example 运行后实测 System 预设、命名预设、PatternComposer 三个区块的按钮,hook 链路同样可用
在这里插入图片描述点 Buzz 时 vibrator 服务收到的时长为 120ms,与标定表中 Buzz 的值一致,说明命中了标定表而非回落分支

10-26 10:13:54.099 E C02701/com.gewislab.pulsar.example/VibratorServiceClient:
  in Vibrate Vibrate time failed, ret:-1, time:120, usage:7

点 Buzz 时 vibrator 服务收到的时长为 120ms,与标定表中 Buzz 的值一致,说明命中了标定表而非回落分支

10-26 10:13:54.099 E C02701/com.gewislab.pulsar.example/VibratorServiceClient:
  in Vibrate Vibrate time failed, ret:-1, time:120, usage:7

另一个依赖问题:pulsar 的高层 hook(usePatternComposer 等)依赖 react-native-worklets,该包暂无鸿蒙移植,直接 import 运行时报 createSynchronizable of undefined

处理方式是将该依赖移除:useSharableState 改为 useRef 实现。pulsar 的 hook 只在同一 JS 线程内读写 patternId,没有跨线程共享需求,createSynchronizable 在鸿蒙侧是多余的。改用 ref 后 hook 恢复可用,example 中 usePatternComposer 的播放与停止均正常

结论:先查依赖树,再判断该依赖在鸿蒙场景下是否确有必要,确认多余就直接移除,比等上游适配快

七、几条经验

  1. 先画映射表再动手:19 个方法对 vibrator 能力,三类结论一页纸看清
  2. 库级 codegen 用 codegen-lib-harmony,和 app 级的产物命名空间不一样,混用会链接冲突
  3. 模块 build-profile 不配 native,C++ 交给 app 统一编译
  4. 量纲转换需兼容两种表示,0-1 与 0-100 在边界都要收敛
  5. 能力边界要写明:RealtimeComposer 无对应通道就明确标注不支持,hapticSupport 上报真实等级
  6. 先查依赖树,未适配的原生依赖会直接中断 hook 链路,需评估其在鸿蒙场景下是否确有必要,多余则移除

pulsar 已用于 RoboDeck 的遥操反馈模块,example 工程随源码仓一并提供,代码与文档均已提交社区

资源地址
适配源码仓库https://atomgit.com/lhwLHWjackL/react-native-pulsar
社区使用文档 PRhttps://atomgit.com/CPF-RN/usage-docs/merge_requests/853
适配状态与待维护者事项https://atomgit.com/CPF-RN/usage-docs/issues/828

待办:真机复验。模拟器无振动硬件,振幅与频率的实际表现需在真机上确认。


欢迎加入 CPF-RN 社区:https://atomgit.com/CPF-RN

本文为 AtomGit 开源鸿蒙跨平台技术征文参赛作品,所有代码与截图来自真实适配过程

Logo

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

更多推荐