为开源鸿蒙跨平台工程集成传感器数据获取能力

欢迎加入开源鸿蒙跨平台社区: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 基类包含 sensorTypetimestampvalues 三个字段,分别标识传感器类型、时间戳和原始数据数组。子类的 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

Logo

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

更多推荐