flutter 实现旋转星球

先看效果

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;/// 启动加载或者重新加载的时候用的Controllerlate AnimationController reloadAnimationController;double preAngle = 0.0;double _radius = -1.0;List<PlanetTagInfo>? childTagList = [];/// 当前操作的向量信息Vector3 currentOperateVector = Vector3(1.0, 0.0, 0.0);@overridevoid 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());});}@overridevoid didChangeDependencies() {super.didChangeDependencies();if (widget.children.isNotEmpty) {initData();}}@overridevoid didUpdateWidget(covariant PlanetWidget oldWidget) {if (oldWidget.children != this.widget.children) {if (widget.children.isNotEmpty) {animationController.reset();reloadAnimationController.reset();initData();}}super.didUpdateWidget(oldWidget);}@overrideWidget 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());}@overridevoid 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.mzph.cn/bicheng/15523.shtml

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

相关文章

echarts-树图、关系图、桑基图、日历图

树图 树图主要用来表达关系结构。 树图的端点也收symbol的调节 树图的特有属性&#xff1a; 树图的方向&#xff1a; layout、orient子节点收起展开&#xff1a;initialTreeDepth、expandAndCollapse叶子节点设置&#xff1a; leaves操作设置&#xff1a;roam线条&#xff1a…

2、xss-labs之level2

1、打开页面 2、传入xss代码 payload&#xff1a;<script>alert(xss)</script>&#xff0c;发现返回<script>alert(xss)</script> 3、分析原因 打开f12&#xff0c;没什么发现 看后端源码&#xff0c;在这form表单通过get获取keyword的值赋给$str&am…

【Vue】input框自动聚焦且输入验证码后跳至下一位

场景&#xff1a;PC端 样式&#xff1a; <div class"verification-code-input"><input v-model"code[index]" v-for"(_, index) in 5" :key"index" type"text" maxlength"1" input"handleInput(i…

渲染管线——应用阶段

知识必备——CPU和GPU 应用阶段都做了什么 应用阶段为渲染准备了什么 1.把不可见的数据剔除 2.准备好模型相关数据&#xff08;顶点、法线、切线、贴图、着色器等等&#xff09; 3.将数据加载到显存中 4.设置渲染状态&#xff08;设置网格需要使用哪个着色器、材质、光源属性等…

【机器学习】机器学习与大型预训练模型的前沿探索:跨模态理解与生成的新纪元

&#x1f512;文章目录&#xff1a; &#x1f4a5;1.引言 ☔2.跨模态理解与生成技术概述 &#x1f6b2;3.大型预训练模型在跨模态理解与生成中的应用 &#x1f6f4;4.前沿探索与挑战并存 &#x1f44a;5.未来趋势与展望 &#x1f4a5;1.引言 近年来&#xff0c;机器学习领…

著名书法家王杰宝做客央视频《笔墨写人生》艺坛人物经典访谈节目

印象网北京讯&#xff08;张春兄、冯爱云&#xff09;展示艺术风采&#xff0c;构建时代精神。5月25日&#xff0c;著名书法家、羲之文化传承人王杰宝&#xff0c;做客央视频《笔墨写人生》艺坛人物经典访谈节目&#xff0c;与中央电视台纪录频道主持人姚文倩一起&#xff0c;分…

SpringBoot 集成 ChatGPT(附实战源码)

建项目 项目结构 application.properties openai.chatgtp.modelgpt-3.5-turbo openai.chatgtp.api.keyREPLACE_WITH_YOUR_API_KEY openai.chatgtp.api.urlhttps://api.openai.com/v1/chat/completionsopenai.chatgtp.max-completions1 openai.chatgtp.temperature0 openai.cha…

全局平均池化笔记

全局平均池化&#xff08;Global Average Pooling, GAP&#xff09;是一种用于卷积神经网络&#xff08;CNN&#xff09;中的池化操作&#xff0c;其主要作用和优点包括&#xff1a; 减少参数数量&#xff1a;全局平均池化层将每个特征图通过取其所有元素的平均值&#xff0c;压…

ubuntu安装yum方法【最新可用】

一、安装命令 在根目录&#xff08;root&#xff09;下执行 sudo apt-get install build-essential sudo apt-get install yum二、出错处理 1、E: Package yum has no installation candidate 解决&#xff1a;更换镜像源&#xff0c;找到自己的系统版本用vim进行更换&#xff…

GmSSL3.X编译iOS和Android动态库

一、环境准备 我用的Mac电脑编译&#xff0c;Xcode版本15.2&#xff0c;安卓的NDK版本是android-ndk-r21e。 1.1、下载国密源码 下载最新的国密SDK源码到本地。 1.2、安装Xcode 前往Mac系统的AppStore下载安装最新Xcode。 1.3、安卓NDK下载 下载NDK到本地&#xff0c;选…

Protobuf - 语法、字段使用规则、注意事项

目录 前言 一、Protobuf 基本语法 1.1、Protoc 版本 1.2、文件格式配置 1.3、消息字段规则 1.3.1、字段数据类型 1.3.2、字段修饰规则 1.3.3、消息类型定义 1.3.4、enum 类型 1.3.5、Any 类型 1.3.6、oneof 类型 1.3.7、map 类型 1.3.8、默认值 1.3.9、更新消息…

css设置文字在固定宽度中等距分开(仅限于单行文本)

一、要实现的效果&#xff1a; 二、代码 要在CSS中设置文本在一个固定宽度的容器中等距分开&#xff0c; 可以使用text-align: justify;属性&#xff0c;它可以让文本两端对齐&#xff0c;看起来就像是等距分开的。 但是要注意&#xff0c;单独使用text-align:justify;只能对单…

【Qt】Qt多元素控件深入解析与实战应用:列表(QListWidget)、表格(QTableWidget)与树形(QTreeWidget)结构

文章目录 前言&#xff1a;Qt中多元素控件&#xff1a;1. List Widget1.1. 代码示例: 使用 ListWidget 2.Table Widget2.1. 代码示例: 使用 QTableWidget 3. Tree Widget3.1. 代码示例: 使用 QTreeWidget 总结&#xff1a; 前言&#xff1a; 在Qt框架中&#xff0c;用户界面的…

C语言内存函数超详细讲解

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 C语言内存函数超详细讲解 收录于专栏【C语言学习】 本专栏旨在分享学习C语言学习的一点学习笔记&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目录 1. m…

C++面向对象程序设计-北京大学-郭炜【课程笔记(十一)】

C面向对象程序设计-北京大学-郭炜【课程笔记&#xff08;十一&#xff09;】 1、string&#xff08;重要知识点&#xff09;1.2、string的赋值和链接1.3、比较string1.4、子串1.5、交换string1.6、寻找string中的字符1.7、删除string中的字符1.8、替换string中的字符1.9、在str…

结构体;结构成员访问操作符

结构体&#xff1a; 虽然c语言已经提供了内置类型&#xff0c;比如&#xff1a;char、short、int、long等&#xff0c;但还是不够用&#xff0c;就好比我描述一个人&#xff0c;我需要描述他的身高&#xff0c;体重&#xff0c;年龄&#xff0c;名字等信息&#xff0c…

微软密谋超级AI大模型!LangChain带你轻松玩转大模型开发

此前&#xff0c;据相关媒体报道&#xff0c;微软正在研发一款名为MAI-1的最新AI大模型&#xff0c;其参数规模或将达5000亿以上&#xff0c;远超此前微软推出的相关开源模型&#xff0c;其性能或能与谷歌的Gemini 1.5、Anthropic的Claude 3和OpenAI的GPT-4等知名大模型相匹敌。…

Linux文本处理三剑客(详解)

一、文本三剑客是什么&#xff1f; 1. 对于接触过Linux操作系统的人来说&#xff0c;应该都听过说Linux中的文本三剑客吧&#xff0c;即awk、grep、sed&#xff0c;也是必须要掌握的Linux命令之一&#xff0c;三者都是用来处理文本的&#xff0c;但侧重点各不相同&#xff0c;a…

Sam Altman微软Build 2024最新演讲:AI可能是下一个移动互联网

大家好&#xff0c;我是木易&#xff0c;一个持续关注AI领域的互联网技术产品经理&#xff0c;国内Top2本科&#xff0c;美国Top10 CS研究生&#xff0c;MBA。我坚信AI是普通人变强的“外挂”&#xff0c;所以创建了“AI信息Gap”这个公众号&#xff0c;专注于分享AI全维度知识…

【C++11】lambda匿名函数和包装器

目录 一&#xff0c;lambda匿名函数 1-1&#xff0c;lambda的引入 1-2&#xff0c;lambda表达式书写格式 1-3&#xff0c;lambda函数的名称 1-4&#xff0c;lambda捕获列表的使用 1-5&#xff0c;函数对象与lambda表达式 二&#xff0c;包装器 2-1&#xff0c;function…