8、实战:磁力计罗盘:结合地磁场传感器实现简易电子罗盘、角度计算与UI绘制
好,终于到了我们最激动人心的实战环节。前面几章我们把霍尔传感器、地磁场传感器的基础知识都过了一遍,现在该动手了。
这一章,我们要做一个简易的电子罗盘。说白了,就是让手机告诉你,哪边是北。听起来简单,但里面涉及的东西可不少:传感器数据的读取、噪声滤波、角度计算,最后还要画一个漂亮的UI指针。
我个人习惯是,先搭好框架,再一步步填肉。咱们先看看整体流程。
8.1 整体架构:数据流怎么走?
先别急着写代码,理清思路最重要。我画了一张流程图,你看一眼就明白了。
嗯,流程很清晰。传感器拿到原始数据,经过滤波去掉噪声,然后算角度,最后画到屏幕上。每一步都有坑,咱们一个一个踩过去。
8.2 第一步:注册传感器监听
在Android里用传感器,第一步永远是拿到 SensorManager。我建议你在 onResume() 里注册,在 onPause() 里注销。为什么?省电啊。屏幕关了还一直读传感器,电池扛不住的。
private SensorManager sensorManager;
private Sensor magnetometer;
private float[] magValues = new float[3];
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_compass);
sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
magnetometer = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
}
@Override
protected void onResume() {
super.onResume();
if (magnetometer != null) {
sensorManager.registerListener(this, magnetometer, SensorManager.SENSOR_DELAY_UI);
}
}
@Override
protected void onPause() {
super.onPause();
sensorManager.unregisterListener(this);
}
这里有个细节:SENSOR_DELAY_UI 是每秒大概60次的数据更新,对罗盘来说足够了。你要是做游戏可能用 GAME 或 FASTEST,但罗盘不需要那么快。
8.3 第二步:读取数据与滤波
传感器返回三个值:values[0] 是X轴磁场强度,values[1] 是Y轴,values[2] 是Z轴。单位是微特斯拉(μT)。
但原始数据噪声很大。你拿着手机原地转一圈,数值会跳来跳去。怎么办?加个低通滤波。
我曾经在一个项目里直接用原始数据做角度,结果指针像抽风一样抖个不停。后来加了个简单的滑动平均,效果立竿见影。
private float[] filtered = new float[3];
private static final float ALPHA = 0.2f; // 滤波系数
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
// 低通滤波:新值 = 旧值 * (1-alpha) + 新采样 * alpha
for (int i = 0; i < 3; i++) {
filtered[i] = filtered[i] * (1 - ALPHA) + event.values[i] * ALPHA;
}
float azimuth = calculateAzimuth(filtered);
updateUI(azimuth);
}
}
ALPHA 值越小,滤波越强,但响应也越慢。0.2是个不错的平衡点。你想想看,如果设成0.05,转手机半天指针才动,那体验就很差了。
8.4 第三步:角度计算——atan2 的妙用
现在我们有滤波后的X和Y值了。怎么算出方位角?
核心公式就一个:azimuth = atan2(Y, X)。注意是 atan2,不是 atan。为什么?因为 atan2 能处理四个象限的情况,直接返回 -π 到 π 之间的角度。
private float calculateAzimuth(float[] mag) {
float x = mag[0];
float y = mag[1];
// 弧度转角度
float azimuth = (float) Math.toDegrees(Math.atan2(y, x));
// 归一化到 0~360
if (azimuth < 0) {
azimuth += 360;
}
return azimuth;
}
这里有个坑:atan2(y, x) 返回的是弧度,要转成角度。另外,手机平放时Z轴数据基本用不上,但如果你手机是竖着拿的,那就得用加速度计做倾斜补偿了。嗯,那是进阶内容,咱们先不展开。
8.5 第四步:UI绘制——用Canvas画一个罗盘
角度算出来了,接下来就是画界面。我个人喜欢用自定义View,用Canvas画一个圆形的罗盘,中间一根指针指向北方。
public class CompassView extends View {
private Paint circlePaint;
private Paint needlePaint;
private float azimuth = 0f;
public CompassView(Context context, AttributeSet attrs) {
super(context, attrs);
circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
circlePaint.setStyle(Paint.Style.STROKE);
circlePaint.setStrokeWidth(4);
circlePaint.setColor(Color.DKGRAY);
needlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
needlePaint.setColor(Color.RED);
needlePaint.setStrokeWidth(6);
}
public void setAzimuth(float azimuth) {
this.azimuth = azimuth;
invalidate(); // 触发重绘
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float cx = getWidth() / 2f;
float cy = getHeight() / 2f;
float radius = Math.min(cx, cy) - 20;
// 画外圈
canvas.drawCircle(cx, cy, radius, circlePaint);
// 画指针(指向北方)
float endX = cx + (float) (radius * 0.7 * Math.sin(Math.toRadians(azimuth)));
float endY = cy - (float) (radius * 0.7 * Math.cos(Math.toRadians(azimuth)));
canvas.drawLine(cx, cy, endX, endY, needlePaint);
// 画方向标记
Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
textPaint.setTextSize(36);
textPaint.setColor(Color.BLACK);
canvas.drawText("N", cx - 15, cy - radius + 40, textPaint);
canvas.drawText("S", cx - 15, cy + radius - 10, textPaint);
canvas.drawText("E", cx + radius - 40, cy + 10, textPaint);
canvas.drawText("W", cx - radius + 10, cy + 10, textPaint);
}
}
这段代码的核心逻辑:sin 和 cos 算出指针终点的坐标,然后画一条从圆心到终点的线。注意角度是顺时针的,所以Y轴方向要取反(屏幕坐标系Y轴向下)。
Path 画一个箭头形状,或者用渐变色。我一般会在指针尾部加一个小圆点,看起来更专业。
8.6 完整代码整合
把上面几块拼起来,就是一个完整的电子罗盘了。在 MainActivity 里,把 CompassView 加到布局中,然后在 onSensorChanged 里调用 compassView.setAzimuth(azimuth) 就行。
public class MainActivity extends AppCompatActivity implements SensorEventListener {
private CompassView compassView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
compassView = new CompassView(this, null);
setContentView(compassView);
// ... 传感器初始化代码同上
}
@Override
public void onSensorChanged(SensorEvent event) {
// ... 滤波和角度计算代码
compassView.setAzimuth(azimuth);
}
}
运行一下,转手机,指针应该会跟着转。如果不动,检查一下传感器有没有注册成功,或者看看日志里有没有报错。
8.7 避坑指南
做这个项目,我遇到过几个典型问题,列出来给你参考:
- 指针抖动: 滤波系数
ALPHA设太大。试试 0.1 或 0.15。 - 方向反了: 检查
atan2的参数顺序,或者试试azimuth = 360 - azimuth。 - 数值不准: 手机附近有强磁场干扰(比如音箱、金属桌)。远离它们再试。
- UI不刷新: 确认调用了
invalidate()。我刚开始学的时候经常忘这步。
我曾经在一个客户现场调试罗盘,发现指针总是偏30度。查了半天,原来是手机壳里有个磁吸支架。拆掉壳子,立马正常。所以,如果你发现数据异常,先看看周围有没有磁性物体。
8.8 扩展思考
这个简易罗盘只用了地磁场传感器。实际产品中,通常会融合加速度计和陀螺仪的数据,做更精确的方位估算。Google 的 SensorManager.getRotationMatrix() 和 getOrientation() 就是干这个的。
但基础原理不变:读数据、滤波、算角度、画UI。你把这个流程吃透了,后面学更复杂的传感器融合就轻松多了。
好,这一章就到这里。代码不多,但每一步都是实战经验。你动手试试,有问题随时翻回来看看。
公众号:蓝海资料掘金营,微信deep3321