Android 傳感器的數(shù)據(jù)流和框架
應用程序怎么樣設置可以讓自己隨著設備的傾斜度變化而旋轉方向呢?在AndroidManifest.xml文件中的android:screenOrientation就可以了。這里追蹤一下它的內部機制。
先看一個最關鍵的部件:/frameworks/base/core/java/android/view/WindowOrientationListener.java
這個接口注冊一個accelerator,并負責把accelerator的數(shù)據(jù)轉化為orientation。這個API對應用程序不公開,我看Android2.3的源碼時發(fā)現(xiàn)只有PhoneWindowManager使用到它了。
/frameworks/base/policy//PhoneWindowManager。java
PhonwWindowManager注冊了一個WindowOrientationListener,就可以異步獲取當前設備的orientation了。再結合應用程序在AndroidManifest。xml中設置的值來管理著應用程序界面的旋轉方向。以下是PhoneWindowManager。java中相關的兩個代碼片段。
java代碼:
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屬性值就可以確定應用程序界面的旋轉方向了。應用程序設置值的優(yōu)先級大于傳感器確定的優(yōu)先級。
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);
}
? ? 讓應用程序隨屏幕方向自動旋轉的實現(xiàn)原理就這么交待完了。我解決這一步時也沒有費多少力氣,在板子上打開SensorTest,對比一下XYZ三個軸和MileStone上面的數(shù)據(jù),修改一下正負值就可以了。但要解決Teeter運行時Z軸反轉的問題,還得深層次挖一挖。
PhoneWindowManager。java中有這么一句:
mWindowManager。setRotation(rotation, false, mFancyRotationAnimation);
當PhonewindowManager通過WindowOrientationListener這個監(jiān)聽器得知屏幕方向改變時,會通知給WindowManagerService(/frameworks/base/service//WindowManagerService。java)
WindowManagerService中有這么一個監(jiān)聽器集合:mRotationWatchers,誰想監(jiān)聽屏幕方向改變,就會在這里注冊一個監(jiān)聽器
java代碼:
public void onRotationChanged(int rotation) {
synchronized(sListeners) {
sRotation??= rotation;
}
}
static int getRotation() {
synchronized(sListeners) {
return sRotation;
}
}
? ?? ???SensorManager要這個值有什么作用呢?看看在哪里使用了SensorManager。getRotation()吧。只有一個方法:mapSensorDataToWindow
當一個Activity注冊了一個Sensor事件監(jiān)聽器后,總是會通過接口來異步獲取sensor事件的。在這里,新老版本出現(xiàn)了分化。老版本中,Android1。5以前,Sensor事件被分發(fā)給監(jiān)聽者(onSensorChanged)之前,總會先用這個方法處理一下。新版本的監(jiān)聽接口是SensorEventListener,分發(fā)前是沒有處理方法的??匆幌逻@個方法,原來是轉換坐標系用的。應用程序的界面方向隨屏幕發(fā)生變化以后,通過異步分發(fā)接口傳遞給它的傳感器數(shù)據(jù)也要從傳感器的坐標系轉換到應用程序的坐標系。假設屏幕默認方向是豎屏,這個時候分發(fā)給它的SensorEvent里面的值與frameworks層從HAL的sensor。c中讀到的數(shù)據(jù)是一樣的。當設備右側抬起,屏幕切換到橫屏是,應用程序的界面也旋轉了90度,這個時候,SensorEvent在分發(fā)給應用程序之前就需要先把自己的坐標系順時針旋轉90度。
新版本接口中,傳感器數(shù)據(jù)直接通過SensorEventListener分發(fā)給應用程序。而老版本接口中,分發(fā)之前先要結合當前設備的旋轉方向對傳感器數(shù)據(jù)做一個坐標系轉換。到這里,一切都清楚了。WindowOrientationListener借助SensorManager的accelerator數(shù)據(jù)制造了屏幕旋轉方向,而屏幕旋轉方向又被SensorManager用來兼容老版本的SensorListener接口??梢哉f,如果不考慮兼容老版本的接口的話,SensorManager是完全不用向WindowManagerService。java中注冊監(jiān)聽器監(jiān)聽當前設備屏幕旋轉方向的,直接分發(fā)下去就好了。SensorManager的代碼恐怕要減少一半多。framework層是時候把SensorListener相關的一系列API扔到一邊了。
還有一個地方,就是HAL層的sensor。c到SensorManager之間的部分。這個部分把sensor。c中讀到的傳感器數(shù)據(jù)整合成一個服務(SensorService)供SensorManager使用。很好地隔離了API層和HAL層。但從數(shù)據(jù)處理的角度來講,只是扮演了一個數(shù)據(jù)傳遞者的角色,沒有對數(shù)據(jù)進行任何的改變。
上面的寫完了,接下來是HAL層了。各個廠商的寫法都不一樣,有的為了把所有傳感器集成進來,還形成了自己的一個框架。讓我們穿過HAL框架,直接進入sensor。c。這里有我最關心的最終如何與sensor的driver交互,向上層傳遞了哪些信息。再復雜的frameworks,也不過是把sensor。c提供的接口封裝一下而己。sensor的數(shù)據(jù)從來沒有被改變過。這里只是簡述一下sensor。c的大致功能,那里我寫了一個可以通過ADB或者串口運行的C++程序專門演示控制driver和讀取數(shù)據(jù)的細節(jié)。
SensorManager要這個值有什么作用呢?看看在哪里使用了SensorManager.getRotation()吧。只有一個方法:mapSensorDataToWindow
? ?? ? 當一個Activity注冊了一個Sensor事件監(jiān)聽器后,總是會通過接口來異步獲取sensor事件的。在這里,新老版本出現(xiàn)了分化。老版本中,Android1.5以前,Sensor事件被分發(fā)給監(jiān)聽者(onSensorChanged)之前,總會先用這個方法處理一下。新版本的監(jiān)聽接口是SensorEventListener,分發(fā)前是沒有處理方法的。看一下這個方法,原來是轉換坐標系用的。應用程序的界面方向隨屏幕發(fā)生變化以后,通過異步分發(fā)接口傳遞給它的傳感器數(shù)據(jù)也要從傳感器的坐標系轉換到應用程序的坐標系。假設屏幕默認方向是豎屏,這個時候分發(fā)給它的SensorEvent里面的值與frameworks層從HAL的sensor.c中讀到的數(shù)據(jù)是一樣的。當設備右側抬起,屏幕切換到橫屏是,應用程序的界面也旋轉了90度,這個時候,SensorEvent在分發(fā)給應用程序之前就需要先把自己的坐標系順時針旋轉90度。
新版本接口中,傳感器數(shù)據(jù)直接通過SensorEventListener分發(fā)給應用程序。而老版本接口中,分發(fā)之前先要結合當前設備的旋轉方向對傳感器數(shù)據(jù)做一個坐標系轉換。到這里,一切都清楚了。WindowOrientationListener借助SensorManager的accelerator數(shù)據(jù)制造了屏幕旋轉方向,而屏幕旋轉方向又被SensorManager用來兼容老版本的SensorListener接口。可以說,如果不考慮兼容老版本的接口的話,SensorManager是完全不用向
? ?? ?WindowManagerService.java中注冊監(jiān)聽器監(jiān)聽當前設備屏幕旋轉方向的,直接分發(fā)下去就好了。SensorManager的代碼恐怕要減少一半多。framework層是時候把SensorListener相關的一系列API扔到一邊了。
還有一個地方,就是HAL層的sensor.c到SensorManager之間的部分。這個部分把sensor.c中讀到的傳感器數(shù)據(jù)整合成一個服務(SensorService)供SensorManager使用。很好地隔離了API層和HAL層。但從數(shù)據(jù)處理的角度來講,只是扮演了一個數(shù)據(jù)傳遞者的角色,沒有對數(shù)據(jù)進行任何的改變。
? ?? ?上面的寫完了,接下來是HAL層了。各個廠商的寫法都不一樣,有的為了把所有傳感器集成進來,還形成了自己的一個框架。讓我們穿過HAL框架,直接進入sensor.c。這里有我最關心的最終如何與sensor的driver交互,向上層傳遞了哪些信息。再復雜的frameworks,也不過是把sensor.c提供的接口封裝一下而己。sensor的數(shù)據(jù)從來沒有被改變過。這里只是簡述一下sensor.c的大致功能,那里我寫了一個可以通過ADB或者串口運行的C++程序專門演示控制driver和讀取數(shù)據(jù)的細節(jié)。
? ?? ? 1、給上層提供一個獲取sensor list的接口。這個是寫死在sensor.c里面的。往一個設備上面移植frameworks時,這一部分是要根據(jù)設備上的sensor來修改這個文件的。
? ?? ? 2、給上層提供控制接口:active/deactive某一個sensor。set某個sensor的delay值(即,獲取sensor數(shù)據(jù)的頻率,比如設置為200,000的話,就是驅動每隔200毫秒向上面發(fā)一次數(shù)據(jù))。讀取某個sensor的數(shù)據(jù)。
現(xiàn)在我們知道了,調試傳感器時,只在兩個地方下手就可以了:
? ?? ? 1、/frameworks/base/core/java/android/view/WindowOrientation.java,校正accelerator數(shù)據(jù)與屏幕旋轉方向的對應關系。
? ?? ? 2、/hardware/libhardware/modules/sensor/sensor.c,對驅動遞上來的數(shù)據(jù)進行初步校正。這一步可以參考一下已經(jīng)的校正好的機器(我用的是自己的MileStone),然后再運行一下SensorTest(網(wǎng)上有的下),只要同一個擺放姿勢下,我們讀上來的數(shù)據(jù)和它的一樣就可以了。因為前面說過,在新版本(1.5及以上)接口中,數(shù)據(jù)流經(jīng)過sensor.c->SensorService->SensorManager,最后通過onSensorChanged分發(fā)給應用程序的整個過程中,是從來沒有被改變過的。至于老版本中SensorManager部分做的校正,讓他吃屎去吧。
? ? 我再說最后一次:sensor數(shù)據(jù)自從被sensor.c從driver讀上來,一直到傳遞給應用程序的onSensorChanged接口,整個過程中,數(shù)據(jù)都沒有被改變過。這很重要,因為這意味著frameworks層是不需要sensor校正的。我們只需要在WindowOrientationListener里面找到那兩個數(shù)組(THRESHOLDS和ROTATE_TO),然后調調屏幕旋轉方向就可以了。
? ?? ? 兼容傳感器老接口時出現(xiàn)的問題。? ?
? ?? ? 屏幕旋轉后,傳感器數(shù)據(jù)也要變的坐標系,這和以前的理解不一樣,得糾正一下。
? ?? ? 調試好sensor后,屏幕可以正常旋轉了,但HTC手機自帶的Teeter運行起來有問題。跟蹤了一下,發(fā)現(xiàn)Teeter還在使用舊的傳感器監(jiān)聽接口onSensorChanged(int sensor, float[] value)。因此,需要修改/frameworks/base/core/java/android/hardware/SensorManager.java中的mapSensorDataToWindow方法,這個方法負責把HAL讀到的原始數(shù)據(jù)轉化成舊接口的數(shù)據(jù)(利用onSensorChanged(int
sensor, float[] value)接收到的數(shù)據(jù))。
? ?? ? 目前只做了0度和90度兩個方向,所以也修改了一下WindowOrientationListener,讓所有API只能在這兩個方向上旋轉:
? ?? ?1、mAllow180Rotation變量永遠設置為false。
? ?? ?2、修改ROTATE_TO數(shù)組,把270度全部修改為90度。
? ?? ? 另外,sensor.c中poll data的接口會有一個int型返回值,表示讀到的sensors_event_t的個數(shù),這個值一定要等于實際個數(shù)。我自己的程序里面,實際讀到了一個(accelerator),但返回時不管讀到幾個,都返回當前傳感器的個數(shù)。這樣的話,在應用程序中用老接口onSensorChanged(int sensor, float[] value)來監(jiān)聽時,除了一個正常數(shù)據(jù)之外,還會讀到(sensor.c中的poll data函數(shù)返回值-1)個全部為0的冗余數(shù)據(jù)。?
通過AndroidManifest.xml設置屏幕方向的話,安裝后就不能改變,而程序內部設置屏幕方向就不會有這個限制。主要靠這兩個API:getRequestedOrientation()和setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)這兩個API通過ActivityManagerService.java的轉換后,實際上都是調用的WindowManagerService的同名方法。每個Activity在WindowManagerService端都有一個AppWindowToken做代表,而屏幕的方向信息就存儲在這里。
? ?? ? PhoneWindowManager會自動根據(jù)屏幕物理特性決定屏幕方向,看這段代碼:
java代碼:
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;? ? }}
? ?? ? 這里的d.getWidth() 和 d.getHeight()得到的是物理屏幕的寬高。一般來說,平板和手機的是不一樣的。平板是寬比高大(0度時位于landscape模式,右轉90度進入porit模式),手機是高比寬大(0度是位于porit模式,右轉90度進入landscape模式)。如果應用程序只關心當前是橫屏還是豎屏,而不直接使用傳感器的話,沒什么問題。如果像依靠重力感應的游戲那樣直接使用傳感器,就需要自己根據(jù)物理屏幕的坐標系對傳感器數(shù)據(jù)做轉化,否則就會出現(xiàn)坐標系混亂的問題。
? ?? ? 我這里碰到的是Range Thunder和Teeter兩個小游戲。它們都沒有通過上面的d.getWidth()和d.getHeight()來檢測設備的物理屏幕從確定哪個是landscape和porit模式,而是直接假設設備是和手機一樣的模式。由于游戲運行在landscape模式下,它們都把傳感器數(shù)據(jù)右轉90度。這樣做法在手機上是沒有問題,但在平板電腦上是不應該轉化的,這是因為物理屏幕寬比高大的情況下,默認就是landscape模式。
? ?? ? 看到下面一樓讀者提到的問題后,補充一下我針對他說的那個問題的解決方案。? ?? ? 拿新接口來說,我們可以在onSensorChangedLocked接口中,SensorEvent傳遞出去之前,對坐標系調整一下。但是,按照這種方法把使用新接口游戲調整正確后,發(fā)現(xiàn)使用老接口的游戲又亂了,屏幕的旋轉方向也亂了。
? ?? ? 我們已經(jīng)知道,WindowOrientationListener使用SensorManager來確定屏幕的旋轉方向,SensorManager再根據(jù)旋轉方向對底層讀上來的傳感器做坐標系轉換,然后傳遞給onSensorChanged(SensorEvent event)。而老接口onSensorChanged(int sensor,float[] value)是用onSensorChanged(SensorEvent event)的數(shù)據(jù)再做坐標系轉換。所以,你還需要在老接口中根據(jù)新接口中的坐標系轉換也做相應地轉換。這樣,使用老接口的游戲也可以了。但屏幕旋轉不對怎么辦?這個問題陷入了一個怪圈。好吧,就到這,下面記錄一下我的解決方案:? ?? ? 我們先為SensorEvent增加一個屬性來記錄傳感器的原始數(shù)據(jù):
java代碼:/**? ???* 這個注釋一定要加上,要不你編譯時還要先update-api一下。? ???* {@hide}? ???*/float[] originalValue=new float[3];
? ?? ? 有三個地方用到它,在onSensorChangedLocked中為新接口做坐標系轉換,LegacyListener.onSensorChanged接口中為老接口做坐系轉換,在WiindowOrientationListener中根據(jù)傳感器數(shù)據(jù)計算屏幕旋轉方向。
? ?? ? 在這三個地方進行計算時都使用originalValue里面存放的原始數(shù)據(jù)進行計算,計算結果放到SensorEvent.value中。這樣一來,哪個接口不對調整哪個接口,因為都是使用的原始數(shù)據(jù),所以互不影響,再不會出現(xiàn)按下葫蘆起來瓢的事情了。
? ?? ? 不過呢,要是寫游戲的人比較認真,不是只簡單地考慮Landscape/Porit模式,而是使用Display.getRotation()來獲取屏幕的旋轉實際角度來做gsensor數(shù)據(jù)坐標系的轉換,那他的程序在我們的板子上就悲劇了:
? ?? ? 獲取當前屏幕旋轉角度:
java代碼:
復制代碼
? ?? ? 很不幸,Gallery3D就是這樣來做圖片翻轉特效的。下面代碼段位于/packages/apps/Gallery3D/src/com/cooliris/media/GridInputProcessor.java文件中,根據(jù)屏幕旋轉角度計算出圖片的傾斜度。只好讓它使用原始數(shù)據(jù)了,下面分別是修改前和修改后的代碼。
? ?? ? 修改前:
java代碼:
? ?? ???修改后的代碼:
java代碼:
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;
}
... ...
}
}