Flutter for OpenHarmony 实战:用 dchs_motion_sensors 采集七类传感器并做屏幕方向响应
欢迎加入 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):
| 流 | 事件类型 | 单位 | 说明 |
|---|---|---|---|
accelerometer | AccelerometerEvent(x, y, z) | m/s² | 加速度,含重力 |
userAccelerometer | UserAccelerometerEvent(x, y, z) | m/s² | 线性加速度,去重力 |
gyroscope | GyroscopeEvent(x, y, z) | rad/s | 角速度 |
magnetometer | MagnetometerEvent(x, y, z) | μT | 磁力 |
orientation | OrientationEvent(yaw, pitch, roll) | 弧度 | 姿态 |
absoluteOrientation | AbsoluteOrientationEvent(yaw, pitch, roll) | 弧度 | 绝对姿态 |
screenOrientation | ScreenOrientationEvent(angle) | 度(0/90/180/-90) | 屏幕方向 |
六个采样间隔 setter(单位都是微秒):accelerometerUpdateInterval、gyroscopeUpdateInterval、magnetometerUpdateInterval、userAccelerometerUpdateInterval、orientationUpdateInterval、absoluteOrientationUpdateInterval。
注意没有 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 统一处理 onData 和 onError:
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
更多推荐




所有评论(0)