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.pswp.cn/bicheng/15523.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/15523.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/15523.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

echarts-樹圖、關系圖、桑基圖、日歷圖

樹圖 樹圖主要用來表達關系結構。 樹圖的端點也收symbol的調節 樹圖的特有屬性&#xff1a; 樹圖的方向&#xff1a; layout、orient子節點收起展開&#xff1a;initialTreeDepth、expandAndCollapse葉子節點設置&#xff1a; leaves操作設置&#xff1a;roam線條&#xff1a…

告別 Dart 中的 Future.wait([])

作為 Dart 開發人員&#xff0c;我們對異步編程和 Futures 的強大功能并不陌生。過去&#xff0c;當我們需要同時等待多個 future 時&#xff0c;我們依賴 Future.wait([]) 方法&#xff0c;該方法返回一個 List<T>。然而&#xff0c;這種方法有一個顯著的缺點&#xff1…

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…

跑大模型的經驗

LLama2: 1. 使用torchrun來跑&#xff1a; torchrun --nproc_per_node 1 example_text_completion.py \--ckpt_dir llama-2-7b/ \--tokenizer_path tokenizer.model \--max_seq_len 128 --max_batch_size 4 關于集群分布式torchrun命令踩坑記錄&#xff08;自用&#xff09;…

【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;設置網格需要使用哪個著色器、材質、光源屬性等…

說些什么好呢

大一&#xff1a;提前學C和C。學完語法去洛谷或者Acwing二選一&#xff0c;刷300道左右題目。主要培養編程思維&#xff0c;讓自己的邏輯能夠通過代碼實現出來。 現在對算法有點感興趣但是沒有天賦&#xff0c;打不了acm&#xff0c;為就業做準備咯。 大二(算法競賽)&#xff1…

常用損失函數學習

損失函數&#xff08;Loss Function&#xff09;&#xff0c;在機器學習和統計學中&#xff0c;是用來量化模型預測輸出與真實結果之間差異的函數。簡而言之&#xff0c;損失函數衡量了模型預測的好壞&#xff0c;目標是通過最小化這個函數來優化模型參數&#xff0c;從而提高預…

簡述js的事件循環以及宏任務和微任務

前言 在JavaScript中&#xff0c;任務被分為同步任務和異步任務。 同步任務&#xff1a;這些任務在主線程上順序執行&#xff0c;不會進入任務隊列&#xff0c;而是直接在主線程上排隊等待執行。每個同步任務都會阻塞后續任務的執行&#xff0c;直到它自身完成。常見的同步任…

【機器學習】機器學習與大型預訓練模型的前沿探索:跨模態理解與生成的新紀元

&#x1f512;文章目錄&#xff1a; &#x1f4a5;1.引言 ?2.跨模態理解與生成技術概述 &#x1f6b2;3.大型預訓練模型在跨模態理解與生成中的應用 &#x1f6f4;4.前沿探索與挑戰并存 &#x1f44a;5.未來趨勢與展望 &#x1f4a5;1.引言 近年來&#xff0c;機器學習領…

著名書法家王杰寶做客央視頻《筆墨寫人生》藝壇人物經典訪談節目

印象網北京訊&#xff08;張春兄、馮愛云&#xff09;展示藝術風采&#xff0c;構建時代精神。5月25日&#xff0c;著名書法家、羲之文化傳承人王杰寶&#xff0c;做客央視頻《筆墨寫人生》藝壇人物經典訪談節目&#xff0c;與中央電視臺紀錄頻道主持人姚文倩一起&#xff0c;分…

MyBatis 中的動態 SQL 的相關使用方法(Javaee/MyBatis)

MyBatis 的動態 SQL 是一種強大的特性&#xff0c;它可以讓你在 XML 映射文件內&#xff0c;根據不同的條件編寫不同的 SQL 語句。MyBatis 動態 SQL 主要元素有&#xff1a; <if>: 根據提供的條件來動態拼接 SQL。 接口定義 Integer insertUserByCondition(UserInfo u…

c++ list容器

std::list 是 C 標準庫中的一個雙向鏈表容器。與 std::vector&#xff08;動態數組&#xff09;和 std::deque&#xff08;雙端隊列&#xff09;不同&#xff0c;std::list 的元素在內存中不是連續存儲的&#xff0c;而是分散存儲并通過節點進行連接。這使得 std::list 在插入和…

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…

make是什么

make是什么工具 make是一個自動化編譯工具,它本身并沒有編譯和鏈接的功能,而是用類似于批處理的方式——通過makefile文件中指示的依賴關系,調用makefile文件中使用的命令來完成編譯和鏈接的。makefile文件中記錄了源代碼文件之間的依賴關系,并說明了如何編譯各個源代碼文…

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;只能對單…