25、LiveData与Sensor:传感器数据监听、加速度计数据展示、实战:计步器

传感器开发,在Android里一直是个「看起来简单,做起来坑多」的领域。尤其是加速度计,你想想看,手机放桌上不动,它还在输出数值——因为重力还在。很多新手第一次拿到的数据,直接懵了。

我个人习惯把传感器和LiveData搭配使用。为什么?因为传感器的数据是持续变化的,而LiveData天生就适合这种「观察者模式」。说白了,它就是为这种场景量身定做的。

传感器基础:加速度计到底在测什么?

加速度计测量的是三个轴上的加速度,单位是m/s²。但有个关键点:它测的是「相对于自由落体的加速度」。所以手机平放桌上时,Z轴会显示约9.8 m/s²——那是重力。

我在项目中遇到过一个问题:测试人员说「手机没动,步数却在涨」。排查了半天,原来是加速度计的阈值设得太低了。手机放桌上,轻微的震动都被当成了步数。

核心概念:加速度计返回的是三个浮点数(x, y, z),分别对应设备的三个轴向。静止状态下,数值是重力在各轴上的分量。

用LiveData封装传感器数据

直接使用SensorEventListener,代码会散落在Activity或Fragment里。我建议用LiveData把它包装起来,这样任何观察者都能优雅地订阅数据。

class AccelerometerLiveData(context: Context) : LiveData<SensorEvent>() {

    private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
    private val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)

    private val listener = object : SensorEventListener {
        override fun onSensorChanged(event: SensorEvent?) {
            event?.let { postValue(it) }
        }

        override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
            // 精度变化时,我一般会打个日志
        }
    }

    override fun onActive() {
        sensorManager.registerListener(listener, accelerometer, SensorManager.SENSOR_DELAY_NORMAL)
    }

    override fun onInactive() {
        sensorManager.unregisterListener(listener)
    }
}

注意看,onActive()onInactive() 是LiveData的生命周期回调。当有观察者开始观察时,自动注册监听;没有观察者时,自动注销。这比手动管理生命周期优雅太多了。

我的习惯:传感器采样率不要一味追求快。SENSOR_DELAY_NORMAL 对计步器来说足够了。调成 GAME 或 FASTEST,电池会哭的。

加速度计数据展示:从原始数据到可视化

拿到原始数据后,直接显示三个浮点数,用户看不懂。我一般会做两件事:

  1. 计算合加速度: sqrt(x² + y² + z²),然后减去重力(约9.8),得到「净加速度」。
  2. 做平滑处理: 用滑动平均或低通滤波,去掉高频噪声。
fun calculateNetAcceleration(event: SensorEvent): Float {
    val x = event.values[0]
    val y = event.values[1]
    val z = event.values[2]
    val raw = sqrt(x * x + y * y + z * z)
    return raw - 9.8f  // 减去重力
}

展示的时候,我习惯用三个进度条或三个圆形指示器,分别显示x、y、z轴的数值。颜色用红绿蓝区分,用户一眼就能看出手机的姿态。

实战:计步器的核心逻辑

计步器的原理,说白了就是检测「走路时的冲击波」。人走路时,合加速度会呈现一个波峰-波谷的周期性变化。我们只需要检测波峰,然后计数。

我曾经踩过一个坑:把阈值设成固定值。结果不同的人走路,步幅和力度完全不同。后来我改用动态阈值——根据最近一段时间的最大值和最小值,实时调整判断标准。

避坑指南:不要只检测波峰。手机在口袋里晃动、坐车颠簸,都会产生波峰。一定要加「时间间隔过滤」——两次有效步数之间至少间隔200毫秒。
class StepCounterViewModel(context: Context) : ViewModel() {

    private val accelerometerLiveData = AccelerometerLiveData(context)
    private val _stepCount = MutableLiveData<Int>(0)
    val stepCount: LiveData<Int> = _stepCount

    private var lastPeakTime = 0L
    private var lastAcceleration = 0f
    private var threshold = 2.0f  // 动态调整

    fun startCounting() {
        accelerometerLiveData.observeForever { event ->
            val netAcc = calculateNetAcceleration(event)
            val currentTime = System.currentTimeMillis()

            // 检测波峰:当前值大于阈值,且比上一个值大
            if (netAcc > threshold && netAcc > lastAcceleration) {
                if (currentTime - lastPeakTime > 200) {  // 200ms防抖
                    _stepCount.value = (_stepCount.value ?: 0) + 1
                    lastPeakTime = currentTime
                }
            }

            // 动态更新阈值(取最近100ms内的最大值的一半)
            // 这里简化处理,实际项目中用队列维护滑动窗口
            lastAcceleration = netAcc
        }
    }

    override fun onCleared() {
        super.onCleared()
        accelerometerLiveData.removeObserver { }
    }
}

嗯,这里要注意:observeForever 不会自动注销,所以一定要在 onCleared() 里手动移除。否则ViewModel都销毁了,传感器还在跑,内存泄漏妥妥的。

知识体系总览

下面这张图,把本章的核心脉络梳理了一遍。从传感器硬件到LiveData封装,再到计步器业务逻辑,每一步都有它的设计考量。

LiveData与传感器:知识体系 加速度计硬件 Sensor.TYPE_ACCELEROMETER LiveData包装器 onActive / onInactive ViewModel 业务逻辑 原始数据 (x, y, z) 合加速度计算 波峰检测 + 防抖 步数计数 关键点:动态阈值 | 时间间隔过滤 | 生命周期管理 | 重力补偿 避坑:不要固定阈值、不要忽略重力、记得注销监听

总结一下

传感器开发,本质上就是「原始数据 → 特征提取 → 业务判断」的流水线。LiveData在这里扮演了数据管道的角色,让整个流程变得干净、可测试。

我个人觉得,计步器是理解传感器最好的入门项目。它不复杂,但涵盖了滤波、阈值、防抖这些通用技术。你把这个搞懂了,后面做手势识别、姿态检测,思路都是一样的。

一个小建议:调试传感器时,把数据实时打印到Logcat,然后用手机晃一晃,观察数值变化。这比盯着代码看有效十倍。
公众号:蓝海资料掘金营,微信deep3321