在Android的时候自定义过蛛网图,花了半天时间。复刻到Flutter只用了不到20分钟
不得不说Flutter中的Canvas对安卓玩家还是非常友好的,越来越觉得Flutter非常有趣。
在视图方面,Flutter确实要比原生更胜一筹。本文你将学到:
1.三角函数的使用
2.Flutter中如何用绘制文字
3.动画在绘图中的实际运用
4.Canvas绘图的相关相关方法
5.Flutter中一个组件的封装
image
image
---->[使用方法]-------------
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,
};
image
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.绘制外圈
外圈绘制.png为了减少变量值,让尺寸具有很好的联动性(等比扩缩),小黑条的长宽将取决于最大半径
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.绘制内圈
内圈绘制.png同样尺寸和最外圆看齐,这里绘制有一丢丢复杂,你需要了解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.绘制文字
imageFlutter中绘制文字可有点略坑,我这里简单的封了一个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.绘制范围
image最后也是最难的一块,你准备好草稿纸了吗?
//绘制区域
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],
),
);
}
}
3.组件封装
image到现在逻辑上没有问题了,剩下的就是对组件的封装,将一些量进行提取
下面就是简单封装了一下,还有很多乱七八糟的没封装,比如颜色,动画效果等。
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));
}
}
结语
本文到此接近尾声了,如果想快速尝鲜Flutter,《Flutter七日》会是你的必备佳品;如果想细细探究它,那就跟随我的脚步,完成一次Flutter之旅。
另外本人有一个Flutter微信交流群,欢迎小伙伴加入,共同探讨Flutter的问题,本人微信号:zdl1994328
,期待与你的交流与切磋。
网友评论