Android指南针的实现
GoodSpeed
8年前
<p>本文将介绍如何通过自定义View实现了一个指南针的效果,效果图如下:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/421a592c6e304003c2c1c3990181b4bf.jpg"></p> <p style="text-align:center">指南针效果</p> <p>首先是根据磁力计和加速度计计算南向和手机的夹角。通过Android的SensorManager类进行计算,使用的是右手坐标系:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/ac763741c2341fe692af0fa95773674f.jpg"></p> <p style="text-align:center">右手坐标系</p> <p>获取SensorManager,并初始化磁力计和加速度计:</p> <pre> <code class="language-java">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; } }</code></pre> <p>在onResume里面注册磁力计和加速度计,并在onPause的时候解除注册:</p> <pre> <code class="language-java">@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); } }</code></pre> <p>实现onSensorChanged接口,这样当磁力计或加速度计数值发生变化的时候会调用该函数告知新数值:</p> <pre> <code class="language-java">@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(); }</code></pre> <p>通过calculateOrientation函数计算南向跟手机x轴的夹角,这里用到SensorManager的两个函数,getRotationMatrix和getOrientation。具体计算原理可以参阅两个函数的实现,用法很简单,传入加速度计和磁力计数值即可。得到一个3X1的矩阵,矩阵的三个值代表南向绕三个坐标轴旋转的角度,单位是弧度,我们绘制指南针只需要使用矩阵的第一个值,即南向绕Z轴顺时针旋转过的角度,用alpha表示。当南向指向手机正上方时,alpha=0;指向手机正下方时,alpha=MATH.PI,如下图所示:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/d69b47e4b340e620952502d408a025cd.png"></p> <p style="text-align:center">南向和手机Y轴的夹角</p> <p>为了方便使用极坐标绘制指南针的罗盘,我们把它转换成和X轴的夹角seta,seta=alpha-Math.PI/2,计算的代码如下:</p> <pre> <code class="language-java">// 计算指南针的南向和手机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(); }</code></pre> <p>利用夹角seta,我们就可以利用自定义View绘制指南针了。</p> <p>首先定义指南针View的属性,通过这些属性,我们可以控制指南针的外观:</p> <pre> <code class="language-java"><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></code></pre> <p>在自定义View里解析这些属性:</p> <pre> <code class="language-java">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(); } ... }</code></pre> <p>重写onDraw方法,完成绘制。第一步是在以屏幕中心为原点的XOY坐标系中计算出需要绘制的点的坐标(x, y),第二步是转换成以手机屏幕左上角为原点的屏幕坐标系的值(width/2 + x, height/2 - y),其中height和width是屏幕的宽高,最终的绘制是在屏幕坐标系中进行的:</p> <pre> <code class="language-java">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); }</code></pre> <p> </p> <p> </p>