調用方向傳感器與OpenGL開發簡易水平儀的原理其實很簡單的:就是在一個透明的圓盤中充滿某種液體,液體中留有一個氣泡,當一端翹起時,該氣泡將會浮向翹起的一端。
方向傳感器會反向三個值,其中第二個角度值代表底部翹起的角度(當頂部翹起時為負值);第三個角度值代表右側翹起的角度(當左側翹起時為負值);根據這兩個角度就可以開發出水平儀瞭。
該程序Demo定義一個View,該自定義View很簡單,就是繪制透明圓盤和氣泡——其中氣泡的位置會動態改變。該自定義View的代碼如下。
MyView.java邏輯代碼如下:
package com.fukaimei.gradienter; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.Shader; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.Display; import android.view.View; import android.view.WindowManager; public class MyView extends View { // 定義水平儀儀表盤圖片 Bitmap back; // 定義水平儀中的氣泡圖標 Bitmap bubble; // 定義水平儀中氣泡的X、Y坐標 int bubbleX, bubbleY; public MyView(Context context, AttributeSet attrs) { super(context, attrs); // 獲取窗口管理器 WindowManager wm = (WindowManager) context .getSystemService(Context.WINDOW_SERVICE); // 獲取屏幕的寬度和高度 Display display = wm.getDefaultDisplay(); DisplayMetrics metrics = new DisplayMetrics(); display.getMetrics(metrics); int screenWidth = metrics.widthPixels; // 創建位圖 back = Bitmap.createBitmap(screenWidth, screenWidth , Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(back); Paint paint = new Paint(); paint.setAntiAlias(true); // 設置繪制風格:僅填充 paint.setStyle(Paint.Style.FILL); // 創建一個線性漸變來繪制線性漸變 Shader shader = new LinearGradient(0, screenWidth, screenWidth * 0.8f, screenWidth * 0.2f, Color.YELLOW, Color.WHITE, Shader.TileMode.MIRROR); paint.setShader(shader); // 繪制圓形 canvas.drawCircle(screenWidth / 2, screenWidth / 2, screenWidth / 2, paint); Paint paint2 = new Paint(); paint2.setAntiAlias(true); // 設置繪制風格:僅繪制邊框 paint2.setStyle(Paint.Style.STROKE); // 設置畫筆寬度 paint2.setStrokeWidth(5); // 設置畫筆顏色 paint2.setColor(Color.BLACK); // 繪制圓形邊框 canvas.drawCircle(screenWidth / 2, screenWidth / 2, screenWidth / 2, paint2); // 繪制水平橫線 canvas.drawLine(0, screenWidth / 2, screenWidth, screenWidth / 2, paint2); // 繪制垂直橫線 canvas.drawLine(screenWidth / 2, 0, screenWidth / 2, screenWidth, paint2); // 設置畫筆寬度 paint2.setStrokeWidth(10); // 設置畫筆顏色 paint2.setColor(Color.RED); // 繪制中心的紅色“十”字 canvas.drawLine(screenWidth / 2 - 30, screenWidth / 2 , screenWidth / 2 + 30, screenWidth / 2, paint2); canvas.drawLine(screenWidth / 2, screenWidth / 2 - 30 , screenWidth / 2, screenWidth / 2 + 30, paint2); // 加載氣泡圖片 bubble = BitmapFactory .decodeResource(getResources(), R.drawable.bubble); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 繪制水平儀表盤圖片 canvas.drawBitmap(back, 0, 0, null); // 根據氣泡坐標繪制氣泡 canvas.drawBitmap(bubble, bubbleX, bubbleY, null); } }
上面自定義的View會根據bubbleX、bubbleY動態地繪制氣泡的位置,而這個bubbleX、bubbleY就需要根據方向傳感器返回的第三個角度、第二個角度來動態計算。
layout/activity_main.xml界面佈局代碼如下:
<framelayout android:background="#fff" android:layout_height="match_parent" android:layout_width="match_parent" xmlns:android="https://schemas.android.com/apk/res/android"> </framelayout>
MainActivity.java邏輯代碼如下:
package com.fukaimei.gradienter; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; public class MainActivity extends AppCompatActivity implements SensorEventListener { // 定義水平儀的儀表盤 MyView show; // 定義水平儀能處理的最大傾斜角,超過該角度,氣泡將直接位於邊界 int MAX_ANGLE = 30; // 定義Sensor管理器 SensorManager mSensorManager; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 獲取水平儀的主組件 show = (MyView) findViewById(R.id.show); // 獲取傳感器管理服務 mSensorManager = (SensorManager) getSystemService(SENSOR_SERVICE); } @Override public void onResume() { super.onResume(); // 為系統的方向傳感器註冊監聽器 mSensorManager.registerListener(this, mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION), SensorManager.SENSOR_DELAY_GAME); } @Override protected void onPause() { // 取消註冊 mSensorManager.unregisterListener(this); super.onPause(); } @Override protected void onStop() { // 取消註冊 mSensorManager.unregisterListener(this); super.onStop(); } @Override public void onSensorChanged(SensorEvent event) { float[] values = event.values; // 獲取觸發event的傳感器類型 int sensorType = event.sensor.getType(); if (sensorType == Sensor.TYPE_ORIENTATION) { // 獲取與Y軸的夾角 float yAngle = values[1]; // 獲取與Z軸的夾角 float zAngle = values[2]; // 氣泡位於中間時(水平儀完全水平),氣泡的X、Y坐標 int x = (show.back.getWidth() - show.bubble.getWidth()) / 2; int y = (show.back.getHeight() - show.bubble.getHeight()) / 2; // 如果與Z軸的傾斜角還在最大角度之內 if (Math.abs(zAngle) <= MAX_ANGLE) { // 根據與Z軸的傾斜角度計算X坐標的變化值 // (傾斜角度越大,X坐標變化越大) int deltaX = (int) ((show.back.getWidth() - show.bubble .getWidth()) / 2 * zAngle / MAX_ANGLE); x += deltaX; } // 如果與Z軸的傾斜角已經大於MAX_ANGLE,氣泡應到最左邊 else if (zAngle > MAX_ANGLE) { x = 0; } // 如果與Z軸的傾斜角已經小於負的MAX_ANGLE,氣泡應到最右邊 else { x = show.back.getWidth() - show.bubble.getWidth(); } // 如果與Y軸的傾斜角還在最大角度之內 if (Math.abs(yAngle) <= MAX_ANGLE) { // 根據與Y軸的傾斜角度計算Y坐標的變化值 // (傾斜角度越大,Y坐標變化越大) int deltaY = (int) ((show.back.getHeight() - show.bubble .getHeight()) / 2 * yAngle / MAX_ANGLE); y += deltaY; } // 如果與Y軸的傾斜角已經大於MAX_ANGLE,氣泡應到最下邊 else if (yAngle > MAX_ANGLE) { y = show.back.getHeight() - show.bubble.getHeight(); } // 如果與Y軸的傾斜角已經小於負的MAX_ANGLE,氣泡應到最右邊 else { y = 0; } // 如果計算出來的X、Y坐標還位於水平儀的儀表盤內,更新水平儀的氣泡坐標 if (isContain(x, y)) { show.bubbleX = x; show.bubbleY = y; } // 通知系統重回MyView組件 show.postInvalidate(); } } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { } // 計算x、y點的氣泡是否處於水平儀的儀表盤內 private boolean isContain(int x, int y) { // 計算氣泡的圓心坐標X、Y int bubbleCx = x + show.bubble.getWidth() / 2; int bubbleCy = y + show.bubble.getWidth() / 2; // 計算水平儀儀表盤的圓心坐標X、Y int backCx = show.back.getWidth() / 2; int backCy = show.back.getWidth() / 2; // 計算氣泡的圓心與水平儀儀表盤的圓心之間的距離 double distance = Math.sqrt((bubbleCx - backCx) * (bubbleCx - backCx) + (bubbleCy - backCy) * (bubbleCy - backCy)); // 若兩個圓心的距離小於它們的半徑差,即可認為處於該點的氣泡依然位於儀表盤內 if (distance < (show.back.getWidth() - show.bubble.getWidth()) / 2) { return true; } else { return false; } } }
註意:該應用必須在有方向傳感器的真機中安裝運行才能看到效果。
Demo程序運行效果界面截圖如下: