Flutter中如何按图表曲线裁剪容器的指定区域?
解决曲线图表容器贴合曲线轮廓的问题
你需要实现的效果是让背景容器顶部贴合图表曲线,同时保持图表高度固定、填充色铺满曲线下方的屏幕区域。可以通过自定义裁剪路径(CustomClipper)+ ClipPath组件来实现,核心是让裁剪路径和图表的曲线路径完全一致。
实现步骤
- 抽离图表曲线的路径计算逻辑,让裁剪器和绘图器复用同一套逻辑,避免路径错位
- 创建自定义Clipper,生成和图表曲线匹配的裁剪路径,并闭合到屏幕底部
- 用ClipPath包裹背景容器,实现顶部曲线裁剪
- 调整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
相关产品推荐
相关产品推荐

