
flutter 五維雷達(dá)圖.png
思路:
一、繪制5個正五邊形,按照五邊形的半徑等分繪制。至于五邊形需要計(jì)算出5個定點(diǎn)的位置。繪制一個正五邊形,一個頂點(diǎn)在y軸上,半徑為r,s標(biāo)識sin,c標(biāo)識cos順勢正依次是(0,r)(rc18,rs18)(rc54,-rs54)(-rc54,-rs54)(-rc18,rs18),然后注意角度需要轉(zhuǎn)換成程序需要的弧度angle / 180.0 * pi,然后使用path連接即可。
///畫n個五邊形
for (int i = 0; i < n; i++) {
List<Offset> points = [
Offset(0, -r * (i + 1) / n),
Offset(r * (i + 1) / n * cos(angleToRadian(18)),
-r * (i + 1) / n * sin(angleToRadian(18))),
Offset(r * (i + 1) / n * cos(angleToRadian(54)),
r * (i + 1) / n * sin(angleToRadian(54))),
Offset(-r * (i + 1) / n * cos(angleToRadian(54)),
r * (i + 1) / n * sin(angleToRadian(54))),
Offset(-r * (i + 1) / n * cos(angleToRadian(18)),
r * (i + 1) / n * -sin(angleToRadian(18))),
];
drawPentagon(points, canvas, pentagonPaint);
}
///畫五邊形
void drawPentagon(List<Offset> points, Canvas canvas, Paint paint) {
Path path = Path();
path.moveTo(0, points[0].dy);
for (int i = 1; i < points.length; i++) {
path.lineTo(points[i].dx, points[i].dy);
}
path.close();
canvas.drawPath(path, paint);
}
二、連接原點(diǎn)和五個定點(diǎn)
void drawZeroToPoint(List<Offset> points, Canvas canvas) {
points.forEach((element) {
canvas.drawLine(
Offset.zero,
element,
zeroToPointPaint,
);
});
}
三、在繪制我們需要顯示的雷達(dá)圖,也是五邊形,所以,只需要算出五個頂點(diǎn)就可以套用上面的繪制五邊圖繪制。
List<Offset> list = converPoint(points, score);
drawPentagon(list, canvas, contentPaint);
List<Offset> converPoint(List<Offset> points, List<RadarBean> score) {
List<Offset> list = [];
for (int i = 0; i < points.length; i++) {
list.add(points[i].scale(score[i].score / 100, score[i].score / 100));
}
return list;
}
四、因?yàn)?個頂點(diǎn)位置的文字都需要不同的調(diào)整,所以分別設(shè)置位置
///根據(jù)位置繪制文字
for (int i = 0; i < points.length; i++) {
int type = 0;
switch (i) {
case 0:
type = 1;
points[i] -= Offset(0, padding * 2);
break;
case 1:
type = 0;
points[i] += Offset(padding, -padding);
break;
case 2:
type = 1;
points[i] += Offset(bottomPadding, padding);
break;
case 3:
type = 1;
points[i] += Offset(-bottomPadding, padding);
break;
case 4:
type = 2;
points[i] -= Offset(padding, padding);
break;
default:
}
drawText(canvas, points[i], score[i].name,
TextStyle(fontSize: 14, color: Colors.black54), type);
}
/// 右邊的文字不需要移動 有的文字要移動一半居中 左邊的文字需要左移動整個距離
///type 0 1 2
void drawText(Canvas canvas, Offset offset, String text, TextStyle style,
int type) {
var textPainter = TextPainter(
text: TextSpan(text: text, style: style),
textAlign: TextAlign.center,
textDirection: TextDirection.rtl);
textPainter.layout();
Size size = textPainter.size;
Offset offsetResult;
switch (type) {
case 1:
offsetResult = Offset(offset.dx - size.width / 2, offset.dy);
break;
case 2:
offsetResult = Offset(offset.dx - size.width, offset.dy);
break;
default:
offsetResult = offset;
}
textPainter.paint(canvas, offsetResult);
}
實(shí)際上思路很簡單,就是需要算頂點(diǎn)位置,然后文字位置處理并不優(yōu)雅。
五、新增動畫效果
思路:通過修改 ValueNotifier<List<Offset>> values 的值,可以觸發(fā)重繪。使用AnimationController來控制values的值的變化,使用CurvedAnimation來增加一個加速插值器。
//創(chuàng)建一個1s的動畫
ctrl = AnimationController(vsync: this, duration: Duration(seconds: 1))
..addListener(() {
//監(jiān)聽動畫的進(jìn)度,并修改對應(yīng)的值,重繪
values.value = converPoint(widget.points, widget.scoreList, animation.value);
});
//關(guān)聯(lián)控制器,設(shè)置加速器
animation = CurvedAnimation(parent: ctrl,curve: Curves.bounceOut);
添加動畫需要注意的幾個點(diǎn):
- 動畫需要用到SingleTickerProviderStateMixin 這個類
class RadarMapState extends State<RadarMap> with SingleTickerProviderStateMixin {
- ValueNotifier<List<Offset>>的值的修改,不能放到自定義的CustomPainter中,會無限重繪報(bào)錯。還是在RadarMapState 中修改對應(yīng)的值實(shí)現(xiàn)動畫。
ctrl = AnimationController(vsync: this, duration: Duration(seconds: 1))
..addListener(() {
values.value = converPoint(widget.points, widget.scoreList, animation.value);
});
- 從什么時候開始動畫呢?太早還未繪制完成,會報(bào)錯。可以監(jiān)聽繪制完成,然后再去開始動畫。
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
print("界面繪制完成");
ctrl.forward(from: 0);
});
附上整體代碼
import 'dart:math';
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
///五維雷達(dá)圖
/// 設(shè)置雷達(dá)圖的半徑,根據(jù)分5等分來計(jì)算
/// 繪制一個正五邊形,一個頂點(diǎn)在y軸上,半徑為r,順勢正依次是(0,r)(r*c18,r*s18)
/// (r*c54,-r*s54)(-r*c54,-r*s54)(-r*c18,r*s18)
///
class RadarBean {
double score;
String name;
RadarBean(this.score, this.name);
}
/// 右邊的文字不需要移動 有的文字要移動一半居中 左邊的文字需要左移動整個距離
///type 0 1 2
///
enum MoveType { noMove, halfMove, allMove }
class RadarMap extends StatefulWidget {
///半徑
double r;
// static final double defaultR = setWidth(80.0);
///正五邊形個數(shù) 目前只支持五邊形
int n = 5;
///文字和圖像的間距
double padding;
static const double defaultPadding = 10;
///最下面兩個的間距
double bottomPadding = 8;
static const double defaultBottomPadding = 8;
static const double strokeWidth_0_5 = 0.5;
static const double strokeWidth_1 = 1;
static const double strokeWidth_2 = 2;
Paint zeroToPointPaint;
Paint pentagonPaint;
Paint contentPaint;
///當(dāng)前的分?jǐn)?shù) ///對應(yīng)的文案
List<RadarBean> scoreList;
List<Offset> points;
RadarMap(this.scoreList,
{this.r,
this.padding = defaultPadding,
this.bottomPadding = defaultBottomPadding,
this.zeroToPointPaint,
this.pentagonPaint,
this.contentPaint}) {
assert(scoreList.length == 5);
r = 80.0;
///原點(diǎn)到5個定點(diǎn)的連線
zeroToPointPaint = Paint()
..style = PaintingStyle.stroke
..color = Colors.black12
..strokeWidth = strokeWidth_0_5;
///5層五邊形畫筆
pentagonPaint = Paint()
..color = Colors.black12
..strokeWidth = strokeWidth_1
..style = PaintingStyle.fill;
///覆蓋內(nèi)容顏色
contentPaint = Paint()
..color = Colors.lightBlue[300].withAlpha(100)
..strokeWidth = strokeWidth_2
..style = PaintingStyle.fill;
points = [
Offset(0, -r),
Offset(r * cos(angleToRadian(18)), -r * sin(angleToRadian(18))),
Offset(r * cos(angleToRadian(54)), r * sin(angleToRadian(54))),
Offset(-r * cos(angleToRadian(54)), r * sin(angleToRadian(54))),
Offset(-r * cos(angleToRadian(18)), r * -sin(angleToRadian(18))),
];
}
@override
State<StatefulWidget> createState() {
return RadarMapState();
}
}
class RadarMapState extends State<RadarMap>
with SingleTickerProviderStateMixin {
ValueNotifier<List<Offset>> values = ValueNotifier([]);
AnimationController ctrl;
Animation animation;
@override
void initState() {
// TODO: implement initState
super.initState();
ctrl = AnimationController(vsync: this, duration: Duration(seconds: 1))
..addListener(() {
values.value = converPoint(widget.points, widget.scoreList, animation.value);
});
animation = CurvedAnimation(parent: ctrl,curve: Curves.bounceOut);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
print("界面繪制完成");
ctrl.forward(from: 0);
});
}
List<Offset> converPoint(
List<Offset> points, List<RadarBean> score, double scale) {
List<Offset> list = [];
for (int i = 0; i < points.length; i++) {
list.add(points[i]
.scale(score[i].score * scale / 100, score[i].score * scale / 100));
}
return list;
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: CustomPaint(
painter: RadarmapPainter(widget.scoreList, ctrl, values,
r: widget.r,
n: widget.n,
padding: widget.padding,
bottomPadding: widget.bottomPadding,
zeroToPointPaint: widget.zeroToPointPaint,
pentagonPaint: widget.pentagonPaint,
contentPaint: widget.contentPaint),
),
);
}
}
class RadarmapPainter extends CustomPainter {
double r;
int n;
double padding;
double bottomPadding;
Paint zeroToPointPaint;
Paint pentagonPaint;
Paint contentPaint;
List<RadarBean> score;
AnimationController ctrl;
ValueNotifier<List<Offset>> values;
RadarmapPainter(this.score, this.ctrl, this.values,
{this.r,
this.n,
this.padding,
this.bottomPadding,
this.zeroToPointPaint,
this.pentagonPaint,
this.contentPaint})
: super(repaint: values);
@override
void paint(Canvas canvas, Size size) {
final List<Offset> points = [
Offset(0, -r),
Offset(r * cos(angleToRadian(18)), -r * sin(angleToRadian(18))),
Offset(r * cos(angleToRadian(54)), r * sin(angleToRadian(54))),
Offset(-r * cos(angleToRadian(54)), r * sin(angleToRadian(54))),
Offset(-r * cos(angleToRadian(18)), r * -sin(angleToRadian(18))),
];
canvas.save();
canvas.translate(size.width / 2, size.height / 2);
canvas.drawPoints(
PointMode.points,
[Offset(0, 0)],
Paint()
..color = Colors.green
..strokeWidth = 2);
///畫n個五邊形
for (int i = 0; i < n; i++) {
List<Offset> points = [
Offset(0, -r * (i + 1) / n),
Offset(r * (i + 1) / n * cos(angleToRadian(18)),
-r * (i + 1) / n * sin(angleToRadian(18))),
Offset(r * (i + 1) / n * cos(angleToRadian(54)),
r * (i + 1) / n * sin(angleToRadian(54))),
Offset(-r * (i + 1) / n * cos(angleToRadian(54)),
r * (i + 1) / n * sin(angleToRadian(54))),
Offset(-r * (i + 1) / n * cos(angleToRadian(18)),
r * (i + 1) / n * -sin(angleToRadian(18))),
];
drawPentagon(points, canvas, pentagonPaint);
}
///連接最外層的五個定點(diǎn)
drawZeroToPoint(points, canvas);
///修改成對應(yīng)的分?jǐn)?shù),繪制覆蓋內(nèi)容
drawPentagon(values.value, canvas, contentPaint);
///根據(jù)位置繪制文字
drawTextByPosition(points, canvas);
canvas.restore();
}
///根據(jù)位置來繪制文字
void drawTextByPosition(List<Offset> points, Canvas canvas) {
for (int i = 0; i < points.length; i++) {
MoveType type = MoveType.noMove;
switch (i) {
case 0:
type = MoveType.halfMove;
points[i] -= Offset(0, padding * 2);
break;
case 1:
type = MoveType.noMove;
points[i] += Offset(padding, -padding);
break;
case 2:
type = MoveType.halfMove;
points[i] += Offset(bottomPadding, padding);
break;
case 3:
type = MoveType.halfMove;
points[i] += Offset(-bottomPadding, padding);
break;
case 4:
type = MoveType.allMove;
points[i] -= Offset(padding, padding);
break;
default:
}
drawText(canvas, points[i], score[i].name,
TextStyle(fontSize: 14, color: Colors.black54), type);
}
}
/// 右邊的文字不需要移動 有的文字要移動一半居中 左邊的文字需要左移動整個距離
void drawText(Canvas canvas, Offset offset, String text, TextStyle style,
MoveType type) {
var textPainter = TextPainter(
text: TextSpan(text: text, style: style),
textAlign: TextAlign.center,
textDirection: TextDirection.rtl);
textPainter.layout();
Size size = textPainter.size;
Offset offsetResult;
switch (type) {
case MoveType.halfMove:
offsetResult = Offset(offset.dx - size.width / 2, offset.dy);
break;
case MoveType.allMove:
offsetResult = Offset(offset.dx - size.width, offset.dy);
break;
default:
offsetResult = offset;
}
textPainter.paint(canvas, offsetResult);
}
void drawZeroToPoint(List<Offset> points, Canvas canvas) {
points.forEach((element) {
canvas.drawLine(
Offset.zero,
element,
zeroToPointPaint,
);
});
}
///畫五邊形
void drawPentagon(List<Offset> points, Canvas canvas, Paint paint) {
if(points.length == 0){
return;
}
Path path = Path();
path.moveTo(0, points[0].dy);
for (int i = 1; i < points.length; i++) {
path.lineTo(points[i].dx, points[i].dy);
}
path.close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
///轉(zhuǎn)換角度 18/180.0 *pi
}
double angleToRadian(double angle) {
return angle / 180.0 * pi;
}