最终效果如下:
绘制
先创建一个CustomPainter,使用canvas绘制。
import 'dart:ui'; import 'package:flutter/material.dart'; /// 自定义折线图 class LineChartPainter extends CustomPainter { Paint _outlinePaint; Paint _axisXPaint; Paint _valuePaint; /// 左边Y轴标签 TextPainter _leftLabelPainter; List<int> _yTitle = [40, 30, 20, 10, 0, -10]; // y轴刻度数量 List<double> _yData; final int averageY = 5; // y轴平均份数 final int gapY = 10; // y轴平均间隔值 final double _yTitlePadding = 5; LineChartPainter(this._yData, {Listenable repaint}) : super(repaint: repaint) { _outlinePaint = Paint() ..color = Colors.grey ..style = PaintingStyle.stroke ..strokeWidth = 1; _axisXPaint = Paint() ..color = Colors.grey ..style = PaintingStyle.stroke ..strokeWidth = 1; _valuePaint = Paint() ..color = Colors.blue ..style = PaintingStyle.stroke ..strokeWidth = 1.5; _leftLabelPainter = TextPainter()..textDirection = TextDirection.ltr; } @override void paint(Canvas canvas, Size size) { print("${this},,,$size"); /// 外边框矩形 canvas.drawRect( Rect.fromLTRB(0, 0, size.width, size.height), _outlinePaint); /// x轴 for (var i = 0; i < 4; i++) { canvas.drawLine(Offset(0, size.height / averageY * (i + 1)), Offset(size.width, size.height / averageY * (i + 1)), _axisXPaint); } /// y轴标题 for (var i = 0; i <= 5; i++) { _leftLabelPainter.text = TextSpan( text: _yTitle[i].toString(), style: TextStyle(color: Colors.black)); _leftLabelPainter.layout(); _leftLabelPainter.paint( canvas, Offset(-_leftLabelPainter.width - _yTitlePadding, size.height / 5 * i - _leftLabelPainter.height / 2), ); } /// 数据 /// 需要将y轴数据转换为height中对应比例的高度。 var points = <Offset>[]; for (var i = 0; i < _yData.length; i++) { points.add(Offset(size.width / _yData.length * i, translateValue(size.height, _yData[i]))); } canvas.drawPoints(PointMode.polygon, points, _valuePaint); } @override bool shouldRepaint(LineChartPainter oldDelegate) { return oldDelegate._yData != _yData; } /// 转换y坐标 double translateValue(double height, double rawValue) { /// y轴真实值总长度 var valueSum = averageY * gapY; /// 真实值和height计算比例 var scale = height / valueSum; var result = rawValue * scale; return height - result - height / averageY; } }
实现过渡动画
使用CustomPaint包裹CustomPainter,为了在数据变化时,有动画效果,使用ImplicitlyAnimatedWidget。
实现动画效果的重点是forEachTween
和lerp
方法。
注意:State不要继承ImplicitlyAnimatedWidgetState
,直接继承AnimatedWidgetBaseState
即可,它内部实现了对AnimationController
的监听,并实时刷新Animation的value,自动调用forEachTween方法。
import 'dart:ui'; import 'package:flutter/material.dart'; import 'line_chart_painter.dart'; /// 包装折线图 数据更新时展示过渡动画 class LineChart extends ImplicitlyAnimatedWidget { final Size size; final LineChartData lineChartData; final Duration duration; const LineChart( {Key key, this.size, @required this.lineChartData, @required this.duration}) : super(key: key, duration: duration); @override _CustomCanvasState createState() { return _CustomCanvasState(); } } class _CustomCanvasState extends AnimatedWidgetBaseState<LineChart> { DataTween _dataTween; @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraint) { print( "widget size:${widget.size} ${constraint.maxWidth}...${constraint.maxHeight}"); var constrainSize = widget.size ?? Size(constraint.maxWidth, constraint.maxHeight); if (constrainSize.width == double.infinity) { throw FlutterError("必须为组件或父widget设置一个有效宽度"); } if (constrainSize.height == double.infinity) { throw FlutterError("必须为组件或父widget设置一个有效高度"); } return CustomPaint( foregroundPainter: LineChartPainter( _dataTween.evaluate(animation).data, repaint: animation), size: constrainSize, ); }, ); } @override void forEachTween(TweenVisitor visitor) { /// 第一个参数是初始的tween,第二个参数是目标值,第三个是生成tween的回调。 _dataTween = visitor( _dataTween, widget.lineChartData, (value) => DataTween(begin: value)); } } class DataTween extends Tween<LineChartData> { DataTween({LineChartData begin, LineChartData end}) : super(begin: begin, end: end); @override LineChartData lerp(double t) => LineChartData.lerp(begin, end, t); } class LineChartData { List<double> data; LineChartData({this.data}); /// 计算动画更新时的数据 /// begin表示动画开始时的数据,end是结束时的数据,t是动画估值器,从0到1,代表动画运行的进度。 static LineChartData lerp(LineChartData begin, LineChartData end, double t) { /// 根据begin和end每个对应的值,因为值类型是double,所以使用系统自带的lerpDouble来计算值。 LineChartData result; if (begin.data != null && end.data != null && begin.data.length == end.data.length) { result = LineChartData( data: List.generate(begin.data.length, (index) { return lerpDouble(begin.data[index], end.data[index], t); })); } else if (begin.data.length > end.data.length) { result = LineChartData( data: List.generate(end.data.length, (index) { return lerpDouble(begin.data[index], end.data[index], t); })); } else if (begin.data.length < end.data.length) { result = LineChartData( data: List.generate(begin.data.length, (index) { return lerpDouble(begin.data[index], end.data[index], t); })); } return result; } }
使用
class StudyCanvasPage extends StatefulWidget { const StudyCanvasPage({Key key}) : super(key: key); @override _StudyCanvasPageState createState() => _StudyCanvasPageState(); } class _StudyCanvasPageState extends State<StudyCanvasPage> { var check = false; List<double> _yData = [29.2, 29.8, 29.3, 29.2, 29.4, 29.1, 29.3, 29.2, 29.5, 29.2, 29.2, 29, 29.3, 29.2, 29.4, 29.1, 29.3, 29.2, 29.5, 29.2]; List<double> _yData1 = [12.2, 19.8, 19.3, 9.2, 19.4, 19.1, 19.3, 19.2, 19.5, 19.2, 19.2, 19, 19.3, 19.2, 19.4, 19.1, 19.3, 19.2, 19.5, 19.2,]; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('study canvas'), ), body: SafeArea( child: Container( child: Column( children: [ ElevatedButton( onPressed: () { setState(() { check = !check; }); }, child: Text("切换数据"), ), Expanded( child: Padding( padding: const EdgeInsets.all(28.0), child: LineChart( lineChartData: LineChartData(data: check ? _yData : _yData1), duration: Duration(milliseconds: 300), ), ), ), ], ), ), ), ); } }