news 2026/2/25 14:14:22

Flutter for OpenHarmony 动效实战:打造一个会“跳”的幸运骰子应用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flutter for OpenHarmony 动效实战:打造一个会“跳”的幸运骰子应用

Flutter for OpenHarmony 动效实战:打造一个会“跳”的幸运骰子应用

在游戏、决策辅助甚至冥想练习中,掷骰子这一古老行为因其随机性与仪式感而历久弥新。而在移动应用时代,如何将物理世界的“摇、掷、滚、停”转化为数字屏幕上的沉浸式体验?答案在于——精细的动画设计

🌐 加入社区 欢迎加入开源鸿蒙跨平台开发者社区,获取最新资源与技术支持: 👉开源鸿蒙跨平台开发者社区


完整效果

一、核心体验:让骰子“活”起来

该应用的核心亮点在于其双重动画系统

💡 这不是简单的“转圈”,而是对真实物理过程的抽象与艺术化再现。

当用户点击骰子或按钮,骰子会:

  1. 快速旋转多圈(12π 弧度,即6整圈);
  2. 伴随不规则缩放:先放大(撞击地面)、再压缩(反弹)、最后轻微回弹至原尺寸;
  3. 动画结束瞬间,显示一个1~6之间的随机点数。

整个过程耗时1.2秒,节奏紧凑而不失趣味,完美复刻了“掷骰—滚动—停稳”的心理预期。


二、动画系统详解

1. 旋转动画:CurvedAnimation+fastLinearToSlowEaseIn

_rotationAnimation=Tween<double>(begin:0,end:12*pi).animate(CurvedAnimation(parent:_rollController,curve:Curves.fastLinearToSlowEaseIn,),);

2. 弹跳动画:TweenSequence模拟物理回弹

_bounceAnimation=TweenSequence([TweenSequenceItem(tween:Tween(begin:1.0,end:1.2),weight:30),// 落地冲击(放大)TweenSequenceItem(tween:Tween(begin:1.2,end:0.9),weight:40),// 主要压缩(缩小)TweenSequenceItem(tween:Tween(begin:0.9,end:1.05),weight:20),// 第一次回弹TweenSequenceItem(tween:Tween(begin:1.05,end:1.0),weight:10),// 微调归位]).animate(CurvedAnimation(parent:_rollController,curve:Curves.bounceOut));

🎯设计哲学:动画不仅是装饰,更是传达状态与反馈的媒介


三、UI/UX 设计细节

1. 拟物化骰子设计

2. 智能状态反馈

状态视觉表现
空闲显示上次结果 + “轻触开始”提示
滚动中骰子变为“?” + 文字提示“掷出中…” + FAB 显示加载圈
完成显示新点数 + “上次结果: X • N分钟前”

3. 顶部计数器

4. 底部知识彩蛋


四、交互逻辑与状态管理

核心状态变量

int _currentValue=1;// 当前显示点数bool _isRolling=false;// 是否正在动画中int _rollCount=0;// 今日摇动次数DateTime?_lastRollTime;// 上次摇动时间

关键方法

健壮性:通过_isRolling锁防止用户狂点导致状态错乱。


五、技术亮点总结

技术点应用场景价值
with TickerProviderStateMixin提供 vsync防止后台动画消耗资源
AnimatedBuilder驱动 Transform高效重建局部 UI
Transform.rotate+Transform.scale复合动画实现旋转+缩放同步
TweenSequence多阶段动画精细控制弹跳节奏
CurvedAnimation非线性缓动增强物理真实感
BoxShadow+Border拟物设计提升视觉质感

六、扩展与应用场景

可扩展方向

应用场景


七、结语:小应用,大匠心

这个看似简单的骰子应用,实则凝聚了动画设计、用户体验、状态管理的多重考量。它证明了:即使是最基础的功能,只要注入对细节的关注与对用户心理的理解,就能创造出令人愉悦的数字体验。

正如骰子本身所象征的——在确定的规则中拥抱不确定性,开发者也应在严谨的代码框架下,大胆探索动效与交互的可能性。而这颗会跳的紫色骰子,正是 Flutter 强大表现力的最佳注脚。

完整代码

import'dart:math';import'package:flutter/material.dart';voidmain(){runApp(const DiceRollerApp());}class DiceRollerApp extends StatelessWidget{const DiceRollerApp({super.key});@override Widget build(BuildContext context){returnMaterialApp(debugShowCheckedModeBanner: false, title:'🎲 摇骰子', theme: ThemeData(brightness: Brightness.light, primarySwatch: Colors.deepPurple, scaffoldBackgroundColor: const Color(0xFFF5F0FF), appBarTheme: const AppBarTheme(backgroundColor: Colors.deepPurple, foregroundColor: Colors.white, elevation:0,), floatingActionButtonTheme: const FloatingActionButtonThemeData(backgroundColor: Colors.deepPurple, foregroundColor: Colors.white, elevation:8,),), home: const DiceRollerScreen(),);}}class DiceRollerScreen extends StatefulWidget{const DiceRollerScreen({super.key});@override State<DiceRollerScreen>createState()=>_DiceRollerScreenState();}class _DiceRollerScreenState extends State<DiceRollerScreen>with TickerProviderStateMixin{late AnimationController _rollController;late Animation<double>_rotationAnimation;late Animation<double>_bounceAnimation;int _currentValue=1;bool _isRolling=false;final Random _random=Random();int _rollCount=0;DateTime? _lastRollTime;@override voidinitState(){super.initState();_rollController=AnimationController(duration: const Duration(milliseconds:1200), vsync: this,)..addStatusListener((status){ if(status==AnimationStatus.completed){ _finalizeRoll();} });_rotationAnimation=Tween<double>(begin:0,end:12*pi).animate(CurvedAnimation(parent:_rollController,curve:Curves.fastLinearToSlowEaseIn),);_bounceAnimation=TweenSequence([ TweenSequenceItem(tween:Tween<double>(begin:1.0,end:1.2),weight:30),TweenSequenceItem(tween:Tween<double>(begin:1.2,end:0.9),weight:40),TweenSequenceItem(tween:Tween<double>(begin:0.9,end:1.05),weight:20),TweenSequenceItem(tween:Tween<double>(begin:1.05,end:1.0),weight:10),]).animate(CurvedAnimation(parent:_rollController,curve:Curves.bounceOut));}void_finalizeRoll(){setState((){ _isRolling=false;_currentValue=_random.nextInt(6)+1;_rollCount++;_lastRollTime=DateTime.now();});} void _rollDice(){ if(_isRolling)return;setState((){ _isRolling=true;});//重置并启动动画 _rollController.reset();_rollController.forward();} @override void dispose(){ _rollController.dispose();super.dispose();} @override Widget build(BuildContext context){ return Scaffold(appBar:AppBar(title:const Text('幸运骰子',style:TextStyle(fontSize:22,fontWeight:FontWeight.bold),),centerTitle:true,actions:[ if(_rollCount>0)Padding(padding:const EdgeInsets.only(right:16),child:Column(mainAxisAlignment:MainAxisAlignment.center,children:[ const Text('今日次数',style:const TextStyle(fontSize:12,color:Color(0xE6FFFFFF)),), Text('$_rollCount', style: const TextStyle(fontSize:16, fontWeight: FontWeight.bold),),],),),],), body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children:[// 骰子容器 GestureDetector(onTap: _rollDice, child: AnimatedBuilder(animation: _rollController, builder:(context, child){returnTransform.rotate(angle: _rotationAnimation.value, child: Transform.scale(scale: _bounceAnimation.value, child: _buildDiceFace(_isRolling ? null:_currentValue),),);},),), const SizedBox(height:40), // 操作提示 Column(children:[Text(_isRolling ?'🎲 掷出中...':'轻触骰子或按钮开始摇动', style: TextStyle(fontSize:20, fontWeight: FontWeight.w500, color: _isRolling ? Colors.deepPurple:Colors.grey[700], height:1.4,), textAlign: TextAlign.center,),if(!_isRolling&&_lastRollTime!=null)...[const SizedBox(height:12), Text('上次结果: $_currentValue • ${_formatTimeAgo(_lastRollTime!)}', style: const TextStyle(fontSize:15, color: Colors.deepPurple, fontWeight: FontWeight.w500,),),],],), const Spacer(), // 历史记录提示 Container(width: double.infinity, padding: const EdgeInsets.all(16), margin: const EdgeInsets.only(bottom:30, left:20, right:20), decoration: BoxDecoration(color: Colors.deepPurple.shade50, borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.deepPurple.shade200),), child: Row(children:[const Icon(Icons.history, size:20, color: Colors.deepPurple), const SizedBox(width:12), Expanded(child: Text('🎲 小知识:标准骰子相对两面点数之和恒为7(1-6, 2-5, 3-4)', style: TextStyle(fontSize:14, height:1.4, color: Colors.deepPurple.shade900,),),),],),),],),), floatingActionButton: AnimatedScale(scale: _isRolling ?0.9:1.0, duration: const Duration(milliseconds:200), curve: Curves.easeInOut, child: FloatingActionButton.extended(onPressed: _rollDice, icon: _isRolling ? const SizedBox(width:20, height:20, child: CircularProgressIndicator(strokeWidth:2, valueColor: AlwaysStoppedAnimation(Colors.white)),):const Icon(Icons.casino, size:28), label: Text(_isRolling ?'摇动中...':'摇骰子', style: const TextStyle(fontSize:18, fontWeight: FontWeight.bold),), elevation:8,),),);}Widget _buildDiceFace(int? value){returnContainer(width:220, height:220, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(28), boxShadow:[BoxShadow(color: Colors.black.withOpacity(0.18), blurRadius:25, offset: const Offset(0,10),), BoxShadow(color: Colors.deepPurple.withOpacity(0.15), blurRadius:15, offset: const Offset(0,5),),], border: Border.all(color: Colors.deepPurple.shade100, width:2),), child: value==null ? const Center(child: Text('?', style: TextStyle(fontSize:80, fontWeight: FontWeight.bold, color: Colors.deepPurple,),),):_buildDiceDots(value),);}Widget _buildDiceDots(int value){// 骰子点位定义(使用3x3网格坐标)final Map<int, List<Alignment>>patterns={1:[Alignment.center],2:[Alignment.topLeft, Alignment.bottomRight],3:[Alignment.topLeft, Alignment.center, Alignment.bottomRight],4:[Alignment.topLeft, Alignment.topRight, Alignment.bottomLeft, Alignment.bottomRight],5:[Alignment.topLeft, Alignment.topRight, Alignment.center, Alignment.bottomLeft, Alignment.bottomRight],6:[Alignment.topLeft, Alignment.centerLeft, Alignment.bottomLeft, Alignment.topRight, Alignment.centerRight, Alignment.bottomRight,],};returnStack(alignment: Alignment.center, children:[// 背景装饰 Container(margin: const EdgeInsets.all(16), decoration: BoxDecoration(border: Border.all(color: Colors.deepPurple.shade100, width:1.5), borderRadius: BorderRadius.circular(20),),), // 点阵...?patterns[value]?.map((alignment){returnAlign(alignment: alignment, child: Container(width:28, height:28, decoration: BoxDecoration(color: Colors.deepPurple, shape: BoxShape.circle, boxShadow:[BoxShadow(color: Colors.deepPurple.withOpacity(0.3), blurRadius:6, offset: const Offset(0,2),),],),),);}),],);}String _formatTimeAgo(DateTimetime){final now=DateTime.now();final difference=now.difference(time);if(difference.inMinutes<1)return'刚刚';if(difference.inMinutes<60)return'${difference.inMinutes}分钟前';if(difference.inHours<24)return'${difference.inHours}小时前';if(difference.inDays<7)return'${difference.inDays}天前';return'${time.month}月${time.day}日';}}
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/24 8:36:18

计算机毕业设计springboot音乐推荐系统 基于协同过滤算法的个性化音乐推送平台开发 Spring Boot驱动的智能化歌曲推荐服务平台构建

计算机毕业设计springboot音乐推荐系统4ds179 &#xff08;配套有源码 程序 mysql数据库 论文&#xff09; 本套源码可以在文本联xi,先看具体系统功能演示视频领取&#xff0c;可分享源码参考。 随着互联网技术的飞速发展和数字化娱乐的普及&#xff0c;音乐已成为人们日常生活…

作者头像 李华
网站建设 2026/2/23 19:46:12

Java开发者破局指南:跳出内卷,借AI赋能,搭建系统化知识体系

Java开发者破局指南&#xff1a;跳出内卷&#xff0c;借AI赋能&#xff0c;搭建系统化知识体系 在技术迭代加速、AI快速渗透的当下&#xff0c;Java领域的内卷愈发严重——“初级码农”过剩、简历同质化、基础编码工作被AI替代&#xff0c;很多开发者陷入“只会CRUD、不懂底层…

作者头像 李华
网站建设 2026/2/24 20:56:45

php python+vue婚庆礼品网站 开题报告

目录开题报告背景技术选型依据系统功能模块创新点分析预期成果项目技术支持可定制开发之功能亮点源码获取详细视频演示 &#xff1a;文章底部获取博主联系方式&#xff01;同行可合作开题报告背景 婚庆礼品行业近年来发展迅速&#xff0c;线上线下结合的需求日益增长。传统的婚…

作者头像 李华
网站建设 2026/2/23 3:10:52

新手也能上手 AI论文写作软件,千笔 VS Checkjie,专科生专属神器!

随着人工智能技术的迅猛发展&#xff0c;AI辅助写作工具正逐步成为高校学生完成毕业论文的重要助手。尤其是在专科生群体中&#xff0c;面对繁重的论文任务与时间压力&#xff0c;越来越多的学生开始借助AI工具提升写作效率、优化内容质量。然而&#xff0c;市场上AI写作工具种…

作者头像 李华