梳理一下感應器的資料流和架構是怎麼樣讓旋轉螢幕的。

來源:互聯網
上載者:User

這篇文章寫的感應器資料從驅動傳遞到應用程式的整個流程,還有資料校正的問題。

應用程式怎麼樣設定可以讓自己隨著裝置的傾斜度變化而旋轉方向呢?在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中相關的兩個程式碼片段。

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);            }

讓應用程式隨螢幕方向自動旋轉的實現原理就這麼交待完了。我解決這一步時也沒有費多少力氣,在板子上開啟SensorTest,對比一下XYZ三個軸和MileStone上面的資料,修改一下正負值就可以了。但要解決Teeter運行時Z軸反轉的問題,還得深層次挖一挖。

PhoneWindowManager.java中有這麼一句:
mWindowManager.setRotation(rotation, false, mFancyRotationAnimation);
當PhonewindowManager通過WindowOrientationListener這個監聽器得知螢幕方向改變時,會通知給WindowManagerService(/frameworks/base/service/../WindowManagerService.java)
WindowManagerService中有這麼一個監聽器集合:mRotationWatchers,誰想監聽螢幕方向改變,就會在這裡註冊一個監聽器。SensorManager就這麼幹了。然後,通過下面這個非同步方法呼叫獲知當前的螢幕方向

public void onRotationChanged(int rotation) {        synchronized(sListeners) {            sRotation  = rotation;        }    }static int getRotation() {        synchronized(sListeners) {            return sRotation;        }    }

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會自動根據螢幕物理特性決定螢幕方向,看這段代碼:

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模式)。如果應用程式只關心當前是橫屏還是豎屏,而不直接使用感應器的話,沒什麼問題。如果像依靠重力感應的遊戲那樣直接使用感應器,就需要自己根據物理螢幕的座標系對感應器資料做轉化,否則就會出現座標系混亂的問題。

我這裡碰到的是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中根據感應器資料計算旋轉螢幕方向。
在這三個地方進行計算時都使用originalValue裡面存放的未經處理資料進行計算,計算結果放到SensorEvent.value中。這樣一來,哪個介面不對調整哪個介面,因為都是使用的未經處理資料,所以互不影響,再不會出現按下葫蘆起來瓢的事情了。

補充於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;            }           ... ...        }    }

期待那個不分平板和手機的系統早早降臨吧。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.