本文讲解如何使用 Flutter 绘制饼状图
前言
接上文,本文讲解如何使用 Flutter 绘制饼状图,最终效果如图
在线查看
定义 PieChart & PiePart
第一步定义 PieChart
和 PiePart
类。PieChart
是整个饼状图控件,有 datas
和 legends
两个属性,表示饼图的数据和每部分的标识。
PiePart
表示饼图的一部分,有 color
, startAngle
, sweepAngle
三个属性,分别表示颜色,起始弧度值,占据圆形的弧度值。PeiChartPainter
类实现了具体的绘制方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| class PiePart { double sweepAngle; final Color color; final double startAngle;
PiePart( this.startAngle, this.sweepAngle, this.color, ); }
class PieChart extends StatefulWidget { final List<double> datas; final List<String> legends;
const PieChart({ @required this.datas, @required this.legends, });
@override _PieChartState createState() => _PieChartState(); }
class _PieChartState extends State<PieChart> with TickerProviderStateMixin { double _total = 0.0; final List _parts = [];
@override Widget build(BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 300, height: 300, child: CustomPaint( painter: PeiChartPainter( total: _total, parts: _parts, datas: widget.datas, legends: widget.legends ), ), ), ], ); } }
class PeiChartPainter extends CustomPainter { final double total; final List<double> datas; final List parts; final List<String> legends;
PeiChartPainter({ @required this.total, @required this.datas, @required this.parts, @required this.legends, });
@override void paint(Canvas canvas, Size size) { }
@override bool shouldRepaint(PeiChartPainter oldDelegate) => true; }
|
绘制圆框
先绘制图表的圆框,在 PeiChartPainter
上添加 drawCircle
方法,以圆的中心点和圆的半径绘制一个空心圆形。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| void drawCircle(Canvas canvas, Size size) { final sw = size.width; final sh = size.height; final double radius = math.min(sw, sh) / 2; final Offset center = Offset(sw / 2, sh / 2);
final paint = Paint() ..style = PaintingStyle.stroke ..color = Colors.grey ..strokeWidth = 1.0;
canvas.drawCircle(center, radius, paint); }
@override void paint(Canvas canvas, Size size) { drawCircle(canvas, size); }
|
绘制标识
这一步需要先在 _PieChartState
里面进行数据的初始化,然后绘制每个数据对应的标识,分以下几步进行
- 计算出每个数据占总和的占比
- 根据占比计算数据占据圆的弧度值
- 根据之前数据占据圆形的弧度值计算出下一个数据的起始弧度值
- 根据计算出的起始弧度值和占据弧度值创建
PiePart
对象 - 使用
PiePart
对象绘制标识
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| class _PieChartState extends State<PieChart> with TickerProviderStateMixin { double _total = 0.0; final List _parts = [];
@override void initState() { super.initState();
List<double> datas = widget.datas; _total = datas.reduce((a, b) => a + b); double startAngle = 0.0;
for (int i = 0; i < datas.length; i++) { final data = datas[i]; final angle = (data / _total) * -math.pi * 2; PiePart peiPart;
if (i > 0) { double lastSweepAngle = _parts[i - 1].sweepAngle; startAngle += lastSweepAngle; peiPart = PiePart(startAngle, angle, colors[i]); } else { peiPart = PiePart(0.0, angle, colors[i]); } _parts.add(peiPart); } }
@override Widget build(BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 300, height: 300, child: CustomPaint( painter: PeiChartPainter( total: _total, parts: _parts, datas: widget.datas, legends: widget.legends, ), ), ), ], ); } }
|
在 PeiChartPainter 上添加 drawLegends
方法,在圆框的外围绘制每部分对应的标识。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| void drawLegends(Canvas canvas, Size size) { final sw = size.width; final sh = size.height; final double radius = math.min(sw, sh) / 2; final double fontSize = 12.0;
for (int i = 0; i < datas.length; i++) { final PiePart part = parts[i]; final String legend = legends[i]; final radians = part.startAngle + part.sweepAngle / 2; double x = math.cos(radians) * (radius + 32) + sw / 2 - fontSize; double y = math.sin(radians) * (radius + 32) + sh / 2; final offset = Offset(x, y);
TextPainter( textAlign: TextAlign.center, text: TextSpan( text: legend, style: TextStyle( fontSize: fontSize, color: Colors.black, ), ), textDirection: TextDirection.ltr, ) ..layout( minWidth: 0, maxWidth: size.width, ) ..paint(canvas, offset); } }
@override void paint(Canvas canvas, Size size) { drawCircle(canvas, size); drawLegends(canvas, size); }
|
计算文字位置用到的的三角函数是
绘制数据对应的弧形
在 PeiChartPainter
上添加 drawParts
方法,绘制每个数据对应的弧形。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| void drawParts(Canvas canvas, Size size) { final sw = size.width; final sh = size.height; final double fontSize = 10.0; final double radius = math.min(sw, sh) / 2; final Offset center = Offset(sw / 2, sh / 2);
final rect = Rect.fromCenter( center: center, width: radius * 2, height: radius * 2, ); final paint = Paint() ..strokeWidth = 0.0 ..isAntiAlias = true ..style = PaintingStyle.fill;
for (int i = 0; i < parts.length; i++) { final PiePart part = parts[i]; paint.color = part.color; canvas.drawArc(rect, part.startAngle, part.sweepAngle, true, paint);
final double data = datas[i]; final String percent = (data / total * 100).toStringAsFixed(1); final double radians = part.startAngle + part.sweepAngle / 2; double x = math.cos(radians) * radius / 2 + sw / 2 - fontSize * 3; double y = math.sin(radians) * radius / 2 + sh / 2; final Offset offset = Offset(x, y);
TextPainter( textAlign: TextAlign.start, text: TextSpan( text: '$data $percent%', style: TextStyle( fontSize: fontSize, color: Colors.white, fontWeight: FontWeight.bold, ), ), textDirection: TextDirection.ltr, ) ..layout( minWidth: 0, maxWidth: size.width, ) ..paint(canvas, offset); } }
@override void paint(Canvas canvas, Size size) { drawCircle(canvas, size); drawLegends(canvas, size); drawParts(canvas, size); }
|
添加动画
最后给饼图添加一个数据不断增长的动画效果,在 _PieChartState
添加动画的控制器 _controller
和保存动画数据的 _animateDatas
数组。在 initState
中初始化动画控制器和填充 _animateDatas
数组。然后创建两个 double
类型的补间动画,将动画值传给 PeiChartPainter
使用即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
| class _PieChartState extends State<PieChart> with TickerProviderStateMixin { double _total = 0.0; AnimationController _controller; List<double> _animateDatas = []; final List _parts = [];
@override void initState() { super.initState(); _controller = AnimationController( duration: Duration(milliseconds: 3000), vsync: this, );
List<double> datas = widget.datas; _total = datas.reduce((a, b) => a + b); double startAngle = 0.0;
for (int i = 0; i < datas.length; i++) { _animateDatas.add(0.0); final data = datas[i]; final angle = (data / _total) * -math.pi * 2; PiePart peiPart;
if (i > 0) { double lastSweepAngle = _parts[i - 1].sweepAngle; startAngle += lastSweepAngle; peiPart = PiePart(startAngle, angle, colors[i]); } else { peiPart = PiePart(0.0, angle, colors[i]); } _parts.add(peiPart);
CurvedAnimation curvedAnimation = CurvedAnimation( parent: _controller, curve: Curves.ease, );
final partTween = Tween<double>(begin: 0.0, end: peiPart.sweepAngle); Animation<double> animation = partTween.animate(curvedAnimation);
final percentTween = Tween<double>(begin: 0.0, end: data); Animation<double> percentAnimation = percentTween.animate(curvedAnimation);
_controller.addListener(() { _parts[i].sweepAngle = animation.value; _animateDatas[i] = double.parse(percentAnimation.value.toStringAsFixed(1)); setState(() {}); }); _controller.forward(); } }
@override Widget build(BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 300, height: 300, child: CustomPaint( painter: PeiChartPainter( total: _total, parts: _parts, datas: _animateDatas, legends: widget.legends, ), ), ), SizedBox(height: 80), Container( decoration: BoxDecoration( color: Colors.blue, shape: BoxShape.circle, ), child: IconButton( color: Colors.white, icon: Icon(Icons.refresh), onPressed: () { _controller.reset(); _controller.forward(); }, ), ), ], ); } }
|
至此整个饼状图的绘制就完成了,传入数据即可使用 🎉🎉🎉
1 2 3 4
| PieChart( datas: [60.0, 50.0, 40.0, 30.0, 90.0], legends: ['一月', '二月', '三月', '四月', '五月'], );
|
完整代码地址:pie_chart.dart
总结
本文说明了如何使用 Flutter 绘制一个饼状图,使用了一点三角函数,关键点在于计算出每个数据占据整个圆形的弧度值,以及数据的起始弧度值。
数值增长的动画效果使用一个 AnimationController
在开始动画后不断的更新绘制使用的数据,在将数据传递给 PeiChartPainter
使用即可实现。
附言
准备写一系列关于用 Flutter 画图表的文章,用来分享这方面的知识,这篇文章是这个系列的第二篇,预计 6 篇。
- Flutter 绘制图表(一)柱状图📊
- Flutter 绘制图表(二)饼状图🍪(本文)
- Flutter 绘制图表(三)折线图📈
- Flutter 绘制图表(四)雷达图🎯
- Flutter 绘制图表(五)环状图🍩
- Flutter 绘制图表(六)条形图📏