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

上一篇把 dchs_motion_sensors 的鸿蒙化适配讲完了——通道怎么映射、单位怎么换算、权限怎么申请。这一篇站在使用者的角度:库已经在 AtomGit 上,在一个真实页面里怎么把它读出来、显示出来、又不留坑

场景我选的是"传感器仪表盘 + 屏幕方向响应":一个页面同时订阅七类传感器,实时刷新读数,设备横竖屏切换时界面跟着变。这个场景能把这类库的常见问题全带出来:订阅怎么成套、读数怎么上屏、权限被拒怎么办、采样频率怎么调。

完整示例工程在适配仓库的 example/ 目录下:https://atomgit.com/oh-flutter/dchs_motion_sensors

在这里插入图片描述


一、库给我们的东西

先列清楚 API,后面就不用一直翻文档。dchs_motion_sensors 暴露一个全局单例 motionSensors,里面是七个流 + 六个采样间隔 setter + 一组可用性查询

七个流(对应七个 EventChannel):

事件类型单位说明
accelerometerAccelerometerEvent(x, y, z)m/s²加速度,含重力
userAccelerometerUserAccelerometerEvent(x, y, z)m/s²线性加速度,去重力
gyroscopeGyroscopeEvent(x, y, z)rad/s角速度
magnetometerMagnetometerEvent(x, y, z)μT磁力
orientationOrientationEvent(yaw, pitch, roll)弧度姿态
absoluteOrientationAbsoluteOrientationEvent(yaw, pitch, roll)弧度绝对姿态
screenOrientationScreenOrientationEvent(angle)度(0/90/180/-90)屏幕方向

六个采样间隔 setter(单位都是微秒):accelerometerUpdateIntervalgyroscopeUpdateIntervalmagnetometerUpdateIntervaluserAccelerometerUpdateIntervalorientationUpdateIntervalabsoluteOrientationUpdateInterval

注意没有 screenOrientation 的 setter——它由系统显示事件驱动,不归采样频率管,和上游 Android 行为一致。

可用性查询:isAccelerometerAvailable()isGyroscopeAvailable()isMagnetometerAvailable()isUserAccelerationAvailable()isOrientationAvailable()isAbsoluteOrientationAvailable(),外加一个通用的 isSensorAvailable(int sensorType)

二、最小可用:先订阅一路

拿加速度计举例,最短的写法是这样:

import 'package:dchs_motion_sensors/dchs_motion_sensors.dart';

StreamSubscription<AccelerometerEvent>? _sub;

void _start() {
  _sub = motionSensors.accelerometer.listen((AccelerometerEvent e) {
    print('${e.x}, ${e.y}, ${e.z}');
  });
}

e.x / e.y / e.z 是三个方向的加速度,单位 m/s²。设备平放时 z 轴约等于重力加速度(9.8 左右),这是"含重力"的含义——它测的是设备整体受力,不是运动量。

如果只想要"运动了多少",用去重力的 userAccelerometer,同样的字段结构。

三、读数不会自己上屏

这是最容易犯的错,我在写示例时自己也栽过一次。

订阅回调里拿到值之后,如果不调 setState,界面上的数字永远是 0——因为 listen 的回调发生在 Flutter 的 UI 框架之外,改了字段也不会触发重建:

// 反面示例:界面不会变
motionSensors.accelerometer.listen((AccelerometerEvent e) {
  _x = e.x;          // 字段变了,但没人通知界面重画
});

正确写法是在回调里包一层 setState

motionSensors.accelerometer.listen((AccelerometerEvent e) {
  setState(() {
    _x = e.x;
    _y = e.y;
    _z = e.z;
  });
});

这个坑在"读传感器做 UI"的场景里特别隐蔽,因为日志里事件一直在推、看起来一切正常,只有界面死着不动。

四、单位与显示:拿到的已经是标准制

库返回的都是标准单位:加速度 m/s²、角速度 rad/s、磁力 μT、姿态弧度。适配层已经把鸿蒙原生接口的单位(度、纳秒、四元数)换算好了,使用者直接读就行。

唯一需要自己动手的是显示:弧度看着不直观,界面一般显示角度:

import 'dart:math' as math;

motionSensors.orientation.listen((OrientationEvent e) {
  final double yawDeg = e.yaw * 180 / math.pi;
  final double pitchDeg = e.pitch * 180 / math.pi;
  final double rollDeg = e.roll * 180 / math.pi;
  // 再 setState 显示 yawDeg / pitchDeg / rollDeg
});

两个姿态流的区别值得记住:

  • orientation 取自姿态传感器,参考基准是设备本身,适合"设备相对自己转了多少";
  • absoluteOrientation 由旋转矢量换算,是绝对朝向,适合指南针这类需要地理参考的场景。

选错流的结果不是报错,而是数值方向和你预期的不一样,排查起来很费劲。

五、采样间隔:1 / 30 / 60 怎么设

六个 setter 的单位是微秒,所以"每秒 N 次"要自己换算:

// 30 FPS = 每秒 30 次 = 每次约 33333 微秒
motionSensors.accelerometerUpdateInterval =
    Duration.microsecondsPerSecond ~/ 30;

注意这是 setter 不是方法,赋值即生效。另外鸿蒙侧没有"改频率"的接口,底层实现是先退订再按新频率重订——这是适配层的事,使用者无感,但意味着不要在每帧里反复设频率,否则底层会反复重建订阅。

六、权限与错误处理:onError 不能省

加速度计、陀螺仪、线性加速度需要权限(ohos.permission.ACCELEROMETER / ohos.permission.GYROSCOPE),宿主应用要在 module.json5 里声明:

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

声明之后,插件会在订阅前主动申请。如果申请被拒,订阅会以错误收场,而不是一直没数据。所以 listen 一定要带 onError,否则被拒时就是一个未处理的异步异常:

motionSensors.gyroscope.listen(
  (GyroscopeEvent e) { /* 正常数据 */ },
  onError: (Object error, StackTrace st) {
    // 权限被拒、传感器不可用都会走到这里
    setState(() => _unavailable.add('陀螺仪'));
  },
);

onError 里把不可用的项记下来、在界面上提示,比让用户盯着一个永远不动的数字猜原因强得多。

七、订阅要成套:listen 之后必须 cancel

传感器流是长连接,页面销毁时不 cancel,回调会在页面已销毁后继续打过来,setState 直接抛异常。所有订阅都要存起来、在 dispose 里取消:

final List<StreamSubscription<dynamic>> _subs = [];


void initState() {
  super.initState();
  _subs.add(motionSensors.accelerometer.listen(_onAccelerometer));
  _subs.add(motionSensors.gyroscope.listen(_onGyroscope));
  // ... 其余流
}


void dispose() {
  for (final s in _subs) {
    s.cancel();
  }
  super.dispose();
}

传感器应用经常是"进页面开订阅、退页面关订阅"的循环,漏一次就会积累一次,最后一次数据触发 N 次回调。

八、实战:一个带 FPS 控制与屏幕方向响应的仪表盘

把上面的点串起来,就是一个完整的仪表盘。核心是三个部分。

订阅入口:用一个泛型 helper 统一处理 onDataonError

void _listen<T>(String name, Stream<T> stream, void Function(T) onData) {
  stream.listen(onData, onError: (Object e, StackTrace st) {
    if (!mounted) return;
    setState(() => _denied.add(name));   // 记录不可用项,用于顶部提示
  });
}


void initState() {
  super.initState();
  _listen('加速度计', motionSensors.accelerometer, (AccelerometerEvent e) {
    setState(() {
      _ax = e.x; _ay = e.y; _az = e.z;
    });
  });
  _listen('屏幕方向', motionSensors.screenOrientation,
      (ScreenOrientationEvent e) => setState(() => _screenAngle = e.angle));
  // 陀螺仪、磁力计、线性加速度、姿态、绝对姿态同理
}

采样频率:用 SegmentedButton 三个档位,切换时写六个 setter:

SegmentedButton<int>(
  segments: const [
    ButtonSegment(value: 1, label: Text('1 FPS')),
    ButtonSegment(value: 2, label: Text('30 FPS')),
    ButtonSegment(value: 3, label: Text('60 FPS')),
  ],
  selected: <int>{_fpsGroup},
  onSelectionChanged: (Set<int> s) => _setInterval(s.first),
)

void _setInterval(int group) {
  final int interval =
      Duration.microsecondsPerSecond ~/ (group == 1 ? 1 : group == 2 ? 30 : 60);
  motionSensors.accelerometerUpdateInterval = interval;
  motionSensors.gyroscopeUpdateInterval = interval;
  // ... 其余四个 setter
  setState(() => _fpsGroup = group);
}

不可用提示_denied 非空时在顶部放一个横幅,带"知道了"关闭按钮:

if (_denied.isNotEmpty)
  MaterialBanner(
    content: Text('以下传感器暂不可用:${_denied.join('')}'),
    leading: const Icon(Icons.info_outline),
    backgroundColor: Colors.amber.shade50,
    actions: [
      TextButton(
        onPressed: () => setState(() => _denied.clear()),
        child: const Text('知道了'),
      ),
    ],
  ),

数据卡片那部分就是把每条流的值映射到带单位标签的 Card 上,没有新知识点,不展开。完整代码见仓库的 example/lib/main.dart

九、模拟器能证什么、不能证什么

在 HarmonyOS 7.0.0(26.0.0) 的 API 26 模拟器上,这个仪表盘能正常跑起来,七条通道里六条都能收到数据并刷新界面。唯一例外是线性加速度:模拟器不支持 LINEAR_ACCELEROMETER 传感器,订阅报"参数无效",正好被 onError 接住、显示在顶部提示条里——这本身也算验证了错误处理是通的。

但要诚实说清楚:模拟器上运动类传感器的读数是静态的。加速度计、陀螺仪恒为 0(设备没在动),磁力计是个常数,姿态几乎不动。所以模拟器能证明的是"通道通、数据能反序列化、UI 会刷新",证明不了"数值量级对不对"。

要验收数值正确,得在真机上做两件事:

  • 把设备倾斜,看 orientation / absoluteOrientation 是否跟着变、量级是否合理;
  • 走两步,看 accelerometer / userAccelerometer 是否出现非零读数。

在这里插入图片描述

小结

用这个库的关键不在"调 API",而在把订阅这条长连接管住

  • 回调里一定要 setState,否则界面死着不动;
  • 单位库已换算好,显示时弧度转角度自己动手;
  • 权限被拒会以错误形式收场,onError 不能省;
  • 页面销毁必须成套 cancel,否则订阅越积越多。

这个库 API 简单,但七个流同时订阅时,上面任何一条漏掉都会变成"看起来没数据"或者"偶发崩溃"这类难查的问题。把它们一次性做对,传感器这块就能放心用了。

依赖配置:

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

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

更多推荐