要求效果:
实现的效果:
代码:
选择点的界面:
Dart
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:kq_flutter_widgets/widgets/animate/mapChart/map_chart.dart';
import 'package:kq_flutter_widgets/widgets/button/kq_bottom_button.dart';
import 'package:kq_flutter_widgets/widgets/image/kq_image.dart';
import 'package:kq_flutter_widgets/widgets/titleBar/kq_title_bar.dart';
import 'package:kq_flutter_widgets_example/router/route_map.dart';
import '../../resources/Images.dart';
class MapChartChooseDemo extends StatefulWidget {
const MapChartChooseDemo({super.key});
@override
State<StatefulWidget> createState() => MapChartChooseDemoState();
}
class MapChartChooseDemoState extends State<MapChartChooseDemo> {
MapChartData data = MapChartData(
pLine: Paint()
..color = Colors.red
..style = PaintingStyle.stroke,
pStart: Paint()..color = Colors.redAccent,
pEnd: Paint()..color = Colors.cyan,
pCur: Paint()..color = Colors.amberAccent,
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: KqHeadBar(
headTitle: 'MapChart控件点选择界面',
back: () {
Get.back();
},
),
body: Column(
children: [
Stack(
children: [
Listener(
child: KqImage(
url: Images.demoWorld6,
imageType: ImageType.assets,
fit: BoxFit.contain,
),
onPointerDown: (event) {
if (data.end == null) {
data.end =
Point(event.localPosition.dx, event.localPosition.dy);
} else {
data.starts ??= [];
data.starts!.add(
Point(event.localPosition.dx, event.localPosition.dy));
}
setState(() {});
},
),
CustomPaint(
painter: PointPainter(data),
),
],
),
KqBottomButton(
title: "完成选择",
onTap: (disabled) {
RouteMap.pushMapChartDemo(data);
},
),
],
),
);
}
}
class PointPainter extends CustomPainter {
final MapChartData data;
PointPainter(this.data);
@override
void paint(Canvas canvas, Size size) {
if (data.starts != null) {
for (int i = 0; i < data.starts!.length; i++) {
Point<double> start = data.starts![i];
///画起始点
canvas.drawCircle(Offset(start.x, start.y), 5, data.pStart ?? Paint());
}
}
if (data.end != null) {
///画终点
canvas.drawCircle(
Offset(data.end!.x, data.end!.y), 5, data.pEnd ?? Paint());
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
演示界面:
Dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:kq_flutter_widgets/utils/ex/kq_ex.dart';
import 'package:kq_flutter_widgets/widgets/animate/mapChart/map_chart.dart';
import 'package:kq_flutter_widgets/widgets/image/kq_image.dart';
import 'package:kq_flutter_widgets/widgets/titleBar/kq_title_bar.dart';
import '../../resources/Images.dart';
class MapChartDemo extends StatefulWidget {
const MapChartDemo({super.key});
@override
State<StatefulWidget> createState() => MapChartDemoState();
}
class MapChartDemoState extends State<MapChartDemo> {
/*MapChartData data = MapChartData(
starts: const [
Point<double>(0, 0),
Point<double>(300, 10),
Point<double>(10, 400),
Point<double>(300, 400),
],
end: const Point<double>(200, 200),
pLine: Paint()..color = Colors.red..style=PaintingStyle.stroke,
pStart: Paint()..color = Colors.blueAccent,
pEnd: Paint()..color = Colors.cyan,
pCur: Paint()..color = Colors.amberAccent,
);*/
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: KqHeadBar(
headTitle: 'MapChart控件动画演示',
back: () {
Get.back();
},
),
body: Stack(
children: [
KqImage(
url: Images.demoWorld6,
imageType: ImageType.assets,
fit: BoxFit.contain,
),
MapChart(data: Get.getArgOrParams<MapChartData>("data")!),
],
),
);
}
}
关键代码---MapChart控件:
Dart
import 'dart:math';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:kq_flutter_widgets/widgets/chart/ex/extension.dart';
class MapChart<T extends MapChartData> extends StatefulWidget {
final T data;
const MapChart({super.key, required this.data});
@override
State<StatefulWidget> createState() => MapChartState();
}
class MapChartState extends State<MapChart> with TickerProviderStateMixin {
///动画最大值
static double maxValue = 1000.0;
late AnimationController controller;
late Animation<double> animation;
@override
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween(begin: 0.0, end: maxValue).animate(controller)
..addListener(_animationListener);
controller.repeat();
}
void _animationListener() {
if (mounted) {
setState(() {});
}
}
@override
Widget build(BuildContext context) {
if (widget.data.starts != null && widget.data.end != null) {
return LayoutBuilder(builder: (v1, v2) {
return CustomPaint(
size: Size(v2.maxWidth, v2.maxHeight),
painter: LineAnimate(
widget.data.starts!,
widget.data.end!,
animation.value / maxValue,
pLine: widget.data.pLine,
pStart: widget.data.pStart,
pEnd: widget.data.pEnd,
pCur: widget.data.pCur,
),
);
});
} else {
return Container();
}
}
@override
void dispose() {
controller.removeListener(_animationListener);
controller.dispose();
super.dispose();
}
}
class MapChartData {
List<Point<double>>? starts;
Point<double>? end;
Paint? pLine;
Paint? pStart;
Paint? pEnd;
Paint? pCur;
MapChartData({
this.starts,
this.end,
this.pLine,
this.pStart,
this.pEnd,
this.pCur,
});
}
class LineAnimate extends CustomPainter {
final List<Point<double>> starts;
final Point<double> end;
final double mix;
final Paint? pLine;
final Paint? pStart;
final Paint? pEnd;
final Paint? pCur;
///拖尾长度
final double trailingLength;
LineAnimate(
this.starts,
this.end,
this.mix, {
this.pLine,
this.pStart,
this.pEnd,
this.pCur,
this.trailingLength = 80,
});
@override
void paint(Canvas canvas, Size size) {
for (int i = 0; i < starts.length; i++) {
Point<double> start = starts[i];
///计算出两点间中间点往上垂直两点距地的点的坐标
//计算起点到终点的两点距离
double lineLength = sqrt((end.x - start.x) * (end.x - start.x) +
(end.y - start.y) * (end.y - start.y));
//计算坐标系中起点与终点连线与x坐标的夹角的弧度值
double radians = atan2(end.y - start.y, end.x - start.x);
//根据三角函数计算出偏移点相对于起点为原的坐标系的X的坐标
double centerOffsetPointX =
cos(45 * pi / 180 + radians) * sqrt(2) * lineLength / 2;
//根据三角函数计算出偏移点相对于起点为原的坐标系的Y的坐标
double centerOffsetPointY =
sin(45 * pi / 180 + radians) * sqrt(2) * lineLength / 2;
///坐标系平移
double moveX = centerOffsetPointX + start.x;
double moveY = centerOffsetPointY + start.y;
///画线
Path path = Path();
path.moveTo(start.x, start.y);
path.cubicTo(start.x, start.y, moveX, moveY, end.x, end.y);
canvas.drawPath(path, pLine ?? Paint());
///画起始点
canvas.drawCircle(Offset(start.x, start.y), 5, pStart ?? Paint());
///画终点
canvas.drawCircle(Offset(end.x, end.y), 5, pEnd ?? Paint());
///画动画点
PathMetric? pathMetric = path.computeMetric();
if (pathMetric != null) {
double length = pathMetric.length;
double curDistance = length * mix;
Tangent? tangent = pathMetric.getTangentForOffset(curDistance);
double startDistance = 0;
if (curDistance == 0) {
startDistance = 0;
} else if (curDistance > 0 && curDistance < trailingLength) {
startDistance = 0;
} else {
startDistance = curDistance - trailingLength;
}
Path path2 = pathMetric.extractPath(startDistance, curDistance);
//画拖尾
//_particleTrailingDraw(canvas, 1, 8, path2, 10, 1);
_lineTrailingDraw(canvas, path2, 2);
if (tangent != null) {
Offset cur = tangent.position;
//画运动点
canvas.drawCircle(Offset(cur.dx, cur.dy), 8, pCur ?? Paint());
}
}
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
///粒子拖尾
_particleTrailingDraw(
Canvas canvas, double cr, int rr, Path path, int start, int end) {
PathMetric? pathMetric1 = path.computeMetric();
if (pathMetric1 != null) {
int length1 = pathMetric1.length.toInt();
double diff = (start - end) / length1;
for (int i = 0; i < length1.toInt(); i++) {
int left = (start - diff * i).toInt();
Tangent? tangent1 =
pathMetric1.getTangentForOffset(length1 - i.toDouble());
if (tangent1 != null) {
Offset cur = tangent1.position;
for (int j = 0; j < left; j++) {
double mix = Random().nextDouble();
int r = Random().nextInt(rr);
double radians1 = j * 2 * pi / left;
double x1 = r * cos(radians1) + cur.dx;
double y1 = r * sin(radians1) + cur.dy;
///计算出两点间中间点往上垂直两点距地的点的坐标
//计算坐标系中起点与终点连线与x坐标的夹角的弧度值
double radians2 = atan2(y1 - cur.dy, x1 - cur.dx);
//根据三角函数计算出偏移点相对于起点为原的坐标系的X的坐标
double centerOffsetPointX = cos(Random().nextInt(2) == 1
? (45 * pi / 180 + radians2)
: (45 * pi / 180 - radians2)) *
sqrt(2) *
r /
2;
//根据三角函数计算出偏移点相对于起点为原的坐标系的Y的坐标
double centerOffsetPointY = sin(Random().nextInt(2) == 1
? (45 * pi / 180 + radians2)
: (45 * pi / 180 - radians2)) *
sqrt(2) *
r /
2;
///坐标系平移
double moveX = centerOffsetPointX + cur.dx;
double moveY = centerOffsetPointY + cur.dy;
Path path2 = Path();
path2.moveTo(cur.dx, cur.dy);
path2.cubicTo(cur.dx, cur.dy, moveX, moveY, x1, y1);
///画动画点
PathMetric? pathMetric2 = path2.computeMetric();
if (pathMetric2 != null) {
double length2 = pathMetric2.length;
Tangent? tangent2 =
pathMetric2.getTangentForOffset(length2 * mix);
if (tangent2 != null) {
Offset cur2 = tangent2.position;
canvas.drawCircle(
Offset(cur2.dx, cur2.dy),
cr * (1 - mix).toDouble(),
Paint()
..color = Colors.redAccent
..maskFilter =
const MaskFilter.blur(BlurStyle.normal, 2));
}
}
}
}
}
}
}
///线性拖尾
_lineTrailingDraw(Canvas canvas, Path path, double r) {
PathMetric? pathMetric1 = path.computeMetric();
if (pathMetric1 != null) {
int length1 = pathMetric1.length.toInt();
for (int i = 0; i < length1.toInt(); i++) {
Tangent? tangent1 = pathMetric1.getTangentForOffset(i.toDouble());
double mix = i / length1.toInt();
if (tangent1 != null) {
Offset cur = tangent1.position;
canvas.drawCircle(
cur,
r * mix,
Paint()
..color = Colors.redAccent
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 2));
}
}
}
}
}
主要思路:
主要是绘制多个点到一个点的路径,使用的是三点绘制贝塞尔曲线,利用坐标系与三角函数,计算出两个点的中间点直角偏两点一半的位置的坐标为贝塞尔控制点绘制二阶贝塞尔曲线,并获取路径,加上我们上一篇文章中的拖尾效果,有两种拖尾形式,一种是用的粒子,一种用的线性,粒子的比较耗性能,但是动画效果好,线性的不耗性能,动画没那么细腻,但是也能达到预效果。思路很简单,本文主要是起到抛砖引玉的效果,我也只实现了基本功能,还有那些文本绘制,点的样式绘制等,需要读者自己添砖加瓦。