当前位置: 首页 > news >正文

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(),),),],),

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 零基础HTML教程(33)--HTML5表单新功能
  • 【LVGL_Linux安装NXP的Gui-Guider】
  • android ndc firewall 命令type 黑名单 白名单差异
  • make是什么
  • VBA即用型代码手册:删除Excel中空白行Delete Blank Rows in Excel
  • Android Studio 问题集锦
  • Java JUnit单元测试
  • Spring MVC/Web
  • 人才测评的应用:人才选拔,岗位晋升,面试招聘测评
  • 开源网页视频会议,WebRTC音视频功能比较
  • kafka 消费模式基础架构
  • Flutter 中的 ExpansionTile 小部件:全面指南
  • BWVS 靶场测试
  • CSS布局和定位应用方案
  • 网络编程-TCP并发服务器-多点通信-域套接字
  • Android开发 - 掌握ConstraintLayout(四)创建基本约束
  • Babel配置的不完全指南
  • css属性的继承、初识值、计算值、当前值、应用值
  • Dubbo 整合 Pinpoint 做分布式服务请求跟踪
  • es的写入过程
  • Java 实战开发之spring、logback配置及chrome开发神器(六)
  • JDK 6和JDK 7中的substring()方法
  • Laravel 实践之路: 数据库迁移与数据填充
  • vue从创建到完整的饿了么(18)购物车详细信息的展示与删除
  • 不发不行!Netty集成文字图片聊天室外加TCP/IP软硬件通信
  • 搭建gitbook 和 访问权限认证
  • 基于 Ueditor 的现代化编辑器 Neditor 1.5.4 发布
  • 如何用vue打造一个移动端音乐播放器
  • 入门到放弃node系列之Hello Word篇
  • 数组大概知多少
  • 我从编程教室毕业
  • 我感觉这是史上最牛的防sql注入方法类
  • 一起参Ember.js讨论、问答社区。
  • media数据库操作,可以进行增删改查,实现回收站,隐私照片功能 SharedPreferences存储地址:
  • ionic异常记录
  • 不要一棍子打翻所有黑盒模型,其实可以让它们发挥作用 ...
  • ​​​​​​​​​​​​​​Γ函数
  • #单片机(TB6600驱动42步进电机)
  • ${ }的特别功能
  • $GOPATH/go.mod exists but should not goland
  • (2)空速传感器
  • (6)【Python/机器学习/深度学习】Machine-Learning模型与算法应用—使用Adaboost建模及工作环境下的数据分析整理
  • (ISPRS,2023)深度语义-视觉对齐用于zero-shot遥感图像场景分类
  • (Matalb分类预测)GA-BP遗传算法优化BP神经网络的多维分类预测
  • (poj1.3.2)1791(构造法模拟)
  • (pytorch进阶之路)CLIP模型 实现图像多模态检索任务
  • (ros//EnvironmentVariables)ros环境变量
  • (带教程)商业版SEO关键词按天计费系统:关键词排名优化、代理服务、手机自适应及搭建教程
  • (二)Eureka服务搭建,服务注册,服务发现
  • (非本人原创)史记·柴静列传(r4笔记第65天)
  • (附源码)ssm高校运动会管理系统 毕业设计 020419
  • (回溯) LeetCode 131. 分割回文串
  • (排序详解之 堆排序)
  • (七)Appdesigner-初步入门及常用组件的使用方法说明
  • (十八)用JAVA编写MP3解码器——迷你播放器