在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();//釋放圖層
}
複製程式碼
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;
}
@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.元件封裝
到現在邏輯上沒有問題了,剩下的就是對元件的封裝,將一些量進行提取
下面就是簡單封裝了一下,還有很多亂七八糟的沒封裝,比如顏色,動畫效果等。
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
,期待與你的交流與切磋。