大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~

前言

在移动应用开发中,“摇一摇”是一个非常经典的交互动作,常用于抽奖、分享、意见反馈或隐藏功能调出等场景。在 HarmonyOS 开发中,实现“摇一摇”功能的核心并不在于复杂的 AI 算法,而是依赖于 Sensor Service Kit(传感器服务) 提供的加速度传感器能力,配合合理的数学计算与防抖策略。

本文将基于 HarmonyOS 最新 API(API 10+ / HarmonyOS NEXT 规范),通过一个最小化代码示例,展示如何查询传感器状态、订阅加速度数据、计算摇动阈值以及正确管理传感器生命周期。

一、加速度传感器测量的是什么?

加速度传感器(Accelerometer)用于测量作用在设备上的加速度,包括重力加速度。传感器返回的数据通常包含三轴坐标值:

  • X 轴:水平方向(沿设备屏幕横向向右为正)
  • Y 轴:垂直方向(沿设备屏幕纵向向上为正)
  • Z 轴:垂直于屏幕方向(沿屏幕向外指向用户为正)

当设备静止放置在平整桌面上时,Z 轴受重力影响,读数约为 (9.8 , \text{m/s}^2)(或根据设备传感器单位调整),而 X 和 Y 轴接近 (0)。当用户快速摇晃设备时,三轴上的加速度值会发生剧烈波动。通过监控这种波动幅度,就能判定是否发生了“摇动”行为。

二、官方接口规则与生命周期要求

在 HarmonyOS 中使用传感器能力,需要注意以下关键规则:

  1. 套件导入:在 ArkTS 中,传感器相关 API 均由 @kit.SensorServiceKit 提供,引入模块为 sensor。
  2. 配对使用:通过 sensor.on() 订阅传感器数据后,必须在不需要时使用 sensor.off() 取消订阅。如果页面销毁或退至后台时未及时取消订阅,传感器将持续工作,导致严重的设备发热与耗电问题。
  3. 设备能力校验:不同硬件设备(如平板、2in1 设备、手表或特定穿戴设备)配备的传感器种类不同,调用前应先查询设备是否支持加速度传感器。

三、搭建最小动作识别 Demo

我们设计一个最小场景:页面上展示当前三轴加速度数值以及摇动状态,当识别到摇动动作时,界面背景改变并提示“识别到摇动”,同时通过时间间隔完成防抖,防止多次连续重复触发。

1. 配置文件检查

加速度传感器(SensorId.ACCELEROMETER)在 HarmonyOS 中属于通用传感器能力,一般情况下无需申请受限敏感权限,但在工程中需确保正确引用了 Sensor Service Kit 库。

2. 核心代码实现

在 ArkUI 页面组件中实现完整逻辑:

import { sensor } from '@kit.SensorServiceKit';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct ShakeDemoPage {
  // 显示的三轴数值
  @State xValue: number = 0;
  @State yValue: number = 0;
  @State zValue: number = 0;
  
  // 摇动状态提示
  @State shakeStatus: string = '请摇动手机';
  @State isShakeDetected: boolean = false;
  
  // 硬件支持状态
  @State isSensorSupported: boolean = true;

  // 上次检测与触发时间,用于计算差值与防抖
  private lastX: number = 0;
  private lastY: number = 0;
  private lastZ: number = 0;
  private lastUpdateTime: number = 0;
  private lastTriggerTime: number = 0;

  // 算法参数设定
  private readonly SHAKE_THRESHOLD: number = 15; // 加速度变化量阈值
  private readonly MIN_TIME_INTERVAL: number = 50; // 采样计算最小间隔 (ms)
  private readonly DEBOUNCE_INTERVAL: number = 1500; // 防重复触发冷却时间 (ms)

  aboutToAppear(): void {
    this.checkAndStartSensor();
  }

  aboutToDisappear(): void {
    this.stopSensor();
  }

  /**
   * 检查传感器支持情况并开始订阅
   */
  private checkAndStartSensor(): void {
    try {
      // 查询设备上的传感器列表
      sensor.getSensorList().then((sensorList: Array<sensor.Sensor>) => {
        const hasAccel = sensorList.some(item => item.sensorId === sensor.SensorId.ACCELEROMETER);
        if (!hasAccel) {
          this.isSensorSupported = false;
          this.shakeStatus = '设备不支持加速度传感器';
          return;
        }
        this.startSensorSubscription();
      }).catch((err: BusinessError) => {
        console.error(`Failed to get sensor list. Code: ${err.code}, message: ${err.message}`);
        this.isSensorSupported = false;
      });
    } catch (error) {
      const e = error as BusinessError;
      console.error(`Exception in getSensorList: ${e.code}, ${e.message}`);
    }
  }

  /**
   * 订阅加速度传感器
   */
  private startSensorSubscription(): void {
    try {
      // 订阅 ACCELEROMETER 传感器,设置采样间隔为 ui (适合界面交互)
      sensor.on(sensor.SensorId.ACCELEROMETER, (data: sensor.AccelerometerResponse) => {
        this.xValue = data.x;
        this.yValue = data.y;
        this.zValue = data.z;

        this.processShakeLogic(data.x, data.y, data.z);
      }, { interval: 'ui' });
    } catch (error) {
      const e = error as BusinessError;
      console.error(`Failed to subscribe accelerometer: ${e.code}, ${e.message}`);
    }
  }

  /**
   * 取消订阅加速度传感器
   */
  private stopSensor(): void {
    try {
      sensor.off(sensor.SensorId.ACCELEROMETER);
    } catch (error) {
      const e = error as BusinessError;
      console.error(`Failed to unsubscribe accelerometer: ${e.code}, ${e.message}`);
    }
  }

  /**
   * 摇动识别与防抖算法
   */
  private processShakeLogic(x: number, y: number, z: number): void {
    const currentTime = Date.now();
    const timeDiff = currentTime - this.lastUpdateTime;

    // 控制计算频率,避免过于频繁计算
    if (timeDiff < this.MIN_TIME_INTERVAL) {
      return;
    }

    this.lastUpdateTime = currentTime;

    // 计算三轴增量绝对值之和
    const deltaX = Math.abs(x - this.lastX);
    const deltaY = Math.abs(y - this.lastY);
    const deltaZ = Math.abs(z - this.lastZ);

    // 更新上一次的轴数值
    this.lastX = x;
    this.lastY = y;
    this.lastZ = z;

    // 计算合变化量(也可以使用向量模长差值计算)
    const speed = deltaX + deltaY + deltaZ;

    // 判断变化量是否超过预设阈值
    if (speed > this.SHAKE_THRESHOLD) {
      // 防重复触发控制
      if (currentTime - this.lastTriggerTime > this.DEBOUNCE_INTERVAL) {
        this.lastTriggerTime = currentTime;
        this.onShakeTriggered();
      }
    }
  }

  /**
   * 触发摇一摇逻辑
   */
  private onShakeTriggered(): void {
    this.isShakeDetected = true;
    this.shakeStatus = '🎉 成功识别到摇一摇!';

    // 2秒后重置界面状态
    setTimeout(() => {
      this.isShakeDetected = false;
      this.shakeStatus = '请摇动手机';
    }, 2000);
  }

  build() {
    Column({ space: 20 }) {
      Text('加速度传感器 Demo')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 40 });

      if (!this.isSensorSupported) {
        Text(this.shakeStatus)
          .fontSize(16)
          .fontColor(Color.Red);
      } else {
        CardContainer() {
          Column({ space: 10 }) {
            Text(`X 轴: ${this.xValue.toFixed(2)} m/s²`)
            Text(`Y 轴: ${this.yValue.toFixed(2)} m/s²`)
            Text(`Z 轴: ${this.zValue.toFixed(2)} m/s²`)
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')
          .padding(16)
        }

        Column() {
          Text(this.shakeStatus)
            .fontSize(20)
            .fontWeight(FontWeight.Medium)
            .fontColor(this.isShakeDetected ? Color.White : Color.Black)
        }
        .width('80%')
        .height(120)
        .borderRadius(16)
        .backgroundColor(this.isShakeDetected ? '#317AC6' : '#F1F3F5')
        .justifyContent(FlexAlign.Center)
      }
    }
    .width('100%')
    .height('100%')
    .padding(16)
  }
}

@Builder
function CardContainer(customBuilder: () => void) {
  Column() {
    customBuilder()
  }
  .backgroundColor('#F5F5F5')
  .borderRadius(12)
  .width('100%')
}

四、关键逻辑拆解

1. 阈值判断与向量差值计算

在 processShakeLogic 方法中,采用了三轴增量绝对值累加的方式:

(\text{speed} = |\Delta X| + |\Delta Y| + |\Delta Z|)

这种计算方式计算量极小,非常适合在低时延要求的回调中频繁执行。如果不希望使用变化量累加,也可以计算总加速度向量模长 (G = \sqrt{x^2 + y^2 + z^2}) 并与重力加速度(约 (9.8))求差值,但在快速来回摇晃的动作下,利用三轴变化量求和可以更敏感地捕获晃动动作。

2. 防重复触发(冷却防抖)

摇动动作通常持续数百毫秒甚至一秒以上,传感器在此期间会回调多次并触发阈值。如果不加限制,会导致业务逻辑(如弹窗、网络请求)被多次调用。

示例中通过记录 lastTriggerTime,只有在 currentTime - lastTriggerTime > DEBOUNCE_INTERVAL(例如 1.5 秒)时才执行 onShakeTriggered(),确保一次完整的摇动动作只触发一次响应。

3. 生命周期配对

在 ArkUI 组件中,aboutToAppear 与 aboutToDisappear 是处理传感器订阅的最安全位置。如果业务需要仅在页面可见时响应,也可以选用 onPageShow 和 onPageHide 进行订阅与取消。

五、开发过程中值得注意的技术点

  1. 传感器采样率选择:
    在调用 sensor.on 时,interval 参数支持 'normal'、'game'、'ui' 等策略(或微秒/纳秒级数值)。对于一般的动作识别,使用 'ui'(约 60ms 采样一次)即可满足要求,无需盲目开启最高采样率(如 'game'),以降低系统功耗。
  2. 避免在回调中执行耗时任务:
    sensor.on 的回调函数是在高频触发的上下文环境中执行的。避免在回调闭包内部进行复杂的数据解析、文件 IO 或 同步网络请求,算法逻辑应保持精简。
  3. 设备兼容性处理:
    在折叠屏展开态、平板或 2in1 设备上,设备的默认坐标轴方向与普通手机可能存在差异,但基于“加速度差值绝对值”的摇动算法与坐标轴朝向无关,因此具有良好的跨设备兼容性。

六、实际项目排查清单

当在开发或测试过程中发现摇一摇无响应或行为异常时,可以按以下顺序进行检查:

  1. 检查硬件支持:通过 sensor.getSensorList() 确认目标设备上是否包含 SensorId.ACCELEROMETER。
  2. 检查订阅生命周期:在日志中打印 sensor.on 成功或失败的返回值,确认订阅是否因为页面未加载完成而未能正确挂载。
  3. 调整阈值参数:不同型号设备传感器的灵敏度可能略有差异。如果触发过于灵敏,可适当调大 SHAKE_THRESHOLD(如调至 20 以上);若难以触发,可降低阈值。
  4. 核对取消订阅机制:通过日志跟踪 aboutToDisappear 或 onPageHide 是否正确执行了 sensor.off(),避免多次进入页面造成重复订阅。

七、总结

实现一个稳健的“摇一摇”动作识别,核心在于正确的 Sensor Service Kit 接口调用、轻量化的差值算法以及严格的防抖与生命周期控制。通过本文的最小 Demo,开发者可以快速将动作识别能力集成到 HarmonyOS 应用中,并根据具体业务场景进一步拓展(如结合 vibrator 振动反馈服务提升交互体验)。

参考资料

  1. 《Sensor Service Kit 开发指南(ArkTS)》
    华为开发者联盟
    链接:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/sensor-development-overview-0000001820880521

  2. 《@ohos.sensor (传感器) API 参考》
    华为开发者联盟
    链接:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-sensor-0000001821000805

社区互动:如果你正在开发动作识别类功能,你更倾向于使用单纯的加速度传感器,还是结合陀螺仪(Gyroscope)做复合动作识别?欢迎在评论区分享你的场景与算法思考。

如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~

Logo

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

更多推荐