Android 传感器实践
前言
继续补充我练习模块的硬件部分,这里简单练一下传感器相关内容,感觉没什么好说的,憋来憋去,我只像到用传感器来判断下屏幕方向和指南针,也许游戏里面可以多用到,下次摸鱼写小游戏的时候再看看喽。
实际效果
这篇文章主要是用了下这些传感器,实际实践上就获取屏幕方向和做了一个指南针,就贴下指南针的效果吧:
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获取到的结果:
确实有几个没获取到,比如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一些常用的传感器写了一边,并且用传感器判断了下屏幕方向,做了一个指南针效果,内容较多,但是比较简单。