在Android的时候自定义过蛛网图,花了半天时间。复刻到Flutter只用了不到20分钟
不得不说Flutter中的Canvas对安卓玩家还是非常友好的,越来越觉得Flutter非常有趣。
在视图方面,Flutter确实要比原生更胜一筹。本文你将学到:
1.三角函数的使用 2.Flutter中如何用绘制文字 3.动画在绘图中的实际运用 4.Canvas绘图的相关相关方法 5.Flutter中一个组件的封装 复制代码
---->[使用方法]------------- var show = AbilityWidget( ability: Ability(duration: 1500, image: AssetImage("images/lifei.jpeg"), radius: 100, color: Colors.black, data: { "语文": 40.0, "数学": 30.0, "英语": 20.0, "政治": 40.0, "音乐": 80.0, "生物": 50.0, "化学": 60.0, "地理": 80.0, }));
1.静态蛛网图
第一步就是如何将一串数据映射成下面的图表:
var data = { "攻击力": 70.0, "生命": 90.0, "闪避": 50.0, "暴击": 70.0, "破格": 80.0, "格挡": 100.0, };
1.1:创建AbilityWidget组件
线新建一个StatelessWidget的组件使用AbilityPainter进行绘制
这里先定义画笔、路径等成员变量
import 'package:flutter/material.dart'; class AbilityWidget extends StatefulWidget { @override _AbilityWidgetState createState() => _AbilityWidgetState(); } class _AbilityWidgetState extends State<AbilityWidget>{ @override Widget build(BuildContext context) { var paint = CustomPaint( painter: AbilityPainter(), ); return SizedBox(width: 200, height: 200, child: paint,); } } class AbilityPainter extends CustomPainter { var data = { "攻击力": 70.0, "生命": 90.0, "闪避": 50.0, "暴击": 70.0, "破格": 80.0, "格挡": 100.0, }; double mRadius = 100; //外圆半径 Paint mLinePaint; //线画笔 Paint mAbilityPaint; //区域画笔 Paint mFillPaint;//填充画笔 Path mLinePath;//短直线路径 Path mAbilityPath;//范围路径 AbilityPainter() { mLinePath = Path(); mAbilityPath = Path(); mLinePaint = Paint() ..color = Colors.black ..style = PaintingStyle.stroke ..strokeWidth=0.008 * mRadius ..isAntiAlias = true; mFillPaint = Paint() //填充画笔 ..strokeWidth = 0.05 * mRadius ..color = Colors.black ..isAntiAlias = true; mAbilityPaint = Paint() ..color = Color(0x8897C5FE) ..isAntiAlias = true; } @override void paint(Canvas canvas, Size size) { } @override bool shouldRepaint(CustomPainter oldDelegate) { return true; } }
1.2.绘制外圈
为了减少变量值,让尺寸具有很好的联动性(等比扩缩),小黑条的长宽将取决于最大半径
mRadius
则:小黑条长:mRadius*0.08
小黑条宽:mRadius*0.05
所以r2=mRadius-mRadius*0.08
@override void paint(Canvas canvas, Size size) { canvas.translate(mRadius, mRadius); //移动坐标系 drawOutCircle(canvas); } //绘制外圈 void drawOutCircle(Canvas canvas) { canvas.save();//新建图层 canvas.drawCircle(Offset(0, 0), mRadius, mLinePaint);//圆形的绘制 double r2 = mRadius - 0.08 * mRadius; //下圆半径 canvas.drawCircle(Offset(0, 0), r2, mLinePaint); for (var i = 0.0; i < 22; i++) {//循环画出小黑条 canvas.save();//新建图层 canvas.rotate(360 / 22 * i / 180 * pi);//旋转:注意传入的是弧度(与Android不同) canvas.drawLine(Offset(0, -mRadius), Offset(0, -r2), mFillPaint);//线的绘制 canvas.restore();//释放图层 } canvas.restore();//释放图层 }
同样尺寸和最外圆看齐,这里绘制有一丢丢复杂,你需要了解canvas和path的使用
看不懂的可转到canvas和path,如果看了这两篇还问绘制有什么技巧的,可转到这里
@override void paint(Canvas canvas, Size size) { canvas.translate(mRadius, mRadius); //移动坐标系 drawOutCircle(canvas); drawInnerCircle(canvas); } //绘制内圈圆 drawInnerCircle(Canvas canvas) { double innerRadius = 0.618 * mRadius;//内圆半径 canvas.drawCircle(Offset(0, 0), innerRadius, mLinePaint); canvas.save(); for (var i = 0; i < 6; i++) {//遍历6条线 canvas.save(); canvas.rotate(60 * i.toDouble() / 180 * pi); //每次旋转60° mPath.moveTo(0, -innerRadius); mPath.relativeLineTo(0, innerRadius); //线的路径 for (int j = 1; j < 6; j++) { mPath.moveTo(-mRadius * 0.02, innerRadius / 6 * j); mPath.relativeLineTo(mRadius * 0.02 * 2, 0); } //加5条小线 canvas.drawPath(mPath, mLinePaint); //绘制线 canvas.restore(); } canvas.restore(); }
1.3.绘制文字
Flutter中绘制文字可有点略坑,我这里简单的封了一个drawText函数用来画文字
记得导入ui库,使用Paragraph进行文字的设置,drawParagraph进行绘制
import 'dart:ui' as ui; //绘制文字 void drawInfoText(Canvas canvas) { double r2 = mRadius - 0.08 * mRadius; //下圆半径 for (int i = 0; i < data.length; i++) { canvas.save(); canvas.rotate(360 / data.length * i / 180 * pi + pi); drawText(canvas, data.keys.toList()[i], Offset(-50, r2 - 0.22 * mRadius), fontSize: mRadius * 0.1); canvas.restore(); } } //绘制文字 drawText(Canvas canvas, String text, Offset offset, {Color color=Colors.black, double maxWith = 100, double fontSize, String fontFamily, TextAlign textAlign=TextAlign.center, FontWeight fontWeight=FontWeight.bold}) { // 绘制文字 var paragraphBuilder = ui.ParagraphBuilder( ui.ParagraphStyle( fontFamily: fontFamily, textAlign: textAlign, fontSize: fontSize, fontWeight: fontWeight, ), ); paragraphBuilder.pushStyle( ui.TextStyle(color: color, textBaseline: ui.TextBaseline.alphabetic)); paragraphBuilder.addText(text); var paragraph = paragraphBuilder.build(); paragraph.layout(ui.ParagraphConstraints(width: maxWith)); canvas.drawParagraph(paragraph, Offset(offset.dx, offset.dy)); }
1.4.绘制范围
最后也是最难的一块,你准备好草稿纸了吗?
//绘制区域 drawAbility(Canvas canvas, List<double> value) { double step = mRadius*0.618 / 6; //每小段的长度 mAbilityPath.moveTo(0, -value[0] / 20 * step); //起点 for (int i = 1; i < 6; i++) { double mark = value[i] / 20;//占几段 mAbilityPath.lineTo( mark * step * cos(pi / 180 * (-30 + 60 * (i - 1))), mark * step * sin(pi / 180 * (-30 + 60 * (i - 1)))); } mAbilityPath.close(); canvas.drawPath(mAbilityPath, mAbilityPaint); }
2.动画效果
让外圈转和内圈相反方向转,所以可以让内圈和外圈分成两个组件放在一个Stack里
2.1:抽离外圈
class OutlinePainter extends CustomPainter { double mRadius = 100; //外圆半径 Paint mLinePaint; //线画笔 Paint mFillPaint; //填充画笔 OutlinePainter() { mLinePaint = Paint() ..color = Colors.black ..style = PaintingStyle.stroke ..strokeWidth = 0.008 * mRadius ..isAntiAlias = true; mFillPaint = Paint() //填充画笔 ..strokeWidth = 0.05 * mRadius ..color = Colors.black ..isAntiAlias = true; } @override void paint(Canvas canvas, Size size) { drawOutCircle(canvas); } @override bool shouldRepaint(CustomPainter oldDelegate) { // TODO: implement shouldRepaint return true; } //绘制外圈 void drawOutCircle(Canvas canvas) { canvas.save(); //新建图层 canvas.drawCircle(Offset(0, 0), mRadius, mLinePaint); //圆形的绘制 double r2 = mRadius - 0.08 * mRadius; //下圆半径 canvas.drawCircle(Offset(0, 0), r2, mLinePaint); for (var i = 0.0; i < 22; i++) { //循环画出小黑条 canvas.save(); //新建图层 canvas.rotate(360 / 22 * i / 180 * pi); //旋转:注意传入的是弧度(与Android不同) canvas.drawLine(Offset(0, -mRadius), Offset(0, -r2), mFillPaint); //线的绘制 canvas.restore(); //释放图层 } canvas.restore(); //释放图层 } }
2.2:使用动画
这里用Stack进行组件的堆叠
class _AbilityWidgetState extends State<AbilityWidget> with SingleTickerProviderStateMixin { var _angle = 0.0; AnimationController controller; Animation<double> animation; @override void initState() { super.initState(); controller = AnimationController( ////创建 Animation对象 duration: const Duration(milliseconds: 2000), //时长 vsync: this); var tween = Tween(begin: 0.0, end: 360.0); //创建从25到150变化的Animatable对象 animation = tween.animate(controller); //执行animate方法,生成 animation.addListener(() { setState(() { _angle = animation.value; }); }); controller.forward(); } @override Widget build(BuildContext context) { var paint = CustomPaint( painter: AbilityPainter(), ); var outlinePainter = Transform.rotate( angle: _angle / 180 * pi, child: CustomPaint( painter: OutlinePainter(), ), ); var img = Transform.rotate( angle: _angle / 180 * pi, child: Opacity( opacity: animation.value / 360 * 0.4, child: ClipOval( child: Image.asset( "images/娜美.jpg", width: 200, height: 200, fit: BoxFit.cover, ), ), ), ); var center = Transform.rotate( angle: -_angle / 180 * pi, child: Transform.scale( scale: 0.5 + animation.value / 360 / 2, child: SizedBox( width: 200, height: 200, child: paint, ), )); return Center( child: Stack( alignment: Alignment.center, children: <Widget>[img, center, outlinePainter], ), ); } }
到现在逻辑上没有问题了,剩下的就是对组件的封装,将一些量进行提取
下面就是简单封装了一下,还有很多乱七八糟的没封装,比如颜色,动画效果等。
import 'dart:math'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; class Ability { double radius; int duration; ImageProvider image; Map<String,double> data; Color color; Ability({this.radius, this.duration, this.image, this.data, this.color}); } class AbilityWidget extends StatefulWidget { AbilityWidget({Key key, this.ability}) : super(key: key); final Ability ability; @override _AbilityWidgetState createState() => _AbilityWidgetState(); } class _AbilityWidgetState extends State<AbilityWidget> with SingleTickerProviderStateMixin { var _angle = 0.0; AnimationController controller; Animation<double> animation; @override void initState() { super.initState(); controller = AnimationController( ////创建 Animation对象 duration: Duration(milliseconds: widget.ability.duration), //时长 vsync: this); var curveTween = CurveTween(curve:Cubic(0.96, 0.13, 0.1, 1.2));//创建curveTween var tween=Tween(begin: 0.0, end: 360.0); animation = tween.animate(curveTween.animate(controller)); animation.addListener(() { setState(() { _angle = animation.value; print(_angle); }); }); controller.forward(); } @override Widget build(BuildContext context) { var paint = CustomPaint( painter: AbilityPainter(widget.ability.radius,widget.ability.data), ); var outlinePainter = Transform.rotate( angle: _angle / 180 * pi, child: CustomPaint( painter: OutlinePainter(widget.ability.radius ), ), ); var img = Transform.rotate( angle: _angle / 180 * pi, child: Opacity( opacity: animation.value / 360 * 0.4, child: ClipRRect( borderRadius: BorderRadius.circular(widget.ability.radius), child: Image( image: widget.ability.image, width: widget.ability.radius * 2, height: widget.ability.radius * 2, fit: BoxFit.cover, ), ), ), ); var center = Transform.rotate( angle: -_angle / 180 * pi, child: Transform.scale( scale: 0.5 + animation.value / 360 / 2, child: SizedBox( width: widget.ability.radius * 2, height: widget.ability.radius * 2, child: paint, ), )); return Center( child: Stack( alignment: Alignment.center, children: <Widget>[img, center, outlinePainter], ), ); } } class OutlinePainter extends CustomPainter { double _radius; //外圆半径 Paint mLinePaint; //线画笔 Paint mFillPaint; //填充画笔 OutlinePainter(this._radius) { mLinePaint = Paint() ..color = Colors.black ..style = PaintingStyle.stroke ..strokeWidth = 0.008 * _radius ..isAntiAlias = true; mFillPaint = Paint() //填充画笔 ..strokeWidth = 0.05 * _radius ..color = Colors.black ..isAntiAlias = true; } @override void paint(Canvas canvas, Size size) { drawOutCircle(canvas); } @override bool shouldRepaint(CustomPainter oldDelegate) { // TODO: implement shouldRepaint return true; } //绘制外圈 void drawOutCircle(Canvas canvas) { canvas.save(); //新建图层 canvas.drawCircle(Offset(0, 0), _radius, mLinePaint); //圆形的绘制 double r2 = _radius - 0.08 * _radius; //下圆半径 canvas.drawCircle(Offset(0, 0), r2, mLinePaint); for (var i = 0.0; i < 22; i++) { //循环画出小黑条 canvas.save(); //新建图层 canvas.rotate(360 / 22 * i / 180 * pi); //旋转:注意传入的是弧度(与Android不同) canvas.drawLine(Offset(0, -_radius), Offset(0, -r2), mFillPaint); //线的绘制 canvas.restore(); //释放图层 } canvas.restore(); //释放图层 } } class AbilityPainter extends CustomPainter { Map<String, double> _data; double _r; //外圆半径 Paint mLinePaint; //线画笔 Paint mAbilityPaint; //区域画笔 Paint mFillPaint; //填充画笔 Path mLinePath; //短直线路径 Path mAbilityPath; //范围路径 AbilityPainter(this._r, this._data) { mLinePath = Path(); mAbilityPath = Path(); mLinePaint = Paint() ..color = Colors.black ..style = PaintingStyle.stroke ..strokeWidth = 0.008 * _r ..isAntiAlias = true; mFillPaint = Paint() //填充画笔 ..strokeWidth = 0.05 * _r ..color = Colors.black ..isAntiAlias = true; mAbilityPaint = Paint() ..color = Color(0x8897C5FE) ..isAntiAlias = true; } @override void paint(Canvas canvas, Size size) { //剪切画布 Rect rect = Offset.zero & size; canvas.clipRect(rect); canvas.translate(_r, _r); //移动坐标系 drawInnerCircle(canvas); drawInfoText(canvas); drawAbility(canvas, _data.values.toList()); } @override bool shouldRepaint(CustomPainter oldDelegate) { return true; } //绘制内圈圆 drawInnerCircle(Canvas canvas) { double innerRadius = 0.618 * _r; //内圆半径 canvas.drawCircle(Offset(0, 0), innerRadius, mLinePaint); canvas.save(); for (var i = 0; i < _data.length; i++) { //遍历6条线 canvas.save(); canvas.rotate(360/_data.length * i.toDouble() / 180 * pi); //每次旋转60° mLinePath.moveTo(0, -innerRadius); mLinePath.relativeLineTo(0, innerRadius); //线的路径 for (int j = 1; j < _data.length; j++) { mLinePath.moveTo(-_r * 0.02, innerRadius / _data.length * j); mLinePath.relativeLineTo(_r * 0.02 * 2, 0); } //加5条小线 canvas.drawPath(mLinePath, mLinePaint); //绘制线 canvas.restore(); } canvas.restore(); } //绘制文字 void drawInfoText(Canvas canvas) { double r2 = _r - 0.08 * _r; //下圆半径 for (int i = 0; i < _data.length; i++) { canvas.save(); canvas.rotate(360 / _data.length * i / 180 * pi + pi); drawText(canvas, _data.keys.toList()[i], Offset(-50, r2 - 0.22 * _r), fontSize: _r * 0.1); canvas.restore(); } } //绘制区域 drawAbility(Canvas canvas, List<double> value) { double step = _r * 0.618 / _data.length; //每小段的长度 mAbilityPath.moveTo(0, -value[0] / (100/_data.length) * step); //起点 for (int i = 1; i < _data.length; i++) { double mark = value[i] / (100/_data.length); var deg=pi/180*(360/_data.length * i - 90); mAbilityPath.lineTo(mark * step * cos(deg), mark * step * sin(deg)); } mAbilityPath.close(); canvas.drawPath(mAbilityPath, mAbilityPaint); } //绘制文字 drawText(Canvas canvas, String text, Offset offset, {Color color = Colors.black, double maxWith = 100, double fontSize, String fontFamily, TextAlign textAlign = TextAlign.center, FontWeight fontWeight = FontWeight.bold}) { // 绘制文字 var paragraphBuilder = ui.ParagraphBuilder( ui.ParagraphStyle( fontFamily: fontFamily, textAlign: textAlign, fontSize: fontSize, fontWeight: fontWeight, ), ); paragraphBuilder.pushStyle( ui.TextStyle(color: color, textBaseline: ui.TextBaseline.alphabetic)); paragraphBuilder.addText(text); var paragraph = paragraphBuilder.build(); paragraph.layout(ui.ParagraphConstraints(width: maxWith)); canvas.drawParagraph(paragraph, Offset(offset.dx, offset.dy)); } }