【maaath】为开源鸿蒙跨平台工程集成传感器数据获取能力
在移动端应用开发中,传感器数据是实现交互体验和智能响应的核心能力之一。加速度计帮助我们感知设备姿态与运动,陀螺仪用于测量角速度实现精准旋转检测,光线传感器让应用能够随环境光强自适应调节亮度,而距离传感器则广泛用于通话时息屏等场景。Flutter for OpenHarmony(简称 Flutter OHOS)作为 Flutter 官方对开源鸿蒙操作系统的适配版本,允许开发者使用 Dart 语言构建
为开源鸿蒙跨平台工程集成传感器数据获取能力
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
作者:maaath
一、前言
在移动端应用开发中,传感器数据是实现交互体验和智能响应的核心能力之一。加速度计帮助我们感知设备姿态与运动,陀螺仪用于测量角速度实现精准旋转检测,光线传感器让应用能够随环境光强自适应调节亮度,而距离传感器则广泛用于通话时息屏等场景。Flutter for OpenHarmony(简称 Flutter OHOS)作为 Flutter 官方对开源鸿蒙操作系统的适配版本,允许开发者使用 Dart 语言构建跨平台界面,同时通过平台通道(Platform Channel)调用 OpenHarmony 原生能力。
本文将基于一个完整的开源鸿蒙跨平台工程实例,讲解如何在 ArkTS 侧封装传感器服务、在 Flutter 界面中接收并实时可视化展示数据的全流程。工程地址托管于 AtomGit:https://atomgit.com/maaath/oh_sensor_demo
二、整体架构设计
本工程采用经典的三层分离架构:
┌─────────────────────────────────────────────┐
│ Flutter 界面层 (Dart) │
│ ┌─────────┐ ┌─────────┐ ┌─────────────┐ │
│ │IndexPage│ │SensorPage│ │实时图表组件 │ │
│ └────┬────┘ └────┬────┘ └──────┬──────┘ │
└───────┼────────────┼──────────────┼─────────┘
│ FlutterPage │ (Platform Channel)
┌───────┼────────────┼──────────────┼─────────┐
│ ▼ ▼ ▼ │
│ ┌────────────────────────────────────────┐ │
│ │ ArkTS 页面层 (SensorPage.ets) │ │
│ │ - 接收 Flutter 侧事件 │ │
│ │ - 订阅/取消 SensorService │ │
│ │ - 通过 EventHub 回传数据到 Flutter │ │
│ └─────────────────┬──────────────────────┘ │
│ ▼ │
│ ┌────────────────────────────────────────┐ │
│ │ ArkTS 服务层 (SensorService.ets) │ │
│ │ - 调用 @kit.SensorServiceKit │ │
│ │ - 传感器启动/停止/数据转发 │ │
│ └─────────────────┬──────────────────────┘ │
│ ▼ │
│ ┌────────────────────────────────────────┐ │
│ │ ArkTS 数据层 (SensorModels.ets) │ │
│ │ - 数据模型定义 (AccelerometerData 等) │ │
│ └────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
Flutter 界面层与 ArkTS 原生层之间通过 FlutterPage 组件传入的 viewId 建立连接,各自通过 eventHub 双向通信。这种架构的优势在于:传感器硬件操作完全在原生侧完成,保证了实时性和稳定性,而 UI 层仍然使用熟悉的 Flutter 开发模式。
三、数据模型层
传感器数据模型的合理设计是整个工程的基础。在 ArkTS 严格编译模式下,对象字面量(Object Literal)有严格的类型约束,因此我们将每种传感器数据定义为独立的类,通过继承 SensorData 基类实现统一接口。
entry/src/main/ets/model/SensorModels.ets
export class SensorData {
sensorType: number = 0;
timestamp: number = 0;
values: number[] = [];
}
export class AccelerometerData extends SensorData {
constructor(x: number, y: number, z: number) {
super();
this.sensorType = 1;
this.timestamp = Date.now();
this.values = [x, y, z];
}
}
export class GyroscopeData extends SensorData {
constructor(x: number, y: number, z: number) {
super();
this.sensorType = 4;
this.timestamp = Date.now();
this.values = [x, y, z];
}
}
export class LightData extends SensorData {
constructor(intensity: number) {
super();
this.sensorType = 5;
this.timestamp = Date.now();
this.values = [intensity];
}
}
export class ProximityData extends SensorData {
constructor(distance: number) {
super();
this.sensorType = 8;
this.timestamp = Date.now();
this.values = [distance];
}
}
这里需要特别说明几点设计考量。SensorData 基类包含 sensorType、timestamp 和 values 三个字段,分别标识传感器类型、时间戳和原始数据数组。子类的 values 数组遵循 [x, y, z] 或 [single] 的固定格式,调用方通过下标访问具体分量,避免了使用字符串键名带来的类型不安全风险。使用 Date.now() 记录数据产生的时间戳,为后续的数据分析(例如计算采样率)提供了依据。
四、传感器服务封装
服务层是整个传感器能力的中枢。我们使用单例模式封装 SensorService,确保整个应用生命周期内只有一份传感器订阅,避免重复订阅导致的数据混乱。
entry/src/main/ets/service/SensorService.ets
import { sensor } from '@kit.SensorServiceKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import {
SensorData,
AccelerometerData,
GyroscopeData,
LightData,
ProximityData
} from '../model/SensorModels';
const TAG = 'SensorService';
const DOMAIN = 0xFF00;
const SENSOR_INTERVAL = 100_000_000; // 100ms,纳秒单位
type SensorCallback = (data: SensorData) => void;
export class SensorService {
private static instance: SensorService | null = null;
private accelerometerActive: boolean = false;
private gyroscopeActive: boolean = false;
private lightActive: boolean = false;
private proximityActive: boolean = false;
private constructor() {}
static getInstance(): SensorService {
if (SensorService.instance === null) {
SensorService.instance = new SensorService();
}
return SensorService.instance;
}
startAccelerometer(callback: SensorCallback): boolean {
if (this.accelerometerActive) {
hilog.info(DOMAIN, TAG, 'Accelerometer already started');
return true;
}
this.accelerometerActive = true;
try {
sensor.on(sensor.SensorId.ACCELEROMETER, (data) => {
const x = data.x ?? 0;
const y = data.y ?? 0;
const z = data.z ?? 0;
callback(new AccelerometerData(x, y, z));
}, { interval: SENSOR_INTERVAL });
hilog.info(DOMAIN, TAG, 'Accelerometer started');
return true;
} catch (error) {
const err = error as BusinessError;
hilog.error(DOMAIN, TAG, `Failed to start accelerometer: ${err.code} ${err.message}`);
this.accelerometerActive = false;
return false;
}
}
stopAccelerometer(): void {
if (!this.accelerometerActive) { return; }
try {
sensor.off(sensor.SensorId.ACCELEROMETER);
this.accelerometerActive = false;
hilog.info(DOMAIN, TAG, 'Accelerometer stopped');
} catch (error) {
const err = error as BusinessError;
hilog.error(DOMAIN, TAG, `Failed to stop accelerometer: ${err.code}`);
}
}
startGyroscope(callback: SensorCallback): boolean {
if (this.gyroscopeActive) { return true; }
this.gyroscopeActive = true;
try {
sensor.on(sensor.SensorId.GYROSCOPE, (data) => {
const x = data.x ?? 0;
const y = data.y ?? 0;
const z = data.z ?? 0;
callback(new GyroscopeData(x, y, z));
}, { interval: SENSOR_INTERVAL });
return true;
} catch (error) {
const err = error as BusinessError;
hilog.error(DOMAIN, TAG, `Failed to start gyroscope: ${err.code} ${err.message}`);
this.gyroscopeActive = false;
return false;
}
}
stopGyroscope(): void {
if (!this.gyroscopeActive) { return; }
try {
sensor.off(sensor.SensorId.GYROSCOPE);
this.gyroscopeActive = false;
} catch (error) {
const err = error as BusinessError;
hilog.error(DOMAIN, TAG, `Failed to stop gyroscope: ${err.code}`);
}
}
startLightSensor(callback: SensorCallback): boolean {
if (this.lightActive) { return true; }
this.lightActive = true;
try {
sensor.on(sensor.SensorId.AMBIENT_LIGHT, (data: sensor.LightResponse) => {
const intensity = data.intensity ?? 0;
callback(new LightData(intensity));
}, { interval: SENSOR_INTERVAL });
return true;
} catch (error) {
const err = error as BusinessError;
hilog.error(DOMAIN, TAG, `Failed to start light sensor: ${err.code} ${err.message}`);
this.lightActive = false;
return false;
}
}
stopLightSensor(): void {
if (!this.lightActive) { return; }
try {
sensor.off(sensor.SensorId.AMBIENT_LIGHT);
this.lightActive = false;
} catch (error) {
const err = error as BusinessError;
hilog.error(DOMAIN, TAG, `Failed to stop light sensor: ${err.code}`);
}
}
startProximitySensor(callback: SensorCallback): boolean {
if (this.proximityActive) { return true; }
this.proximityActive = true;
try {
sensor.on(sensor.SensorId.PROXIMITY, (data: sensor.ProximityResponse) => {
const distance = data.distance ?? -1;
callback(new ProximityData(distance));
}, { interval: SENSOR_INTERVAL });
return true;
} catch (error) {
const err = error as BusinessError;
hilog.error(DOMAIN, TAG, `Failed to start proximity sensor: ${err.code} ${err.message}`);
this.proximityActive = false;
return false;
}
}
stopProximitySensor(): void {
if (!this.proximityActive) { return; }
try {
sensor.off(sensor.SensorId.PROXIMITY);
this.proximityActive = false;
} catch (error) {
const err = error as BusinessError;
hilog.error(DOMAIN, TAG, `Failed to stop proximity sensor: ${err.code}`);
}
}
stopAll(): void {
this.stopAccelerometer();
this.stopGyroscope();
this.stopLightSensor();
this.stopProximitySensor();
}
}
关于 ArkTS 传感器 API 的使用规范,有几个关键细节需要注意。第一,sensor.on() 的第三个参数是采样间隔,单位为纳秒(nanoseconds),而非毫秒。因此 100 毫秒对应 100_000_000 纳秒,使用数字字面量下划线分隔符(_)可以增强可读性。第二,官方 API 文档中存在 SensorOptions 类的引用,但在 ArkTS 严格模式下 sensor.on() 的 options 参数应使用对象字面量 { interval: number },直接传入 SensorOptions 实例会导致编译错误。第三,sensor.off() 方法只需要传入传感器 ID,不需要额外参数,这一点与一些旧版文档描述不同。
此外,光线传感器对应的 ID 是 sensor.SensorId.AMBIENT_LIGHT(环境光),而非 LIGHT;距离传感器的 ID 是 PROXIMITY。陀螺仪和加速度计使用字符串键访问(如 data.x)时,建议使用空值合并运算符 ?? 提供默认值,防止某些设备在特定时刻返回 undefined 导致数值为 NaN。
五、页面状态管理与 UI 构建
UI 层使用 ArkTS 的 @State 装饰器和 @Builder 模板方法构建声明式界面。每个传感器的运行时状态(是否激活、最新数据值)独立管理,通过 Toggle 开关控制订阅与取消。
entry/src/main/ets/pages/SensorPage.ets
import router from '@ohos.router';
import { SensorService } from '../service/SensorService';
import { SensorData } from '../model/SensorModels';
class AccelState {
active: boolean = false;
data: SensorData | null = null;
x: number = 0;
y: number = 0;
z: number = 0;
mag: number = 0;
}
class GyroState {
active: boolean = false;
data: SensorData | null = null;
x: number = 0;
y: number = 0;
z: number = 0;
speed: number = 0;
}
class LightState {
active: boolean = false;
data: SensorData | null = null;
intensity: number = 0;
}
class ProxState {
active: boolean = false;
data: SensorData | null = null;
distance: number = -1;
isNear: boolean = false;
}
@Entry
@Component
struct SensorPage {
@State accel: AccelState = new AccelState();
@State gyro: GyroState = new GyroState();
@State light: LightState = new LightState();
@State prox: ProxState = new ProxState();
@State totalSamples: number = 0;
@State lastUpdateTime: string = '--:--:--';
private sensorService: SensorService = SensorService.getInstance();
private dataCount: number = 0;
private timerId: number = -1;
aboutToAppear(): void {
this.timerId = setInterval(() => {
if (this.dataCount > 0) {
const now = new Date();
this.lastUpdateTime =
this.pad(now.getHours()) + ':' +
this.pad(now.getMinutes()) + ':' +
this.pad(now.getSeconds());
}
}, 1000) as number;
}
aboutToDisappear(): void {
this.stopAll();
if (this.timerId !== -1) {
clearInterval(this.timerId);
}
}
pad(n: number): string {
return n.toString().padStart(2, '0');
}
stopAll(): void {
this.sensorService.stopAccelerometer();
this.sensorService.stopGyroscope();
this.sensorService.stopLightSensor();
this.sensorService.stopProximitySensor();
}
toggleAccel(): void {
if (this.accel.active) {
this.sensorService.stopAccelerometer();
const s = new AccelState();
s.active = false;
this.accel = s;
} else {
const started = this.sensorService.startAccelerometer((data) => {
this.dataCount++;
this.totalSamples = this.dataCount;
const s = new AccelState();
s.active = true;
s.data = data;
s.x = data.values[0] ?? 0;
s.y = data.values[1] ?? 0;
s.z = data.values[2] ?? 0;
s.mag = Math.sqrt(s.x * s.x + s.y * s.y + s.z * s.z);
this.accel = s;
});
if (started) {
const s = new AccelState();
s.active = true;
this.accel = s;
}
}
}
toggleGyro(): void {
if (this.gyro.active) {
this.sensorService.stopGyroscope();
const s = new GyroState();
s.active = false;
this.gyro = s;
} else {
const started = this.sensorService.startGyroscope((data) => {
this.dataCount++;
this.totalSamples = this.dataCount;
const s = new GyroState();
s.active = true;
s.data = data;
s.x = data.values[0] ?? 0;
s.y = data.values[1] ?? 0;
s.z = data.values[2] ?? 0;
s.speed = Math.sqrt(s.x * s.x + s.y * s.y + s.z * s.z);
this.gyro = s;
});
if (started) {
const s = new GyroState();
s.active = true;
this.gyro = s;
}
}
}
toggleLight(): void {
if (this.light.active) {
this.sensorService.stopLightSensor();
const s = new LightState();
s.active = false;
this.light = s;
} else {
const started = this.sensorService.startLightSensor((data) => {
this.dataCount++;
this.totalSamples = this.dataCount;
const s = new LightState();
s.active = true;
s.data = data;
s.intensity = data.values[0] ?? 0;
this.light = s;
});
if (started) {
const s = new LightState();
s.active = true;
this.light = s;
}
}
}
toggleProx(): void {
if (this.prox.active) {
this.sensorService.stopProximitySensor();
const s = new ProxState();
s.active = false;
s.distance = -1;
this.prox = s;
} else {
const started = this.sensorService.startProximitySensor((data) => {
this.dataCount++;
this.totalSamples = this.dataCount;
const s = new ProxState();
s.active = true;
s.data = data;
s.distance = data.values[0] ?? -1;
s.isNear = (data.values[0] ?? -1) >= 0 && (data.values[0] ?? -1) < 5;
this.prox = s;
});
if (started) {
const s = new ProxState();
s.active = true;
this.prox = s;
}
}
}
activeCount(): number {
let n = 0;
if (this.accel.active) { n++; }
if (this.gyro.active) { n++; }
if (this.light.active) { n++; }
if (this.prox.active) { n++; }
return n;
}
build() {
Column() {
this.buildHeader()
this.buildStatusBar()
Scroll() {
Column() {
this.buildAccelCard()
this.buildGyroCard()
this.buildLightCard()
this.buildProxCard()
}
.padding({ left: 16, right: 16, top: 8, bottom: 16 })
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.Spring)
}
.width('100%')
.height('100%')
.backgroundColor('#FFF0F2F5')
}
关于状态更新策略,这里采用了每次回调创建新实例的不可变更新模式。在 ArkTS 中,@State 变量的赋值会触发 UI 重渲染。如果直接修改现有对象的字段(如 this.accel.x = data.values[0]),框架无法感知变化,界面将不会更新。因此在每个传感器回调中,我们构造一个全新的 AccelState(或其他状态类)实例,将更新后的所有字段填入,再整体赋值给 @State 变量。这种模式在 Flutter 的 setState 中同样常见,核心思想是让框架通过引用变化检测来触发重建。
六、传感器卡片 UI 组件
四个传感器卡片组件分别展示不同类型的数据可视化。加速度计和陀螺仪展示三轴数值和进度条,强度变化直观可见;光线传感器展示 lux 值和光照等级描述;距离传感器展示近/远离状态和距离数值。
@Builder
buildAccelCard() {
Column() {
this.buildCardHeader('Accelerometer', '⚡', '#FF4CAF50',
this.accel.active, () => this.toggleAccel())
if (this.accel.active) {
Column() {
Divider().color('#FFF0F0F0').strokeWidth(1)
}
.padding({ left: 16, right: 16 })
this.buildAxisRow('X', this.accel.x, '#FFE53935')
this.buildAxisRow('Y', this.accel.y, '#FF43A047')
this.buildAxisRow('Z', this.accel.z, '#FF1E88E5')
this.buildMagBar(this.accel.mag)
} else {
this.buildOfflineHint()
}
}
.width('100%')
.margin({ top: 10 })
.backgroundColor('#FFFFFFFF')
.borderRadius(16)
.shadow({ radius: 8, color: '#1A000000', offsetX: 0, offsetY: 2 })
.clip(true)
}
@Builder
buildAxisRow(label: string, value: number, barColor: string) {
Row() {
Text(label).fontSize(12).fontWeight(FontWeight.Bold)
.fontColor(barColor).width(18)
Stack() {
Rect().width('100%').height(6).fill('#FFF0F0F0').radius(3)
Rect()
.width(this.getBarWidth(value) + '%')
.height(6)
.fill(barColor)
.radius(3)
}
.layoutWeight(1)
Text(value.toFixed(4))
.fontSize(12)
.fontColor('#FF424242')
.fontFamily('monospace')
.width(90)
.textAlign(TextAlign.End)
}
.margin({ bottom: 8 })
}
getBarWidth(value: number): string {
const pct = Math.min(Math.abs(value) / 20, 1.0) * 100;
return pct.toFixed(1);
}
@Builder
buildCardHeader(title: string, icon: string, color: string,
active: boolean, onToggle: () => void) {
Row() {
Row({ space: 10 }) {
Text(icon).fontSize(20)
.padding(8)
.backgroundColor(active ? color + '1A' : '#FFF5F5F5')
.borderRadius(10)
Column() {
Text(title).fontSize(14).fontWeight(FontWeight.Bold)
.fontColor(active ? color : '#FF757575')
Row({ space: 4 }) {
Circle().size({ width: 6, height: 6 })
.fill(active ? '#FF4CAF50' : '#FFE0E0E0')
Text(active ? 'Streaming' : 'Stopped')
.fontSize(11)
.fontColor(active ? '#FF4CAF50' : '#FFBDBDBD')
}
}
.alignItems(HorizontalAlign.Start)
}
Blank()
Toggle({ type: ToggleType.Switch, isOn: active })
.selectedColor(color)
.onChange((isOn: boolean) => onToggle())
}
.padding(16)
.width('100%')
}
进度条组件使用 ArkTS 的 Rect 形状组件配合百分比宽度实现,这种方式比填充背景图方案更加轻量且分辨率无关。Math.min(Math.abs(value) / 20, 1.0) * 100 将任意数值归一化到 [0, 100] 范围,其中分母 20 是凭经验设定的加速度量程参考值,实际项目中可根据预期最大值调整。
七、权限配置与路由
传感器属于系统敏感能力,必须在 module.json5 中声明权限,否则 sensor.on() 调用将直接抛出异常。
entry/src/main/module.json5(权限部分)
"requestPermissions": [
{"name": "ohos.permission.INTERNET"},
{"name": "ohos.permission.ACCELEROMETER"},
{"name": "ohos.permission.GYROSCOPE"}
]
页面路由在 main_pages.json 中配置:
{
"src": [
"pages/Index",
"pages/SensorPage"
]
}
从首页导航到传感器页面的代码如下:
import router from '@ohos.router';
Text('Sensors >')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#2196F3')
.onClick(() => {
router.pushUrl({ url: 'pages/SensorPage' })
})
值得注意的是,aboutToDisappear() 生命周期回调中必须调用 stopAll() 取消所有传感器订阅。这不仅是良好的资源管理实践——传感器在后台持续运行会显著消耗电量——也是防止内存泄漏的关键。在 Flutter OHOS 架构中,ArkTS 页面的 aboutToDisappear 对应 Flutter Widget 的 dispose 时机,因此应确保传感器服务的清理逻辑与 UI 销毁保持同步。
八、运行验证与效果截图
工程编译完成后,在鸿蒙设备上安装运行,可以看到以下效果:
首页截图说明:首页底部栏显示"Sensor Monitor"标题,右侧"Sensors >"链接用于跳转。点击后进入传感器监控页面。
传感器监控页截图说明:页面顶部为状态栏,显示当前激活的传感器数量和总采样数。四个传感器卡片从上至下排列:加速度计卡片展示 X/Y/Z 三轴数值和进度条,卡片头部有绿色电源图标表示已激活;陀螺仪卡片展示角速度数据;光线传感器卡片展示当前环境光照强度(lux)和文字描述等级;距离传感器卡片展示近/远离状态。
以加速度计为例,开启 Toggle 开关后,卡片立即开始显示实时三轴数据,数值随设备姿态变化实时更新。进度条颜色区分 X(红)、Y(绿)、Z(蓝)三轴,底部显示向量模长(Magnitude)。

读者可通过以下命令将工程克隆至本地后运行:
# 克隆工程
git clone https://atomgit.com/maaath/oh_sensor_demo.git
# 进入目录
cd oh_sensor_demo
# 安装依赖
ohpm install
# 启动调试
hvigorw assembleDebug --compile-mode=joint
九、总结与扩展思路
本文详细介绍了在 Flutter for OpenHarmony 跨平台工程中集成传感器数据获取能力的完整方案,涵盖数据建模、服务封装、UI 构建、权限配置和路由导航等各个环节。通过 SensorService 单例封装传感器订阅逻辑、不可变状态更新模式保证 UI 实时响应、@State + @Builder 构建声明式界面,三个层次各司其职,工程结构清晰可维护。
在此基础上,读者可以进一步探索以下扩展方向:
滤波与平滑:原始传感器数据通常包含噪声,可以使用简单的低通滤波器(如指数移动平均)对数据进行平滑处理,提升 UI 展示的流畅度。实现方式是维护一个固定大小的历史窗口,每次取平均值作为展示值。
数据导出:增加数据记录功能,将采样数据以 CSV 格式导出到设备存储,供后续分析使用。结合 fileIo API 实现文件写入。
多传感器联动:将加速度计和陀螺仪数据融合,使用卡尔曼滤波算法计算更精确的设备姿态角,可用于实现手势识别或游戏控制器等功能。
Flutter 侧封装:当前方案中数据展示逻辑在 ArkTS 侧实现。也可以将 SensorService 通过 Flutter Platform Channel 暴露为 Dart API,让传感器数据流入 Flutter Widget Tree,从而完全使用 Flutter 的声明式 UI 和生态体系来构建可视化界面。这需要在 ArkTS 侧定义 MethodChannel 并将传感器事件通过 EventChannel 推送到 Dart 层。
社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)