Flutter快速实现自定义折线图,支持数据改变过渡动画

心已赠人 2022-09-13 11:24 306阅读 0赞

最终效果如下:
在这里插入图片描述

绘制

先创建一个CustomPainter,使用canvas绘制。

  1. import 'dart:ui';
  2. import 'package:flutter/material.dart';
  3. /// 自定义折线图
  4. class LineChartPainter extends CustomPainter {
  5. Paint _outlinePaint;
  6. Paint _axisXPaint;
  7. Paint _valuePaint;
  8. /// 左边Y轴标签
  9. TextPainter _leftLabelPainter;
  10. List<int> _yTitle = [40, 30, 20, 10, 0, -10]; // y轴刻度数量
  11. List<double> _yData;
  12. final int averageY = 5; // y轴平均份数
  13. final int gapY = 10; // y轴平均间隔值
  14. final double _yTitlePadding = 5;
  15. LineChartPainter(this._yData, { Listenable repaint})
  16. : super(repaint: repaint) {
  17. _outlinePaint = Paint()
  18. ..color = Colors.grey
  19. ..style = PaintingStyle.stroke
  20. ..strokeWidth = 1;
  21. _axisXPaint = Paint()
  22. ..color = Colors.grey
  23. ..style = PaintingStyle.stroke
  24. ..strokeWidth = 1;
  25. _valuePaint = Paint()
  26. ..color = Colors.blue
  27. ..style = PaintingStyle.stroke
  28. ..strokeWidth = 1.5;
  29. _leftLabelPainter = TextPainter()..textDirection = TextDirection.ltr;
  30. }
  31. @override
  32. void paint(Canvas canvas, Size size) {
  33. print("${this},,,$size");
  34. /// 外边框矩形
  35. canvas.drawRect(
  36. Rect.fromLTRB(0, 0, size.width, size.height), _outlinePaint);
  37. /// x轴
  38. for (var i = 0; i < 4; i++) {
  39. canvas.drawLine(Offset(0, size.height / averageY * (i + 1)),
  40. Offset(size.width, size.height / averageY * (i + 1)), _axisXPaint);
  41. }
  42. /// y轴标题
  43. for (var i = 0; i <= 5; i++) {
  44. _leftLabelPainter.text = TextSpan(
  45. text: _yTitle[i].toString(), style: TextStyle(color: Colors.black));
  46. _leftLabelPainter.layout();
  47. _leftLabelPainter.paint(
  48. canvas,
  49. Offset(-_leftLabelPainter.width - _yTitlePadding,
  50. size.height / 5 * i - _leftLabelPainter.height / 2),
  51. );
  52. }
  53. /// 数据
  54. /// 需要将y轴数据转换为height中对应比例的高度。
  55. var points = <Offset>[];
  56. for (var i = 0; i < _yData.length; i++) {
  57. points.add(Offset(size.width / _yData.length * i,
  58. translateValue(size.height, _yData[i])));
  59. }
  60. canvas.drawPoints(PointMode.polygon, points, _valuePaint);
  61. }
  62. @override
  63. bool shouldRepaint(LineChartPainter oldDelegate) {
  64. return oldDelegate._yData != _yData;
  65. }
  66. /// 转换y坐标
  67. double translateValue(double height, double rawValue) {
  68. /// y轴真实值总长度
  69. var valueSum = averageY * gapY;
  70. /// 真实值和height计算比例
  71. var scale = height / valueSum;
  72. var result = rawValue * scale;
  73. return height - result - height / averageY;
  74. }
  75. }

实现过渡动画

使用CustomPaint包裹CustomPainter,为了在数据变化时,有动画效果,使用ImplicitlyAnimatedWidget。
实现动画效果的重点是forEachTweenlerp方法。
注意:State不要继承ImplicitlyAnimatedWidgetState,直接继承AnimatedWidgetBaseState即可,它内部实现了对AnimationController的监听,并实时刷新Animation的value,自动调用forEachTween方法。

  1. import 'dart:ui';
  2. import 'package:flutter/material.dart';
  3. import 'line_chart_painter.dart';
  4. /// 包装折线图 数据更新时展示过渡动画
  5. class LineChart extends ImplicitlyAnimatedWidget {
  6. final Size size;
  7. final LineChartData lineChartData;
  8. final Duration duration;
  9. const LineChart(
  10. { Key key,
  11. this.size,
  12. @required this.lineChartData,
  13. @required this.duration})
  14. : super(key: key, duration: duration);
  15. @override
  16. _CustomCanvasState createState() {
  17. return _CustomCanvasState();
  18. }
  19. }
  20. class _CustomCanvasState extends AnimatedWidgetBaseState<LineChart> {
  21. DataTween _dataTween;
  22. @override
  23. Widget build(BuildContext context) {
  24. return LayoutBuilder(
  25. builder: (context, constraint) {
  26. print(
  27. "widget size:${widget.size} ${constraint.maxWidth}...${constraint.maxHeight}");
  28. var constrainSize =
  29. widget.size ?? Size(constraint.maxWidth, constraint.maxHeight);
  30. if (constrainSize.width == double.infinity) {
  31. throw FlutterError("必须为组件或父widget设置一个有效宽度");
  32. }
  33. if (constrainSize.height == double.infinity) {
  34. throw FlutterError("必须为组件或父widget设置一个有效高度");
  35. }
  36. return CustomPaint(
  37. foregroundPainter: LineChartPainter(
  38. _dataTween.evaluate(animation).data,
  39. repaint: animation),
  40. size: constrainSize,
  41. );
  42. },
  43. );
  44. }
  45. @override
  46. void forEachTween(TweenVisitor visitor) {
  47. /// 第一个参数是初始的tween,第二个参数是目标值,第三个是生成tween的回调。
  48. _dataTween = visitor(
  49. _dataTween, widget.lineChartData, (value) => DataTween(begin: value));
  50. }
  51. }
  52. class DataTween extends Tween<LineChartData> {
  53. DataTween({ LineChartData begin, LineChartData end})
  54. : super(begin: begin, end: end);
  55. @override
  56. LineChartData lerp(double t) => LineChartData.lerp(begin, end, t);
  57. }
  58. class LineChartData {
  59. List<double> data;
  60. LineChartData({ this.data});
  61. /// 计算动画更新时的数据
  62. /// begin表示动画开始时的数据,end是结束时的数据,t是动画估值器,从0到1,代表动画运行的进度。
  63. static LineChartData lerp(LineChartData begin, LineChartData end, double t) {
  64. /// 根据begin和end每个对应的值,因为值类型是double,所以使用系统自带的lerpDouble来计算值。
  65. LineChartData result;
  66. if (begin.data != null &&
  67. end.data != null &&
  68. begin.data.length == end.data.length) {
  69. result = LineChartData(
  70. data: List.generate(begin.data.length, (index) {
  71. return lerpDouble(begin.data[index], end.data[index], t);
  72. }));
  73. } else if (begin.data.length > end.data.length) {
  74. result = LineChartData(
  75. data: List.generate(end.data.length, (index) {
  76. return lerpDouble(begin.data[index], end.data[index], t);
  77. }));
  78. } else if (begin.data.length < end.data.length) {
  79. result = LineChartData(
  80. data: List.generate(begin.data.length, (index) {
  81. return lerpDouble(begin.data[index], end.data[index], t);
  82. }));
  83. }
  84. return result;
  85. }
  86. }

使用

  1. class StudyCanvasPage extends StatefulWidget {
  2. const StudyCanvasPage({ Key key}) : super(key: key);
  3. @override
  4. _StudyCanvasPageState createState() => _StudyCanvasPageState();
  5. }
  6. class _StudyCanvasPageState extends State<StudyCanvasPage> {
  7. var check = false;
  8. 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];
  9. 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,];
  10. @override
  11. Widget build(BuildContext context) {
  12. return Scaffold(
  13. appBar: AppBar(
  14. title: Text('study canvas'),
  15. ),
  16. body: SafeArea(
  17. child: Container(
  18. child: Column(
  19. children: [
  20. ElevatedButton(
  21. onPressed: () {
  22. setState(() {
  23. check = !check;
  24. });
  25. },
  26. child: Text("切换数据"),
  27. ),
  28. Expanded(
  29. child: Padding(
  30. padding: const EdgeInsets.all(28.0),
  31. child: LineChart(
  32. lineChartData:
  33. LineChartData(data: check ? _yData : _yData1),
  34. duration: Duration(milliseconds: 300),
  35. ),
  36. ),
  37. ),
  38. ],
  39. ),
  40. ),
  41. ),
  42. );
  43. }
  44. }

发表评论

表情:
评论列表 (有 0 条评论,306人围观)

还没有评论,来说两句吧...

相关阅读

    相关 H5动画实现---过渡

    通过过渡transition,我们可以在不使用 Flash 动画或 JavaScript 的情况下,当元素从一种样式变换为另一种样式时为元素添加效果. 要实现这一点,必须规定两

    相关 flutter定义appbar

    场景描述: 因为写很多页面,每个顶部都搞一个返回键、分享,中间标题这种东东,弄的比较繁琐,索性就把这个appbar给单独抽离出来,重新定义成一个widget这个内容。 ...