flutter 实现旋转星球

news2024/12/30 3:40:13

先看效果

planet_widget.dart

import 'dart:math';
import 'package:flutter/material.dart';
import 'package:vector_math/vector_math_64.dart' show Vector3;
import 'package:flutter/gestures.dart';
import 'package:flutter/physics.dart';

class PlanetWidget extends StatefulWidget {
  const PlanetWidget({Key? key, required this.children, this.minRadius = 50})
      : super(key: key);

  @override
  _PlanetWidgetState createState() => _PlanetWidgetState();

  final List<Widget> children;
  final double minRadius;
}

class _PlanetWidgetState extends State<PlanetWidget>
    with TickerProviderStateMixin {
  late AnimationController animationController;

  /// 启动加载或者重新加载的时候用的Controller
  late AnimationController reloadAnimationController;

  double preAngle = 0.0;
  double _radius = -1.0;

  List<PlanetTagInfo>? childTagList = [];

  /// 当前操作的向量信息
  Vector3 currentOperateVector = Vector3(1.0, 0.0, 0.0);

  @override
  void initState() {
    super.initState();
    animationController =
        AnimationController(lowerBound: 0, upperBound: pi * 2, vsync: this);
    reloadAnimationController = AnimationController(
        lowerBound: 0,
        upperBound: 1,
        duration: Duration(milliseconds: 300),
        vsync: this);

    animationController.addListener(() {
      setState(() {
        calTagInfo(animationController.value - preAngle);
      });
    });
    reloadAnimationController.addListener(() {
      setState(() {});
    });

    // initData();
  }

  void initData() {
    childTagList = widget.children
        .map((e) => PlanetTagInfo(child: e, planetTagPos: Vector3.zero()))
        .toList();

    currentOperateVector = updateOperateVector(Offset(-1.0, 1.0));

    initTagInfo();

    WidgetsBinding.instance!.addPostFrameCallback((_) {
      reloadAnimationController.forward().then((value) => _reStartAnimation());
    });
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    if (widget.children.isNotEmpty) {
      initData();
    }
  }

  @override
  void didUpdateWidget(covariant PlanetWidget oldWidget) {
    if (oldWidget.children != this.widget.children) {
      if (widget.children.isNotEmpty) {
        animationController.reset();
        reloadAnimationController.reset();
        initData();
      }
    }
    super.didUpdateWidget(oldWidget);
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (BuildContext context, BoxConstraints constraints) {
        var radius = min(constraints.maxWidth, constraints.maxHeight) / 2.0;

        /// 太小就不显示了
        if (radius < widget.minRadius) {
          return SizedBox.shrink();
        }

        if (_radius != radius) {
          if (_radius == -1.0) {
            _radius = radius;
            initTagInfo();
          } else {
            _radius = radius;
            resizeTagInfo();
          }
        }

        final Map<Type, GestureRecognizerFactory> gestures =
            <Type, GestureRecognizerFactory>{};
        gestures[PanGestureRecognizer] =
            GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
          () => PanGestureRecognizer(debugOwner: this),
          (PanGestureRecognizer instance) {
            instance
              ..onDown = (detail) {
                if (animationController.isAnimating) {
                  _stopAnimation();
                }
              }
              ..onStart = (detail) {
                if (animationController.isAnimating) {
                  _stopAnimation();
                }
              }
              ..onUpdate = (detail) {
                if (detail.delta.dx == 0 && detail.delta.dy == 0) {
                  return;
                }
                double distance = sqrt(detail.delta.dx * detail.delta.dx +
                    detail.delta.dy * detail.delta.dy);
                setState(() {
                  currentOperateVector = updateOperateVector(detail.delta);
                  calTagInfo(distance / _radius);
                });
              }
              ..onEnd = (detail) {
                startFlingAnimation(detail);
              }
              ..onCancel = () {
                _reStartAnimation();
              }
              ..dragStartBehavior = DragStartBehavior.start
              ..gestureSettings =

                  /// 为了能竞争过 HorizontalDragGestureRecognizer ,不得不使用一些下作手段;
                  /// 比如说卷起来,判断阈值比 HorizontalDragGestureRecognizer 的阈值小;
                  /// PS :默认的PanGestureRecognizer 的判断阈值是 touchSlop * 2;
                  const DeviceGestureSettings(touchSlop: kTouchSlop / 4);
          },
        );

        gestures[TapGestureRecognizer] =
            GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
          () => TapGestureRecognizer(debugOwner: this),
          (TapGestureRecognizer instance) {
            instance
              ..onTapDown = (detail) {
                _stopAnimation();
              }
              ..onTapUp = (detail) {
                _reStartAnimation();
              };
          },
        );

        return RawGestureDetector(
          gestures: gestures,
          behavior: HitTestBehavior.translucent,
          excludeFromSemantics: false,
          child: Container(
            width: _radius * 2,
            height: _radius * 2,
            child: LayoutBuilder(
              builder: (BuildContext context, BoxConstraints constraints) {
                /// 要根据Z轴高度更新Stack中的叠放顺序;
                /// 要不然点击重叠部分的时候,可能点击事件并非最上面的处理;
                /// PS :实在不行搞个获取Z轴的Stack,修改hitTest让它遍历顺序根据Z轴来制定?
                childTagList?.sort((item1, item2) =>
                    item1.planetTagPos.z.compareTo(item2.planetTagPos.z));

                var itemOpacity =
                    ((_radius - widget.minRadius) / widget.minRadius);

                if (itemOpacity <= 0.1) {
                  return SizedBox.shrink();
                }

                return Opacity(
                  opacity: _radius >= widget.minRadius * 2 ? 1.0 : itemOpacity,
                  child: Stack(
                    alignment: Alignment.center,
                    children: childTagList
                            ?.map((e) => Transform(
                                  transform: calTransformByTagInfo(
                                      e, animationController.value),

                                  /// 聊胜于无的优化,如果基本看不到了,那没必要显示
                                  child: e.opacity >= 0.15
                                      ? Opacity(
                                          opacity: e.opacity,
                                          child: RepaintBoundary(
                                            child: e.child,
                                          ),
                                        )
                                      : SizedBox.shrink(),
                                ))
                            .toList() ??
                        [],
                  ),
                );
              },
            ),
          ),
        );
      },
    );
  }

  void _stopAnimation() {
    animationController.stop();
  }

  void _reStartAnimation() {
    animationController.value = preAngle;
    animationController.repeat(
        min: 0, max: pi * 2, period: Duration(seconds: 20));
  }

  void startFlingAnimation(DragEndDetails detail) {
    /// 计算手势要滑动多少距离
    var velocityPerDis = sqrt(pow(detail.velocity.pixelsPerSecond.dx, 2) +
        pow(detail.velocity.pixelsPerSecond.dy, 2));

    if (velocityPerDis < 5) {
      _reStartAnimation();
      return;
    }

    /// 距离处以周长就是变化的角度,最大一周
    var angle = min(
        2 * pi,
        animationController.value +
            velocityPerDis / (2 * pi * _radius) * (2 * pi));

    animationController
        .animateWith(SpringSimulation(
            SpringDescription.withDampingRatio(
              mass: 1.0,
              stiffness: 500.0,
            ),
            animationController.value,
            angle,
            1)
          ..tolerance = Tolerance(
            velocity: double.infinity,
            distance: 0.01,
          ))
        .then((value) => _reStartAnimation());
  }

  @override
  void dispose() {
    animationController.dispose();
    reloadAnimationController.dispose();
    super.dispose();
  }

  /// 设置Tag们的初始位置
  void initTagInfo() {
    final itemCount = childTagList?.length ?? 0;

    for (var index = 1; index < itemCount + 1; index++) {
      final phi = (acos(-1.0 + (2.0 * index - 1.0) / itemCount));
      final theta = sqrt(itemCount * pi) * phi;

      final x = _radius * cos(theta) * sin(phi);
      final y = _radius * sin(theta) * sin(phi);
      final z = _radius * cos(phi);

      var childItem = childTagList?[index - 1];
      childItem?.planetTagPos = Vector3(x, y, z);
      childItem?.currentAngle = phi;
      childItem?.radius = _radius;
    }
  }

  /// 重新根据当前的半径,修改大小
  void resizeTagInfo() {
    final itemCount = childTagList?.length ?? 0;

    for (var index = 0; index < itemCount; index++) {
      var childItem = childTagList![index];
      var pos = childItem.planetTagPos;
      pos.x = (_radius / childItem.radius) * pos.x;
      pos.y = (_radius / childItem.radius) * pos.y;
      pos.z = (_radius / childItem.radius) * pos.z;

      childItem.radius = _radius;
    }
  }

  /// 根据变化的角度计算最新位置
  void calTagInfo(double dAngle) {
    var currentAngle = preAngle + dAngle;

    final itemCount = childTagList?.length ?? 0;

    for (var index = 1; index < itemCount + 1; index++) {
      var childItem = childTagList![index - 1];

      var point = childItem.planetTagPos;

      double x = cos(dAngle) * point.x +
          (1 - cos(dAngle)) *
              (currentOperateVector.x * point.x +
                  currentOperateVector.y * point.y) *
              currentOperateVector.x +
          sin(dAngle) * (currentOperateVector.y * point.z);

      double y = cos(dAngle) * point.y +
          (1 - cos(dAngle)) *
              (currentOperateVector.x * point.x +
                  currentOperateVector.y * point.y) *
              currentOperateVector.y -
          sin(dAngle) * (currentOperateVector.x * point.z);

      double z = cos(dAngle) * point.z +
          sin(dAngle) *
              (currentOperateVector.x * point.y -
                  currentOperateVector.y * point.x);
      if (x.isNaN || y.isNaN || z.isNaN) {
        continue;
      }

      childItem.planetTagPos = Vector3(x, y, z);
      childItem.currentAngle = currentAngle;
    }

    if (animationController.isAnimating) {
      preAngle = currentAngle;
    }
  }

  Vector3 updateOperateVector(Offset operateOffset) {
    double x = -operateOffset.dy;
    double y = operateOffset.dx;
    double module = sqrt(x * x + y * y);
    return Vector3(x / module, y / module, 0.0);
  }

  Matrix4 calTransformByTagInfo(PlanetTagInfo tagInfo, double currentAngle) {
    var result = Matrix4.identity();
    result.translate(
        tagInfo.planetTagPos.x * reloadAnimationController.value,
        tagInfo.planetTagPos.y * reloadAnimationController.value,
        tagInfo.planetTagPos.z * reloadAnimationController.value);
    result.scale(tagInfo.scale);
    return result;
  }
}

class PlanetTagInfo {
  Vector3 planetTagPos = Vector3(0, 0, 0);
  Widget child;
  double currentAngle = 0;
  double radius = 0;

  PlanetTagInfo({required this.planetTagPos, required this.child});

  double get opacity {
    var result = 0.9 * ((radius + planetTagPos.z) / (radius * 2)) + 0.1;
    return result.isNaN || result.isNegative ? 0.0 : result;
  }

  double get scale {
    var result = ((radius + planetTagPos.z) / (radius * 2)) * 6 / 8 + 2 / 8;
    return result.isNaN || result.isNegative ? 0.0 : result;
  }
}

使用

children内为任意Widget 就是星球中个一个点

PlanetWidget(
            children: [
              Container(
                width: 80,
                height: 80,
                child: ClipRRect(
                  borderRadius: BorderRadius.circular(40),
                  child: Assets.images.head3.image(),
                ),
              ),
              Container(
                width: 80,
                height: 80,
                child: ClipRRect(
                  borderRadius: BorderRadius.circular(40),
                  child: Assets.images.head2.image(),
                ),
              ),
              Container(
                width: 80,
                height: 80,
                child: ClipRRect(
                  borderRadius: BorderRadius.circular(40),
                  child: Assets.images.head1.image(),
                ),
              ),
              Container(
                width: 80,
                height: 80,
                child: ClipRRect(
                  borderRadius: BorderRadius.circular(40),
                  child: Assets.images.head3.image(),
                ),
              ),
              Container(
                width: 80,
                height: 80,
                child: ClipRRect(
                  borderRadius: BorderRadius.circular(40),
                  child: Assets.images.head2.image(),
                ),
              ),
              Container(
                width: 80,
                height: 80,
                child: ClipRRect(
                  borderRadius: BorderRadius.circular(40),
                  child: Assets.images.head1.image(),
                ),
              ),
              Container(
                width: 80,
                height: 80,
                child: ClipRRect(
                  borderRadius: BorderRadius.circular(40),
                  child: Assets.images.head3.image(),
                ),
              ),
              Container(
                width: 80,
                height: 80,
                child: ClipRRect(
                  borderRadius: BorderRadius.circular(40),
                  child: Assets.images.head2.image(),
                ),
              ),
              Container(
                width: 80,
                height: 80,
                child: ClipRRect(
                  borderRadius: BorderRadius.circular(40),
                  child: Assets.images.head1.image(),
                ),
              ),
              Container(
                width: 80,
                height: 80,
                child: ClipRRect(
                  borderRadius: BorderRadius.circular(40),
                  child: Assets.images.head3.image(),
                ),
              ),
              Container(
                width: 80,
                height: 80,
                child: ClipRRect(
                  borderRadius: BorderRadius.circular(40),
                  child: Assets.images.head2.image(),
                ),
              ),
              Container(
                width: 80,
                height: 80,
                child: ClipRRect(
                  borderRadius: BorderRadius.circular(40),
                  child: Assets.images.head1.image(),
                ),
              ),
            ],
          ),

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

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

相关文章

Android14 - 绘制系统 - 概览

从Android 12开始&#xff0c;Android的绘制系统有结构性变化&#xff0c; 在绘制的生产消费者模式中&#xff0c;新增BLASTBufferQueue&#xff0c;客户端进程自行进行queue的生产和消费&#xff0c;随后通过Transation提交到SurfaceFlinger&#xff0c;如此可以使得各进程将缓…

Altium Designer 中键拖动,滚轮缩放,并修改缩放速度

我的版本是AD19&#xff0c;其他版本应该都一样。 滚轮缩放 首先&#xff0c;要用滚轮缩放&#xff0c;先要调整一下AD 设置&#xff0c;打开Preferences&#xff0c;在Mouse Wheel Configuration 里&#xff0c;把Zoom Main Window 后面Ctrl 上的对勾取消掉&#xff0c;再把…

翻译《The Old New Thing》- What‘s the deal with the EM_SETHILITE message?

Whats the deal with the EM_SETHILITE message? - The Old New Thing (microsoft.com)https://devblogs.microsoft.com/oldnewthing/20071025-00/?p24693 Raymond Chen 2007年10月25日 简要 文章讨论了EM_SETHILITE和EM_GETHILITE消息在文档中显示为“未实现”的原因。这些…

C语言 | Leetcode C语言题解之第112题路径总和

题目&#xff1a; 题解&#xff1a; bool hasPathSum(struct TreeNode *root, int sum) {if (root NULL) {return false;}if (root->left NULL && root->right NULL) {return sum root->val;}return hasPathSum(root->left, sum - root->val) ||ha…

与用户沟通获取需求的方法

1 访谈 访谈是最早开始使用的获取用户需求的技术&#xff0c;也是迄今为止仍然广泛使用的需求分析技术。 访谈有两种基本形式&#xff0c;分别是正式的和非正式的访谈。正式访谈时&#xff0c;系统分析员将提出一些事先准备好的具体问题&#xff0c;例如&#xff0…

C++_string简单源码剖析:模拟实现string

文章目录 &#x1f680;1.构造与析构函数&#x1f680;2.迭代器&#x1f680;3.获取&#x1f680; 4.内存修改&#x1f680;5. 插入&#x1f680;6. 删除&#x1f680;7. 查找&#x1f680;8. 交换swap&#x1f680;9. 截取substr&#x1f680;10. 比较符号重载&#x1f680;11…

pod install 报错 ‘SDK does not contain ‘libarclite‘ at the path...‘

报错内容&#xff1a; SDK does not contain ‘libarclite’ at the path ‘/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphoneos.a’; 这是报错已经很明确告诉我们&#xff0c;Xcode默认的工具链中缺少一个工具…

Linux Tcpdump抓包入门

Linux Tcpdump抓包入门 一、Tcpdump简介 tcpdump 是一个在Linux系统上用于网络分析和抓包的强大工具。它能够捕获网络数据包并提供详细的分析信息&#xff0c;有助于网络管理员和开发人员诊断网络问题和监控网络流量。 安装部署 # 在Debian/Ubuntu上安装 sudo apt-get install…

爬虫100个Python例子优化

今天看到一个Python 100例的在线资源,感觉每个都需要去点,太费时间了,于是,使用Python将数据爬取下来,方便查看。实际效果如下: 。。。。。。 用了13分钟,当然,这是优化后的效果,如果没有优化,需要的时间更长。 爬取url如下: https://www.runoob.com/python/pytho…

Pytorch深度学习实践笔记1

&#x1f3ac;个人简介&#xff1a;一个全栈工程师的升级之路&#xff01; &#x1f4cb;个人专栏&#xff1a;pytorch深度学习 &#x1f380;CSDN主页 发狂的小花 &#x1f304;人生秘诀&#xff1a;学习的本质就是极致重复! 《PyTorch深度学习实践》完结合集_哔哩哔哩_bilibi…

【数据结构与算法 刷题系列】移除链表元素

&#x1f493; 博客主页&#xff1a;倔强的石头的CSDN主页 &#x1f4dd;Gitee主页&#xff1a;倔强的石头的gitee主页 ⏩ 文章专栏&#xff1a;数据结构与算法刷题系列&#xff08;C语言&#xff09; 期待您的关注 目录 一、问题描述 二、解题思路 三、源代码实现 一、问题…

不靠后端,前端也能搞定接口!

嘿&#xff0c;前端开发达人们&#xff01;有个超酷的消息要告诉你们&#xff1a;MemFire Cloud来袭啦&#xff01;这个神奇的东东让你们不用依赖后端小伙伴们&#xff0c;也能妥妥地搞定 API 接口。是不是觉得有点不可思议&#xff1f;但是事实就是这样&#xff0c;让我们一起…

软件项目详细设计说明书实际项目参考(word原件下载及全套软件资料包)

系统详细设计说明书案例&#xff08;直接套用&#xff09; 1.系统总体设计 2.性能设计 3.系统功能模块详细设计 4.数据库设计 5.接口设计 6.系统出错处理设计 7.系统处理规定 软件开发全文档下载&#xff08;下面链接或者本文末个人名片直接获取)&#xff1a;软件开发全套资料-…

指纹识别概念解析

目录 1. 指纹是物证之首 1.1 起源于中国 1.2 发展于欧洲 1.3 流行于全世界 2. 指纹图像 3. 指纹特征 4. 指纹注册 5. 指纹验证 6. 指纹辨识 1. 指纹是物证之首 指纹识别技术起源于中国、发展于欧洲、流行于全世界。自20世纪以来&#xff0c;指纹在侦破刑事案件、解决诉…

06.逻辑回归

文章目录 Generate Model优化边界为线性证明损失函数比较逻辑回归不能用均方误差Generative v.s. DiscriminativeMulti-class Classification逻辑回归的限制自己找线性变换 Generate Model 假设样本符合高斯分布 即找 μ \mu μ和 σ \sigma σ 优化 共用 Σ \Sigma Σ减少…

Vue3实战笔记(45)—VUE3封装一些echarts常用的组件,附源码

文章目录 前言一、柱状图框选二、折线图堆叠总结 前言 日前使用hooks的方式封装组件&#xff0c;在我使用复杂的图标时候遇到了些问题&#xff0c;预想在onMounted中初始化echarts&#xff0c;在使用hooks的时候&#xff0c;组件没有渲染完&#xff0c;使用实例会出现各种各样…

MongoDB(介绍,安装,操作,Springboot整合MonggoDB)

目录 MongoDB 1 MongoDB介绍 MongoDB简介 MongoDB的特点 MongoDB使用场景 小结 2 MongoDB安装 安装MongoDB 连接MongoDB MongoDB逻辑结构 MongoDB数据类型 小结 3 MongoDB操作 操作库和集合 操作文档-增删改 操作文档-查询 MongoDB索引 小结 4 SpringBoot整合…

java -spring 15 配置类 ConfigurationClassPostProcessor

01Spring中定义的配置类 ConfigurationClassPostProcessor是一个BeanFactory的后置处理器&#xff0c;因此它的主要功能是参与BeanFactory的建造&#xff0c;在这个类中&#xff0c;会解析加了Configuration的配置类&#xff0c;还会解析ComponentScan、ComponentScans注解扫描…

C语言笔记21 •模拟atoi函数•

1.atoi的使用 atoi是将字符串转化为int类型数字的一个库函数 int main() { char str[] "123568"; int a; a atoi(str); /*将字符串转化为int型的数字*/ printf("%d\n", a); } 2.模拟atoi函数 #define _CRT_SECURE_NO_WARNINGS…

Mac JDK和SDK环境变量配置

一、Java JDK配置 1.下载并安装Java jdk1.8及以上&#xff0c;这个可以在网上自行搜索下载&#xff0c;这里不在详细描述 2.如果不知道JAVA_HOME的安装路径&#xff0c;可以输入命令查看&#xff1a;/usr/libexec/java_home -V &#xff0c;如图 3.在终端输入命令&#xff1…