云林县网站建设_网站建设公司_产品经理_seo优化
2026/1/5 11:46:23 网站建设 项目流程

效果图

代码实现步骤

1.定义文本控制器和焦点控制器

2.定义动画

3.定义默认的数据点

4.定义图标的相应配置

5.定义坐标范围

6.初始化操作

7.销毁操作

8.取消焦点的方法

9.添加数据点的方法

10.删除最后一个数据点的方法

11.清除所有数据点的方法

12.生成随机数据的方法

13.UI架构(标题栏,输入点区域,控制按钮区域,图表标题,自定义折线图,数据统计)

14.定义底部栏的子项

15.自定义绘制折线图的类

认识自定义折线图的类

数据流

数据源 (points) ↓ 坐标转换 (_convertToCanvas) ↓ 动画处理 (animationValue) ↓ 分层绘制 (网格→坐标轴→阴影→折线→数据点) ↓ 画布输出 (Canvas)

坐标

画布坐标系 (Canvas Coordinate System) 原点(0,0) ──────→ X轴正方向 │ │ ↓ Y轴正方向 数据坐标系 (Data Coordinate System) 原点(minX,minY) ───→ X轴正方向 │ ↑ Y轴正方向 │ 需要转换:数据Y轴向上,画布Y轴向下

核心代码--自定义的绘制类

class _LineChartPainter extends CustomPainter { final List<Offset> points; final double minX; final double maxX; final double minY; final double maxY; final double padding; final Color axisColor; final double axisWidth; final Color lineColor; final double lineWidth; final Color pointColor; final double pointRadius; final int xGridLines; final int yGridLines; final double animationValue; //构造函数 _LineChartPainter({ required this.points, // 数据点列表 required this.minX, // X轴最小值 required this.maxX, // X轴最大值 required this.minY, // Y轴最小值 required this.maxY, // Y轴最大值 required this.padding, // 图表内边距 required this.axisColor, // 坐标轴颜色 required this.axisWidth, // 坐标轴宽度 required this.lineColor, // 折线颜色 required this.lineWidth, // 折线宽度 required this.pointColor, // 数据点颜色 required this.pointRadius, // 数据点半径 required this.xGridLines, // X轴网格线数量 required this.yGridLines, // Y轴网格线数量 required this.animationValue, // 动画进度值(0-1) }); @override void paint(Canvas canvas, Size size) { //网格线画笔 final Paint gridPaint = Paint() ..color = Colors.grey[200]! ..strokeWidth = 0.5 ..style = PaintingStyle.stroke; //坐标轴画笔 final Paint axisPaint = Paint() ..color = axisColor ..strokeWidth = axisWidth ..style = PaintingStyle.stroke; //只描边 //折线画笔 final Paint linePaint = Paint() ..color = lineColor.withOpacity(0.8) ..strokeWidth = lineWidth ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round;//线头圆角 //数据点画笔 final Paint pointPaint = Paint() ..color = pointColor ..style = PaintingStyle.fill; //实心填充 //阴影区域画笔 final Paint shadowPaint = Paint() ..color = lineColor.withOpacity(0.1) ..style = PaintingStyle.fill; // 绘制背景 final Rect chartArea = Rect.fromLTWH( padding, //左 padding, //上 size.width - 2 * padding, //宽度 size.height - 2 * padding, //高度 ); // 绘制网格 _drawGrid(canvas, size, gridPaint); // 绘制坐标轴 _drawAxes(canvas, size, axisPaint); // 绘制坐标轴标签 _drawAxisLabels(canvas, size); // 如果有数据点,绘制阴影区域和折线 if (points.length > 1) { // 转换所有点为画布坐标 final List<Offset> canvasPoints = points.map((point) { return _convertToCanvas(point, size); }).toList(); // 根据动画值计算要绘制的点数 final int visiblePoints = (points.length * animationValue).ceil(); final List<Offset> visibleCanvasPoints = canvasPoints.sublist(0, visiblePoints.clamp(0, canvasPoints.length)); // 绘制阴影区域 if (visibleCanvasPoints.length > 1) { final Path shadowPath = Path(); shadowPath.moveTo(visibleCanvasPoints.first.dx, chartArea.bottom); for (final point in visibleCanvasPoints) { shadowPath.lineTo(point.dx, point.dy); } shadowPath.lineTo(visibleCanvasPoints.last.dx, chartArea.bottom); shadowPath.close(); canvas.drawPath(shadowPath, shadowPaint); } // 绘制折线(带动画) if (visibleCanvasPoints.length > 1) { final Path linePath = Path(); linePath.moveTo(visibleCanvasPoints.first.dx, visibleCanvasPoints.first.dy); for (int i = 1; i < visibleCanvasPoints.length; i++) { final current = visibleCanvasPoints[i]; final previous = visibleCanvasPoints[i - 1]; // 使用贝塞尔曲线使线条更平滑 final controlPoint1 = Offset( previous.dx + (current.dx - previous.dx) / 2, previous.dy, ); final controlPoint2 = Offset( current.dx - (current.dx - previous.dx) / 2, current.dy, ); linePath.cubicTo( controlPoint1.dx, controlPoint1.dy, controlPoint2.dx, controlPoint2.dy, current.dx, current.dy, ); } canvas.drawPath(linePath, linePaint); } // 绘制数据点(带缩放动画) for (int i = 0; i < visibleCanvasPoints.length; i++) { final point = visibleCanvasPoints[i]; final double pointAnimation = min(1.0, animationValue * 2 - i * 0.1); if (pointAnimation > 0) { // 绘制点 canvas.drawCircle( point, pointRadius * pointAnimation, pointPaint, ); // 绘制点的光晕效果 canvas.drawCircle( point, pointRadius * 1.5 * pointAnimation, Paint() ..color = pointColor.withOpacity(0.2 * pointAnimation) ..style = PaintingStyle.fill, ); // 绘制点的标签 final textPainter = TextPainter( text: TextSpan( text: "(${points[i].dx.toInt()}, ${points[i].dy.toStringAsFixed(1)})", style: const TextStyle( color: Colors.black87, fontSize: 10, fontWeight: FontWeight.w500, ), ), textDirection: TextDirection.ltr, )..layout(); textPainter.paint( canvas, Offset(point.dx - textPainter.width / 2, point.dy - 20), ); } } } else if (points.length == 1) { // 只有一个点时 final Offset canvasPoint = _convertToCanvas(points.first, size); canvas.drawCircle(canvasPoint, pointRadius, pointPaint); } else { // 没有数据点时显示提示 final textPainter = TextPainter( text: const TextSpan( text: "暂无数据,请添加数据点", style: TextStyle( color: Colors.grey, fontSize: 14, ), ), textDirection: TextDirection.ltr, )..layout(); textPainter.paint( canvas, Offset( size.width / 2 - textPainter.width / 2, size.height / 2 - textPainter.height / 2, ), ); } } // 绘制网格 void _drawGrid(Canvas canvas, Size size, Paint paint) { final double chartWidth = size.width - 2 * padding; final double chartHeight = size.height - 2 * padding; // 水平网格线 for (int i = 0; i <= yGridLines; i++) { final double y = padding + (chartHeight / yGridLines) * i; canvas.drawLine( Offset(padding, y), Offset(size.width - padding, y), paint, ); } // 垂直网格线 for (int i = 0; i <= xGridLines; i++) { final double x = padding + (chartWidth / xGridLines) * i; canvas.drawLine( Offset(x, padding), Offset(x, size.height - padding), paint, ); } } // 绘制坐标轴 void _drawAxes(Canvas canvas, Size size, Paint paint) { // X轴 canvas.drawLine( Offset(padding, size.height - padding), Offset(size.width - padding, size.height - padding), paint, ); // Y轴 canvas.drawLine( Offset(padding, padding), Offset(padding, size.height - padding), paint, ); // X轴箭头 canvas.drawLine( Offset(size.width - padding, size.height - padding), Offset(size.width - padding - 8, size.height - padding - 4), paint, ); canvas.drawLine( Offset(size.width - padding, size.height - padding), Offset(size.width - padding - 8, size.height - padding + 4), paint, ); // Y轴箭头 canvas.drawLine( Offset(padding, padding), Offset(padding - 4, padding + 8), paint, ); canvas.drawLine( Offset(padding, padding), Offset(padding + 4, padding + 8), paint, ); } // 绘制坐标轴标签 void _drawAxisLabels(Canvas canvas, Size size) { final double chartWidth = size.width - 2 * padding; final double chartHeight = size.height - 2 * padding; // X轴标签 for (int i = 0; i <= xGridLines; i++) { final double xValue = minX + (maxX - minX) / xGridLines * i; final double x = padding + (chartWidth / xGridLines) * i; final textPainter = TextPainter( text: TextSpan( text: xValue.toStringAsFixed(1), style: const TextStyle( color: Colors.black87, fontSize: 10, ), ), textDirection: TextDirection.ltr, )..layout(); textPainter.paint( canvas, Offset(x - textPainter.width / 2, size.height - padding + 5), ); } // Y轴标签 for (int i = 0; i <= yGridLines; i++) { final double yValue = maxY - (maxY - minY) / yGridLines * i; final double y = padding + (chartHeight / yGridLines) * i; final textPainter = TextPainter( text: TextSpan( text: yValue.toStringAsFixed(1), style: const TextStyle( color: Colors.black87, fontSize: 10, ), ), textDirection: TextDirection.ltr, )..layout(); textPainter.paint( canvas, Offset(padding - textPainter.width - 10, y - textPainter.height / 2), ); } // 坐标轴标题 final textX = TextPainter( text: const TextSpan( text: "X轴", style: TextStyle( color: Colors.black87, fontSize: 12, fontWeight: FontWeight.bold, ), ), textDirection: TextDirection.ltr, )..layout(); final textY = TextPainter( text: const TextSpan( text: "Y轴", style: TextStyle( color: Colors.black87, fontSize: 12, fontWeight: FontWeight.bold, ), ), textDirection: TextDirection.ltr, )..layout(); textX.paint( canvas, Offset(size.width - padding - textX.width / 2, size.height - padding + 20), ); textY.paint( canvas, Offset(padding - 30, padding - textY.height / 2 -20), ); } // 坐标转换 Offset _convertToCanvas(Offset point, Size size) { // 线性映射:数据坐标 → 画布坐标 // 公式:画布坐标 = 起点 + (数据值 - 最小值) / 范围 × 可用空间 final double width = size.width - 2 * padding; final double height = size.height - 2 * padding; final double x = padding + (point.dx - minX) / (maxX - minX) * width; final double y = size.height - padding - (point.dy - minY) / (maxY - minY) * height; // 注意Y轴需要翻转:画布原点在左上角,数学原点在左下角 return Offset(x, y); } @override bool shouldRepaint(_LineChartPainter oldDelegate) { return oldDelegate.points != points || //数据变化时重绘 oldDelegate.animationValue != animationValue; //动画变化时重绘 } }

代码实例

import 'dart:math'; import 'package:flutter/material.dart'; class HomePage extends StatefulWidget { const HomePage({super.key}); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin { //文本控制器 final TextEditingController _xController = TextEditingController(); final TextEditingController _yController = TextEditingController(); final FocusNode _xFocusNode = FocusNode(); final FocusNode _yFocusNode = FocusNode(); // 动画 late AnimationController _animationController; //动画控制器 late Animation<double> _animation; //创建动画值 // 数据点 List<Offset> points = [ const Offset(0, 1), const Offset(1, 2), const Offset(2, 3), const Offset(3, 1.5), const Offset(4, 4), const Offset(5, 2.5), const Offset(6, 3.5), const Offset(7, 2), const Offset(8, 5), const Offset(9, 3), const Offset(10, 4), ]; // 图表配置 final double padding = 40.0; //图标内边距 final double axisWidth = 1.5; //坐标轴线条粗细 final Color axisColor = Colors.grey; //坐标轴颜色 final Color lineColor = Colors.blue; //折线颜色 final Color pointColor = Colors.blueAccent; //数据点颜色 final double lineWidth = 2.5; //折线粗细 final double pointRadius = 4.0; //数据点半径 final int xGridLines = 10; //x轴方向网格线数量 final int yGridLines = 10; //y轴方向网格线数量 // 坐标范围 final double minX = 0; final double maxX = 10; final double minY = 0; final double maxY = 6; @override void initState() { super.initState(); // 初始化动画 _animationController = AnimationController( duration: const Duration(milliseconds: 1500), //动画时长1.5秒 vsync: this, ); _animation = CurvedAnimation( parent: _animationController, curve: Curves.easeInOut, //动画效果 ); // 开始动画 _animationController.forward(); } @override void dispose() { _animationController.dispose(); _xController.dispose(); _yController.dispose(); _xFocusNode.dispose(); _yFocusNode.dispose(); super.dispose(); } //===============================取消焦点的方法================================= void _unfocusAll(){ _xFocusNode.unfocus(); _yFocusNode.unfocus(); } //==============================添加数据点的方法================================== void _addPoint() { final double? x = double.tryParse(_xController.text); final double? y = double.tryParse(_yController.text); if (x != null && y != null && x >= minX && x <= maxX && y >= minY && y <= maxY) { setState(() { //添加新点 points.add(Offset(x, y)); // 按x坐标排序 points.sort((a, b) => a.dx.compareTo(b.dx)); }); // 重置输入和动画 _xController.clear(); _yController.clear(); _animationController.reset(); _animationController.forward(); _unfocusAll(); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text("请输入 ${minX}-${maxX} 的X和 ${minY}-${maxY} 的Y"), backgroundColor: Colors.red, ), ); _unfocusAll(); } } //=================================删除最后一个点=================================== void _removeLastPoint() { if (points.isNotEmpty) { setState(() { points.removeLast(); }); _animationController.reset(); //重置动画到开始状态 _animationController.forward();//启动动画 } } //=======================================清除所有点========================= void _clearAll() { setState(() { points.clear(); }); _animationController.reset(); //重置动画到开始状态 _animationController.forward();//启动动画 _unfocusAll(); } //===================================生成随机数据============================= void _generateRandomData() { setState(() { points.clear();//清空已有数据点 final Random random = Random(); //创建随机数生成器 //循环11次,生成11个点 for (int i = 0; i <= 10; i++) { final double x = i.toDouble(); //x轴坐标 final double y = random.nextDouble() * (maxY - minY) + minY; //随机y坐标 points.add(Offset(x, y)); //将点添加到列表 } //按x坐标升序排序 points.sort((a, b) => a.dx.compareTo(b.dx)); }); _animationController.reset(); //重置动画到开始状态 _animationController.forward();//启动动画 _unfocusAll(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("自定义动画折线图"), backgroundColor: Colors.blue, foregroundColor: Colors.white, ), body: Column( children: [ // 输入区域 Padding( padding: const EdgeInsets.all(16.0), child: Row( children: [ Expanded( child: TextField( controller: _xController, focusNode: _xFocusNode, keyboardType: TextInputType.number, decoration: InputDecoration( labelText: "X坐标", border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), ), ), ), const SizedBox(width: 12), Expanded( child: TextField( controller: _yController, focusNode: _yFocusNode, keyboardType: TextInputType.number, decoration: InputDecoration( labelText: "Y坐标", border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), ), ), ), const SizedBox(width: 12), ElevatedButton.icon( onPressed: _addPoint, label: const Text("添加"), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15), ), ), ], ), ), // 控制按钮区域 Padding( padding: const EdgeInsets.symmetric(horizontal: 5.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ OutlinedButton.icon( onPressed: _removeLastPoint, icon: const Icon(Icons.remove), label: const Text("删除最后的点",style: TextStyle(fontSize: 12),), ), OutlinedButton.icon( onPressed: _clearAll, icon: const Icon(Icons.delete), label: const Text("清空",style: TextStyle(fontSize: 12),), style: OutlinedButton.styleFrom( foregroundColor: Colors.red, ), ), ElevatedButton.icon( onPressed: _generateRandomData, icon: const Icon(Icons.shuffle), label: const Text("随机数据",style: TextStyle(fontSize: 12),), style: ElevatedButton.styleFrom( backgroundColor: Colors.green, foregroundColor: Colors.white, ), ), ], ), ), const SizedBox(height: 20), // 图表标题 Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "折线图", style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, ), ), //动态显示动画进度的文本 AnimatedBuilder( animation: _animationController, //监听的动画控制器 builder: (context, child) { //构建函数,当动画值变化时调用 return Text( //返回要显示的组件 "动画进度: ${(_animation.value * 100).toStringAsFixed(0)}%", //_animation.value:获取当前动画值 style: TextStyle( color: Colors.blue, fontSize: 14, fontWeight: FontWeight.w500, ), ); }, ), ], ), ), const SizedBox(height: 10), // 自定义折线图 Expanded( child: Padding( padding: const EdgeInsets.all(16.0), child: Container( decoration: BoxDecoration( color: Colors.grey[50], borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey[300]!), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.2), blurRadius: 8, offset: const Offset(0, 4), ), ], ), child: LayoutBuilder( builder: (context, constraints) { return AnimatedBuilder( animation: _animation, builder: (context, child) { return CustomPaint( painter: _LineChartPainter( //自定义绘制器 //传递各种参数 points: points, minX: minX, maxX: maxX, minY: minY, maxY: maxY, padding: padding, axisColor: axisColor, axisWidth: axisWidth, lineColor: lineColor, lineWidth: lineWidth, pointColor: pointColor, pointRadius: pointRadius, xGridLines: xGridLines, yGridLines: yGridLines, animationValue: _animation.value, ), size: Size(constraints.maxWidth, constraints.maxHeight), //指定绘制区域大小 ); }, ); }, ), ), ), ), // 数据统计 Container( padding: const EdgeInsets.all(16.0), color: Colors.grey[100], child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildStatCard("数据点数", points.length.toString(), Icons.data_array), _buildStatCard("X范围", "$minX - $maxX", Icons.horizontal_rule), _buildStatCard("Y范围", "$minY - $maxY", Icons.vertical_align_center), _buildStatCard( "平均值", points.isNotEmpty ? (points.map((p) => p.dy).reduce((a, b) => a + b) / points.length).toStringAsFixed(2) : "0", Icons.bar_chart, ), //points.map((p) => p.dy)提取所有y坐标 //reduce((a, b) => a + b 计算总和 ], ), ), ], ), ); } //================================底部栏的子项==================================== Widget _buildStatCard(String title, String value, IconData icon) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.1), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: Column( children: [ Icon(icon, size: 20, color: Colors.blue), const SizedBox(height: 4), Text( value, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), Text( title, style: const TextStyle( fontSize: 12, color: Colors.grey, ), ), ], ), ); } } //===================================自定义折线图画笔================================= class _LineChartPainter extends CustomPainter { final List<Offset> points; final double minX; final double maxX; final double minY; final double maxY; final double padding; final Color axisColor; final double axisWidth; final Color lineColor; final double lineWidth; final Color pointColor; final double pointRadius; final int xGridLines; final int yGridLines; final double animationValue; //构造函数 _LineChartPainter({ required this.points, // 数据点列表 required this.minX, // X轴最小值 required this.maxX, // X轴最大值 required this.minY, // Y轴最小值 required this.maxY, // Y轴最大值 required this.padding, // 图表内边距 required this.axisColor, // 坐标轴颜色 required this.axisWidth, // 坐标轴宽度 required this.lineColor, // 折线颜色 required this.lineWidth, // 折线宽度 required this.pointColor, // 数据点颜色 required this.pointRadius, // 数据点半径 required this.xGridLines, // X轴网格线数量 required this.yGridLines, // Y轴网格线数量 required this.animationValue, // 动画进度值(0-1) }); @override void paint(Canvas canvas, Size size) { //网格线画笔 final Paint gridPaint = Paint() ..color = Colors.grey[200]! ..strokeWidth = 0.5 ..style = PaintingStyle.stroke; //坐标轴画笔 final Paint axisPaint = Paint() ..color = axisColor ..strokeWidth = axisWidth ..style = PaintingStyle.stroke; //只描边 //折线画笔 final Paint linePaint = Paint() ..color = lineColor.withOpacity(0.8) ..strokeWidth = lineWidth ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round;//线头圆角 //数据点画笔 final Paint pointPaint = Paint() ..color = pointColor ..style = PaintingStyle.fill; //实心填充 //阴影区域画笔 final Paint shadowPaint = Paint() ..color = lineColor.withOpacity(0.1) ..style = PaintingStyle.fill; // 绘制背景 final Rect chartArea = Rect.fromLTWH( padding, //左 padding, //上 size.width - 2 * padding, //宽度 size.height - 2 * padding, //高度 ); // 绘制网格 _drawGrid(canvas, size, gridPaint); // 绘制坐标轴 _drawAxes(canvas, size, axisPaint); // 绘制坐标轴标签 _drawAxisLabels(canvas, size); // 如果有数据点,绘制阴影区域和折线 if (points.length > 1) { // 转换所有点为画布坐标 final List<Offset> canvasPoints = points.map((point) { return _convertToCanvas(point, size); }).toList(); // 根据动画值计算要绘制的点数 final int visiblePoints = (points.length * animationValue).ceil(); final List<Offset> visibleCanvasPoints = canvasPoints.sublist(0, visiblePoints.clamp(0, canvasPoints.length)); // 绘制阴影区域 if (visibleCanvasPoints.length > 1) { final Path shadowPath = Path(); shadowPath.moveTo(visibleCanvasPoints.first.dx, chartArea.bottom); for (final point in visibleCanvasPoints) { shadowPath.lineTo(point.dx, point.dy); } shadowPath.lineTo(visibleCanvasPoints.last.dx, chartArea.bottom); shadowPath.close(); canvas.drawPath(shadowPath, shadowPaint); } // 绘制折线(带动画) if (visibleCanvasPoints.length > 1) { final Path linePath = Path(); linePath.moveTo(visibleCanvasPoints.first.dx, visibleCanvasPoints.first.dy); for (int i = 1; i < visibleCanvasPoints.length; i++) { final current = visibleCanvasPoints[i]; final previous = visibleCanvasPoints[i - 1]; // 使用贝塞尔曲线使线条更平滑 final controlPoint1 = Offset( previous.dx + (current.dx - previous.dx) / 2, previous.dy, ); final controlPoint2 = Offset( current.dx - (current.dx - previous.dx) / 2, current.dy, ); linePath.cubicTo( controlPoint1.dx, controlPoint1.dy, controlPoint2.dx, controlPoint2.dy, current.dx, current.dy, ); } canvas.drawPath(linePath, linePaint); } // 绘制数据点(带缩放动画) for (int i = 0; i < visibleCanvasPoints.length; i++) { final point = visibleCanvasPoints[i]; final double pointAnimation = min(1.0, animationValue * 2 - i * 0.1); if (pointAnimation > 0) { // 绘制点 canvas.drawCircle( point, pointRadius * pointAnimation, pointPaint, ); // 绘制点的光晕效果 canvas.drawCircle( point, pointRadius * 1.5 * pointAnimation, Paint() ..color = pointColor.withOpacity(0.2 * pointAnimation) ..style = PaintingStyle.fill, ); // 绘制点的标签 final textPainter = TextPainter( text: TextSpan( text: "(${points[i].dx.toInt()}, ${points[i].dy.toStringAsFixed(1)})", style: const TextStyle( color: Colors.black87, fontSize: 10, fontWeight: FontWeight.w500, ), ), textDirection: TextDirection.ltr, )..layout(); textPainter.paint( canvas, Offset(point.dx - textPainter.width / 2, point.dy - 20), ); } } } else if (points.length == 1) { // 只有一个点时 final Offset canvasPoint = _convertToCanvas(points.first, size); canvas.drawCircle(canvasPoint, pointRadius, pointPaint); } else { // 没有数据点时显示提示 final textPainter = TextPainter( text: const TextSpan( text: "暂无数据,请添加数据点", style: TextStyle( color: Colors.grey, fontSize: 14, ), ), textDirection: TextDirection.ltr, )..layout(); textPainter.paint( canvas, Offset( size.width / 2 - textPainter.width / 2, size.height / 2 - textPainter.height / 2, ), ); } } // 绘制网格 void _drawGrid(Canvas canvas, Size size, Paint paint) { final double chartWidth = size.width - 2 * padding; final double chartHeight = size.height - 2 * padding; // 水平网格线 for (int i = 0; i <= yGridLines; i++) { final double y = padding + (chartHeight / yGridLines) * i; canvas.drawLine( Offset(padding, y), Offset(size.width - padding, y), paint, ); } // 垂直网格线 for (int i = 0; i <= xGridLines; i++) { final double x = padding + (chartWidth / xGridLines) * i; canvas.drawLine( Offset(x, padding), Offset(x, size.height - padding), paint, ); } } // 绘制坐标轴 void _drawAxes(Canvas canvas, Size size, Paint paint) { // X轴 canvas.drawLine( Offset(padding, size.height - padding), Offset(size.width - padding, size.height - padding), paint, ); // Y轴 canvas.drawLine( Offset(padding, padding), Offset(padding, size.height - padding), paint, ); // X轴箭头 canvas.drawLine( Offset(size.width - padding, size.height - padding), Offset(size.width - padding - 8, size.height - padding - 4), paint, ); canvas.drawLine( Offset(size.width - padding, size.height - padding), Offset(size.width - padding - 8, size.height - padding + 4), paint, ); // Y轴箭头 canvas.drawLine( Offset(padding, padding), Offset(padding - 4, padding + 8), paint, ); canvas.drawLine( Offset(padding, padding), Offset(padding + 4, padding + 8), paint, ); } // 绘制坐标轴标签 void _drawAxisLabels(Canvas canvas, Size size) { final double chartWidth = size.width - 2 * padding; final double chartHeight = size.height - 2 * padding; // X轴标签 for (int i = 0; i <= xGridLines; i++) { final double xValue = minX + (maxX - minX) / xGridLines * i; final double x = padding + (chartWidth / xGridLines) * i; final textPainter = TextPainter( text: TextSpan( text: xValue.toStringAsFixed(1), style: const TextStyle( color: Colors.black87, fontSize: 10, ), ), textDirection: TextDirection.ltr, )..layout(); textPainter.paint( canvas, Offset(x - textPainter.width / 2, size.height - padding + 5), ); } // Y轴标签 for (int i = 0; i <= yGridLines; i++) { final double yValue = maxY - (maxY - minY) / yGridLines * i; final double y = padding + (chartHeight / yGridLines) * i; final textPainter = TextPainter( text: TextSpan( text: yValue.toStringAsFixed(1), style: const TextStyle( color: Colors.black87, fontSize: 10, ), ), textDirection: TextDirection.ltr, )..layout(); textPainter.paint( canvas, Offset(padding - textPainter.width - 10, y - textPainter.height / 2), ); } // 坐标轴标题 final textX = TextPainter( text: const TextSpan( text: "X轴", style: TextStyle( color: Colors.black87, fontSize: 12, fontWeight: FontWeight.bold, ), ), textDirection: TextDirection.ltr, )..layout(); final textY = TextPainter( text: const TextSpan( text: "Y轴", style: TextStyle( color: Colors.black87, fontSize: 12, fontWeight: FontWeight.bold, ), ), textDirection: TextDirection.ltr, )..layout(); textX.paint( canvas, Offset(size.width - padding - textX.width / 2, size.height - padding + 20), ); textY.paint( canvas, Offset(padding - 30, padding - textY.height / 2 -20), ); } // 坐标转换 Offset _convertToCanvas(Offset point, Size size) { // 线性映射:数据坐标 → 画布坐标 // 公式:画布坐标 = 起点 + (数据值 - 最小值) / 范围 × 可用空间 final double width = size.width - 2 * padding; final double height = size.height - 2 * padding; final double x = padding + (point.dx - minX) / (maxX - minX) * width; final double y = size.height - padding - (point.dy - minY) / (maxY - minY) * height; // 注意Y轴需要翻转:画布原点在左上角,数学原点在左下角 return Offset(x, y); } @override bool shouldRepaint(_LineChartPainter oldDelegate) { return oldDelegate.points != points || //数据变化时重绘 oldDelegate.animationValue != animationValue; //动画变化时重绘 } }

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询