Android指南針的實現
本文將介紹如何通過自定義View實現了一個指南針的效果,效果圖如下:
指南針效果
首先是根據磁力計和加速度計計算南向和手機的夾角。通過Android的SensorManager類進行計算,使用的是右手坐標系:
右手坐標系
獲取SensorManager,并初始化磁力計和加速度計:
public class CompassActivity extends AppCompatActivity implements SensorEventListener {
private SensorManager mSensorManager;
private Sensor mMagneticSensor;
private Sensor mAccelerateSensor;
@Overrideprotected void onCreate(Bundle savedInstanceState) {
...
mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
if (mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) != null && mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null)
{
mMagneticSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
mAccelerateSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
mHasNeededSensors = true;
} else {
Toast.makeText(this, "沒有磁力計或加速度計", Toast.LENGTH_SHORT).show();
return;
}
}
在onResume里面注冊磁力計和加速度計,并在onPause的時候解除注冊:
@Override
protected void onResume() {
super.onResume();
if (mHasNeededSensors) {
mSensorManager.registerListener(this, mMagneticSensor, SensorManager.SENSOR_DELAY_NORMAL);
mSensorManager.registerListener(this, mAccelerateSensor, SensorManager.SENSOR_DELAY_NORMAL);
}
}
@Override
protected void onPause() {
super.onPause();
if (mHasNeededSensors) {
mSensorManager.unregisterListener(this);
}
}
實現onSensorChanged接口,這樣當磁力計或加速度計數值發生變化的時候會調用該函數告知新數值:
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
mMagneticFieldValues = event.values;
} else if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
mAccelerometerValues = event.values;
}
calculateOrientation();
}
通過calculateOrientation函數計算南向跟手機x軸的夾角,這里用到SensorManager的兩個函數,getRotationMatrix和getOrientation。具體計算原理可以參閱兩個函數的實現,用法很簡單,傳入加速度計和磁力計數值即可。得到一個3X1的矩陣,矩陣的三個值代表南向繞三個坐標軸旋轉的角度,單位是弧度,我們繪制指南針只需要使用矩陣的第一個值,即南向繞Z軸順時針旋轉過的角度,用alpha表示。當南向指向手機正上方時,alpha=0;指向手機正下方時,alpha=MATH.PI,如下圖所示:
南向和手機Y軸的夾角
為了方便使用極坐標繪制指南針的羅盤,我們把它轉換成和X軸的夾角seta,seta=alpha-Math.PI/2,計算的代碼如下:
// 計算指南針的南向和手機x軸的角度,以弧度表示(-PI, PI]
private void calculateOrientation() {
float[] results = new float[3];
float[] rotates = new float[9];
SensorManager.getRotationMatrix(rotates, null, mAccelerometerValues, mMagneticFieldValues);
SensorManager.getOrientation(rotates, results);
// alpha是南向和手機Y軸的夾角
float alpha = results[0];
float seta;
// 將alpha轉換成南向和手機X軸的夾角,便于使用極坐標系繪制指南針的圓盤
if ((alpha - (-Math.PI)) < 0.000000001) {
seta = (float) (Math.PI / 2);
} else {
seta = (float) (alpha - Math.PI / 2);
}
Log.i("compass:", Math.toDegrees(seta)+"");
mCompassView.setSouth(seta);
mCompassView.invalidate();
}
利用夾角seta,我們就可以利用自定義View繪制指南針了。
首先定義指南針View的屬性,通過這些屬性,我們可以控制指南針的外觀:
<resources>
<declare-styleable name="CompassViewStyle">
<attr name="radius" format="dimension" /> <!--羅盤半徑-->
<attr name="short_dash" format="dimension" /> <!--羅盤外圈短輻射線的長度-->
<attr name="long_dash" format="dimension" /> <!--羅盤外圈長輻射線的長度-->
<attr name="text_size" format="dimension" /> <!--羅盤上文字的尺寸-->
</declare-styleable>
</resources>
在自定義View里解析這些屬性:
public class CompassView extends View {
private Paint mPaint;
private double mRadius;
private double mDash_short;
private double mDash_long;
private double mTextSize;
private double mSeta = -Math.PI / 2;
public CompassView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CompassViewStyle);
mRadius = typedArray.getDimension(R.styleable.CompassViewStyle_radius, 0);
mDash_short = typedArray.getDimension(R.styleable.CompassViewStyle_short_dash, 20);
mDash_long = typedArray.getDimension(R.styleable.CompassViewStyle_long_dash, 30);
mTextSize = typedArray.getDimension(R.styleable.CompassViewStyle_text_size, 10);
typedArray.recycle();
}
...
}
重寫onDraw方法,完成繪制。第一步是在以屏幕中心為原點的XOY坐標系中計算出需要繪制的點的坐標(x, y),第二步是轉換成以手機屏幕左上角為原點的屏幕坐標系的值(width/2 + x, height/2 - y),其中height和width是屏幕的寬高,最終的繪制是在屏幕坐標系中進行的:
public class CompassView extends View {
@Override
protected void onDraw(Canvas canvas) {
mPaint.setColor(getResources().getColor(R.color.text_bg_green_stroke));
double width = getWidth();
double height = getHeight();
// choose the shortest in width, height, radius as actual radius
double radius = (mRadius < width && mRadius < height) ? mRadius : Math.min(width, height);
// draw the outer circle
for (int seta = -180; seta < 180; seta++) {
float x = (float) (width / 2 + radius * Math.cos(seta * Math.PI/180));
float y = (float) (height / 2 + radius * Math.sin(seta * Math.PI / 180));
// 羅盤外邊緣每隔1°畫一條短輻射線,每隔20°畫一條長輻射線
if ((seta - (-180)) % 20 == 0) {
float x2 = (float) (width / 2 + (radius + mDash_long) * Math.cos(seta * Math.PI/180));
float y2 = (float) (height / 2 + (radius + mDash_long) * Math.sin(seta * Math.PI / 180));
canvas.drawLine(x, y, x2, y2, mPaint);
} else {
float x2 = (float) (width / 2 + (radius + mDash_short) * Math.cos(seta * Math.PI/180));
float y2 = (float) (height / 2 + (radius + mDash_short) * Math.sin(seta * Math.PI / 180));
canvas.drawLine(x, y, x2, y2, mPaint);
}
}
// draw 東西南北
double spacing = 10;
mPaint.setTextSize((float)mTextSize);
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
canvas.drawText("東", (float)(width / 2 + radius - spacing - mTextSize), (float)(height / 2 + (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.descent - fontMetrics.leading), mPaint);
canvas.drawText("西", (float)(width / 2 - radius + spacing), (float)(height / 2 + (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.descent - fontMetrics.leading), mPaint);
canvas.drawText("南", (float)(width /2 - mTextSize /2), (float)(height / 2 + radius - spacing - fontMetrics.descent), mPaint);
canvas.drawText("北", (float)(width /2 - mTextSize /2), (float)(height / 2 - radius + spacing + fontMetrics.leading - fontMetrics.ascent), mPaint);
// draw 8 triangles
double triangle_half_bottom = 40;
double triangle_vertical_line = 200;
double triangle_vertical_line_2 = 300;
Point p0 = new Point((int)(width / 2), (int)(height / 2 - triangle_vertical_line_2));
Point p00 = new Point((int)(width / 2 + triangle_half_bottom), (int)(height / 2));
Point p000 = new Point((int)(width / 2 - triangle_half_bottom), (int)(height / 2));
Point p1 = new Point((int)(width / 2 + triangle_vertical_line / Math.sqrt(2)), (int)(height / 2 - triangle_vertical_line / Math.sqrt(2)));
Point p11 = new Point((int)(width / 2 + triangle_half_bottom / Math.sqrt(2)), (int)(height / 2 + triangle_half_bottom / Math.sqrt(2)));
Point p111 = new Point((int)(width / 2 - triangle_half_bottom / Math.sqrt(2)), (int)(height / 2 - triangle_half_bottom / Math.sqrt(2)));
Point p2 = new Point((int)(width / 2 + triangle_vertical_line_2), (int)(height / 2));
Point p22 = new Point((int)(width / 2), (int)(height / 2 + triangle_half_bottom));
Point p222 = new Point((int)(width / 2), (int)(height / 2 - triangle_half_bottom));
Point p3 = new Point((int)(width / 2 + triangle_vertical_line / Math.sqrt(2)), (int)(height / 2 + triangle_vertical_line / Math.sqrt(2)));
Point p33 = new Point((int)(width / 2 - triangle_half_bottom / Math.sqrt(2)), (int)(height / 2 + triangle_half_bottom / Math.sqrt(2)));
Point p333 = new Point((int)(width / 2 + triangle_half_bottom / Math.sqrt(2)), (int)(height / 2 - triangle_half_bottom / Math.sqrt(2)));
Point p4 = new Point((int)(width / 2), (int)(height / 2 + triangle_vertical_line_2));
Point p44 = new Point((int)(width / 2 - triangle_half_bottom), (int)(height / 2));
Point p444 = new Point((int)(width / 2 + triangle_half_bottom), (int)(height / 2));
Point p5 = new Point((int)(width / 2 - triangle_vertical_line / Math.sqrt(2)), (int)(height / 2 + triangle_vertical_line / Math.sqrt(2)));
Point p55 = new Point((int)(width / 2 - triangle_half_bottom / Math.sqrt(2)), (int)(height / 2 - triangle_half_bottom / Math.sqrt(2)));
Point p555 = new Point((int)(width / 2 + triangle_half_bottom / Math.sqrt(2)), (int)(height / 2 + triangle_half_bottom / Math.sqrt(2)));
Point p6 = new Point((int)(width / 2 - triangle_vertical_line_2), (int)(height / 2));
Point p66 = new Point((int)(width / 2 ), (int)(height / 2 - triangle_half_bottom));
Point p666 = new Point((int)(width / 2), (int)(height / 2 + triangle_half_bottom));
Point p7 = new Point((int)(width / 2 - triangle_vertical_line / Math.sqrt(2)), (int)(height / 2 - triangle_vertical_line / Math.sqrt(2)));
Point p77 = new Point((int)(width / 2 + triangle_half_bottom / Math.sqrt(2)), (int)(height / 2 - triangle_half_bottom / Math.sqrt(2)));
Point p777 = new Point((int)(width / 2 - triangle_half_bottom / Math.sqrt(2)), (int)(height / 2 + triangle_half_bottom / Math.sqrt(2)));
Path path = new Path();
path.moveTo(p0.x, p0.y);
path.lineTo(p00.x, p00.y);
path.lineTo(p000.x, p000.y);
path.close();
path.moveTo(p1.x, p1.y);
path.lineTo(p11.x, p11.y);
path.lineTo(p111.x, p111.y);
path.close();
path.moveTo(p2.x, p2.y);
path.lineTo(p22.x, p22.y);
path.lineTo(p222.x, p222.y);
path.close();
path.moveTo(p3.x, p3.y);
path.lineTo(p33.x, p33.y);
path.lineTo(p333.x, p333.y);
path.close();
path.moveTo(p4.x, p4.y);
path.lineTo(p44.x, p44.y);
path.lineTo(p444.x, p444.y);
path.close();
path.moveTo(p5.x, p5.y);
path.lineTo(p55.x, p55.y);
path.lineTo(p555.x, p555.y);
path.close();
path.moveTo(p6.x, p6.y);
path.lineTo(p66.x, p66.y);
path.lineTo(p666.x, p666.y);
path.close();
path.moveTo(p7.x, p7.y);
path.lineTo(p77.x, p77.y);
path.lineTo(p777.x, p777.y);
path.close();
mPaint.setStyle(Paint.Style.FILL);
canvas.drawPath(path, mPaint);
// 計算指南針上的四個點
double spacing3 = 50;
Point b = new Point((int)(width / 2 + radius * Math.cos(mSeta)), (int)(height / 2 - radius * Math.sin(mSeta)));
Point bb = new Point((int)(width / 2 + spacing3 * Math.cos(mSeta - Math.PI / 2)), (int)(height / 2 - spacing3 * Math.sin(mSeta - Math.PI / 2)));
Point bbb = new Point((int)(width / 2 + spacing3 * Math.cos(Math.PI / 2 + mSeta)), (int)(height / 2 - spacing3 * Math.sin(Math.PI / 2 + mSeta)));
Point b2 = new Point((int)(width / 2 - radius * Math.cos(mSeta)), (int)(height / 2 + radius * Math.sin(mSeta)));
// 畫南向指針
Path path2 = new Path();
path2.moveTo(b.x, b.y);
path2.lineTo(bb.x, bb.y);
path2.lineTo(bbb.x, bbb.y);
path2.close();
mPaint.setColor(getResources().getColor(R.color.red_a11));
canvas.drawPath(path2, mPaint);
// 北向指針
Path path3 = new Path();
path3.moveTo(b2.x, b2.y);
path3.lineTo(bbb.x, bbb.y);
path3.lineTo(bb.x, bb.y);
path3.close();
mPaint.setColor(getResources().getColor(R.color.blue_a1));
canvas.drawPath(path3, mPaint);
}