欢迎加入 CPF-Flutter 鸿蒙社区:https://atomgit.com/CPF-Flutter

上一篇做的是音量控制,这一篇换到传感器。选 dchs_motion_sensors 的理由是它的通道结构比一般插件复杂——一个 MethodChannel 加七个 EventChannel,覆盖加速度计、陀螺仪、磁力计、线性加速度、姿态、绝对姿态和屏幕方向。通道一多,单位换算和坐标系约定的坑就会集中暴露出来。

适配后的仓库:https://atomgit.com/oh-flutter/dchs_motion_sensors

在这里插入图片描述


一、查重:这一轮排掉了四个库

动手前照例先查重。这轮的结果值得单独说,因为待适配清单又坑了三次

open_fileapp_settingsflutter_displaymode 在适配清单里都还标着"需要适配",实际到 AtomGit 上一查:

✅ 存在  CPF-Flutter/fluttertpc_open_file        ohos/=✓
✅ 存在  CPF-Flutter/fluttertpc_app_settings     ohos/=✓
✅ 存在  oh-flutter/flutter_displaymode          ohos/=✓

三个都已经适配完了,文档齐全。清单的数据抓取于一周前,而适配是持续在发生的——只信清单,这一轮就白干三个库

还有一个更隐蔽的情况:device_apps(1.3 万下载,看起来是个很值得做的库)查重是干净的,但它做不了。原因是它的核心功能是枚举已安装应用,而鸿蒙的对应接口有权限门槛:

L1048  getBundleInfo(bundleName, bundleFlags, callback)
       @permission ohos.permission.GET_BUNDLE_INFO_PRIVILEGED or ohos.permission.GET_BUNDLE_INFO
L1513  ...  @permission ohos.permission.GET_BUNDLE_INFO_PRIVILEGED

GET_BUNDLE_INFO_PRIVILEGED 是系统级特权权限,普通三方应用申请不到。也就是说这个插件在鸿蒙上只能查到自己的信息,核心场景直接不成立——选它等于交一个功能残缺的适配。

这条经验值得记住:查重只能确认"没人做过",不能确认"能不能做"。 选库时要顺着上游的 API 逐个确认鸿蒙侧有没有对等能力,以及那个能力是否需要拿不到的权限。

二、先看清通道契约

dchs_motion_sensors 的通道结构:

通道类型推送内容
motion_sensors/methodMethodChannel可用性查询、采样间隔设置
motion_sensors/accelerometerEventChannel[x, y, z],含重力,m/s²
motion_sensors/gyroscopeEventChannel[x, y, z],rad/s
motion_sensors/magnetometerEventChannel[x, y, z],μT
motion_sensors/user_accelerometerEventChannel[x, y, z],已去重力
motion_sensors/orientationEventChannel[yaw, pitch, roll],弧度
motion_sensors/absolute_orientationEventChannel[yaw, pitch, roll],弧度
motion_sensors/screen_orientationEventChanneldouble,角度

两个 MethodChannel 方法:

Future<bool> isSensorAvailable(int sensorType);
Future<void> setSensorUpdateInterval(int sensorType, int interval);

sensorType 用的是 Android 的 Sensor.TYPE_* 常量(1 加速度计、2 磁力计、4 陀螺仪、10 线性加速度、11 旋转矢量、15 姿态),所以鸿蒙侧需要一张映射表。

Android 实现里有两个细节必须照搬,否则行为会不一致:

// 姿态由旋转矢量算得,且 yaw / pitch 取负
val sensorValues = listOf(-orientation[0], -orientation[1], orientation[2])

// 屏幕方向只在变化时上报
if (rotation != lastRotation) { eventSink?.success(rotation); lastRotation = rotation }

三、鸿蒙侧的映射:三处必须做的换算

@ohos.sensor 的接口形状和 Android 很接近,但有三处语义不一致,逐条踩过。

换算一:采样间隔,微秒 vs 纳秒

Dart 侧的 accelerometerUpdateInterval 单位是微秒,而鸿蒙的 Options.interval纳秒

/** 微秒 → 纳秒。 */
const MICROS_TO_NANOS: number = 1000;

setUpdateInterval(intervalMicros: number): void {
  this.intervalNanos = intervalMicros * MICROS_TO_NANOS;
  if (this.listening) {
    this.unsubscribe();
    this.subscribe();
  }
}

顺带一个鸿蒙特有的约束:没有单独的"改频率"接口,只能先退订再按新间隔重新订阅。所以 Handler 里必须记住当前是否处于订阅状态,否则改频率会把订阅弄丢。

换算二:姿态角,度 vs 弧度

Dart 侧约定弧度,而鸿蒙的 OrientationResponse 直接给度数:

alpha: 绕 z 轴转角,单位度,取值 0-360
beta:  绕 x 轴转角,单位度,取值 ±180
gamma: 绕 y 轴转角,单位度,取值 ±90

对照 Android 的 getOrientation 输出顺序(yaw 绕 z、pitch 绕 x、roll 绕 y),三者正好一一对应,换算加取负即可:

private onData = (data: sensor.OrientationResponse): void => {
  const yaw: number = -data.alpha * DEG_TO_RAD;
  const pitch: number = -data.beta * DEG_TO_RAD;
  const roll: number = data.gamma * DEG_TO_RAD;
  this.emit([yaw, pitch, roll]);
};

换算三:绝对姿态,四元数 → 欧拉角

这个最麻烦。Android 的 TYPE_ROTATION_VECTOR 和鸿蒙的 SensorId.ROTATION_VECTOR 都给四元数,但 Dart 侧要的是欧拉角,得自己转:

private onData = (data: sensor.RotationVectorResponse): void => {
  const x: number = data.x;
  const y: number = data.y;
  const z: number = data.z;
  const w: number = data.w;

  // 四元数转欧拉角:yaw 绕 z、pitch 绕 y、roll 绕 x
  const yaw: number = Math.atan2(2 * (w * z + x * y), 1 - 2 * (y * y + z * z));
  // 中间量需钳制到 [-1, 1],浮点误差会让 asin 的参数越界并得到 NaN
  const sinPitch: number = Math.min(1, Math.max(-1, 2 * (w * y - z * x)));
  const pitch: number = Math.asin(sinPitch);
  const roll: number = Math.atan2(2 * (w * x + y * z), 1 - 2 * (x * x + y * y));

  this.emit([-yaw, -pitch, roll]);
};

asin 那个钳制不是多余的。四元数带浮点误差时 2(wy - zx) 可能算出 1.0000001,asin 直接返回 NaN,而这个 NaN 会一路传到 Dart 侧的 toStringAsFixed 里才炸——排查成本很高。

四、屏幕方向:换了个更靠谱的实现

Android 那边把 screen_orientation 挂在加速度计上,每次加速度事件读一次屏幕旋转角再去重。这个做法能用,但代价是:没有加速度计的设备上这个流就废了。

鸿蒙有直接的显示变化事件,所以这里换了个实现:

onListen(args: Object | null, events: EventSink): void {
  this.sink = events;
  display.on('change', this.onDisplayChange);
  // 立即上报一次当前方向,避免订阅方要等到方向改变才拿到首个值
  this.report();
}

对外行为一致(只在方向变化时上报),但不再占用传感器。这属于适配中值得做的改进——上游的实现方式不一定是最适合目标平台的,映射的是行为而不是代码

五、权限:一个会静默失败的坑

这一节是这次适配里最值得记的部分。

第一次跑起来,磁力计有数据(0.0625),姿态有数据(0.0096),加速度计和陀螺仪全是 0.0000

看上去像"这两个传感器模拟器没有",但实际是权限问题。查鸿蒙的接口声明:

L206   @permission ohos.permission.ACCELEROMETER
L221   function on(type: SensorId.ACCELEROMETER, ...)
L308   @permission ohos.permission.GYROSCOPE
L323   function on(type: SensorId.GYROSCOPE, ...)
L427   function on(type: SensorId.MAGNETIC_FIELD, ...)     ← 无权限注解

加速度计、陀螺仪、线性加速度需要权限;磁力计、姿态、旋转矢量不需要。所以现象完全对得上:无权限的那几个没数据,有权限的那几个正常。

麻烦在于失败是静默的。插件里 sensor.on() 抛错时如果只写个 catch 吞掉,调用方看到的就是"这个流一直没数据",无从判断是设备没有传感器、还是权限被拒、还是代码写错了。

处理办法有两步。

第一步:让应用声明权限。 插件是 HAR,无法代为声明,必须由宿主应用在 module.json5 里写:

"requestPermissions": [
  { "name": "ohos.permission.ACCELEROMETER" },
  { "name": "ohos.permission.GYROSCOPE" }
]

第二步:插件在订阅前主动申请,并把失败经错误通道上报。

申请权限需要 UIAbilityContext,而 FlutterPluginBinding 只给 ApplicationContext。要拿前者得额外实现 AbilityAware

export default class MotionSensorsPlugin implements FlutterPlugin, MethodCallHandler, AbilityAware {
  private uiAbilityContext: common.UIAbilityContext | null = null;

  onAttachedToAbility(binding: AbilityPluginBinding): void {
    this.uiAbilityContext = binding.getAbility().context;
  }

  onDetachedFromAbility(): void {
    this.uiAbilityContext = null;
  }
}

拿到 context 后就能申请,并把结果回传而不是吞掉:

this.requestPermission(this.permission)
  .then((granted: boolean) => {
    if (!granted) {
      events.error('PERMISSION_DENIED',
        `OpenHarmony 需要 ${this.permission} 权限才能读取该传感器`, null);
      return;
    }
    this.listening = true;
    this.subscribe();
  });

实测结论补充一句:在 HarmonyOS 7.0.0(26.0.0) 上,这两个权限是声明即授予reqPermissionStates 返回 0,未弹出确认框),属于 system_grant 级。运行时申请代码没有触发弹窗。保留它的价值在于防御性——一旦设备或系统版本把它们当 user_grant 处理,这段代码能保证不静默失败,而不是等用户来报"加速度计没数据"。

六、另一个真实的编译错误

鸿蒙的 MethodCall 接口和 Flutter 官方的不完全一样。isSensorAvailable(int) 传的是位置参数,而 Dart 侧的方法签名是 invokeMethod('isSensorAvailable', sensorType),参数没有包成 Map。

第一版我按 Flutter 的习惯写了 call.arguments,编译直接报错:

10505001 ArkTS Compiler Error
Property 'arguments' does not exist on type 'MethodCall'. Did you mean 'argument'?
MotionSensorsPlugin.ets:131:54

鸿蒙的 MethodCall 暴露的是这两个:

成员用途
args: Any位置参数(本次要用这个)
argument(key: string): Any命名参数,要求参数是 Map 或对象

改成 call.args as number 后编译通过。这个错误的提示信息很准,直接给出了正确写法,比盲试省事。

七、真机验证

验证在 HarmonyOS 7.0.0(26.0.0) 的 API 26 模拟器上完成。结论分两类。

验证在 HarmonyOS 7.0.0(26.0.0) 的 API 26 模拟器上完成。先把"能证的"和"证不了的"分开说。

能证的:通道与链路

通道实测读数说明
accelerometer0.0000hilog 显示以约 6.7 Hz 持续推送事件,通道是通的
magnetometer0.0625通道通、能反序列化;但值是常数,见下方说明
orientation0.0000, -0.0096, 0.0096通道通;示例界面显示的是,见下方说明
screenOrientation0.0000竖屏,符合预期

证不了的:数值量级与单位换算

这里必须纠正一处我最初的误读。示例工程里姿态那行显示的是:

Text(degrees(_orientation.y).toStringAsFixed(4))

vector_mathdegrees() 是"弧度→度"——也就是说,示例把我插件输出的弧度又转回了度再显示。所以界面上的 -0.0096-0.0096 度,不是弧度。

这意味着:

  • 模拟器的姿态传感器读数本身几乎为 0(设备水平放置),无论换算做没做,显示出来都是接近 0 的数;
  • 那组 -0.0096 不能证明"度→弧度换算生效"——我最初把它当成"非零弧度",这是错的。

同理,磁力计的 0.0625 是个常数(1/16),大概率是模拟器上报的固定值,而非真实地磁数据。

所以诚实的结论是:模拟器能证明七条事件通道都通、事件能反序列化到 Dart 侧,但证明不了任何数值量级或单位换算。 度→弧度、四元数→欧拉角这两处换算目前只能靠代码审查确认,最终要用真机读数来验证。

在这里插入图片描述

如果要验收"数值正确",请在真机上跑:把设备倾斜,看 orientationabsoluteOrientation 是否随姿态变化、量级是否合理。模拟器做不了这一步。

在这里插入图片描述

八、已知限制

除了上面那条硬件限制,还有三处需要写进文档:

  • setSensorUpdateInterval 对屏幕方向无效——它由系统事件驱动,不接受采样间隔,与上游 Android 行为一致。
  • 姿态与绝对姿态的符号约定沿用了上游 Android 的取值方式(对 yaw、pitch 取负),以保证跨平台行为一致。如果应用原本依赖 iOS 实现的符号,需要实测确认。
  • orientationabsoluteOrientation 的参考基准不同:前者取自姿态传感器、不含磁力计基准,后者由旋转矢量换算、是绝对朝向。这一点上游 Dart 代码的注释里没说,容易误用。

小结

这个库的适配工作量主要在"对齐语义"而不是"写接口":

  • 三个 EventChannel 变成七个,每个都要处理订阅生命周期;
  • 三处单位与表示形式的换算(微秒/纳秒、度/弧度、四元数/欧拉角);
  • 权限导致的静默失败必须先解决,否则一半的传感器对用户是废的;
  • 上游挂在加速度计上的实现方式,在鸿蒙上可以换成更直接的系统事件。

另外这轮最大的收获其实是查重环节:三个库已经被适配、一个库因权限门槛做不了。选库这一步花的半小时,省下了至少一个库的无效工作量。

适配后的仓库和完整文档:

https://atomgit.com/oh-flutter/dchs_motion_sensors

dependencies:
  dchs_motion_sensors:
    git:
      url: https://atomgit.com/oh-flutter/dchs_motion_sensors.git
      ref: 2.0.2-ohos-1.0.0-beta.1

欢迎加入 CPF-Flutter 鸿蒙社区:https://atomgit.com/CPF-Flutter

Flutter 三方库鸿蒙适配清单:https://atomgit.com/oh-flutter/flutter-ohos-adaptation-checklist

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐