这篇文章写的传感器数据从驱动传递到应用程序的整个流程,还有数据校正的问题。
public void onOrientationChanged(int rotation) { // Send updates based on orientation value if (localLOGV) Log.v(TAG, "onOrientationChanged, rotation changed to " +rotation); try { mWindowManager.setRotation(rotation, false, mFancyRotationAnimation); } catch (RemoteException e) { // Ignore } } ... ... switch (orientation) {//这个值就是当前设备屏幕的旋转方向,再结合应用程序设置的android:configChanges属性值就可以确定应用程序界面的旋转方向了。应用程序设置值的优先级大于传感器确定的优先级。 case ActivityInfo.SCREEN_ORIENTATION_PORTRAIT: //always return portrait if orientation set to portrait return mPortraitRotation; case ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE: //always return landscape if orientation set to landscape return mLandscapeRotation; case ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT: //always return portrait if orientation set to portrait return mUpsideDownRotation; case ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE: //always return seascape if orientation set to reverse landscape return mSeascapeRotation; case ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE: //return either landscape rotation based on the sensor mOrientationListener.setAllow180Rotation( isLandscapeOrSeascape(Surface.ROTATION_180)); return getCurrentLandscapeRotation(lastRotation); case ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT: mOrientationListener.setAllow180Rotation( !isLandscapeOrSeascape(Surface.ROTATION_180)); return getCurrentPortraitRotation(lastRotation); }
public void onRotationChanged(int rotation) { synchronized(sListeners) { sRotation = rotation; } } static int getRotation() { synchronized(sListeners) { return sRotation; } }
好了,这篇文章总算是写完了。别闲我啰嗦,我再说最后一次: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会自动根据屏幕物理特性决定屏幕方向,看这段代码:
if (mPortraitRotation < 0) { // Initialize the rotation angles for each orientation once. Display d = ((WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE)) .getDefaultDisplay(); if (d.getWidth() > d.getHeight()) { mPortraitRotation = Surface.ROTATION_90; mLandscapeRotation = Surface.ROTATION_0; mUpsideDownRotation = Surface.ROTATION_270; mSeascapeRotation = Surface.ROTATION_180; } else { mPortraitRotation = Surface.ROTATION_0; mLandscapeRotation = Surface.ROTATION_90; mUpsideDownRotation = Surface.ROTATION_180; mSeascapeRotation = Surface.ROTATION_270; } }
我这里碰到的是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增加一个属性来记录传感器的原始数据:
/** * 这个注释一定要加上,要不你编译时还要先update-api一下。 * {@hide} */ float[] originalValue=new float[3];有三个地方用到它,在onSensorChangedLocked中为新接口做坐标系转换,LegacyListener.onSensorChanged接口中为老接口做坐系转换,在WiindowOrientationListener中根据传感器数据计算屏幕旋转方向。
补充于2011.8.5
不过呢,要是写游戏的人比较认真,不是只简单地考虑Landscape/Porit模式,而是使用Display.getRotation()来获取屏幕的旋转实际角度来做gsensor数据坐标系的转换,那他的程序在我们的板子上就悲剧了:
获取当前屏幕旋转角度:
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); Display mDisplay = windowManager.getDefaultDisplay(); mDisplay.getRotation();很不幸,Gallery3D就是这样来做图片翻转特效的。下面代码段位于/packages/apps/Gallery3D/src/com/cooliris/media/GridInputProcessor.java文件中,根据屏幕旋转角度计算出图片的倾斜度。只好让它使用原始数据了,下面分别是修改前和修改后的代码。
public void onSensorChanged(RenderView view, SensorEvent event, int state) { if (mZoomGesture) return; switch (event.sensor.getType()) { case Sensor.TYPE_ACCELEROMETER: float[] values = event.values; float valueToUse; switch (mDisplay.getRotation()) { case Surface.ROTATION_0: valueToUse = values[0]; break; case Surface.ROTATION_90: valueToUse = -event.values[1]; break; case Surface.ROTATION_180: valueToUse = -event.values[0]; break; case Surface.ROTATION_270: valueToUse = event.values[1]; break; default: valueToUse = 0.0f; } ... ... } }
public void onSensorChanged(RenderView view, SensorEvent event, int state) { if (mZoomGesture) return; switch (event.sensor.getType()) { case Sensor.TYPE_ACCELEROMETER: float[] values = event.original; float valueToUse; switch (mDisplay.getRotation()) { case Surface.ROTATION_0: valueToUse = values[0]; break; case Surface.ROTATION_90: valueToUse = -values[1]; break; case Surface.ROTATION_180: valueToUse = -values[0]; break; case Surface.ROTATION_270: valueToUse = values[1]; break; default: valueToUse = 0.0f; } ... ... } }