27、传感器数据可视化:使用Canvas或MPAndroidChart绘制实时传感器数据波形图。

做Android传感器开发,最头疼的是什么?

我个人觉得,是调试阶段看不到数据变化。你写了个距离传感器监听,log里打印出一堆数字,比如5.0、3.2、0.0……光看这些数字,你很难直观感受到传感器在“想什么”。

所以,数据可视化就变得特别重要。说白了,就是把数字变成波形图,一眼就能看出趋势。

这一章,我们就来聊聊怎么在手机上画出实时波形图。我会介绍两种方式:Canvas自绘MPAndroidChart库。两种我都用过,各有各的适用场景。

27.1 为什么需要实时波形图?

你想想看,距离传感器的数据是连续变化的。手靠近,数值从5降到0;手远离,数值又从0升到5。如果只看log,你只能看到一个个孤立的点。

但波形图不一样。它能展示出整个变化过程。比如,你可以看到数据是否有毛刺、是否有延迟、是否稳定。

我在项目中遇到过一个问题:某款手机的距离传感器在快速靠近时,数据会跳变。光看log根本发现不了,但波形图上一眼就能看到那个“尖刺”。

所以,学会画波形图,是传感器调试的必备技能。

27.2 方案一:使用Canvas自绘波形图

Canvas是Android原生的绘图API。它的好处是轻量、灵活、没有额外依赖。如果你只需要画一个简单的波形,用Canvas就够了。

27.2.1 核心思路

我们需要一个自定义View,在onDraw()方法里不断重绘。数据存储在一个队列里,每次新数据进来,就把旧数据往左推。

嗯,这里要注意:队列长度决定了波形图的宽度。我一般用屏幕宽度除以每个数据点的间距,比如每个点占2像素,屏幕宽1080,那就存540个点。

27.2.2 代码实现

下面是一个简单的实现。我把它封装成了一个WaveformView

public class WaveformView extends View {
    private Paint linePaint;
    private Paint gridPaint;
    private float[] dataPoints;
    private int maxDataPoints;
    private int currentIndex = 0;
    private float maxValue = 10.0f; // 距离传感器最大距离

    public WaveformView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        linePaint = new Paint();
        linePaint.setColor(Color.parseColor("#2196F3"));
        linePaint.setStrokeWidth(3f);
        linePaint.setStyle(Paint.Style.STROKE);
        linePaint.setAntiAlias(true);

        gridPaint = new Paint();
        gridPaint.setColor(Color.parseColor("#E0E0E0"));
        gridPaint.setStrokeWidth(1f);

        // 假设每个数据点占2像素,屏幕宽度为1080
        maxDataPoints = 540;
        dataPoints = new float[maxDataPoints];
        // 初始化为无效值
        Arrays.fill(dataPoints, -1f);
    }

    public void addDataPoint(float value) {
        dataPoints[currentIndex] = value;
        currentIndex = (currentIndex + 1) % maxDataPoints;
        postInvalidate(); // 触发重绘
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawGrid(canvas);
        drawWaveform(canvas);
    }

    private void drawGrid(Canvas canvas) {
        float height = getHeight();
        float width = getWidth();
        // 画水平网格线
        for (int i = 0; i <= 4; i++) {
            float y = height * i / 4;
            canvas.drawLine(0, y, width, y, gridPaint);
        }
    }

    private void drawWaveform(Canvas canvas) {
        float height = getHeight();
        float width = getWidth();
        float stepX = width / maxDataPoints;

        Path path = new Path();
        boolean firstPoint = true;

        for (int i = 0; i < maxDataPoints; i++) {
            int index = (currentIndex + i) % maxDataPoints;
            float value = dataPoints[index];
            if (value < 0) continue; // 跳过无效数据

            float x = i * stepX;
            float y = height - (value / maxValue) * height;

            if (firstPoint) {
                path.moveTo(x, y);
                firstPoint = false;
            } else {
                path.lineTo(x, y);
            }
        }
        canvas.drawPath(path, linePaint);
    }
}

使用起来也很简单:

WaveformView waveformView = findViewById(R.id.waveform_view);
// 在传感器回调中调用
waveformView.addDataPoint(distance);
我的小技巧: 如果数据更新频率很高(比如每10ms一次),建议用postInvalidate()而不是invalidate()。前者可以在非UI线程调用,避免卡顿。

27.3 方案二:使用MPAndroidChart库

如果你需要更专业的图表效果——比如缩放、拖拽、坐标轴标签、动画——那我建议直接用MPAndroidChart。这是一个非常成熟的开源库。

27.3.1 添加依赖

build.gradle里加上:

implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'

27.3.2 布局文件

<com.github.mikephil.charting.charts.LineChart
    android:id="@+id/lineChart"
    android:layout_width="match_parent"
    android:layout_height="300dp" />

27.3.3 实时更新数据

核心逻辑和Canvas类似,但MPAndroidChart帮我们处理了坐标轴、标签、动画等细节。

public class ChartHelper {
    private LineChart lineChart;
    private LineDataSet dataSet;
    private LineData lineData;
    private int maxVisiblePoints = 100; // 最多显示100个点

    public ChartHelper(LineChart chart) {
        this.lineChart = chart;
        setupChart();
    }

    private void setupChart() {
        // 初始化数据集
        dataSet = new LineDataSet(new ArrayList<Entry>(), "距离传感器");
        dataSet.setColor(Color.parseColor("#FF5722"));
        dataSet.setLineWidth(2f);
        dataSet.setDrawCircles(false);
        dataSet.setMode(LineDataSet.Mode.CUBIC_BEZIER); // 平滑曲线

        lineData = new LineData(dataSet);
        lineChart.setData(lineData);
        lineChart.getDescription().setEnabled(false);

        // 限制可见范围
        lineChart.setVisibleXRangeMaximum(maxVisiblePoints);
        lineChart.setAutoScaleMinMaxEnabled(true);
    }

    public void addEntry(float value) {
        LineData data = lineChart.getData();
        if (data != null) {
            ILineDataSet set = data.getDataSetByIndex(0);
            if (set == null) {
                set = createDataSet();
                data.addDataSet(set);
            }
            // 添加新数据点
            data.addEntry(new Entry(set.getEntryCount(), value), 0);
            // 通知数据更新
            data.notifyDataChanged();
            lineChart.notifyDataSetChanged();
            // 移动到最新位置
            lineChart.setVisibleXRangeMaximum(maxVisiblePoints);
            lineChart.moveViewToX(data.getEntryCount() - maxVisiblePoints);
        }
    }

    private ILineDataSet createDataSet() {
        LineDataSet set = new LineDataSet(new ArrayList<Entry>(), "距离传感器");
        set.setColor(Color.parseColor("#FF5722"));
        set.setLineWidth(2f);
        set.setDrawCircles(false);
        set.setMode(LineDataSet.Mode.CUBIC_BEZIER);
        return set;
    }
}

在传感器回调里调用:

chartHelper.addEntry(distance);
注意: 我曾经在项目里遇到过一个坑——MPAndroidChart在数据点非常多时(比如超过1000个),内存占用会飙升。建议设置setVisibleXRangeMaximum()限制可见范围,并且定期清理旧数据。

27.4 两种方案的对比

特性 Canvas自绘 MPAndroidChart
依赖 需引入第三方库
灵活性 极高,可定制任何效果 较高,但受限于库的API
功能丰富度 低,需自己实现坐标轴、标签等 高,自带缩放、拖拽、动画
性能 好,适合高频更新 一般,数据量大时需优化
学习成本 需要了解Canvas API 需要了解库的API

27.5 核心逻辑流程图

下面这张图展示了实时波形图的核心数据流。从传感器监听,到数据存储,再到UI绘制,每一步都很关键。

传感器监听 onSensorChanged() 数据队列 循环数组 / List UI绘制 onDraw() / notifyDataSetChanged() 循环:持续更新 实时波形图数据流

27.6 性能优化建议

实时绘制波形图,最怕的就是卡顿。我总结了几条经验:

  • 控制数据点数量:不要无限存储。设置一个最大长度,比如500个点。旧数据该丢就丢。
  • 减少重绘频率:如果传感器每秒更新100次,你没必要每秒画100次。可以每50ms重绘一次,或者用postInvalidateOnAnimation()
  • 使用硬件加速:在自定义View的构造器里调用setLayerType(LAYER_TYPE_HARDWARE, null),能显著提升Canvas绘制性能。
  • 避免在onDraw里创建对象:比如Path、Paint这些,最好在初始化时创建好,复用。
核心要点: 实时波形图的关键在于“数据队列 + 增量绘制”。不要每次重绘都从头画到尾,而是只画变化的部分。MPAndroidChart内部就是这么做的。

好了,这一章的内容就到这里。Canvas和MPAndroidChart两种方式,你都可以试试。我个人习惯是:简单调试用Canvas,正式产品用MPAndroidChart。毕竟,少写代码就是少出bug嘛。


公众号:蓝海资料掘金营,微信deep3321