Android 传感器学习(一)——指南针的实现

Always make sure to disable sensors you don't need, especially when your activity is paused. Failing to do so can drain the battery in just a few hours. Note that the system will not disable sensors automatically when the screen turns off.
确保在你不需要使用传感器的时候关掉它,特别是在 Activity 暂停的时候。如若不然,几个小时就会耗尽电池电量。请注意,系统在关闭屏幕的时候是不会自动停止传感器的。
Google APIs -- SensorManager

智能手机经过近十年的发展,机身上集成了多种传感器(Sensor),一些传感器是基于硬件直接得出结果,另一些则是通过软件复合运算得出的。常见的重力计、陀螺仪以及距离感应计都属于硬件传感器,而方向计则是通过复合计算得出结果,属于软件传感器。

Sensor Description Common Uses TYPE_ORIENTATION 测量设备围绕三个物理轴(x,y,z)的旋转度。API 等级大于3的可以通过调用重力计和磁场计获取倾斜矩阵和旋转矩阵,再结合 getRotationMatrix() 方法获取方向矩阵 确定设备位置 TYPE_PRESSURE 测量周围的空气压力(hPa or mbar) 检测气压变化 TYPE_PROXIMITY 测量一个物件到屏幕的距离(cm).该传感器通常用于确定手机是否靠近人耳 通话时的设备位置 TYPE_RELATIVE_HUMIDITY 测量周围的湿度(%) 检测露点,绝对和相对湿度 TYPE_ROTATION_VECTOR 软件或硬件 通过提供设备的三个旋转矢量元素确定设备的方向 运动和旋转检测

获取传感器

要使用传感器,需要先获取手机的传感器服务。通过 getSystemService 创建一个 SensorManager 实例:

private SensorManager mSensorManager;
mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);

接着,可以传入TYPE_ALL参数获取传感器列表:

List<Sensor> deviceSensors = mSensorManager.getSensorList(Sensor.TYPE_ALL);

如果你想获取指定类型的传感器列表,可以通过传入其他的类型常量,如TYPE_GYROSCOPE或者TYPE_GRAVITY来取代TYPE_ALL.
所有支持的传感器,并不会在所有的手机上都存在,类似于气压计这样的传感器,目前也只在高端机型上存在。所以在调取传感器的时候,需要对其做代码检查,例如

private SensorManager mSensorManager;
mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
if(mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) != null){
  //Success!存在磁力计
}else{
  //Failure!不存在磁力计

监控传感器事件

要监控传感器事件,必须实现SensorEventListener接口的两个回调方法:onAccuracyChanged()onSensorChanged().

A sensor's accuracy changes.

传感器的精度变化会调用onAccuracyChanged()方法。精度有四个常量:SENSOR_STATUS_ACCURACY_LOW,SENSOR_STATUS_ACCURACY_MEDIUM,SENSOR_STATUS_ACCURACY_HIGHSENSOR_STATUS_UNRELIABLE

A sensor reports a new value.

传感器返回新的数据时候会调用onSensorChanged()方法,提供SensorEvent对象,该对象包含传感器的数据,包含数据精度,传感器,数据生成时间和传感器生成的数据。

接下来将通过一个指南针的例子来介绍传感器事件监听的运行机制。通过对 API 的查询了解,SENSOR_ORIENTATION已经在 API level 8废弃了,想要通过getDefaultSensor()方法直接获取方向参数已经不可取。官方给出的替代方法是一个静态(static)方法getOrientation(float[] R, float[] values).

getOrientation

基于旋转矩阵获取设备的方向。

getRotationMatrix

getRotationMatrix(float[] R, float[] I, float[] gravity, float[] geomagnetic)
计算倾斜矩阵I和旋转矩阵R并将其通过矢量变换从设备的坐标系转换成真实世界正交坐标系:

  • X 定义为Y·Z的向量积(在设备所在的位置和地面相切,并大致指向东方)。
  • Y 在设备所在的位置与地面相切,并指向磁北极。
  • Z 指向天空并垂直于地面。

    构建一个指南针

    在现实世界的正交坐标系中,磁北极是固定方向的。想要构造一个指南针应用,最直观的思考是获取到设备朝向和所处位置磁北极方向的夹角。这个时候,直观的思考就是正确的思考,如何获取这样一个夹角呢。通过上面介绍的 API 来看,getRotationMatrix()方法,能够获得设备的旋转矩阵和倾斜矩阵。旋转矩阵是一个33或者44的矩阵,通过getOrientation()方法就能直接获得设备关于正交系的三轴旋转数据。设备的指向必然是设备关于z轴的旋转角度。如何取值可以参考源码或者文档:

  • values[0]: Azimuth, angle of rotation about the -z axis. This value represents the angle between the device's y axis and the magnetic north pole. When facing north, this angle is 0, when facing south, this angle is π. Likewise, when facing east, this angle is π/2, and when facing west, this angle is -π/2. The range of values is -π to π.
  • values[1]: Pitch, angle of rotation about the x axis. This value represents the angle between a plane parallel to the device's screen and a plane parallel to the ground. Assuming that the bottom edge of the device faces the user and that the screen is face-up, tilting the top edge of the device toward the ground creates a positive pitch angle. The range of values is -π to π.
  • values[2]: Roll, angle of rotation about the y axis. This value represents the angle between a plane perpendicular to the device's screen and a plane perpendicular to the ground. Assuming that the bottom edge of the device faces the user and that the screen is face-up, tilting the left edge of the device toward the ground creates a positive roll angle. The range of values is -π/2 to π/2.
  • 可知,此时此刻的values[0]正是我们想要的值。回归到代码的世界,是这样的:

    public class SensorActivity extends Activity implements SensorEventListener{
        //sensor object
        private SensorManager mSensorManager;
        private Sensor mAccelerometer;
        private Sensor mMagneticField;
        //sensor data
        private float[] r = new float[9];   //rotation matrix
        private float[] values = new float[3]   //orientation values
        private float[] accelerometerValues = new float[3]  //data of acclerometer sensor
        private float[] magneticFieldValues = new fooat[3]  //data of magnetic field sensor
        @Ovrride
        protected void onCreate(Bundle savedInstanceState) {
            //init sensor
            mSensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
                mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
                mMagneticField = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
            @Override
            protected void onResume() {
                super.onResume();
            //regist listener
                mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_NORMAL);
                mSensorManager.registerListener(this, mMagneticField, SensorManager.SENSOR_DELAY_NORMAL);
        @Override
            protected void onPause() {
                super.onPause();
            //unregist listener
                mSensorManager.unregisterListener(this);
        @Override
            public void onSensorChanged(SensorEvent sensorEvent) {
                if (sensorEvent.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {    
                    accelerometerValues = sensorEvent.values;
                if (sensorEvent.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) { 
                    magneticFieldValues = sensorEvent.values;
                SensorManager.getRotationMatrix(r, null, accelerometerValues, magneticFieldValues);
                SensorManager.getOrientation(r, values);
                values[0] = (float) Math.toDegrees(values[0]);
            @Override
            public void onAccuracyChanged(Sensor sensor, int i) {