• Flutter实现地图上汇聚到一点的效果。


    要求效果:

    实现的效果:

    代码:

    选择点的界面:

    1. import 'dart:math';
    2. import 'package:flutter/material.dart';
    3. import 'package:get/get.dart';
    4. import 'package:kq_flutter_widgets/widgets/animate/mapChart/map_chart.dart';
    5. import 'package:kq_flutter_widgets/widgets/button/kq_bottom_button.dart';
    6. import 'package:kq_flutter_widgets/widgets/image/kq_image.dart';
    7. import 'package:kq_flutter_widgets/widgets/titleBar/kq_title_bar.dart';
    8. import 'package:kq_flutter_widgets_example/router/route_map.dart';
    9. import '../../resources/Images.dart';
    10. class MapChartChooseDemo extends StatefulWidget {
    11. const MapChartChooseDemo({super.key});
    12. @override
    13. State createState() => MapChartChooseDemoState();
    14. }
    15. class MapChartChooseDemoState extends State<MapChartChooseDemo> {
    16. MapChartData data = MapChartData(
    17. pLine: Paint()
    18. ..color = Colors.red
    19. ..style = PaintingStyle.stroke,
    20. pStart: Paint()..color = Colors.redAccent,
    21. pEnd: Paint()..color = Colors.cyan,
    22. pCur: Paint()..color = Colors.amberAccent,
    23. );
    24. @override
    25. Widget build(BuildContext context) {
    26. return Scaffold(
    27. appBar: KqHeadBar(
    28. headTitle: 'MapChart控件点选择界面',
    29. back: () {
    30. Get.back();
    31. },
    32. ),
    33. body: Column(
    34. children: [
    35. Stack(
    36. children: [
    37. Listener(
    38. child: KqImage(
    39. url: Images.demoWorld6,
    40. imageType: ImageType.assets,
    41. fit: BoxFit.contain,
    42. ),
    43. onPointerDown: (event) {
    44. if (data.end == null) {
    45. data.end =
    46. Point(event.localPosition.dx, event.localPosition.dy);
    47. } else {
    48. data.starts ??= [];
    49. data.starts!.add(
    50. Point(event.localPosition.dx, event.localPosition.dy));
    51. }
    52. setState(() {});
    53. },
    54. ),
    55. CustomPaint(
    56. painter: PointPainter(data),
    57. ),
    58. ],
    59. ),
    60. KqBottomButton(
    61. title: "完成选择",
    62. onTap: (disabled) {
    63. RouteMap.pushMapChartDemo(data);
    64. },
    65. ),
    66. ],
    67. ),
    68. );
    69. }
    70. }
    71. class PointPainter extends CustomPainter {
    72. final MapChartData data;
    73. PointPainter(this.data);
    74. @override
    75. void paint(Canvas canvas, Size size) {
    76. if (data.starts != null) {
    77. for (int i = 0; i < data.starts!.length; i++) {
    78. Point<double> start = data.starts![i];
    79. ///画起始点
    80. canvas.drawCircle(Offset(start.x, start.y), 5, data.pStart ?? Paint());
    81. }
    82. }
    83. if (data.end != null) {
    84. ///画终点
    85. canvas.drawCircle(
    86. Offset(data.end!.x, data.end!.y), 5, data.pEnd ?? Paint());
    87. }
    88. }
    89. @override
    90. bool shouldRepaint(covariant CustomPainter oldDelegate) {
    91. return true;
    92. }
    93. }

    演示界面:

    1. import 'package:flutter/material.dart';
    2. import 'package:get/get.dart';
    3. import 'package:kq_flutter_widgets/utils/ex/kq_ex.dart';
    4. import 'package:kq_flutter_widgets/widgets/animate/mapChart/map_chart.dart';
    5. import 'package:kq_flutter_widgets/widgets/image/kq_image.dart';
    6. import 'package:kq_flutter_widgets/widgets/titleBar/kq_title_bar.dart';
    7. import '../../resources/Images.dart';
    8. class MapChartDemo extends StatefulWidget {
    9. const MapChartDemo({super.key});
    10. @override
    11. State createState() => MapChartDemoState();
    12. }
    13. class MapChartDemoState extends State<MapChartDemo> {
    14. /*MapChartData data = MapChartData(
    15. starts: const [
    16. Point(0, 0),
    17. Point(300, 10),
    18. Point(10, 400),
    19. Point(300, 400),
    20. ],
    21. end: const Point(200, 200),
    22. pLine: Paint()..color = Colors.red..style=PaintingStyle.stroke,
    23. pStart: Paint()..color = Colors.blueAccent,
    24. pEnd: Paint()..color = Colors.cyan,
    25. pCur: Paint()..color = Colors.amberAccent,
    26. );*/
    27. @override
    28. Widget build(BuildContext context) {
    29. return Scaffold(
    30. appBar: KqHeadBar(
    31. headTitle: 'MapChart控件动画演示',
    32. back: () {
    33. Get.back();
    34. },
    35. ),
    36. body: Stack(
    37. children: [
    38. KqImage(
    39. url: Images.demoWorld6,
    40. imageType: ImageType.assets,
    41. fit: BoxFit.contain,
    42. ),
    43. MapChart(data: Get.getArgOrParams("data")!),
    44. ],
    45. ),
    46. );
    47. }
    48. }

    关键代码---MapChart控件:

    1. import 'dart:math';
    2. import 'dart:ui';
    3. import 'package:flutter/material.dart';
    4. import 'package:kq_flutter_widgets/widgets/chart/ex/extension.dart';
    5. class MapChart<T extends MapChartData> extends StatefulWidget {
    6. final T data;
    7. const MapChart({super.key, required this.data});
    8. @override
    9. State createState() => MapChartState();
    10. }
    11. class MapChartState extends State<MapChart> with TickerProviderStateMixin {
    12. ///动画最大值
    13. static double maxValue = 1000.0;
    14. late AnimationController controller;
    15. late Animation<double> animation;
    16. @override
    17. void initState() {
    18. super.initState();
    19. controller =
    20. AnimationController(duration: const Duration(seconds: 2), vsync: this);
    21. animation = Tween(begin: 0.0, end: maxValue).animate(controller)
    22. ..addListener(_animationListener);
    23. controller.repeat();
    24. }
    25. void _animationListener() {
    26. if (mounted) {
    27. setState(() {});
    28. }
    29. }
    30. @override
    31. Widget build(BuildContext context) {
    32. if (widget.data.starts != null && widget.data.end != null) {
    33. return LayoutBuilder(builder: (v1, v2) {
    34. return CustomPaint(
    35. size: Size(v2.maxWidth, v2.maxHeight),
    36. painter: LineAnimate(
    37. widget.data.starts!,
    38. widget.data.end!,
    39. animation.value / maxValue,
    40. pLine: widget.data.pLine,
    41. pStart: widget.data.pStart,
    42. pEnd: widget.data.pEnd,
    43. pCur: widget.data.pCur,
    44. ),
    45. );
    46. });
    47. } else {
    48. return Container();
    49. }
    50. }
    51. @override
    52. void dispose() {
    53. controller.removeListener(_animationListener);
    54. controller.dispose();
    55. super.dispose();
    56. }
    57. }
    58. class MapChartData {
    59. Listdouble>>? starts;
    60. Point<double>? end;
    61. Paint? pLine;
    62. Paint? pStart;
    63. Paint? pEnd;
    64. Paint? pCur;
    65. MapChartData({
    66. this.starts,
    67. this.end,
    68. this.pLine,
    69. this.pStart,
    70. this.pEnd,
    71. this.pCur,
    72. });
    73. }
    74. class LineAnimate extends CustomPainter {
    75. final Listdouble>> starts;
    76. final Point<double> end;
    77. final double mix;
    78. final Paint? pLine;
    79. final Paint? pStart;
    80. final Paint? pEnd;
    81. final Paint? pCur;
    82. ///拖尾长度
    83. final double trailingLength;
    84. LineAnimate(
    85. this.starts,
    86. this.end,
    87. this.mix, {
    88. this.pLine,
    89. this.pStart,
    90. this.pEnd,
    91. this.pCur,
    92. this.trailingLength = 80,
    93. });
    94. @override
    95. void paint(Canvas canvas, Size size) {
    96. for (int i = 0; i < starts.length; i++) {
    97. Point<double> start = starts[i];
    98. ///计算出两点间中间点往上垂直两点距地的点的坐标
    99. //计算起点到终点的两点距离
    100. double lineLength = sqrt((end.x - start.x) * (end.x - start.x) +
    101. (end.y - start.y) * (end.y - start.y));
    102. //计算坐标系中起点与终点连线与x坐标的夹角的弧度值
    103. double radians = atan2(end.y - start.y, end.x - start.x);
    104. //根据三角函数计算出偏移点相对于起点为原的坐标系的X的坐标
    105. double centerOffsetPointX =
    106. cos(45 * pi / 180 + radians) * sqrt(2) * lineLength / 2;
    107. //根据三角函数计算出偏移点相对于起点为原的坐标系的Y的坐标
    108. double centerOffsetPointY =
    109. sin(45 * pi / 180 + radians) * sqrt(2) * lineLength / 2;
    110. ///坐标系平移
    111. double moveX = centerOffsetPointX + start.x;
    112. double moveY = centerOffsetPointY + start.y;
    113. ///画线
    114. Path path = Path();
    115. path.moveTo(start.x, start.y);
    116. path.cubicTo(start.x, start.y, moveX, moveY, end.x, end.y);
    117. canvas.drawPath(path, pLine ?? Paint());
    118. ///画起始点
    119. canvas.drawCircle(Offset(start.x, start.y), 5, pStart ?? Paint());
    120. ///画终点
    121. canvas.drawCircle(Offset(end.x, end.y), 5, pEnd ?? Paint());
    122. ///画动画点
    123. PathMetric? pathMetric = path.computeMetric();
    124. if (pathMetric != null) {
    125. double length = pathMetric.length;
    126. double curDistance = length * mix;
    127. Tangent? tangent = pathMetric.getTangentForOffset(curDistance);
    128. double startDistance = 0;
    129. if (curDistance == 0) {
    130. startDistance = 0;
    131. } else if (curDistance > 0 && curDistance < trailingLength) {
    132. startDistance = 0;
    133. } else {
    134. startDistance = curDistance - trailingLength;
    135. }
    136. Path path2 = pathMetric.extractPath(startDistance, curDistance);
    137. //画拖尾
    138. //_particleTrailingDraw(canvas, 1, 8, path2, 10, 1);
    139. _lineTrailingDraw(canvas, path2, 2);
    140. if (tangent != null) {
    141. Offset cur = tangent.position;
    142. //画运动点
    143. canvas.drawCircle(Offset(cur.dx, cur.dy), 8, pCur ?? Paint());
    144. }
    145. }
    146. }
    147. }
    148. @override
    149. bool shouldRepaint(covariant CustomPainter oldDelegate) {
    150. return true;
    151. }
    152. ///粒子拖尾
    153. _particleTrailingDraw(
    154. Canvas canvas, double cr, int rr, Path path, int start, int end) {
    155. PathMetric? pathMetric1 = path.computeMetric();
    156. if (pathMetric1 != null) {
    157. int length1 = pathMetric1.length.toInt();
    158. double diff = (start - end) / length1;
    159. for (int i = 0; i < length1.toInt(); i++) {
    160. int left = (start - diff * i).toInt();
    161. Tangent? tangent1 =
    162. pathMetric1.getTangentForOffset(length1 - i.toDouble());
    163. if (tangent1 != null) {
    164. Offset cur = tangent1.position;
    165. for (int j = 0; j < left; j++) {
    166. double mix = Random().nextDouble();
    167. int r = Random().nextInt(rr);
    168. double radians1 = j * 2 * pi / left;
    169. double x1 = r * cos(radians1) + cur.dx;
    170. double y1 = r * sin(radians1) + cur.dy;
    171. ///计算出两点间中间点往上垂直两点距地的点的坐标
    172. //计算坐标系中起点与终点连线与x坐标的夹角的弧度值
    173. double radians2 = atan2(y1 - cur.dy, x1 - cur.dx);
    174. //根据三角函数计算出偏移点相对于起点为原的坐标系的X的坐标
    175. double centerOffsetPointX = cos(Random().nextInt(2) == 1
    176. ? (45 * pi / 180 + radians2)
    177. : (45 * pi / 180 - radians2)) *
    178. sqrt(2) *
    179. r /
    180. 2;
    181. //根据三角函数计算出偏移点相对于起点为原的坐标系的Y的坐标
    182. double centerOffsetPointY = sin(Random().nextInt(2) == 1
    183. ? (45 * pi / 180 + radians2)
    184. : (45 * pi / 180 - radians2)) *
    185. sqrt(2) *
    186. r /
    187. 2;
    188. ///坐标系平移
    189. double moveX = centerOffsetPointX + cur.dx;
    190. double moveY = centerOffsetPointY + cur.dy;
    191. Path path2 = Path();
    192. path2.moveTo(cur.dx, cur.dy);
    193. path2.cubicTo(cur.dx, cur.dy, moveX, moveY, x1, y1);
    194. ///画动画点
    195. PathMetric? pathMetric2 = path2.computeMetric();
    196. if (pathMetric2 != null) {
    197. double length2 = pathMetric2.length;
    198. Tangent? tangent2 =
    199. pathMetric2.getTangentForOffset(length2 * mix);
    200. if (tangent2 != null) {
    201. Offset cur2 = tangent2.position;
    202. canvas.drawCircle(
    203. Offset(cur2.dx, cur2.dy),
    204. cr * (1 - mix).toDouble(),
    205. Paint()
    206. ..color = Colors.redAccent
    207. ..maskFilter =
    208. const MaskFilter.blur(BlurStyle.normal, 2));
    209. }
    210. }
    211. }
    212. }
    213. }
    214. }
    215. }
    216. ///线性拖尾
    217. _lineTrailingDraw(Canvas canvas, Path path, double r) {
    218. PathMetric? pathMetric1 = path.computeMetric();
    219. if (pathMetric1 != null) {
    220. int length1 = pathMetric1.length.toInt();
    221. for (int i = 0; i < length1.toInt(); i++) {
    222. Tangent? tangent1 = pathMetric1.getTangentForOffset(i.toDouble());
    223. double mix = i / length1.toInt();
    224. if (tangent1 != null) {
    225. Offset cur = tangent1.position;
    226. canvas.drawCircle(
    227. cur,
    228. r * mix,
    229. Paint()
    230. ..color = Colors.redAccent
    231. ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 2));
    232. }
    233. }
    234. }
    235. }
    236. }

    主要思路:

    主要是绘制多个点到一个点的路径,使用的是三点绘制贝塞尔曲线,利用坐标系与三角函数,计算出两个点的中间点直角偏两点一半的位置的坐标为贝塞尔控制点绘制二阶贝塞尔曲线,并获取路径,加上我们上一篇文章中的拖尾效果,有两种拖尾形式,一种是用的粒子,一种用的线性,粒子的比较耗性能,但是动画效果好,线性的不耗性能,动画没那么细腻,但是也能达到预效果。思路很简单,本文主要是起到抛砖引玉的效果,我也只实现了基本功能,还有那些文本绘制,点的样式绘制等,需要读者自己添砖加瓦。

  • 相关阅读:
    【二叉树-困难】124. 二叉树中的最大路径和
    C/C++超市收银系统
    《上海悠悠接口自动化平台》-5.测试计划与定时任务
    buildadmin+tp8表格操作(3)----表头上方按钮绑定事件处理,实现功能(选中或取消指定行)
    Notepad++ 和正则表达式 只保留自己想要的内容
    【七夕快乐篇】Nacos是如何实现服务注册功能的?
    快速创建软件安装包-ClickOnce
    设计原则之【里式替换原则】
    Nginx安装、配置
    C++终于解决dfs回溯法过程中的环形循环问题了,就是用visit数组记录所有走过的结点啊
  • 原文地址:https://blog.csdn.net/u012800952/article/details/133137483