1. 程式人生 > >觀察者與陀螺儀感測器的應用(基於PanoramaImageView)

觀察者與陀螺儀感測器的應用(基於PanoramaImageView)

實現原理:

首先將自定義PanoramaImageView的ScaleType手動置為CENTER_CROP:將圖片等比例縮放,讓影象的短邊與ImageView的邊長度相同,即不能留有空白,縮放後擷取中間部分進行顯示。影象相對長的邊就會“顯示”在螢幕外邊。然後獲取手機陀螺儀感測器,註冊監聽,並根據陀螺儀感測器傳來的資料判斷手機當前的狀態。

這裡寫圖片描述

手機感測器座標系如上圖所示,陀螺儀感測器的監聽方法onSensorChanged(SensorEvent event)傳過來event引數,其中屬性event.values是一個float型別的陣列,陣列的前三個值event.values[0],event.values[1],event.values[2]分別代表了手機繞X,Y,Z軸旋轉的角速度。我們判斷這個三個角速度絕對值的相對大小既可以粗略的判斷出手機當前所處的狀態。程式碼如下:

     @Override
    public void onSensorChanged(SensorEvent event) {
        if (lastTimestamp == 0) {
            lastTimestamp = event.timestamp;
            return;
        }
        float angularX = Math.abs(event.values[0]);
        float angularY = Math.abs(event.values[1]);
        float angularZ = Math.abs(event
.values[2]); final float dT = (event.timestamp - lastTimestamp) * NS2S; if (angularX > angularY + angularZ) { //圍繞x軸運動,即手機上下襬動(腦補拿手機打手板的動作) rotateRadianX += event.values[0] * dT; rotateRadian = rotateRadianX; type = rotateAroundX; } else if
(angularY > angularX + angularZ) {//圍繞y軸運動,即手機左右旋轉(腦補四驅兄弟裡面的“旋風衝鋒龍捲風”) rotateRadianY += event.values[1] * dT; type = rotateAroundY; rotateRadian = rotateRadianY; } else {//圍繞Z軸運動,可以理解為手機放在桌子上轉圈 rotateRadianZ += event.values[2] * dT; type = rotateAroundZ; rotateRadian = rotateRadianZ; } updata(gyroscopeControllInterface); lastTimestamp = event.timestamp; }

獲取到手機轉動方向和角度後,就可以根據情況設定圖片的顯示狀態了。前面我們將imageView的ScaleType設定為CENTER_CROP,這時會有兩種情況。
第一種情況:圖片寬度/view寬度>圖片高度/view高度(高度邊填充imageview,寬度邊溢位):

這裡寫圖片描述

這裡寫圖片描述

這種情況下檢視圖片的方法是“左右旋轉“手機,即手機繞Y軸轉動。

第二種情況:圖片高度/view高度>圖片寬度/view寬度(寬度邊填充imageview,高度邊溢位):

這裡寫圖片描述

這裡寫圖片描述

這種情況下檢視圖片的方法是“上下襬動“手機,即手機繞X軸轉動。當比例相同的時候暫不考慮,這種情況歸於上面哪種都可以,反正不會自己動~~。
上面我們通過陀螺儀感測器獲取了手機旋轉的狀態和角度,再結合圖片的狀態:如果手機繞Y軸旋轉,且圖片是第一種狀態或者手機繞X軸旋轉,且圖片是第二種狀態 則符合我們重繪的條件,即可以觸發圖片的移動。那麼如何進行繪製來達到Panorama(全景)的效果呢?先從效果來說,全景效果其實就是canvas根據手機旋轉角度進行translate,左右平移或者上下平移,核心程式碼就一句: canvas.translate(currentOffsetX, 0)或者canvas.translate( 0,currentOffsetY)。重點是currentOffsetX,Y的計算過程,手機繞某一軸旋轉的度數(弧度)是沒有限制的,旋轉一週是±2π,旋轉十週是±20π,我們要根據需要取得我們所需要的度數範圍,一般我們手持裝置轉動,合適觀看的度數±π/6即左右或上下偏移30°就差不多了。所以我們先設定旋轉的最大度數maxRotateRadian = Math.PI / 6 (即30°),再根據從感測器獲取的旋轉狀態和度數計算出當前手機旋轉的角度與最大角度的比例值(可以理解為手機轉動的程度):

  @Override
    public void updata(double rotateRadian, int type) {
        //如果type與當前圖片的type一致
        if (orientation == type) {
            //限制旋轉弧度最大為maxRotateRadian
            if (rotateRadian >= maxRotateRadian) {
                rotateRadian = maxRotateRadian;
            } else if (rotateRadian <= -maxRotateRadian) {
                rotateRadian = -maxRotateRadian;
            }
            //計算出比例值
            progress = (float) (rotateRadian / maxRotateRadian);
            invalidate();
        }
    }

這個比例值progress 與canvas平移的比例是呈正比的:

這裡寫圖片描述

得到了比例值progress ,要去求canvas平移的距離,拿這個比例值去乘上我們位移的最大值即可。那麼問題來了,這個最大值從哪裡來的呢?以寬幅照片為例:

這裡寫圖片描述

這個位移的最大值就是上圖中單個陰影的長度x,圖片初始載入時,兩陰影的長度是相等的,也就是我們圖片可以左/右位移的最大值(若豎幅則是上/下位移)。這個最大值如何計算呢?

假設圖片處於第一種情況(圖片寬度/view寬度>圖片高度/view高度):
剛開始我是這麼計算的:最大值=(圖片寬度-imageview寬度)/2,看上圖很明顯嗎~這就too simple了,這種情況其實是imageview把照片根據(圖片高度/view高度)這個比例把圖片縮放了,上圖中圖片的寬度是經過縮放後的寬度,並非原圖寬度,所以這個寬度應該等於原圖寬度去除以這個比例:即
上圖寬幅照片的寬度=原圖寬度/(圖片高度/view高度),
那麼
最大值=(原圖寬度/(圖片高度/view高度)-imageview寬度)/2
假設圖片處於第二種情況(圖片寬度/view寬度<圖片高度/view高度):
最大值=(原圖高度/(圖片寬度/view寬度)-imageview高度)/2。
這裡我用了除法,不是很直觀,原作者的程式碼更加直觀些:


            if (mDrawableWidth * mHeight > mDrawableHeight * mWidth) {
                mOrientation = ORIENTATION_HORIZONTAL;
                float imgScale = (float) mHeight / (float) mDrawableHeight;
                mMaxOffset = Math.abs((mDrawableWidth * imgScale - mWidth) * 0.5f);
            } else if(mDrawableWidth * mHeight < mDrawableHeight * mWidth) {
                mOrientation = ORIENTATION_VERTICAL;
                float imgScale = (float) mWidth / (float) mDrawableWidth;
                mMaxOffset = Math.abs((mDrawableHeight * imgScale - mHeight) * 0.5f);
            }

得到了最大偏移值就可以計算出位移的偏移量了,maxOffset (位移最大值)*progress (比例值)=位移偏移量。之後根據偏移量重繪:

  if (orientation == ORIENTATION_HORIZONTAL) {
            float currentOffsetX = maxOffset * progress;
            canvas.save();
            canvas.translate(currentOffsetX, 0);
            super.onDraw(canvas);
            canvas.restore();
        } else if (orientation == ORIENTATION_VERTICAL) {
            float currentOffsetY = maxOffset * progress;
            canvas.save();
            canvas.translate(0, currentOffsetY);
            super.onDraw(canvas);
            canvas.restore();
        }

大致思路就是這樣,此專案作者構建了一個觀察者模式來獲取陀螺儀感測器的數值併發送給PanoramaImageView繪製,我也重新系統學習了一下觀察者模式並在該專案基礎上做了改動,構建了結合代理模式的觀察者模式,將觀察者(observe)與被觀察者(observed)解耦,由一個控制中心統一調控。上面程式碼是修改後的程式碼,與原專案有些不同,但思路是一致的。我fork的此專案的地址:。先到這裡,吃飯了,明天再記錄一下觀察者模式的實現。