You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何解决Flutter中fl_chart折线图因50Hz BLE数据导致的抖动

解决Flutter中fl_chart 50Hz数据下折线图抖动问题

问题背景

在Flutter移动端应用中使用fl_chart包绘制折线图,数据由BLE设备以50Hz(每秒50次)频率推送,订阅数据通知后绘制图表时出现明显抖动;当数据推送频率低于5Hz时图表稳定,但刷新率无法满足需求,需要实现50Hz以上的稳定绘图。

抖动核心原因

50Hz的数据推送意味着每秒要触发50次UI更新,接近Flutter默认60Hz的UI刷新率上限,加上频繁调用setState触发全量重绘、fl_chart默认配置存在冗余计算,最终导致图表渲染压力过载,出现抖动。

具体解决方案

1. 数据节流+批量更新

不触发每条数据的即时UI更新,而是缓存数据后定时批量同步,平衡刷新率与重绘次数:

List<FlSpot> _cachedData = [];
Timer? _updateTimer;

// 收到BLE数据时先缓存
void onBleDataReceived(double value) {
  final currentX = DateTime.now().millisecondsSinceEpoch.toDouble();
  _cachedData.add(FlSpot(currentX, value));
  
  // 每20ms(对应50Hz)批量更新一次
  if (_updateTimer?.isActive != true) {
    _updateTimer = Timer(const Duration(milliseconds: 20), () {
      setState(() {
        iredData.addAll(_cachedData);
        // 限制数据长度,避免内存溢出
        if (iredData.length > 200) {
          iredData.removeRange(0, iredData.length - 200);
        }
      });
      _cachedData.clear();
    });
  }
}

2. 精简fl_chart配置,降低绘制开销

关闭不必要的渲染元素,简化图表计算逻辑:

LineChartData(
  minY: -50,
  maxY: 50,
  // 固定X轴范围,避免每次更新重新计算边界
  minX: iredData.isNotEmpty ? iredData.first.x : 0,
  maxX: iredData.isNotEmpty ? iredData.last.x : 100,
  lineTouchData: LineTouchData(enabled: false),
  clipData: FlClipData.all(),
  gridData: FlGridData(
    show: true,
    drawVerticalLine: false,
    // 减少网格线数量,降低绘制次数
    horizontalInterval: 20,
    getDrawingHorizontalLine: (value) => const FlLine(
      color: Colors.grey,
      strokeWidth: 0.5,
    ),
  ),
  borderData: FlBorderData(show: false),
  lineBarsData: [
    LineChartBarData(
      spots: iredData,
      dotData: FlDotData(show: false),
      // 替换渐变为纯色,减少色彩计算
      color: widget.sinColor,
      barWidth: 2, // 适度减小线条宽度
      isCurved: false,
      isStrokeCapRound: false, // 关闭圆角描边
    ),
  ],
  titlesData: FlTitlesData(show: false),
)

3. 用ValueNotifier局部更新,避免全局重绘

将图表封装为独立组件,通过ValueNotifier仅在数据变化时更新图表区域,而非触发页面全局重绘:

class StableLineChart extends StatelessWidget {
  final ValueNotifier<List<FlSpot>> dataNotifier;
  final Color sinColor;

  const StableLineChart({super.key, required this.dataNotifier, required this.sinColor});

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<List<FlSpot>>(
      valueListenable: dataNotifier,
      builder: (context, data, child) {
        return AspectRatio(
          aspectRatio: 1.5,
          child: Padding(
            padding: const EdgeInsets.only(bottom: 24.0),
            child: LineChart(
              // 插入上述优化后的LineChartData配置
              ...
            ),
          ),
        );
      },
    );
  }
}

// 更新数据时调用
dataNotifier.value = List.from(iredData);

4. 自定义CustomPainter(可选)

如果fl_chart的封装层级带来过多冗余,可直接用CustomPainter绘制折线,减少中间层开销:

class SimpleLinePainter extends CustomPainter {
  final List<FlSpot> data;
  final Color lineColor;
  final double lineWidth;
  final double minY;
  final double maxY;

  SimpleLinePainter({
    required this.data,
    required this.lineColor,
    required this.lineWidth,
    required this.minY,
    required this.maxY,
  });

  @override
  void paint(Canvas canvas, Size size) {
    if (data.length < 2) return;

    final paint = Paint()
      ..color = lineColor
      ..strokeWidth = lineWidth
      ..style = PaintingStyle.stroke;

    final path = Path();
    // 转换数据坐标到画布坐标
    final firstPoint = data.first;
    path.moveTo(
      0,
      size.height - ((firstPoint.y - minY) / (maxY - minY)) * size.height,
    );

    for (int i = 1; i < data.length; i++) {
      final point = data[i];
      final x = (i / (data.length - 1)) * size.width;
      final y = size.height - ((point.y - minY) / (maxY - minY)) * size.height;
      path.lineTo(x, y);
    }

    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(covariant SimpleLinePainter oldDelegate) {
    return oldDelegate.data != data;
  }
}

// 使用示例
AspectRatio(
  aspectRatio: 1.5,
  child: Padding(
    padding: const EdgeInsets.only(bottom: 24.0),
    child: CustomPaint(
      painter: SimpleLinePainter(
        data: iredData,
        lineColor: widget.sinColor,
        lineWidth: 2,
        minY: -50,
        maxY: 50,
      ),
    ),
  ),
)

效果验证

通过以上优化,既能维持接近50Hz的图表刷新率,又能大幅降低渲染压力,解决抖动问题,同时控制内存占用适配移动端性能。

内容的提问来源于stack exchange,提问作者Shion

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 12:47:43