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

Flutter动态路径曲线绘制与路径动画实现问题求助

Flutter 动态交替曲线路径绘制及动画问题

我在Flutter中实现了一条路径,已完成初始直线部分,但添加后续曲线时无法正常显示。希望能根据传入CustomPaint的数值,动态生成左右交替的曲线,并实现路径沿容器逐步绘制的动画效果。

当前代码

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Scrollable Custom Paint Example'),
        ),
        body: Center(
          child: SingleChildScrollView(
            child: CustomPaint(
              size: Size(200, 200), // Set the height as per your requirement
              painter: DynamicPathPainter(
                numberOfCurves: 10, // Set the number of curves
                curveSpacing: 0.1, // Set the spacing between the curves (fraction of the screen height)
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class DynamicPathPainter extends CustomPainter {
  final int numberOfCurves;
  final double curveSpacing;

  DynamicPathPainter({required this.numberOfCurves, required this.curveSpacing});

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

    Path path = Path();
    path.moveTo(size.width / 2, 0); // Start at the middle top of the screen

    double currentHeight = size.height * 0.3; // Initial straight section height
    path.lineTo(size.width / 2, currentHeight); // Draw the initial straight line

    // Define control points and end point for the first cubic Bezier curve (curve to the left)
    Offset controlPoint1 = Offset(size.width / 2, size.height * 0.5);
    Offset controlPoint2 = Offset(size.width / 2 - 150, size.height * 0.8);
    Offset endPoint1 = Offset(size.width / 2 - 200, size.height);

    // Add the first cubic Bezier curve
    path.cubicTo(controlPoint1.dx, controlPoint1.dy, controlPoint2.dx, controlPoint2.dy, endPoint1.dx, endPoint1.dy);

    // Define control points and end point for the second cubic Bezier curve (curve back to the right)
    Offset controlPoint3 = Offset(size.width / 2 - 250, size.height * 1.2);
    Offset controlPoint4 = Offset(size.width / 2, size.height * 1.1);
    Offset endPoint2 = Offset(size.width / 2 + 50, size.height * 1.2);

    // Add the second cubic Bezier curve
    path.cubicTo(controlPoint3.dx, controlPoint3.dy, controlPoint4.dx, controlPoint4.dy, endPoint2.dx, endPoint2.dy);

    // Define control points and end point for the third cubic Bezier curve (curve back to the right again)
    Offset controlPoint5 = Offset(size.width, size.height * 1.2);
    Offset controlPoint6 = Offset(size.width / 2 + 200, size.height * 1.2);
    Offset endPoint3 = Offset(size.width / 2 + 150, size.height * 1.5);

    // Add the third cubic Bezier curve
    path.cubicTo(controlPoint5.dx, controlPoint5.dy, controlPoint6.dx, controlPoint6.dy, endPoint3.dx, endPoint3.dy);

    canvas.drawPath(path, paint);
  }

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

效果对比

当前效果

当前路径效果

预期效果

预期路径效果

当尝试基于曲线数量动态生成内容时,输出完全不符合预期。


解决方案

完整实现代码

下面的代码同时解决了动态交替曲线生成和路径逐步绘制动画的问题:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Dynamic Curve Path Animation')),
        body: const Center(child: AnimatedCurvePath()),
      ),
    );
  }
}

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

  @override
  State<AnimatedCurvePath> createState() => _AnimatedCurvePathState();
}

class _AnimatedCurvePathState extends State<AnimatedCurvePath> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 5),
      vsync: this,
    )..forward();

    _animation = Tween<double>(begin: 0.0, end: 1.0).animate(_controller);
  }

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

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: CustomPaint(
        size: const Size(300, 1200), // 调整高度适配多条曲线
        painter: DynamicPathPainter(
          numberOfCurves: 10,
          curveSpacing: 0.1,
          animationValue: _animation.value,
        ),
      ),
    );
  }
}

class DynamicPathPainter extends CustomPainter {
  final int numberOfCurves;
  final double curveSpacing;
  final double animationValue;

  DynamicPathPainter({
    required this.numberOfCurves,
    required this.curveSpacing,
    required this.animationValue,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.blue
      ..style = PaintingStyle.stroke
      ..strokeWidth = 10.0
      ..strokeCap = StrokeCap.round;

    final path = Path();
    // 初始直线段
    final startY = 0.0;
    final initialEndY = size.height * 0.05; // 缩短初始直线,给曲线留空间
    path.moveTo(size.width / 2, startY);
    path.lineTo(size.width / 2, initialEndY);

    double currentY = initialEndY;
    final curveHeight = size.height * (1 - 0.05) / numberOfCurves; // 每条曲线的高度
    final curveWidth = size.width * 0.4; // 曲线左右偏移的宽度

    for (int i = 0; i < numberOfCurves; i++) {
      final isLeftCurve = i % 2 == 0; // 偶数次向左,奇数次向右
      final endX = isLeftCurve ? size.width / 2 - curveWidth : size.width / 2 + curveWidth;
      final endY = currentY + curveHeight;

      // 贝塞尔曲线控制点:让曲线平滑过渡
      final control1X = size.width / 2;
      final control1Y = currentY + curveHeight * 0.3;
      final control2X = endX;
      final control2Y = currentY + curveHeight * 0.7;

      path.cubicTo(
        control1X,
        control1Y,
        control2X,
        control2Y,
        endX,
        endY,
      );

      // 回到中间,准备下一条曲线
      final middleY = endY + curveHeight * curveSpacing;
      path.lineTo(size.width / 2, middleY);
      currentY = middleY;
    }

    // 根据动画值截取路径
    if (animationValue < 1.0) {
      final pathMetrics = path.computeMetrics();
      final totalLength = pathMetrics.first.length;
      final currentLength = totalLength * animationValue;
      final clippedPath = pathMetrics.first.extractPath(0.0, currentLength);
      canvas.drawPath(clippedPath, paint);
    } else {
      canvas.drawPath(path, paint);
    }
  }

  @override
  bool shouldRepaint(covariant DynamicPathPainter oldDelegate) {
    return oldDelegate.animationValue != animationValue ||
        oldDelegate.numberOfCurves != numberOfCurves ||
        oldDelegate.curveSpacing != curveSpacing;
  }
}

关键改进点

  1. 动态曲线生成:通过循环+奇偶判断实现左右交替,所有坐标基于CustomPaint的size计算,避免硬编码固定值导致的显示异常。
  2. 路径绘制动画:使用AnimationController控制进度,通过PathMetrics截取路径的部分长度,实现逐步绘制的效果。
  3. 视觉优化:添加strokeCap: StrokeCap.round让路径端点更圆润,调整曲线高度和间距让整体布局更均匀。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 00:35:04