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

Flutter中如何按图表曲线裁剪容器的指定区域?

解决曲线图表容器贴合曲线轮廓的问题

你需要实现的效果是让背景容器顶部贴合图表曲线,同时保持图表高度固定、填充色铺满曲线下方的屏幕区域。可以通过自定义裁剪路径(CustomClipper)+ ClipPath组件来实现,核心是让裁剪路径和图表的曲线路径完全一致。

实现步骤

  1. 抽离图表曲线的路径计算逻辑,让裁剪器和绘图器复用同一套逻辑,避免路径错位
  2. 创建自定义Clipper,生成和图表曲线匹配的裁剪路径,并闭合到屏幕底部
  3. 用ClipPath包裹背景容器,实现顶部曲线裁剪
  4. 调整Widget层级,确保图表显示在裁剪后的容器上方

修改后的完整代码

1. 抽离路径计算逻辑并更新CurvedChartPainter

// 抽离公共的路径计算方法,确保裁剪和绘图路径一致
Path _generateChartPath(List<Map<String, double>> xValues, double maxValue, Size size, double strokeWidth) {
  final path = Path();
  if (xValues.length <= 1) return path;

  final firstValueHeight = size.height * (xValues.first.values.first / maxValue);
  path.moveTo(0.0, size.height - firstValueHeight);

  final itemXDistance = size.width / (xValues.length - 1);

  for (var i = 1; i < xValues.length; i++) {
    final x = itemXDistance * i;
    final valueHeight = size.height - strokeWidth - ((size.height - strokeWidth) * (xValues[i].values.elementAt(0) / maxValue));
    final previousValueHeight = size.height - strokeWidth - ((size.height - strokeWidth) * (xValues[i - 1].values.elementAt(0) / maxValue));

    path.quadraticBezierTo(
      x - (itemXDistance / 2) - (itemXDistance / 8),
      previousValueHeight,
      x - (itemXDistance / 2),
      valueHeight + ((previousValueHeight - valueHeight) / 2),
    );
    path.quadraticBezierTo(
      x - (itemXDistance / 2) + (itemXDistance / 8),
      valueHeight,
      x,
      valueHeight,
    );
  }
  return path;
}

// 曲线图表绘图器
class CurvedChartPainter extends CustomPainter {
  final List<Map<String, double>> xValues;
  final List<Map<String, double>> yValues;
  final Color? color;
  final double strokeWidth;
  final List<Color> gradientColors;
  final List<double> gradientStops;
  final TextStyle labelTextStyle;

  CurvedChartPainter({
    required this.xValues,
    required this.yValues,
    required this.strokeWidth,
    this.color,
    this.gradientColors = const [
      Color(0x00F63E02),
      Color(0xFFFFFFFF),
    ],
    this.gradientStops = const [0.0, 1.0],
    this.labelTextStyle = const TextStyle(color: Colors.grey, fontSize: 12),
  });

  @override
  void paint(Canvas canvas, Size size) {
    var linePaint = Paint()
      ..color = color ?? const Color(0xFFF63E02)
      ..style = PaintingStyle.stroke
      ..strokeWidth = strokeWidth;

    var fillPaint = Paint()..style = PaintingStyle.fill;

    final maxValue = yValues.last.values.last;
    final chartPath = _generateChartPath(xValues, maxValue, size, strokeWidth);
    final fillPath = Path.from(chartPath)
      ..lineTo(size.width, size.height)
      ..lineTo(0.0, size.height)
      ..close();

    // 绘制渐变填充
    final gradient = LinearGradient(
      colors: gradientColors,
      stops: gradientStops,
      begin: Alignment.topCenter,
      end: Alignment.bottomCenter,
    );
    fillPaint.shader = gradient.createShader(Rect.fromLTWH(0, 0, size.width, size.height));
    canvas.drawPath(fillPath, fillPaint);

    // 绘制曲线
    canvas.drawPath(chartPath, linePaint);
  }

  @override
  bool shouldRepaint(covariant CurvedChartPainter oldDelegate) {
    return oldDelegate.xValues != xValues ||
        oldDelegate.yValues != yValues ||
        oldDelegate.color != color ||
        oldDelegate.strokeWidth != strokeWidth ||
        oldDelegate.gradientColors != gradientColors ||
        oldDelegate.gradientStops != gradientStops;
  }
}

2. 创建自定义裁剪器ChartCurveClipper

// 曲线裁剪器,用于裁剪背景容器
class ChartCurveClipper extends CustomClipper<Path> {
  final List<Map<String, double>> xValues;
  final double maxValue;
  final double strokeWidth;

  ChartCurveClipper({
    required this.xValues,
    required this.maxValue,
    required this.strokeWidth,
  });

  @override
  Path getClip(Size size) {
    final chartPath = _generateChartPath(xValues, maxValue, size, strokeWidth);
    // 闭合路径到屏幕底部,形成完整的裁剪区域
    chartPath.lineTo(size.width, size.height);
    chartPath.lineTo(0.0, size.height);
    chartPath.close();
    return chartPath;
  }

  @override
  bool shouldReclip(covariant ChartCurveClipper oldClipper) {
    return oldClipper.xValues != xValues ||
        oldClipper.maxValue != maxValue ||
        oldClipper.strokeWidth != strokeWidth;
  }
}

3. 修改Build方法,使用ClipPath裁剪背景容器

@override
Widget build(BuildContext context) {
  final List<Map<String, double>> xValues = [
    {"day 1": 80.0},
    {"day 2": 50.0},
    {"day 3": 30.0},
    {"day 4": 50.0},
    {"day 5": 10.0},
    {"day 6": 0.0},
    {"day 7": 100.0},
  ];

  final List<Map<String, double>> yValues = [
    {"0": 0.0},
    {"20": 20.0},
    {"40": 40.0},
    {"60": 60.0},
    {"80": 80.0},
    {"100": 100.0},
  ];

  const stroke = 3.0;
  final maxValue = yValues.last.values.last;
  final chartHeight = MediaQuery.of(context).size.height * 0.2;

  return Stack(
    children: [
      // 裁剪后的背景容器,铺满屏幕,顶部贴合曲线
      ClipPath(
        clipper: ChartCurveClipper(
          xValues: xValues,
          maxValue: maxValue,
          strokeWidth: stroke,
        ),
        child: Container(
          height: double.infinity,
          width: double.infinity,
          color: blue2, // 替换为你的背景色
        ),
      ),
      // 固定高度的图表
      SizedBox(
        width: MediaQuery.of(context).size.width,
        height: chartHeight,
        child: CustomPaint(
          painter: CurvedChartPainter(
            color: blue5,
            yValues: yValues,
            strokeWidth: stroke,
            xValues: xValues,
            gradientStops: const [0.22, 0.63], // 修复原代码的语法错误
            gradientColors: [
              calenderLinearGradient2.withOpacity(0.46),
              blue1.withOpacity(0),
            ],
          ),
        ),
      ),
    ],
  );
}

关键说明

  • 抽离_generateChartPath方法,确保裁剪路径和图表曲线完全一致,避免错位
  • ChartCurveClipper生成的路径闭合到屏幕底部,让裁剪后的容器保留曲线下方的全部区域
  • 修复了原代码中gradientStops: [0.22, 063]的语法错误,改为合法的0.63
  • Stack层级保持背景容器在底层,图表在上层,确保曲线和填充效果正常显示

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 05:49:58