2012年10月19日金曜日

这篇文章写的传感器数据从驱动传递到应用程序的整个流程,还有数据校正的问题。

应用程序怎么样设置可以让自己随着设备的倾斜度变化而旋转方向呢?在AndroidManifest.xml文件中的android:screenOrientation就可以了。这里追踪一下它的内部机制。
先看一个最关键的部件:/frameworks/base/core/java/android/view/WindowOrientationListener.java
这个接口注册一个accelerator,并负责把accelerator的数据转化为orientation。这个API对应用程序不公开,我看Android2.3的源码时发现只有PhoneWindowManager使用到它了。
/frameworks/base/policy/../PhoneWindowManager.java
PhonwWindowManager注册了一个WindowOrientationListener,就可以异步获取当前设备的orientation了。再结合应用程序在AndroidManifest.xml中设置的值来管理着应用程序界面的旋转方向。以下是PhoneWindowManager.java中相关的两个代码片段。
  1. public void onOrientationChanged(int rotation) {  
  2.             // Send updates based on orientation value  
  3.             if (localLOGV) Log.v(TAG, "onOrientationChanged, rotation changed to " +rotation);  
  4.             try {  
  5.                 mWindowManager.setRotation(rotation, false,  
  6.                         mFancyRotationAnimation);  
  7.             } catch (RemoteException e) {  
  8.                 // Ignore  
  9.   
  10.   
  11.             }  
  12.         }  
  13. ... ...  
  14. switch (orientation) {//这个值就是当前设备屏幕的旋转方向,再结合应用程序设置的android:configChanges属性值就可以确定应用程序界面的旋转方向了。应用程序设置值的优先级大于传感器确定的优先级。  
  15.                 case ActivityInfo.SCREEN_ORIENTATION_PORTRAIT:  
  16.                     //always return portrait if orientation set to portrait  
  17.                     return mPortraitRotation;  
  18.                 case ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE:  
  19.                     //always return landscape if orientation set to landscape  
  20.                     return mLandscapeRotation;  
  21.                 case ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT:  
  22.                     //always return portrait if orientation set to portrait  
  23.                     return mUpsideDownRotation;  
  24.                 case ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE:  
  25.                     //always return seascape if orientation set to reverse landscape  
  26.                     return mSeascapeRotation;  
  27.                 case ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE:  
  28.                     //return either landscape rotation based on the sensor  
  29.                     mOrientationListener.setAllow180Rotation(  
  30.                             isLandscapeOrSeascape(Surface.ROTATION_180));  
  31.                     return getCurrentLandscapeRotation(lastRotation);  
  32.                 case ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT:  
  33.                     mOrientationListener.setAllow180Rotation(  
  34.                             !isLandscapeOrSeascape(Surface.ROTATION_180));  
  35.                     return getCurrentPortraitRotation(lastRotation);  
  36.             }  

让应用程序随屏幕方向自动旋转的实现原理就这么交待完了。我解决这一步时也没有费多少力气,在板子上打开SensorTest,对比一下XYZ三个轴和MileStone上面的数据,修改一下正负值就可以了。但要解决Teeter运行时Z轴反转的问题,还得深层次挖一挖。

PhoneWindowManager.java中有这么一句:
mWindowManager.setRotation(rotation, false, mFancyRotationAnimation);
当PhonewindowManager通过WindowOrientationListener这个监听器得知屏幕方向改变时,会通知给WindowManagerService(/frameworks/base/service/../WindowManagerService.java)
WindowManagerService中有这么一个监听器集合:mRotationWatchers,谁想监听屏幕方向改变,就会在这里注册一个监听器。SensorManager就这么干了。然后,通过下面这个异步方法获知当前的屏幕方向
  1. public void onRotationChanged(int rotation) {  
  2.         synchronized(sListeners) {  
  3.             sRotation  = rotation;  
  4.         }  
  5.     }  
  6. static int getRotation() {  
  7.         synchronized(sListeners) {  
  8.             return sRotation;  
  9.         }  
  10.     }  

SensorManager要这个值有什么作用呢?看看在哪里使用了SensorManager.getRotation()吧。只有一个方法:mapSensorDataToWindow
当一个Activity注册了一个Sensor事件监听器后,总是会通过接口来异步获取sensor事件的。在这里,新老版本出现了分化。老版本中,Android1.5以前,Sensor事件被分发给监听者(onSensorChanged)之前,总会先用这个方法处理一下。新版本的监听接口是SensorEventListener,分发前是没有处理方法的。看一下这个方法,原来是转换坐标系用的。应用程序的界面方向随屏幕发生变化以后,通过异步分发接口传递给它的传感器数据也要从传感器的坐标系转换到应用程序的坐标系。假设屏幕默认方向是竖屏,这个时候分发给它的SensorEvent里面的值与frameworks层从HAL的sensor.c中读到的数据是一样的。当设备右侧抬起,屏幕切换到横屏是,应用程序的界面也旋转了90度,这个时候,SensorEvent在分发给应用程序之前就需要先把自己的坐标系顺时针旋转90度。
新版本接口中,传感器数据直接通过SensorEventListener分发给应用程序。而老版本接口中,分发之前先要结合当前设备的旋转方向对传感器数据做一个坐标系转换。到这里,一切都清楚了。WindowOrientationListener借助SensorManager的accelerator数据制造了屏幕旋转方向,而屏幕旋转方向又被SensorManager用来兼容老版本的SensorListener接口。可以说,如果不考虑兼容老版本的接口的话,SensorManager是完全不用向WindowManagerService.java中注册监听器监听当前设备屏幕旋转方向的,直接分发下去就好了。SensorManager的代码恐怕要减少一半多。framework层是时候把SensorListener相关的一系列API扔到一边了。
还有一个地方,就是HAL层的sensor.c到SensorManager之间的部分。这个部分把sensor.c中读到的传感器数据整合成一个服务(SensorService)供SensorManager使用。很好地隔离了API层和HAL层。但从数据处理的角度来讲,只是扮演了一个数据传递者的角色,没有对数据进行任何的改变。
上面的写完了,接下来是HAL层了。各个厂商的写法都不一样,有的为了把所有传感器集成进来,还形成了自己的一个框架。让我们穿过HAL框架,直接进入sensor.c。这里有我最关心的最终如何与sensor的driver交互,向上层传递了哪些信息。再复杂的frameworks,也不过是把sensor.c提供的接口封装一下而己。sensor的数据从来没有被改变过。这里只是简述一下sensor.c的大致功能,更详细的分析可以参考一下这一篇文章(http://blog.csdn.net/a345017062/article/details/6558401),那里我写了一个可以通过ADB或者串口运行的C++程序专门演示控制driver和读取数据的细节。
1、给上层提供一个获取sensor list的接口。这个是写死在sensor.c里面的。往一个设备上面移植frameworks时,这一部分是要根据设备上的sensor来修改这个文件的。
2、给上层提供控制接口:active/deactive某一个sensor。set某个sensor的delay值(即,获取sensor数据的频率,比如设置为200,000的话,就是驱动每隔200毫秒向上面发一次数据)。读取某个sensor的数据。
现在我们知道了,调试传感器时,只在两个地方下手就可以了:
1、/frameworks/base/core/java/android/view/WindowOrientation.java,校正accelerator数据与屏幕旋转方向的对应关系。
2、/hardware/libhardware/modules/sensor/sensor.c,对驱动递上来的数据进行初步校正。这一步可以参考一下已经的校正好的机器(我用的是自己的MileStone),然后再运行一下SensorTest(网上有的下),只要同一个摆放姿势下,我们读上来的数据和它的一样就可以了。因为前面说过,在新版本(1.5及以上)接口中,数据流经过sensor.c->SensorService->SensorManager,最后通过onSensorChanged分发给应用程序的整个过程中,是从来没有被改变过的。至于老版本中SensorManager部分做的校正,让他吃屎去吧。(我一开始在这里做了很多的工作,最后发现从1.5就已经deprecate这个接口了,请允许我再一次的Shit!!!)。

好了,这篇文章总算是写完了。别闲我�嗦,我再说最后一次:sensor数据自从被sensor.c从driver读上来,一直到传递给应用程序的onSensorChanged接口,整个过程中,数据都没有被改变过。这很重要,因为这意味着frameworks层是不需要sensor校正的。我们只需要在WindowOrientationListener里面找到那两个数组(THRESHOLDS和ROTATE_TO),然后调调屏幕旋转方向就可以了。

补充于2011.7.25

兼容传感器老接口时出现的问题。
屏幕旋转后,传感器数据也要变的坐标系,这和以前的理解不一样,得纠正一下。
调试好sensor后,屏幕可以正常旋转了,但HTC手机自带的Teeter运行起来有问题。跟踪了一下,发现Teeter还在使用旧的传感器监听接口onSensorChanged(int sensor, float[] value)。因此,需要修改/frameworks/base/core/java/android/hardware/SensorManager.java中的mapSensorDataToWindow方法,这个方法负责把HAL读到的原始数据转化成旧接口的数据(利用onSensorChanged(int sensor, float[] value)接收到的数据)。
目前只做了0度和90度两个方向,所以也修改了一下WindowOrientationListener,让所有API只能在这两个方向上旋转:
1、mAllow180Rotation变量永远设置为false。
2、修改ROTATE_TO数组,把270度全部修改为90度。

另外,sensor.c中poll data的接口会有一个int型返回值,表示读到的sensors_event_t的个数,这个值一定要等于实际个数。我自己的程序里面,实际读到了一个(accelerator),但返回时不管读到几个,都返回当前传感器的个数。这样的话,在应用程序中用老接口onSensorChanged(int sensor, float[] value)来监听时,除了一个正常数据之外,还会读到(sensor.c中的poll data函数返回值-1)个全部为0的冗余数据。

补充于2011.7.26


通过AndroidManifest.xml设置屏幕方向的话,安装后就不能改变,而程序内部设置屏幕方向就不会有这个限制。主要靠这两个API:getRequestedOrientation()和setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
这两个API通过ActivityManagerService.java的转换后,实际上都是调用的WindowManagerService的同名方法。每个Activity在WindowManagerService端都有一个AppWindowToken做代表,而屏幕的方向信息就存储在这里。

PhoneWindowManager会自动根据屏幕物理特性决定屏幕方向,看这段代码:

  1. if (mPortraitRotation < 0) {  
  2.     // Initialize the rotation angles for each orientation once.  
  3.     Display d = ((WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE))  
  4.             .getDefaultDisplay();  
  5.     if (d.getWidth() > d.getHeight()) {  
  6.         mPortraitRotation = Surface.ROTATION_90;  
  7.         mLandscapeRotation = Surface.ROTATION_0;  
  8.         mUpsideDownRotation = Surface.ROTATION_270;  
  9.         mSeascapeRotation = Surface.ROTATION_180;  
  10.     } else {  
  11.         mPortraitRotation = Surface.ROTATION_0;  
  12.         mLandscapeRotation = Surface.ROTATION_90;  
  13.         mUpsideDownRotation = Surface.ROTATION_180;  
  14.         mSeascapeRotation = Surface.ROTATION_270;  
  15.     }  
  16. }  

这里的d.getWidth() 和 d.getHeight()得到的是物理屏幕的宽高。一般来说,平板和手机的是不一样的。平板是宽比高大(0度时位于landscape模式,右转90度进入porit模式),手机是高比宽大(0度是位于porit模式,右转90度进入landscape模式)。如果应用程序只关心当前是横屏还是竖屏,而不直接使用传感器的话,没什么问题。如果像依靠重力感应的游戏那样直接使用传感器,就需要自己根据物理屏幕的坐标系对传感器数据做转化,否则就会出现坐标系混乱的问题。

我这里碰到的是Range Thunder和Teeter两个小游戏。它们都没有通过上面的d.getWidth()和d.getHeight()来检测设备的物理屏幕从确定哪个是landscape和porit模式,而是直接假设设备是和手机一样的模式。由于游戏运行在landscape模式下,它们都把传感器数据右转90度。这样做法在手机上是没有问题,但在平板电脑上是不应该转化的,这是因为物理屏幕宽比高大的情况下,默认就是landscape模式。


补充于2011.8.2


看到下面一楼读者提到的问题后,补充一下我针对他说的那个问题的解决方案。
拿新接口来说,我们可以在onSensorChangedLocked接口中,SensorEvent传递出去之前,对坐标系调整一下。但是,按照这种方法把使用新接口游戏调整正确后,发现使用老接口的游戏又乱了,屏幕的旋转方向也乱了。
我们已经知道,WindowOrientationListener使用SensorManager来确定屏幕的旋转方向,SensorManager再根据旋转方向对底层读上来的传感器做坐标系转换,然后传递给onSensorChanged(SensorEvent event)。而老接口onSensorChanged(int sensor,float[] value)是用onSensorChanged(SensorEvent event)的数据再做坐标系转换。
所以,你还需要在老接口中根据新接口中的坐标系转换也做相应地转换。这样,使用老接口的游戏也可以了。但屏幕旋转不对怎么办?这个问题陷入了一个怪圈。好吧,就到这,下面记录一下我的解决方案:
我们先为SensorEvent增加一个属性来记录传感器的原始数据:

  1. /** 
  2.      * 这个注释一定要加上,要不你编译时还要先update-api一下。 
  3.      * {@hide} 
  4.      */  
  5. float[] originalValue=new float[3];  
有三个地方用到它,在onSensorChangedLocked中为新接口做坐标系转换,LegacyListener.onSensorChanged接口中为老接口做坐系转换,在WiindowOrientationListener中根据传感器数据计算屏幕旋转方向。
在这三个地方进行计算时都使用originalValue里面存放的原始数据进行计算,计算结果放到SensorEvent.value中。这样一来,哪个接口不对调整哪个接口,因为都是使用的原始数据,所以互不影响,再不会出现按下葫芦起来瓢的事情了。

补充于2011.8.5

不过呢,要是写游戏的人比较认真,不是只简单地考虑Landscape/Porit模式,而是使用Display.getRotation()来获取屏幕的旋转实际角度来做gsensor数据坐标系的转换,那他的程序在我们的板子上就悲剧了:
获取当前屏幕旋转角度:

  1. WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);  
  2. Display mDisplay = windowManager.getDefaultDisplay();  
  3. mDisplay.getRotation();  
很不幸,Gallery3D就是这样来做图片翻转特效的。下面代码段位于/packages/apps/Gallery3D/src/com/cooliris/media/GridInputProcessor.java文件中,根据屏幕旋转角度计算出图片的倾斜度。只好让它使用原始数据了,下面分别是修改前和修改后的代码。
修改前:

  1. public void onSensorChanged(RenderView view, SensorEvent event, int state) {  
  2.     if (mZoomGesture)  
  3.         return;  
  4.     switch (event.sensor.getType()) {  
  5.     case Sensor.TYPE_ACCELEROMETER:  
  6.         float[] values = event.values;  
  7.         float valueToUse;  
  8.         switch (mDisplay.getRotation()) {  
  9.         case Surface.ROTATION_0:  
  10.             valueToUse = values[0];  
  11.             break;  
  12.         case Surface.ROTATION_90:  
  13.             valueToUse = -event.values[1];  
  14.             break;  
  15.         case Surface.ROTATION_180:  
  16.             valueToUse = -event.values[0];  
  17.             break;  
  18.         case Surface.ROTATION_270:  
  19.             valueToUse =  event.values[1];  
  20.             break;  
  21.         default:  
  22.             valueToUse = 0.0f;  
  23.         }  
  24.  ...  
  25.     }  
  26. }  

修改后的代码:
 
  1. public void onSensorChanged(RenderView view, SensorEvent event, int state) {  
  2.      if (mZoomGesture)  
  3.          return;  
  4.      switch (event.sensor.getType()) {  
  5.      case Sensor.TYPE_ACCELEROMETER:  
  6.          float[] values = event.original;  
  7.          float valueToUse;  
  8.          switch (mDisplay.getRotation()) {  
  9.          case Surface.ROTATION_0:  
  10.              valueToUse = values[0];  
  11.              break;  
  12.          case Surface.ROTATION_90:  
  13.              valueToUse = -values[1];  
  14.              break;  
  15.          case Surface.ROTATION_180:  
  16.              valueToUse = -values[0];  
  17.              break;  
  18.          case Surface.ROTATION_270:  
  19.              valueToUse =  values[1];  
  20.              break;  
  21.          default:  
  22.              valueToUse = 0.0f;  
  23.          }  
  24.         ... ...  
  25.      }  
  26.  }  

期待那个不分平板和手机的系统早早降临吧。