25、传感器数据可视化:使用Canvas绘制实时磁场强度波形图
数据可视化,说白了就是把枯燥的数字变成眼睛能看懂的东西。
做霍尔传感器开发时,我最头疼的就是调试阶段。你拿着手机晃来晃去,Logcat里刷出一堆数字,根本看不出磁场变化规律。后来我干脆写了个实时波形图,把磁场强度画成曲线,问题一下子清晰了。
这一节,我们就来搞定这件事。
25.1 为什么需要实时波形图
你想想看,传感器数据是连续变化的。单个数值只能告诉你「现在磁场多强」,但你看不到趋势。
- 磁场是平稳的,还是在波动?
- 是突然跳变,还是缓慢变化?
- 噪声有多大?
这些问题,看数字很难判断。但画成波形图,一眼就能看出来。
核心思路:每次传感器回调新数据,就往数组末尾追加一个值,同时把数组最前面的值丢掉。然后重绘Canvas,把数组里的数据连成线。
25.2 数据结构设计
我习惯用一个固定长度的队列来存数据。为什么用固定长度?因为波形图宽度有限,存太多数据也没意义。
// 最大显示点数,对应屏幕宽度
private static final int MAX_DATA_POINTS = 200;
// 存储磁场强度值的队列
private float[] magneticData = new float[MAX_DATA_POINTS];
// 当前有效数据个数
private int dataCount = 0;
每次新数据来了,做两件事:
- 如果数据还没满,直接追加到末尾
- 如果数据已满,把所有数据左移一位,新数据放在最后
public void addDataPoint(float value) {
if (dataCount < MAX_DATA_POINTS) {
magneticData[dataCount] = value;
dataCount++;
} else {
// 左移一位,丢掉最旧的数据
System.arraycopy(magneticData, 1, magneticData, 0, MAX_DATA_POINTS - 1);
magneticData[MAX_DATA_POINTS - 1] = value;
}
// 触发重绘
invalidate();
}
嗯,这里要注意:System.arraycopy 比手动循环快得多。我在早期项目里吃过亏,用for循环左移,结果界面卡顿。后来换成这个,流畅多了。
25.3 Canvas绘制波形
绘制波形图,核心就三步:
- 画背景和网格线
- 把数据映射到屏幕坐标
- 用Path把点连成线
25.3.1 画背景和网格
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float width = getWidth();
float height = getHeight();
// 背景
canvas.drawColor(Color.parseColor("#1a1a2e"));
// 画网格线
Paint gridPaint = new Paint();
gridPaint.setColor(Color.parseColor("#2d2d5e"));
gridPaint.setStrokeWidth(1);
// 水平网格线(5条)
for (int i = 0; i <= 4; i++) {
float y = height * i / 4;
canvas.drawLine(0, y, width, y, gridPaint);
}
// 垂直网格线(10条)
for (int i = 0; i <= 10; i++) {
float x = width * i / 10;
canvas.drawLine(x, 0, x, height, gridPaint);
}
}
25.3.2 数据映射到屏幕坐标
传感器返回的磁场强度单位是微特斯拉(μT),范围大概在 -100 到 100 之间。但屏幕坐标是像素,需要做映射。
// 假设磁场范围 -80μT ~ 80μT
private static final float MIN_VALUE = -80f;
private static final float MAX_VALUE = 80f;
private float mapValueToY(float value, float height) {
// 先把值归一化到 0~1
float normalized = (value - MIN_VALUE) / (MAX_VALUE - MIN_VALUE);
// 翻转Y轴(屏幕Y向下为正)
return height * (1 - normalized);
}
为什么会翻转Y轴?因为Canvas的坐标系Y轴朝下,而我们习惯Y轴朝上表示数值增大。所以用 1 - normalized 做个翻转。
25.3.3 画波形线
private void drawWaveform(Canvas canvas, float width, float height) {
if (dataCount < 2) return;
Paint linePaint = new Paint();
linePaint.setColor(Color.parseColor("#00d2ff"));
linePaint.setStrokeWidth(3);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setAntiAlias(true);
Path path = new Path();
float stepX = width / MAX_DATA_POINTS;
// 第一个点
float firstY = mapValueToY(magneticData[0], height);
path.moveTo(0, firstY);
// 连接后续点
for (int i = 1; i < dataCount; i++) {
float x = i * stepX;
float y = mapValueToY(magneticData[i], height);
path.lineTo(x, y);
}
canvas.drawPath(path, linePaint);
}
小技巧:用 Path.lineTo 画折线就够了。如果想更平滑,可以用 Path.cubicTo 画贝塞尔曲线。但我个人觉得,实时波形图用折线更清晰,能看出每个采样点的真实值。
25.4 完整的绘制流程
把上面几块拼起来,就是完整的 onDraw 方法:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float width = getWidth();
float height = getHeight();
// 1. 画背景
canvas.drawColor(Color.parseColor("#1a1a2e"));
// 2. 画网格
drawGrid(canvas, width, height);
// 3. 画波形
drawWaveform(canvas, width, height);
// 4. 画当前值标注
drawCurrentValue(canvas, width, height);
}
25.5 与霍尔传感器对接
有了自定义View,接下来就是把它和传感器数据连起来。
public class MainActivity extends AppCompatActivity {
private WaveformView waveformView;
private SensorManager sensorManager;
private Sensor magneticSensor;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
waveformView = new WaveformView(this);
setContentView(waveformView);
sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
magneticSensor = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
}
@Override
protected void onResume() {
super.onResume();
sensorManager.registerListener(sensorEventListener,
magneticSensor, SensorManager.SENSOR_DELAY_UI);
}
@Override
protected void onPause() {
super.onPause();
sensorManager.unregisterListener(sensorEventListener);
}
private SensorEventListener sensorEventListener = new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent event) {
// 取X轴磁场强度
float xValue = event.values[0];
// 添加到波形图
waveformView.addDataPoint(xValue);
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// 精度变化时可以做些处理
}
};
}
注意:SENSOR_DELAY_UI 大约是 60ms 回调一次,对波形图来说足够了。如果用 SENSOR_DELAY_FASTEST,数据更新太快,反而看不清趋势,而且耗电。
25.6 知识体系总览
下面这张图,把整个流程串起来了:
25.7 避坑指南
做实时波形图,有几个坑我踩过,分享给你:
- 不要在 onDraw 里 new 对象:Paint、Path 这些对象,在构造函数里创建好,重复使用。否则GC频繁触发,界面会卡顿。
- 数据队列用数组,别用 ArrayList:ArrayList 的 remove(0) 操作是 O(n) 的,而且会触发内存拷贝。用数组加 System.arraycopy 效率高得多。
- 注意线程安全:传感器回调在子线程,onDraw 在主线程。我习惯用
invalidate()触发重绘,而不是直接在回调里画图。 - Y轴范围要动态调整:如果磁场强度突然超出预设范围,波形会跑到屏幕外面。我后来加了个自适应范围,根据最近100个点的最大值最小值自动调整。
我曾经遇到过一个问题:波形图在低端手机上画出来有锯齿。后来发现是没开抗锯齿。加上 setAntiAlias(true) 之后,曲线平滑多了。
25.8 扩展思路
基础波形图做好了,还可以加些实用功能:
- 多通道显示:同时画 X、Y、Z 三轴磁场,用不同颜色区分
- 峰值标记:自动标出当前窗口内的最大值和最小值
- 暂停/继续:点击屏幕暂停波形,方便分析某个瞬间的数据
- 导出数据:把波形数据导出成 CSV 文件,用 Excel 做进一步分析
这些功能我在实际项目里都用过。特别是多通道显示,调试地磁传感器时,能同时看到三个轴的变化,定位问题快很多。
好了,波形图这部分就讲到这里。代码量不大,但实用性很强。你把它跑起来,拿手机在磁铁旁边晃一晃,就能看到磁场强度实时变化的曲线了。
公众号:蓝海资料掘金营,微信deep3321