前言
?此文僅記錄,涉及到較多數學知識,沒有原理解析。
效果
image.png
代碼
- Activity
package com.example.windy.myapplication;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
public class MainActivity extends AppCompatActivity implements SensorEventListener{
private static final int AZUMUTH = 0, //方位 z
PITCH = 1, //水平上下 y
ROLL = 2; //水平左右 x
private SensorManager mSensorManager;
private final float[] mAccelerometerReading = new float[3];
private final float[] mMagnetometerReading = new float[3];
private final float[] mRotationMatrix = new float[9];
private final float[] mOrientationAngles = new float[3];
private LevelView levelView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
levelView = findViewById(R.id.level_view);
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
@Override
protected void onResume() {
super.onResume();
mSensorManager.registerListener(this, mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
SensorManager.SENSOR_DELAY_NORMAL);
mSensorManager.registerListener(this, mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD),
SensorManager.SENSOR_DELAY_NORMAL);
}
@Override
protected void onPause() {
super.onPause();
mSensorManager.unregisterListener(this);
}
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
System.arraycopy(event.values, 0, mAccelerometerReading,
0, mAccelerometerReading.length);
Log.e("TAG", mAccelerometerReading[0] + " " + mAccelerometerReading[1] + " " + mAccelerometerReading[2]);
}
else if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
System.arraycopy(event.values, 0, mMagnetometerReading,
0, mMagnetometerReading.length);
Log.e("TAG", mMagnetometerReading[0] + " " + mMagnetometerReading[1] + " " + mMagnetometerReading[2]);
}
updateOrientationAngles();
}
public void updateOrientationAngles() {
mSensorManager.getRotationMatrix(mRotationMatrix, null, mAccelerometerReading, mMagnetometerReading);
mSensorManager.getOrientation(mRotationMatrix, mOrientationAngles);
Log.e("TAG-ORIENTATION", "AZUMUTH " + mOrientationAngles[0] + " | PITCH " + mOrientationAngles[1] + " | ROLL " + mOrientationAngles[2]);
levelView.setAngle(-mOrientationAngles[ROLL], mOrientationAngles[PITCH]);
}
}
- LevelView
package com.example.windy.myapplication;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.view.View;
/**
* 水平儀控件
* 通過設置{@link #setAngle(double, double)}
* Learn From @author chen.canney@gmail.com
*/
public class LevelView extends View {
/**
* 最大圈半徑
*/
private float mLimitRadius = 0;
/**
* 氣泡半徑
*/
private float mBubbleRadius;
private Bitmap limitCircle;
private Bitmap bubbleBall;
private Paint paint = new Paint();
/**
* 中心點坐標
*/
private PointF centerPnt = new PointF();
/**
* 計算后的氣泡點
*/
private PointF bubblePoint;
private double pitchAngle = -90;
private double rollAngle = -90;
public LevelView(Context context) {
this(context, null);
}
public LevelView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LevelView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
limitCircle = BitmapFactory.decodeResource(context.getResources(), R.mipmap.bg_orientation);
bubbleBall = BitmapFactory.decodeResource(context.getResources(), R.mipmap.ball_orientation);
mLimitRadius = limitCircle.getWidth()/2;
mBubbleRadius = bubbleBall.getWidth()/2;
centerPnt.set(mLimitRadius - mBubbleRadius, mLimitRadius - mBubbleRadius);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width, height;
if (widthMode == MeasureSpec.EXACTLY){
width = widthSize;
}else {
width = getPaddingLeft() + limitCircle.getWidth() + getPaddingRight();
}
if (heightMode == MeasureSpec.EXACTLY){
height = heightSize;
}else {
height = getPaddingBottom() + limitCircle.getHeight() + getPaddingTop();
}
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(limitCircle, 0,0, paint);
drawBubble(canvas);
}
private boolean isCenter(PointF bubblePoint){
if(bubblePoint == null){
return false;
}
float threshold = 3.0f;
return Math.abs(bubblePoint.x - centerPnt.x) < threshold && Math.abs(bubblePoint.y - centerPnt.y) < threshold;
}
private void drawBubble(Canvas canvas) {
if(bubblePoint != null){
canvas.save();
canvas.translate(bubblePoint.x, bubblePoint.y);
canvas.drawBitmap(bubbleBall, 0, 0, paint);
canvas.restore();
}
}
/**
* Convert angle to screen coordinate point.
* @param rollAngle 橫滾角(弧度)
* @param pitchAngle 俯仰角(弧度)
* @return
*/
private PointF convertCoordinate(double rollAngle, double pitchAngle, double radius){
double scale = radius / Math.toRadians(90);
//以圓心為原點,使用弧度表示坐標
double x0 = -(rollAngle * scale);
double y0 = -(pitchAngle * scale);
//使用屏幕坐標表示氣泡點
double x = centerPnt.x - x0;
double y = centerPnt.y - y0;
return new PointF((float)x, (float)y);
}
/**
* @param pitchAngle (弧度)
* @param rollAngle (弧度)
*/
public void setAngle(double rollAngle, double pitchAngle) {
this.pitchAngle = pitchAngle;
this.rollAngle = rollAngle;
//考慮氣泡邊界不超出限制圓,此處減去氣泡的顯示半徑,做為最終的限制圓半徑
float limitRadius = mLimitRadius - mBubbleRadius;
bubblePoint = convertCoordinate(rollAngle, pitchAngle, mLimitRadius);
//坐標超出最大圓,取法向圓上的點
if(outLimit(bubblePoint, limitRadius)){
onCirclePoint(bubblePoint, limitRadius);
}
//水平時調用
if(isCenter(bubblePoint)){
setVisibility(INVISIBLE);
} else {
setVisibility(VISIBLE);
}
invalidate();
}
/**
* 驗證氣泡點是否超過限制{@link #mLimitRadius}
* @param bubblePnt
* @return
*/
private boolean outLimit(PointF bubblePnt, float limitRadius){
float cSqrt = (bubblePnt.x - centerPnt.x) * (bubblePnt.x - centerPnt.x)
+ (centerPnt.y - bubblePnt.y) * (centerPnt.y - bubblePnt.y);
if(cSqrt - limitRadius * limitRadius > 0){
return true;
}
return false;
}
/**
* 計算圓心到 bubblePnt點在圓上的交點坐標
* 即超出圓后的最大圓上坐標
* @param bubblePnt 氣泡點
* @param limitRadius 限制圓的半徑
* @return
*/
private PointF onCirclePoint(PointF bubblePnt, double limitRadius) {
double azimuth = Math.atan2((bubblePnt.y - centerPnt.y), (bubblePnt.x - centerPnt.x));
azimuth = azimuth < 0 ? 2 * Math.PI + azimuth : azimuth;
//圓心+半徑+角度 求圓上的坐標
double x1 = centerPnt.x + limitRadius * Math.cos(azimuth);
double y1 = centerPnt.y + limitRadius * Math.sin(azimuth);
bubblePnt.set((float) x1, (float) y1);
return bubblePnt;
}
}