在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>{@overrideWidget 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;}@overridevoid paint(Canvas canvas, Size size) {}@overridebool 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();//释放图层
}
复制代码

1.3.绘制内圈

同样尺寸和最外圆看齐,这里绘制有一丢丢复杂,你需要了解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;}@overridevoid paint(Canvas canvas, Size size) {drawOutCircle(canvas);}@overridebool shouldRepaint(CustomPainter oldDelegate) {// TODO: implement shouldRepaintreturn 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;@overridevoid 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();}@overrideWidget 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],),);}
}
复制代码

3.组件封装

到现在逻辑上没有问题了,剩下的就是对组件的封装,将一些量进行提取
下面就是简单封装了一下,还有很多乱七八糟的没封装,比如颜色,动画效果等。

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;@overridevoid 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));//创建curveTweenvar tween=Tween(begin: 0.0, end: 360.0);animation = tween.animate(curveTween.animate(controller));animation.addListener(() {setState(() {_angle = animation.value;print(_angle);});});controller.forward();}@overrideWidget 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;}@overridevoid paint(Canvas canvas, Size size) {drawOutCircle(canvas);}@overridebool shouldRepaint(CustomPainter oldDelegate) {// TODO: implement shouldRepaintreturn 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;}@overridevoid 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());}@overridebool 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));}
}复制代码
结语

本文到此接近尾声了,如果想快速尝鲜Flutter,《Flutter七日》会是你的必备佳品;如果想细细探究它,那就跟随我的脚步,完成一次Flutter之旅。
另外本人有一个Flutter微信交流群,欢迎小伙伴加入,共同探讨Flutter的问题,本人微信号:zdl1994328,期待与你的交流与切磋。

[-Flutter 自组篇-] 蛛网图+绘制+动画实践相关推荐

  1. [-Flutter趣玩篇-] 出神入化的Align+动画

    龙少:上一个Align玩的出神入化.现在有个需求,让一个组件以某个函数图像动起来.你说咱们要不先去找块砖头再和设计谈谈. - - 捷特: 别激动,都是成年人.多大点事,有哥在. 龙少:有什么好主意. ...

  2. Flutter(十八)——支付宝咻一咻动画实践

    本文目录 咻一咻设计 代码实现咻一咻 三个动画的实现 构建圆 透明效果 咻一咻设计 对于支付宝咻一咻功能,是在2016年的时候上线到支付宝的,那个时候好像是专门为了集五福而设计的功能,现在肯定已经不在 ...

  3. r语言把多个图合并在一张图_SAS 绘制亚组分析森林图

    背景介绍 随着循证医学(evidence-based medicine)的兴起,森林图(forest plot)已经为人所广泛认识.当前,几乎所有的临床工作者都知道什么是森林图以及如何解读森林图的含义 ...

  4. 微信小程序Canvas绘制曲线图饼图柱状图雷达图蛛网图实现(附源码)

    小程序绘制曲线图 <view class="container"><canvas canvas-id="lineCanvas" disable ...

  5. R语言亚组分析 (Subgroup Analysis)及森林图绘制实战

    R语言亚组分析 (Subgroup Analysis)及森林图绘制实战 目录 R语言亚组分析 (Subgroup Analysis)及森林图绘制实战 #亚组分析

  6. R语言Logistic回归模型亚组分析森林图(forest plot)绘制

    R语言Logistic回归模型亚组分析森林图(forest plot)绘制 目录 R语言Logistic回归模型亚组分析森林图(forest plot)R语言Logistic回归模型亚组分析森林图

  7. R语言广义线性模型Logistic回归模型亚组分析及森林图绘制

    R语言广义线性模型Logistic回归模型亚组分析及森林图绘制 #Logistic回归案例 6 亚组分析森林图 library(forestplot) rs_forest <- read.csv ...

  8. 安利一下好用免费的思维导图绘制软件(实在篇)

    思维导图是一种将思维形象化的方法.我们知道放射性思考是人类大脑的自然思考方式,每一种进入大脑的资料,不论是感觉.记忆或是想法–包括文字.数字.符码.香气.食物.线条.颜色.意象.节奏.音符等,都可以成 ...

  9. matlab画多组数据折线图_使用Origin绘制不相关多组数据折线图的方法

    原标题:使用Origin绘制不相关多组数据折线图的方法 由于不知道这样的标题是否能表达清楚我的意思,我再详细描述一下这几天遇到的问题:首先,想画一个折线图,但又不是普通的y=f(x)这样的图,而是由多 ...

最新文章

  1. Ubuntu16.04运行.run文件
  2. 一个搜索需求搞垮微服务
  3. 人体姿态估计--Learning Feature Pyramids for Human Pose Estimation
  4. 一个mui+ajax+php的demo
  5. ActiveMQ(一)
  6. PaaS云计算平台服务商大集合
  7. RedHat Enterprise Linux 6 配置Xmanager ,实现图形界面连接
  8. 机器学习——贝叶斯分类器
  9. BitMap 的基本原理和实现
  10. 使用Varnish代替Squid做网站缓存加速器的详细解决方案
  11. 《算法概论》第八章NP完全问题部分习题解
  12. V-REP仿真简介与用户界面 | Introduction to V-REP simulation and user interface
  13. 在线合并、分解PDF;PDF格式转换
  14. Web.config配置文件详解(转载)
  15. git提交代码失败 ‘“node“‘ �����ڲ����ⲿ���Ҳ���ǿ����еij������������ļ��� 解决方法
  16. NGS数据分析实践:06. 数据预处理 - 序列比对+PCR重复标记+Indel区域重比对+碱基质量重校正
  17. 独立站引流技巧和营销思路
  18. java注释【单行注释,多行注释,文档注释】
  19. 4.4 基金排行数据(Python)
  20. Java Excel省市区级联菜单设置

热门文章

  1. 浅谈网络之“TCP协议”
  2. 求平方根序列前N项和
  3. java 如何判断对象内的某个属性是空
  4. CSS (Cascading Style Sheets)
  5. 05机器学习--多项式回归与模型泛化及python实现
  6. kafka官方文档中文翻译(kafka参数解释)
  7. 互联网废品回收+废品回收小程序开发这些实际功能要有
  8. halcon像素统计_Halcon(八)亚像素轮廓XLD
  9. LeetCode 会议室 II
  10. GhostNet代码解析