Android 传感器实践

2 阅读12分钟

Android 传感器实践

前言

继续补充我练习模块的硬件部分,这里简单练一下传感器相关内容,感觉没什么好说的,憋来憋去,我只像到用传感器来判断下屏幕方向和指南针,也许游戏里面可以多用到,下次摸鱼写小游戏的时候再看看喽。

实际效果

这篇文章主要是用了下这些传感器,实际实践上就获取屏幕方向和做了一个指南针,就贴下指南针的效果吧:

ezgif-5-e8bbf88788.gif

ps. 这里好像是指的正北-_-||,要验证这个方向的话,可以打开手机自带的指南针看下。

官方文档

官方的文档对这些个传感器讲的还是很详细的,很有阅读价值:

官方文档

这里也简单介绍下传感器,好有个概念,传感器分了三种:

  • 移动传感器,测量三个轴向上的加速力和旋转力
  • 环境传感器,测量各种环境参数
  • 位置传感器,测量设备的物理位置

有些传感器是特定的物理设备,如重力传感器、陀螺仪,也有复合传感器,比如旋转矢量传感器(Rotation Vector Sensor),它是通过陀螺仪、加速度计和磁力计的数据,来提供特定数据的。

传感器的数据有的也有经过处理的,也有原始数据需要自行处理的,比如陀螺仪,可能还要自己对速度进行积分运算德奥位置。

对于各类传感器的用途,官方文档的简介也有列出来,可以看下: 传感器简介

大致就上面这些我觉得可以提一下,下面开搞!

传感器简单使用

下面就简单用下一些传感器吧,主要就是拿到它们的数据,这里我先封装了一个辅助类,可以看下。

传感器辅助类

在对常见的一些传感器使用之前,我这封装了一个辅助类,后面用起来就会简单些:

import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import androidx.core.util.Consumer

/**
 * 传感器辅助类
 *
 * @author frankie
 * @date 2024-05-11
 */
class SensorHelper(
    context: Context
) {
    // 官方文档: https://developer.android.com/develop/sensors-and-location/sensors/sensors_overview?hl=zh-cn
    // 传感器服务(注意要在隐私协议标注,影响上架)
    private val mSensorManager: SensorManager by lazy {
        context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
    }

    // 存储传感器监听器
    private val mListeners: MutableMap<Int, SensorEventListener> = HashMap()

    /**
     * 获取对应类型默认传感器
     *
     * @param type 传感器类型,例如: Sensor.TYPE_ACCELEROMETER
     */
    @Suppress("MemberVisibilityCanBePrivate")
    fun getSensor(type: Int, wakeUp: Boolean = false): Sensor? {
        return mSensorManager.getDefaultSensor(type, wakeUp)
    }

    /**
     * 获取所有类型的传感器
     *
     * @return 所有支持的sensor
     */
    fun getAllSensor(): List<Sensor> {
        return mSensorManager.getSensorList(Sensor.TYPE_ALL)
    }

    /**
     * 注册传感器监听,记得及时关闭监听
     *
     * @param type 监听传感器的类型
     * @param listener 监听回调
     * @param samplingPeriodUs 指定获取传感器频率
     *      SENSOR_DELAY_FASTEST 最快,延迟最小,同时也最消耗资源
     *      SENSOR_DELAY_GAME 适合游戏的频率
     *      SENSOR_DELAY_NORMAL 正常频率
     *      SENSOR_DELAY_UI 适合普通应用的频率,省电低耗
     */
    @Suppress("MemberVisibilityCanBePrivate")
    fun listenTypeOf(
        type: Int,
        listener: SensorEventListener,
        samplingPeriodUs: Int = SensorManager.SENSOR_DELAY_NORMAL
    ) {
        val sensor = getSensor(type)
        val oldListener = mListeners[type]

        // 先解除旧的监听,再监听
        if (oldListener != null) {
            mSensorManager.unregisterListener(oldListener)
        }

        // 注册监听,将listener保存起来
        mSensorManager.registerListener(listener, sensor, samplingPeriodUs)
        mListeners[type] = listener
    }

    /**
     * 取消传感器监听
     *
     * @param type 监听传感器的类型
     */
    fun stopListenTypeOf(type: Int) {
        val listener = mListeners[type]
        mSensorManager.unregisterListener(listener)
    }

    /**
     * 直接监听传感器,等release自动关闭监听
     *
     * @param type 类型
     * @param callback 加速度结果
     */
    fun listenTypeOf(
        type: Int,
        callback: Consumer<SensorEvent?>
    ) {
        // 直接监听,等release里面关闭
        listenTypeOf(type, object : SensorEventListener {
            override fun onSensorChanged(event: SensorEvent?) {
                callback.accept(event)
            }

            override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
        })
    }

    /**
     * 释放资源
     */
    fun release() {
        // 取消所有监听
        mListeners.entries.forEach {
            val listener = it.value
            mSensorManager.unregisterListener(listener)
        }
        mListeners.clear()
    }
}

这个辅助类可以单独获取sensor,也可以主动监听/取消监听特定类型的传感器,或者傻瓜式监听传感器拿回调,下面就用他来简单看下常用的一些个传感器吧。

所有支持的传感器

每个手机的传感器的情况可能不太一样,可以先打印下看看:

override fun onResume() {
    super.onResume()
    // 打印所有支持的传感器
    sensorHelper.getAllSensor().forEach {
        Log.d("SensorInfo", "Name: ${it.name}, Type: ${it.type}")
    }
}

这里最好在onResume去获取sensor,有一些sensor需要在可见状态猜能获取到,下面是荣耀10获取到的结果:

image.png

确实有几个没获取到,比如TYPE_AMBIENT_TEMPERATURE(13)就没有,我这就获取不到温度。

加速度传感器

加速度传感器就是获取x、y、z三个轴上的加速度,这个速度是包含重力加速度的,禁止的时候可以看到一个9.8哈哈,使用代码如下:

// 测量在所有三个物理轴向(x、y 和 z)上施加在设备上的加速力(包括重力),以 m/s2 为单位。
sensorHelper.listenTypeOf(Sensor.TYPE_ACCELEROMETER) {
    it?.let { event ->
        val accelerationX = event.values[0] // X轴方向上的加速度值
        val accelerationY = event.values[1] // Y轴方向上的加速度值
        val accelerationZ = event.values[2] // Z轴方向上的加速度值
        
        binding.typeAccelerometer.text =
            "${getString(R.string.sensor_type_accelerometer)}: \naccelerationX=$accelerationX, accelerationY=$accelerationY, \naccelerationZ=$accelerationZ m/s2"
    }
}

温度传感器

// 以摄氏度 (°C) 为单位测量环境室温。
sensorHelper.listenTypeOf(Sensor.TYPE_AMBIENT_TEMPERATURE) {
    it?.let { event ->
        val temperature = event.values[0] // 温度值(摄氏度)
        binding.typeAmbientTemperature.text =
            "${getString(R.string.sensor_type_ambient_temperature)}: \ntemperature=$temperature °C"
    }
}

重力传感器

重力传感器就是显示把重力加速度分在x、y、z三个轴上的分量,合在一起就是重力加速度的大小:

// 测量在所有三个物理轴向(x、y、z)上施加到设备的重力(以 m/s2 为单位)。
sensorHelper.listenTypeOf(Sensor.TYPE_GRAVITY) {
    it?.let { event ->
        val gravityX = event.values[0] // X轴方向上的重力值
        val gravityY = event.values[1] // Y轴方向上的重力值
        val gravityZ = event.values[2] // Z轴方向上的重力值
        binding.typeGravity.text =
            "${getString(R.string.sensor_type_gravity)}: \ngravityX=$gravityX, \ngravityY=$gravityY, \ngravityZ=$gravityZ  m/s2"
    }
}

陀螺仪

陀螺仪应该是一个很有名的传感器,很多地方都会用到,但是要注意下这里得到的是某个轴上的角速度!

没错它测量得到的是三个速度,怎么说呢,用处很大,但是暂时我没找到利用的它的好例子:

// 测量设备围绕三个物理轴(x、y 和 z)中的各个方向的旋转速率(以 rad/s 为单位)。
sensorHelper.listenTypeOf(Sensor.TYPE_GYROSCOPE) {
    it?.let { event ->
        val gyroscopeX = event.values[0] // X轴方向上的角速度值
        val gyroscopeY = event.values[1] // Y轴方向上的角速度值
        val gyroscopeZ = event.values[2] // Z轴方向上的角速度值
        binding.typeGyroscope.text =
            "${getString(R.string.sensor_type_gyroscope)}: \ngyroscopeX=$gyroscopeX, \ngyroscopeY=$gyroscopeY, \ngyroscopeZ=$gyroscopeZ rad/s"
    }
}

环境亮度传感器

// 测量环境光级(照度),以 lx 为单位。
sensorHelper.listenTypeOf(Sensor.TYPE_LIGHT) {
    it?.let { event ->
        val light = event.values[0] // 光照强度值
        binding.typeLight.text =
            "${getString(R.string.sensor_type_light)}: \ngyroscopeX=$light lx"
    }
}

线性加速度传感器

线性加速度,我理解的就是拆除重力加速度后的传感器,毕竟我们计算速度的时候不需要重力加速度吧:

// 测量在所有三个物理轴向(x、y 和 z)上施加到设备的加速力(不包括重力),以 m/s2 为单位。
sensorHelper.listenTypeOf(Sensor.TYPE_LINEAR_ACCELERATION) {
    it?.let { event ->
        val linearAccelerationX = event.values[0] // X轴方向上的线性加速度值
        val linearAccelerationY = event.values[1] // Y轴方向上的线性加速度值
        val linearAccelerationZ = event.values[2] // Z轴方向上的线性加速度值
        binding.typeLinearAcceleration.text =
            "${getString(R.string.sensor_type_linear_acceleration)}: \nlinearAccelerationX=$linearAccelerationX, \nlinearAccelerationY=$linearAccelerationY, \nlinearAccelerationZ=$linearAccelerationZ m/s2"
    }
}

磁场传感器

如果只用重力加速度传感器和陀螺仪,我觉得是没办法辨别方向的,只能得到和手机相关的一些变量,如果要辨别现实中的方向,就一定要用到磁场传感器,它也是很多其他传感器的基础传感器。

磁场传感器得到的是磁场强度,要直接用的话还是比较麻烦的,看起来指南针应该用它来写,实际上并不会用它,因为其他复合传感器已经对磁场数据进行计算了,我们没必要在这里费工夫:

// 测量所有三个物理轴(x、y、z)的环境地磁场,以 μT 为单位。
sensorHelper.listenTypeOf(Sensor.TYPE_MAGNETIC_FIELD) {
    it?.let { event ->
        val magneticFieldX = event.values[0] // X轴方向上的磁场值
        val magneticFieldY = event.values[1] // Y轴方向上的磁场值
        val magneticFieldZ = event.values[2] // Z轴方向上的磁场值
        binding.typeMagneticField.text =
            "${getString(R.string.sensor_type_magnetic_field)}: \nmagneticFieldX=$magneticFieldX, \nmagneticFieldY=$magneticFieldY, \nmagneticFieldZ=$magneticFieldZ μT"
    }
}

方向传感器

方向传感器可以获得三个角度,看这三个角度的名称,还是很好理解的,方位角携带了现实位置信息,是手机平面和南北方向的夹角(根据我手机判断,0°应该就是正南方向),俯仰角就是我们正着拿着手机的倾斜角度,翻滚角就是手机侧着旋转的角度,可以自己多试试。

要说明一下的是TYPE_ORIENTATION被标记Deprecated了,推荐去TYPE_ROTATION_VECTOR获取,实际上都差不多:

// 测量设备围绕所有三个物理轴(x、y、z)旋转的度数(°)。
// 从 API 级别 3 开始,您可以结合使用重力传感器和地磁场传感器与 getRotationMatrix() 方法来获取设备的倾斜矩阵和旋转矩阵。
sensorHelper.listenTypeOf(Sensor.TYPE_ORIENTATION) {
    it?.let { event ->
        val azimuth = event.values[0] // 方位角(绕Z轴旋转的角度)
        val pitch = event.values[1] // 俯仰角(绕X轴旋转的角度)
        val roll = event.values[2] // 翻滚角(绕Y轴旋转的角度)

        binding.typeOrientation.text =
            "${getString(R.string.sensor_type_orientation)}: \nazimuth(方位角)=$azimuth, \npitch(俯仰角)=$pitch, \nroll(翻滚角)=$roll °"
    }
}

压强传感器

// 测量环境气压,以 hPa 或 mbar 为单位。
sensorHelper.listenTypeOf(Sensor.TYPE_PRESSURE) {
    it?.let { event ->
        val pressure = event.values[0] // 压力值(帕斯卡)
        binding.typePressure.text =
            "${getString(R.string.sensor_type_pressure)}: \npressure=$pressure hPa"
    }
}

距离传感器

我的荣耀10好像有这个传感器,但是没有收到回调信息,感觉这个可以用光线传感器来代替,不就是看看手机是不是放在耳边么,光线强度低就认为是在耳边喽?

// 测量物体相对于设备视图屏幕的距离(以 cm 为单位)。
// 该传感器通常用于确定手机是否被举到人的耳边。
sensorHelper.listenTypeOf(Sensor.TYPE_PROXIMITY) {
    it?.let { event ->
        val distance = event.values[0] // 距离值(单位根据传感器设置而定,通常是厘米)
        binding.typeProximity.text =
            "${getString(R.string.sensor_type_proximity)}: \ndistance=$distance cm"
    }
}

相对湿度传感器

// 测量环境的相对湿度,以百分比 (%) 表示。
sensorHelper.listenTypeOf(Sensor.TYPE_RELATIVE_HUMIDITY) {
    it?.let { event ->
        val humidity = event.values[0] // 相对湿度值(百分比)
        binding.typeRelativeHumidity.text =
            "${getString(R.string.sensor_type_relative_humidity)}: \nhumidity=$humidity %"
    }
}

旋转角度传感器

TYPE_ROTATION_VECTOR本来是获取三个轴上的旋转角度的,注意和陀螺仪的区别,陀螺仪是三个轴上的旋转速度,这里的角度数据貌似就是陀螺仪速度的积分得到的。

但是前面的TYPE_ORIENTATION被标记Deprecated了,这里用TYPE_ROTATION_VECTOR的数据来获取了下方位角、俯视角、翻滚角,感觉其中处理过程应该会用到磁场传感器,但是我们用它的数据就行了,后面就用它来做指南针:

// 通过提供设备旋转矢量的三个元素来测量设备的屏幕方向(弧度rad)。
sensorHelper.listenTypeOf(Sensor.TYPE_ROTATION_VECTOR) {
    it?.let { event ->
//                val azimuth = event.values[0] // X轴方向上的旋转弧度值
//                val pitch = event.values[1] // Y轴方向上的旋转弧度值
//                val roll = event.values[2] // Z轴方向上的旋转弧度值

        // 用来替换TYPE_ORIENTATION
        val rotationMatrix = FloatArray(9)
        SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values)

        // 根据旋转矩阵计算设备的欧拉角
        val orientation = FloatArray(3)
        SensorManager.getOrientation(rotationMatrix, orientation)

        val azimuth = Math.toDegrees(orientation[0].toDouble()) // 方位角(绕Z轴旋转的角度)
        val pitch = Math.toDegrees(orientation[1].toDouble()) // 俯仰角(绕X轴旋转的角度)
        val roll = Math.toDegrees(orientation[2].toDouble()) // 翻滚角(绕Y轴旋转的角度)

        binding.typeRotationVector.text =
            "${getString(R.string.sensor_type_rotation_vector)}: \nazimuth(方位角)=$azimuth, \npitch(俯仰角)=$pitch, \nroll(翻滚角)=$roll °"
    }
}

温度传感器(老)

这个也是被Deprecated了的类型,应该用上面的TYPE_AMBIENT_TEMPERATURE,看官方文档应该是一个指设备温度,一个指的是环境温度,可惜的是我的荣耀10都不支持...

// 测量设备的温度,以摄氏度 (°C) 为单位。
// 该传感器实现因设备而异,在 API 级别 14 中该传感器已替换为 TYPE_AMBIENT_TEMPERATURE 传感器
sensorHelper.listenTypeOf(Sensor.TYPE_TEMPERATURE) {
    it?.let { event ->
        val temperature = event.values[0] // 温度值(摄氏度)
        binding.typeTemperature.text =
            "${getString(R.string.sensor_type_temperature)}: \ntemperature=$temperature °C"
    }
}

步数传感器

Android官方貌似提供了获取开机后步数的传感器,按理来说也够我们用了,我的荣耀10也有这个传感器,但是我没办法获取数据,很奇怪,不知道是不是我把它的运动健康APP卸载了,毕竟微信运动也没数据。

看网上文章,如果要获取准确的步数,好像是要去集成厂商的SDK,咱们这就简单看下代码吧:

// 获取开机后的步数,不是很准,可以使用厂商的SDK
sensorHelper.listenTypeOf(Sensor.TYPE_STEP_COUNTER) {
    it?.let { event ->
        // 获取步数计数器传感器的步数值
        val stepCount = event.values[0].toInt()
        binding.typeStepCounter.text =
            "${getString(R.string.sensor_type_step_counter)}: \nstepCount=$stepCount"
    }
}

// 还得注册下???
sensorHelper.requestTriggerSensor(Sensor.TYPE_STEP_COUNTER) {
    it?.let {
        binding.typeStepCounter.text =
            "${getString(R.string.sensor_type_step_counter)}: \n激活成功!"
    }
}

传感器实践

我这用传感器写了两个小例子,一个是简单的横竖屏判断,另一个是指南针的,下面就简单看下吧,也想不出其他应用了。

判断屏幕方向

这里我用的重力加速度传感器,只要比较X和Y轴上的重力加速度就可以判断横竖屏了,不过最好还是别这么用,因为访问传感器会影响上架,要在隐私协议上写清楚。

// 测量在所有三个物理轴向(x、y、z)上施加到设备的重力(以 m/s2 为单位)。
sensorHelper.listenTypeOf(Sensor.TYPE_GRAVITY) {
    it?.let { event ->
        val gravityX = event.values[0] // X轴方向上的重力值
        val gravityY = event.values[1] // Y轴方向上的重力值
        val gravityZ = event.values[2] // Z轴方向上的重力值

        // 判断竖直
        val isPortrait = isDeviceInPortrait(event.values)
        val str = if (isPortrait) "Portrait" else "Landscape"

        binding.typeGravity.text =
            "${getString(R.string.sensor_type_gravity)}: $str\ngravityX=$gravityX, \ngravityY=$gravityY, \ngravityZ=$gravityZ  m/s2"
    }
}

// 判断设备是否处于竖直方向
private fun isDeviceInPortrait(accelerometerReading: FloatArray): Boolean {
    val x = accelerometerReading[0]
    val y = accelerometerReading[1]
    val z = accelerometerReading[2]

    // 判断条件根据实际情况调整
    return abs(x) < abs(y)
}

指南针

指南针这个也简单,前面也已经说到了,方位角是带实际位置信息的,就是和南北方向的夹角,下面看代码:

// 通过提供设备旋转矢量的三个元素来测量设备的屏幕方向(弧度rad)。
sensorHelper.listenTypeOf(Sensor.TYPE_ROTATION_VECTOR) {
    it?.let { event ->
//                val azimuth = event.values[0] // X轴方向上的旋转弧度值
//                val pitch = event.values[1] // Y轴方向上的旋转弧度值
//                val roll = event.values[2] // Z轴方向上的旋转弧度值

        // 用来替换TYPE_ORIENTATION
        val rotationMatrix = FloatArray(9)
        SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values)

        // 根据旋转矩阵计算设备的欧拉角
        val orientation = FloatArray(3)
        SensorManager.getOrientation(rotationMatrix, orientation)

        val azimuth = Math.toDegrees(orientation[0].toDouble()) // 方位角(绕Z轴旋转的角度)
        val pitch = Math.toDegrees(orientation[1].toDouble()) // 俯仰角(绕X轴旋转的角度)
        val roll = Math.toDegrees(orientation[2].toDouble()) // 翻滚角(绕Y轴旋转的角度)
        
        // 根据roll(水平旋转角度)来指定南方
        val level = ( - azimuth + 360) % 360 / 360f * 10000
        binding.typeRotationVectorIcon.background.level = level.roundToInt()

        binding.typeRotationVector.text =
            "${getString(R.string.sensor_type_rotation_vector)}: \nazimuth(方位角)=$azimuth, \npitch(俯仰角)=$pitch, \nroll(翻滚角)=$roll °"
    }
}

这里偷了下懒,用的RotationDrawable,利用它的level来实现旋转的:

    <TextView
        android:id="@+id/type_rotation_vector_icon"
        android:background="@drawable/ic_drawable_rotate"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="5dp"
        />

对应的RotationDrawable如下:

<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:pivotX="50%"
    android:pivotY="50%"
    android:fromDegrees="0"
    android:toDegrees="360"
    android:visible="true"
    >

    <vector
        xmlns:tools="http://schemas.android.com/tools"
        android:width="80dp"
        android:height="80dp"
        android:viewportWidth="24"
        android:viewportHeight="24"
        >

        <group
            android:name="circle"
            android:pivotX="12"
            android:pivotY="12"
            >

            <!--四段三阶贝塞尔曲线拟合一个圆-->
            <path
                android:name="vector_bezier_circle"
                android:strokeWidth="1"
                android:strokeColor="#9FBF3B"
                android:pathData="M4,12
                            C4,8,8,4,12,4
                            C16,4,20,8,20,12
                            C20,16,16,20,12,20
                            C8,20,4,16,4,12Z"
                tools:ignore="VectorRaster"
                />

        </group>

        <group
            android:pivotX="12"
            android:pivotY="12"
            android:name="heart"
            >

            <!--配合strokeLineCap和strokeLineJoin画一个爱心-->
            <path
                android:strokeWidth="1"
                android:strokeColor="#FF0000"
                android:strokeLineCap="round"
                android:strokeLineJoin="miter"
                android:strokeMiterLimit="4"
                android:pathData="M11.5,2 l0.5,0.5 l0.5,-0.5"
                />
        </group>

    </vector>

</rotate>

对Drawable使用有兴趣的可以看下我之前的文章: 《Android Drawable实践》

反正这样写,方位角的更新,就会触发RotationDrawable的level更新,它的0 - 360°,就对应着level从0变到10000,很简单,效果图在文章上面。

小结

花了点实践把Android一些常用的传感器写了一边,并且用传感器判断了下屏幕方向,做了一个指南针效果,内容较多,但是比较简单。