文章转载地址:http://www.2cto.com/kf/201412/359292.html
一、前言
本篇blog是我的“Android 进阶”的第一篇文章,从初学Android到现在断断续续也有4个多月时间了,也算是有了一些自己的心得体会,也能自己独立做一些东西了,这都要感谢我们公司的安卓 开发璟博和无所不能的鸿洋给我的帮助和指点。本系列blog将记录我在开发中、学习中遇到的较为重点的、值得记录的知识点和技巧,简单的说就不再是基础教程了。由于项目中需要用到方向传感器,所以就借此机会来学一学Android的传感器部分的知识了,自然也就是本篇blog的内容了。
二、传感器基础
官方文档说的很清楚,Android平台支持三大类的传感器,它们分别是:
a. Motion sensors
b. Environmental sensors
c. Position sensors
从另一个角度划分,安卓的传感器又可以分为基于硬件的和基于软件的。基于硬件的传感器往往是通过物理组件去实现的,他们通常是通过去测量特殊环境的属性获取数据,比如:重力加速度、地磁场强度或方位角度的变化。而基于软件的传感器并不依赖物理设备,尽管它们是模仿基于硬件的传感器的。基于软件的传感器通常是通过一个或更多的硬件传感器获取数据,并且有时会调用虚拟传感器或人工传感器等等,线性加速度传感器和重力传感器就是基于软件传感器的例子。下面通过官方的一张图看看安卓平台支持的所有传感器类型:
使用传感器的话那么首先需要了解的必然是传感器API了,在Android中传感器类是通过Sensor类来表示的,它属于android.hardware包下的类,顾名思义,和硬件相关的类。传感器的API不复杂,包含3个类和一个接口。
右上角标有1的是在Android1.5版本添加的,并且在Android2.3之后就无法使用。
右上角标有2的是已经过时的。
很明显,我们需要用到的方向传感器TYPE_ORIENTATION 就已经过时了,后面再说用什么来替代它。
最后看一下常用的传感器的方法:
1.实例化SensorManager
SensorManager mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
2.获取设备支持的全部Sensor的List
List<sensor> deviceSensors = mSensorManager.getSensorList(Sensor.TYPE_ALL);</sensor>
下面就通过这两个方法看一下手机支持哪些传感器,并以列表数据展示出来,代码很简单:
1 2 3 package com.example.sensordemo; import java.util.ArrayList; import java.util.List; import android.app.Activity; import android.content.Context; import android.hardware.Sensor; import android.hardware.SensorManager; import android.os.Bundle; import android.widget.ArrayAdapter; import android.widget.ListView; public class MainActivity extends Activity { private SensorManager mSensorManager; private ListView sensorListView; private List<sensor> sensorList; @Override protected void onCreate(Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); sensorListView = (ListView) findViewById(R.id.lv_all_sensors); mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); sensorList = mSensorManager.getSensorList(Sensor.TYPE_ALL); List<string> sensorNameList = new ArrayList<string>(); for (Sensor sensor : sensorList) { sensorNameList.add(sensor.getName()); } ArrayAdapter<string> adapter = new ArrayAdapter<string>( this , android.R.layout.simple_list_item_1, sensorNameList); sensorListView.setAdapter(adapter); } } </string></string></string></string></sensor>
运行结果:
了解了传感器的基础知识,下面我们就具体看看我们需要用到的Orientation Sensor。
三、Orientation Sensor
安卓平台提供了2个传感器用于让我们判断设备的位置,分别是地磁场传感器(the geomagnetic field sensor)和方向传感器(the orientation sensor)。关于Orientation Sensor在官方文档中的概述里有这样一句话:
The orientation sensor is software-based and derives its data from the accelerometer and the geomagnetic field sensor. (方向传感器是基于软件的,并且它的数据是通过加速度传感器和磁场传感器共同获得的)
至于具体算法 Android平台已经封装好了我们不必去关心实现,下面在了解方向传感器之前我们还需要了解一个重要的概念:传感器坐标系统(Sensor Coordinate System)。
在Android平台中,传感器框架通常是使用一个标准的三维坐标系去表示一个值的。以方向传感器为例,确定一个方向当然也需要一个三维坐标,毕竟我们的设备不可能永远水平端着吧,准确的说android给我们返回的方向值就是一个长度为3的float数组,包含三个方向的值。下面看一下官方提供的传感器API使用的坐标系统示意图:
仔细看一下这张图,不难发现,z是指向地心的方角,x轴是仰俯角(由静止状态开始前后反转),y轴是翻转角(由静止状态开始左右反转) 。下面切入正题,看看如何通过方向传感器API去获取方向。结合上面的图再看看官方提供的方向传感器事件的返回值:
依旧参考官方文档Using the Orientation Sensor部分内容,首先是实例化一个方向传感器:
mOrientation = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
虽然这样做没错,但是如果你在IDE中写了这样一行代码的话,不难发现它已经过期了,但是没关系,我们先用这个看看,后面再介绍代替它的方法。
下面是创建一个自定义传感器事件监听接口:
class MySensorEventListener implements SensorEventListener { @Override public void onSensorChanged(SensorEvent event) { float a = event.values[ 0 ]; azimuthAngle.setText(a + "" ); float b = event.values[ 1 ]; pitchAngle.setText(b + "" ); float c = event.values[ 2 ]; rollAngle.setText(c + "" ); } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { } }
最后通过SensorManager为Sensor注册监听即可:
mSensorManager.registerListener( new MySensorEventListener(), mOrientation, SensorManager.SENSOR_DELAY_NORMAL);
当设备位置发生变化时触发监听,界面上的值改变,由于模拟器无法演示传感器效果,真机也没有root没法用屏幕投影啥的,
所以就贴一张截图象征性看一下,这几个值无时无刻都在变化:
由于我在截图的时候手机是水平端平的,所以后两个值都接近于0,而第一个方位角就代表当前的方向了,好了,现在功能基本算实现了,
那么现在就解决一下Sensor类的常量过期的问题。不难发现,在IDE中这行代码是这样的:
mOrientation = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
既然过期了必定有新的东西去替代它,我们打开源代码可以看到这样的注释:
显而易见,官方推荐我们用SensorManager.getOrientation()这个方法去替代原来的TYPE_ORITNTATION。那我们继续在源码中看看这个方法:
public static float [] getOrientation( float [] R, float values[]) { if (R.length == 9 ) { values[0 ] = ( float )Math.atan2(R[ 1 ], R[ 4 ]); values[1 ] = ( float )Math.asin(-R[ 7 ]); values[2 ] = ( float )Math.atan2(-R[ 6 ], R[ 8 ]); } else { values[0 ] = ( float )Math.atan2(R[ 1 ], R[ 5 ]); values[1 ] = ( float )Math.asin(-R[ 9 ]); values[2 ] = ( float )Math.atan2(-R[ 8 ], R[ 10 ]); } return values; }
再看一下这个方法的注释中的一句话:
第一行讲了这个方法的作用,计算设备的方向基于旋转矩阵 ,这个旋转矩阵我们当成一种计算方向的算法就OK了,不必深究,下面再看我
标出来的这句话,很明显说明了我们通常不需要 这个方法的返回值,这个方法会根据参数R[ ]的数据填充values[ ]而后者就是我们想要的。
既然不需要返回值,那么就是参数的问题了,这两个参数:float[ ] R 和 float[ ] values该怎么获取呢?继续看注释,首先是第一个参数
R:
既然这个方法是基于旋转矩阵去计算方向,那么第一个参数R自然就表示一个旋转矩阵了,实际上它是用来保存磁场和加速度的数据的,根据
注释我们可以发现让我们通过getRotationMatrix这个方法来填充这个参数R[ ],那我们就再去看看这个方法源码,依旧是SensorManager的一
个静态方法:
public static boolean getRotationMatrix( float [] R, float [] I, float [] gravity, float [] geomagnetic) { float Ax = gravity[ 0 ]; float Ay = gravity[ 1 ]; float Az = gravity[ 2 ]; final float Ex = geomagnetic[ 0 ]; final float Ey = geomagnetic[ 1 ]; final float Ez = geomagnetic[ 2 ]; float Hx = Ey*Az - Ez*Ay; float Hy = Ez*Ax - Ex*Az; float Hz = Ex*Ay - Ey*Ax; final float normH = ( float )Math.sqrt(Hx*Hx + Hy*Hy + Hz*Hz); if (normH < 0 .1f) { return false ; } final float invH = 1 .0f / normH; Hx *= invH; Hy *= invH; Hz *= invH; final float invA = 1 .0f / ( float )Math.sqrt(Ax*Ax + Ay*Ay + Az*Az); Ax *= invA; Ay *= invA; Az *= invA; final float Mx = Ay*Hz - Az*Hy; final float My = Az*Hx - Ax*Hz; final float Mz = Ax*Hy - Ay*Hx; if (R != null ) { if (R.length == 9 ) { R[0 ] = Hx; R[ 1 ] = Hy; R[ 2 ] = Hz; R[3 ] = Mx; R[ 4 ] = My; R[ 5 ] = Mz; R[6 ] = Ax; R[ 7 ] = Ay; R[ 8 ] = Az; } else if (R.length == 16 ) { R[0 ] = Hx; R[ 1 ] = Hy; R[ 2 ] = Hz; R[ 3 ] = 0 ; R[4 ] = Mx; R[ 5 ] = My; R[ 6 ] = Mz; R[ 7 ] = 0 ; R[8 ] = Ax; R[ 9 ] = Ay; R[ 10 ] = Az; R[ 11 ] = 0 ; R[12 ] = 0 ; R[ 13 ] = 0 ; R[ 14 ] = 0 ; R[ 15 ] = 1 ; } } if (I != null ) { final float invE = 1 .0f / ( float )Math.sqrt(Ex*Ex + Ey*Ey + Ez*Ez); final float c = (Ex*Mx + Ey*My + Ez*Mz) * invE; final float s = (Ex*Ax + Ey*Ay + Ez*Az) * invE; if (I.length == 9 ) { I[0 ] = 1 ; I[ 1 ] = 0 ; I[ 2 ] = 0 ; I[3 ] = 0 ; I[ 4 ] = c; I[ 5 ] = s; I[6 ] = 0 ; I[ 7 ] =-s; I[ 8 ] = c; } else if (I.length == 16 ) { I[0 ] = 1 ; I[ 1 ] = 0 ; I[ 2 ] = 0 ; I[4 ] = 0 ; I[ 5 ] = c; I[ 6 ] = s; I[8 ] = 0 ; I[ 9 ] =-s; I[ 10 ]= c; I[3 ] = I[ 7 ] = I[ 11 ] = I[ 12 ] = I[ 13 ] = I[ 14 ] = 0 ; I[15 ] = 1 ; } } return true ; }
依旧是4个参数,请观察 30~41行 之间的代码,不难发现这个旋转矩阵无非就是一个 3*3 或 4*4 的数组,再观察一下if语句块中的代码,不难发
现给数组元素依次赋值,而这些值是从哪里来的呢?我们 29行 倒着看,直到第 4行 ,不难发现其实最后的数据源是通过这个方法的后两个参数
提供的,即:float[] gravity, float[] geomagnetic,老规矩,看看这两个参数的注释:
到这里应该就清晰了吧,分别是从加速度传感器和地磁场传感器获取的值,那么很明显,应当在监听中的回调方法onSensorChanged中去获取
数据,同时也验证了上面的判断方向需要两个传感器 的说法,分别是:加速度传感器(Sensor.TYPE_ACCELEROMETER)和地磁场传感器
(TYPE_MAGNETIC_FIELD)。
说完了getRotationMatrix方法的后两个参数,那么前两个参数R 和I 又该如何定义呢?其实很简单,第一个参数R 就是getOrientation()方法
中需要填充的那个数组,大小是9。而第二个参数I 是用于将磁场数据转换进实际的重力坐标系中的,一般默认设置为NULL即可。到这里关于
方向传感器基本就已经介绍完毕,最后看一个完整的例子:
package com.example.sensordemo; import android.app.Activity; import android.content.Context; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.os.Bundle; import android.util.Log; import android.widget.TextView; public class MainActivity extends Activity { private SensorManager mSensorManager; private Sensor accelerometer; private Sensor magnetic; private TextView azimuthAngle; private float [] accelerometerValues = new float [ 3 ]; private float [] magneticFieldValues = new float [ 3 ]; private static final String TAG = "---MainActivity" ; @Override protected void onCreate(Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); accelerometer = mSensorManager .getDefaultSensor(Sensor.TYPE_ACCELEROMETER); magnetic = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); azimuthAngle = (TextView) findViewById(R.id.azimuth_angle_value); calculateOrientation(); } @Override protected void onResume() { mSensorManager.registerListener(new MySensorEventListener(), accelerometer, Sensor.TYPE_ACCELEROMETER); mSensorManager.registerListener(new MySensorEventListener(), magnetic, Sensor.TYPE_MAGNETIC_FIELD); super .onResume(); } @Override protected void onPause() { mSensorManager.unregisterListener(new MySensorEventListener()); super .onPause(); } private void calculateOrientation() { float [] values = new float [ 3 ]; float [] R = new float [ 9 ]; SensorManager.getRotationMatrix(R, null , accelerometerValues, magneticFieldValues); SensorManager.getOrientation(R, values); values[0 ] = ( float ) Math.toDegrees(values[ 0 ]); Log.i(TAG, values[0 ] + "" ); if (values[ 0 ] >= - 5 && values[ 0 ] < 5 ) { azimuthAngle.setText("正北" ); } else if (values[ 0 ] >= 5 && values[ 0 ] < 85 ) { azimuthAngle.setText("东北" ); } else if (values[ 0 ] >= 85 && values[ 0 ] <= 95 ) { azimuthAngle.setText("正东" ); } else if (values[ 0 ] >= 95 && values[ 0 ] < 175 ) { azimuthAngle.setText("东南" ); } else if ((values[ 0 ] >= 175 && values[ 0 ] <= 180 ) || (values[0 ]) >= - 180 && values[ 0 ] < - 175 ) { azimuthAngle.setText("正南" ); } else if (values[ 0 ] >= - 175 && values[ 0 ] < - 95 ) { azimuthAngle.setText("西南" ); } else if (values[ 0 ] >= - 95 && values[ 0 ] < - 85 ) { azimuthAngle.setText("正西" ); } else if (values[ 0 ] >= - 85 && values[ 0 ] < - 5 ) { azimuthAngle.setText("西北" ); } } class MySensorEventListener implements SensorEventListener { @Override public void onSensorChanged(SensorEvent event) { if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) { accelerometerValues = event.values; } if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) { magneticFieldValues = event.values; } calculateOrientation(); } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { } } }
四、总结
第一次研究Android 传感器感觉还是挺难的,继续努力吧,要学的东西还有很多,先去给Map项目集成指南针啦~