前言

摇一摇,这个功能最早是微信搞出来的,结果成了移动端的经典交互。在 HarmonyOS7 上实现摇一摇,比我想象的简单多了——加速度计数据就三个值,判断逻辑十几行代码,回调里加个震动就完事。但传感器开发有不少细节:权限怎么申、采样率怎么选、功耗怎么控。这篇从加速度计接入到摇一摇完整实现,顺便把其他传感器也扫一遍。

传感器能做什么

A hand-drawn doodle illustration on pure white pap

HarmonyOS7 支持的传感器不少,常见的这些:

传感器数据能干啥
加速度计x/y/z 三轴加速度摇一摇、计步、屏幕旋转
陀螺仪x/y/z 三轴角速度姿态检测、VR 头控
磁力计x/y/z 三轴磁场指南针、金属检测
接近光距离值通话时熄屏
环境光光照强度自动亮度
气压计大气压值海拔估算、天气
计步器步数运动统计
心率BPM健康监测

每个传感器的接入方式都一样:on 订阅 → 回调拿数据 → off 取消订阅。套路统一,学会一个就会所有

权限声明

加速度计需要权限,module.json5 里加上:

A hand-drawn doodle illustration on pure white pap

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

这个权限不需要动态申请,声明了就行。但心率传感器那类敏感数据需要 ohos.permission.HEALTH_DATA,那个要动态授权。

加速度计接入

核心就三步:导入模块、订阅数据、取消订阅。

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

@State accelX: number = 0
@State accelY: number = 0
@State accelZ: number = 0
private subscribed: boolean = false

subscribeAccelerometer() {
  try {
    sensor.on(sensor.SensorId.ACCELEROMETER, (data: sensor.AccelerometerResponse) => {
      this.accelX = data.x
      this.accelY = data.y
      this.accelZ = data.z
    }, { interval: 200000000 })  // 采样间隔,单位纳秒
    this.subscribed = true
  } catch (error) {
    let e = error as BusinessError
    console.error(`订阅失败: ${e.code}, ${e.message}`)
  }
}

unsubscribeAccelerometer() {
  try {
    sensor.off(sensor.SensorId.ACCELEROMETER)
    this.subscribed = false
  } catch (error) {
    let e = error as BusinessError
    console.error(`取消订阅失败: ${e.code}`)
  }
}

A hand-drawn doodle illustration on pure white pap

关键讲解:

  • sensor.SensorId.ACCELEROMETER:传感器类型标识,加速度计固定用这个
  • data.x / data.y / data.z:三轴加速度值,单位 m/s²。手机静止平放时,z 轴约等于 9.8(重力),x/y 接近 0
  • { interval: 200000000 }:采样间隔,单位是纳秒。200000000ns = 200ms,也就是每秒采样 5 次。这个值后面详细讲
  • onoff 必须成对出现,页面销毁时一定记得 off,不然后台一直吃电

摇一摇实现

加速度计接好了,摇一摇的判断逻辑很直接——三轴加速度的绝对值之和超过阈值,就是"摇了"

完整代码

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

@Entry
@Component
struct ShakePage {
  @State shakeCount: number = 0
  @State isShaking: boolean = false
  private lastShakeTime: number = 0
  private SHAKE_THRESHOLD: number = 18    // 加速度阈值
  private SHAKE_INTERVAL: number = 500    // 两次摇动最小间隔(ms)
  private ACCEL_INTERVAL: number = 50000000  // 50ms采样间隔

  aboutToAppear() {
    this.startShakeDetection()
  }

  aboutToDisappear() {
    this.stopShakeDetection()
  }

  startShakeDetection() {
    try {
      sensor.on(sensor.SensorId.ACCELEROMETER, (data: sensor.AccelerometerResponse) => {
        const acceleration = Math.abs(data.x) + Math.abs(data.y) + Math.abs(data.z)
        const now = Date.now()

        if (acceleration > this.SHAKE_THRESHOLD && !this.isShaking) {
          if (now - this.lastShakeTime > this.SHAKE_INTERVAL) {
            this.isShaking = true
            this.lastShakeTime = now
            this.onShakeDetected()
            // 300ms后重置状态,避免一次摇动多次触发
            setTimeout(() => {
              this.isShaking = false
            }, 300)
          }
        }
      }, { interval: this.ACCEL_INTERVAL })
    } catch (error) {
      let e = error as BusinessError
      console.error(`启动检测失败: ${e.code}`)
    }
  }

  stopShakeDetection() {
    try {
      sensor.off(sensor.SensorId.ACCELEROMETER)
    } catch (error) {
      // ignore
    }
  }

  onShakeDetected() {
    this.shakeCount++
    // 震动反馈
    try {
      vibration.vibrate(vibration.VibrateType.VIBRATOR_LIGHT)
    } catch (e) {
      // 没有震动模块就算了
    }
  }

  build() {
    Column({ space: 20 }) {
      Text('摇一摇计数器')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)

      Text(`${this.shakeCount}`)
        .fontSize(72)
        .fontColor('#FF6D00')
        .fontWeight(FontWeight.Bold)

      Button('重置')
        .onClick(() => { this.shakeCount = 0 })

      Text('用力摇晃手机试试')
        .fontSize(14)
        .fontColor('#999999')
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

算法讲解

核心判断就这一行:

const acceleration = Math.abs(data.x) + Math.abs(data.y) + Math.abs(data.z)

三轴加速度取绝对值求和。静止时这个值约 9.8(纯重力),使劲摇的时候能到 25+。阈值设 18 是个经验值——正常走路大概 12-15,不会误触;稍微用点力摇就能到 18。

两个防抖机制缺一不可:

  1. SHAKE_INTERVAL = 500ms:两次触发至少间隔 500ms,防止快速连续触发
  2. isShaking 状态锁 + setTimeout(300ms):一次摇动只算一次,300ms 内不再响应

没有防抖的话,一次摇动会触发 5-10 次回调(因为采样间隔 50ms),计数器直接飞。

采样率与功耗

采样间隔怎么选?这事儿得权衡:

间隔采样率精度功耗适用场景
10000000ns (10ms)100Hz游戏、VR
50000000ns (50ms)20Hz摇一摇、手势识别
200000000ns (200ms)5Hz屏幕旋转、基础检测
1000000000ns (1s)1Hz很低很低后台监控

摇一摇用 50ms(20Hz)刚刚好——采样够密不会漏摇,功耗也不会炸。如果你做的是计步器那种长时间运行的,建议 200ms 甚至更低,不然电池扛不住。

真心建议:不用的时候一定 off。我见过一个 App 后台跑着传感器没关,4 小时掉电 20%。用户骂的你听不见。

其他传感器速览

加速度计学会了,其他传感器套路一模一样,换下 SensorId 就行:

  • 陀螺仪sensor.SensorId.GYROSCOPE,回调里 data.x/y/z 是角速度
  • 磁力计sensor.SensorId.MAGNETIC_FIELD,做指南针配合 GeomagneticField 算偏角
  • 接近光sensor.SensorId.PROXIMITY,通话时检测脸靠近就熄屏,data.distance
  • 环境光sensor.SensorId.AMBIENT_LIGHTdata.intensity 是勒克斯值
  • 计步器sensor.SensorId.PEDOMETERdata.steps 直接给你步数,不用自己算
  • 心率sensor.SensorId.HEART_RATE,需要额外权限,穿戴设备才支持

每种传感器的 on/off 用法完全一致,唯一区别是回调数据的字段名不同。官方文档里有每个传感器的 Response 类型定义,查一下就行。

写在最后

传感器开发真没那么难——on 订阅、回调拿数据、off 释放,三步走天下。摇一摇的实现核心就是加速度阈值判断 + 防抖,十几行代码搞定。但功耗控制和资源释放这些细节才是区分"能用"和"好用"的关键——不用就关、采样率别贪高、页面销毁一定 off

下次如果你做计步器、指南针或者体感游戏,回来翻这篇就行,套路都一样。Camera 那篇如果还没看,建议搭配着看,传感器 + 相机组合能做不少有趣的东西。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐