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

Flutter中CustomPainter绘制傅里叶级数波形出现异常连线问题

傅里叶级数动画波形异常连线问题修复

问题描述

我正在还原傅里叶级数的首个动画效果,拖动滑块时波形原本能较好逼近方波,但突然出现异常连线导致画面混乱。以下是当前代码(Testing需作为MaterialApp的首页调用):

import 'package:flutter/material.dart';
import "dart:math";

class Testing extends StatefulWidget {
  const Testing({super.key});

  @override
  State<Testing> createState() => _TestingState();
}

class _TestingState extends State<Testing> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation _animation;
  late double _value = 1;

  @override
  void initState() {
    super.initState();
    _controller =
        AnimationController(vsync: this, duration: const Duration(seconds: 5));
    _animation = Tween(begin: 2 * pi, end: 0.0).animate(_controller);
    _controller.forward();
    _controller.addListener(() {
      if (_animation.isCompleted) {
        _controller.reset();
        _controller.forward();
      }
      setState(() {});
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Column(
      children: [
        Slider(
          min: 1,
          max: 100,
          divisions: 100,
          value: _value,
          onChanged: (new_value) => _value = new_value,
        ),
        SizedBox(
            height: 500,
            width: MediaQuery.sizeOf(context).width,
            child: ClipRRect(
              child: CustomPaint(
                painter: Sky(
                    angle: _animation.value,
                    running: _controller.isAnimating,
                    value: _value.toInt()),
                size: Size(MediaQuery.sizeOf(context).width, 500),
              ),
            )),
      ],
    ));
  }
}
List<Offset> wave = [];
class Sky extends CustomPainter {
  bool running;
  double angle;
  int value;
  Sky({required this.angle, required this.running, required this.value});
  @override
  void paint(Canvas canvas, Size size) {
    var paint = Paint()
      ..color = Colors.black
      ..strokeWidth = 1.0
      ..style = PaintingStyle.stroke;

    double centerX = 100;
    double centerY = size.height / 2;
    canvas.translate(centerX, centerY);

    double circleRadius = 80 * (4 / (1 * pi));
    double x1 = 0;
    double y1 = 0;
    for (int i = 0; i < value; i++) {
      double prevx = x1;
      double prevy = y1;
      int n = (i * 2) + 1;

      circleRadius = 80 * (4 / (n * pi));

      x1 += circleRadius * cos(n*angle);
      y1 += circleRadius * sin(n*angle);

      canvas.drawCircle(Offset(prevx, prevy), circleRadius, paint);

      canvas.drawLine(Offset(prevx, prevy), Offset(x1, y1), paint);
    }
    double waveValue = running ? y1 : 0;
    wave.insert(0, Offset(240, y1));

    if (running) {
      canvas.drawLine(Offset(x1, y1), Offset(240, y1), paint);
    }

    List<Offset> wave2 =
    wave.map((e) => e.translate(wave.indexOf(e).toDouble(), 0)).toList();
    Path path = Path();
    path.moveTo(wave2[0].dx, wave2[0].dy);
    for (int i = 1; i < wave2.length; i++) {
      path.lineTo(wave2[i].dx, wave2[i].dy);
    }
    for (int i = wave2.length-1; i > 1; i--) {
      path.lineTo(wave2[i].dx, wave2[i].dy);
    }
    canvas.drawPath(path, paint);
    if (wave.length > 1000) {
      wave.removeLast();
    }
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}

问题根源

  1. 全局wave列表状态污染:全局变量wave会保留所有历史绘制数据,滑块调整时新的绘制逻辑会混入旧数据,导致异常连线。
  2. 波形平移逻辑错误:用wave.indexOf(e)获取索引会因为重复坐标导致索引混乱,且每次绘制都平移所有点,会让波形出现无规律偏移。
  3. 路径绘制冗余:反向遍历绘制重复点,会产生多余的连线。

修复后的完整代码

import 'package:flutter/material.dart';
import "dart:math";

class Testing extends StatefulWidget {
  const Testing({super.key});

  @override
  State<Testing> createState() => _TestingState();
}

class _TestingState extends State<Testing> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation _animation;
  late double _value = 1;

  @override
  void initState() {
    super.initState();
    _controller =
        AnimationController(vsync: this, duration: const Duration(seconds: 5));
    _animation = Tween(begin: 2 * pi, end: 0.0).animate(_controller);
    _controller.forward();
    _controller.addListener(() {
      if (_animation.isCompleted) {
        _controller.reset();
        _controller.forward();
      }
      setState(() {});
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Column(
      children: [
        Slider(
          min: 1,
          max: 100,
          divisions: 100,
          value: _value,
          onChanged: (new_value) => setState(() => _value = new_value),
        ),
        SizedBox(
            height: 500,
            width: MediaQuery.sizeOf(context).width,
            child: ClipRRect(
              child: CustomPaint(
                painter: Sky(
                    angle: _animation.value,
                    running: _controller.isAnimating,
                    value: _value.toInt()),
                size: Size(MediaQuery.sizeOf(context).width, 500),
              ),
            )),
      ],
    ));
  }
}

class Sky extends CustomPainter {
  final bool running;
  final double angle;
  final int value;
  // 将wave改为类成员变量,避免全局状态污染
  final List<Offset> _wave = [];
  // 记录波形X轴偏移量,保证点依次排列
  int _waveXOffset = 0;

  Sky({required this.angle, required this.running, required this.value});

  @override
  void paint(Canvas canvas, Size size) {
    var paint = Paint()
      ..color = Colors.black
      ..strokeWidth = 1.0
      ..style = PaintingStyle.stroke;

    double centerX = 100;
    double centerY = size.height / 2;
    canvas.translate(centerX, centerY);

    double x1 = 0;
    double y1 = 0;
    for (int i = 0; i < value; i++) {
      double prevx = x1;
      double prevy = y1;
      int n = (i * 2) + 1;
      double circleRadius = 80 * (4 / (n * pi));

      x1 += circleRadius * cos(n * angle);
      y1 += circleRadius * sin(n * angle);

      canvas.drawCircle(Offset(prevx, prevy), circleRadius, paint);
      canvas.drawLine(Offset(prevx, prevy), Offset(x1, y1), paint);
    }

    if (running) {
      // 绘制到波形区域的垂直线
      canvas.drawLine(Offset(x1, y1), Offset(240, y1), paint);
      // 添加新波形点,X坐标基于偏移量递增
      _wave.insert(0, Offset(240 + _waveXOffset, y1));
      _waveXOffset++;

      // 限制波形点数量,防止内存占用过高
      if (_wave.length > 300) {
        _wave.removeLast();
      }
    } else {
      // 停止动画时清空波形
      _wave.clear();
      _waveXOffset = 0;
    }

    // 绘制连续波形路径
    if (_wave.length > 1) {
      Path path = Path();
      path.moveTo(_wave.first.dx, _wave.first.dy);
      for (int i = 1; i < _wave.length; i++) {
        path.lineTo(_wave[i].dx, _wave[i].dy);
      }
      canvas.drawPath(path, paint);
    }
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}

关键修复说明

  • 移除全局wave变量:将波形数据改为Sky类的成员变量,滑块调整时会创建新的Sky实例,自动重置波形数据,避免新旧数据混合。
  • 修正波形坐标逻辑:通过_waveXOffset记录当前波形的X轴位置,每次添加新点时直接设置正确的X坐标,无需重复平移所有点。
  • 简化路径绘制:只从左到右连接波形点,移除冗余的反向绘制逻辑。
  • 滑块回调添加setState:原代码滑块调整后未触发UI更新,现在添加setState实现实时刷新。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 08:04:52