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

news2025/1/12 22:48:07

要求效果:

实现的效果:

代码:

选择点的界面:

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;
  }
}

演示界面:

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控件:

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));
        }
      }
    }
  }
}

主要思路:

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1030145.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Winserver安装Linux虚拟机执行java程序踩坑

前言&#xff1a; “好久没有更新文章了&#xff0c;最近太忙了&#xff01;”一个特别朴实无华的小马哥说到。 “小马蝈蝈&#xff0c;那你现在更新文章了&#xff0c;是不是很闲啊&#xff0c;来帮我....” 耳畔听到一个妹子的声音。咳咳咳~~此处省略一万字&#xff0c;文末也…

WebGL 用鼠标控制物体旋转

目录 鼠标控制物体旋转 如何实现物体旋转 示例程序&#xff08;RotateObject.js&#xff09; 代码详解 示例效果 鼠标控制物体旋转 有时候&#xff0c;WebGL程序需要让用户通过鼠标操作三维物体。这一节来分析示例程序RotateObject&#xff0c;该程序允许用户通过拖动&…

数据通信——传输层TCP(超时时间选择)

引言 TCP每一次发送报文段&#xff0c;就会对这个报文段设置一次计时器。如果时间到了却没有收到确认报文&#xff0c;那么就要重传该报文。 这个之前在TCP传输的机制中提到过&#xff0c;这个章节就来研究一下超时时间问题。 关于加权的概念 有必要提及一下加权的概念&#x…

天地一体化指挥!平战结合的应急感知云来了

面向智慧应急数字化转型需求&#xff0c;天翼物联基于感知云平台创新能力&#xff0c;为客户提供泛协议接入、感知云应急平台、应急感知数据治理、决策处置大屏等在内的应急感知云服务&#xff0c;构建应急感知神经系统新型数字化底座&#xff0c;实现应急感知、预警、决策、处…

程序员必须掌握的算法系列之贪心算法

一&#xff1a;引言 在计算机科学中&#xff0c;贪心算法&#xff08;Greedy Algorithm&#xff09;是一种基于贪心策略的算法思想&#xff0c;它在每一步选择中都采取当前状态下最优的选择&#xff0c;以希望最终能够得到全局最优解。贪心算法通常可以在较短的时间内找到问题…

springcloud3 分布式事务实现逻辑思想2

一 分布式事务逻辑 1.1 CAP理论 CAP原则又称CAP定理&#xff0c;指的是在一个分布式系统中&#xff0c;Consistency&#xff08;一致性&#xff09;、 Availability&#xff08;可用性&#xff09;、Partition tolerance&#xff08;分区容错性&#xff09;这3个基本需求&…

SPI在Java中的实现与应用 | 京东物流技术团队

1 SPI的概念 API API在我们日常开发工作中是比较直观可以看到的&#xff0c;比如在 Spring 项目中&#xff0c;我们通常习惯在写 service 层代码前&#xff0c;添加一个接口层&#xff0c;对于 service 的调用一般也都是基于接口操作&#xff0c;通过依赖注入&#xff0c;可以…

【深度学习实验】前馈神经网络(六):自动求导

目录 一、实验介绍 二、实验环境 1. 配置虚拟环境 2. 库版本介绍 三、实验内容 0. 导入必要的工具包 1. 标量求导 2. 矩阵求导 3. 计算图 一、实验介绍 PyTorch提供了自动求导机制&#xff0c;它是PyTorch的核心功能之一&#xff0c;用于计算梯度并进行反向传播。自动求…

C++流插入和流提取的重载!

C作为C语言的衍生&#xff0c;其弥补了C语言中的很多不足&#xff0c;也对C语言进行了一定的优化&#xff01;今日就来讲解一下C中输入/出流相关的知识&#xff01;以及对输入/出的重载&#xff01;&#xff0c;希望读完本篇文章&#xff0c;能让读者们对C中输入/出流有更深一步…

Java之IO概述以及

1.1 什么是IO 生活中&#xff0c;你肯定经历过这样的场景。当你编辑一个文本文件&#xff0c;忘记了ctrls &#xff0c;可能文件就白白编辑了。当你电脑上插入一个U盘&#xff0c;可以把一个视频&#xff0c;拷贝到你的电脑硬盘里。那么数据都是在哪些设备上的呢&#xff1f;键…

散列(哈希)查找的定义,常见的散列函数设计以及处理哈希冲突方法

1.散列表 1.散列表的定义 散列表(Hash Table)&#xff0c;又称哈希表。 是一种数据结构&#xff0c;特点是:数据元素的关键字与其存储地址直接相关。 特点&#xff1a; 若不同的关键字通过散列函数映射到同一个值&#xff0c;则称它们为“同义词”。通过散列函数确定的位置…

Maven 设置环境变量(Windows、Linux)

文章目录 Windows 配置 Maven 环境变量Linux 配置 Maven 环境变量 如果想在任意路径下都能通过 mvn 命令运行 Maven 程序&#xff0c;就需要将 Maven 程序路径设置到环境变量中&#xff0c; 否则使用 mvn 命令时每次都要加上 Maven 程序的全路径 核心就一句话&#xff0c;把 M…

坚鹏:中国邮政储蓄银行金融科技前沿技术发展与应用场景第4期

中国邮政储蓄银行金融科技前沿技术发展与应用场景第4期培训圆满结束 中国邮政储蓄银行拥有优良的资产质量和显著的成长潜力&#xff0c;是中国领先的大型零售银行。2016年9月在香港联交所挂牌上市&#xff0c;2019年12月在上交所挂牌上市。中国邮政储蓄银行拥有近4万个营业网点…

学习记忆——英语篇——右脑记忆单词

文章目录 英语字母形象起源右脑记忆单词的原则四大步骤第一步&#xff1a;摄取信息第二步&#xff1a;处理信息第三步&#xff1a;储存信息第四步&#xff1a;提取信息 训练例子字母形象训练 右脑记忆单词5大方法字源法编码法字母编码法字母组合编码法 拼音法全拼法拼音组合 熟…

springcloud3 分布式事务解决方案seata之XA模式4

一 seata的模式 1.1 seata的几种模式比较 Seata基于上述架构提供了四种不同的分布式事务解决方案&#xff1a; XA模式&#xff1a;强一致性分阶段事务模式&#xff0c;牺牲了一定的可用性&#xff0c;无业务侵入 TCC模式&#xff1a;最终一致的分阶段事务模式&#xff0c;有…

操作系统:中断和异常

1.中断的作用 CPU上会运行两种程序&#xff0c;一种是操作系统内核程序&#xff08;是整个系统的管理者&#xff09;&#xff0c;一种是应用程序。 1.中断的特点 在合适的情况下&#xff0c;操作系统内核会把CPU的使用权主动让给应用程序。“中断”是让操作系统内核夺回CPu使…

java智慧园区系统源码 智慧园区小程序源码

java智慧园区系统源码 智慧园区小程序源码 技术框架&#xff1a; 核心框架&#xff1a;Spring Boot 2.4.0 安全框架&#xff1a;JwtPermission 3.1.1 前端&#xff1a;Ant Design Vue 1.6.2 持久层框架&#xff1a;MyBatis-Plus 3.4.1 关系型数据库: Mysql 8.0.22 数据库…

【开发篇】一、热部署

文章目录 1、手工启动热部署2、自动启动热部署3、热部署范围配置4、关闭热部署功能 1、手工启动热部署 日常开发与调试&#xff0c;改几行代码想看效果就得手动点重启&#xff0c;很繁琐&#xff0c;接下来考虑启动热部署。首先引入springboot开发者工具&#xff1a; <dep…

找视频背景音乐素材,就上这6个网站。

找背景音乐、BGM、音效素材&#xff0c;就上这6个网站&#xff0c;国内外都有&#xff0c;免费下载&#xff0c;还可以商用&#xff0c;建议收藏起来~ 1、菜鸟图库 https://www.sucai999.com/audio.html?vNTYwNDUx 菜鸟图库是一个综合性素材网站&#xff0c;站内涵盖设计、图…

ceph分布式存储部署

一、概述 是一个统一的分布式存储系统&#xff0c;设计初衷是提供较好的性能、可靠性和可扩展性。 特点 1、统一存储 虽然 ceph 底层是一个分布式文件系统&#xff0c;但由于在上层开发了支持对象和块的接口。所以在开源存储软件中&#xff0c;能够一统江湖。至于能不能千秋万…