Flutter 实战:lucky_number 幸运数字生成器的滚动动画、历史记录与鸿蒙适配解析
2026/6/13 1:02:55 网站建设 项目流程

Flutter 实战:lucky_number 幸运数字生成器的滚动动画、历史记录与鸿蒙适配解析

前言

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

幸运数字生成器是一个很适合拆解 Flutter 动画状态的小项目。它看起来只是点击按钮生成一个数字,但源码里包含了随机数生成动画监听防重复点击滚动数字效果结果高亮历史记录按钮禁用态跨端视觉验证等多个知识点。

lucky_number的核心流程非常清楚:用户点击按钮后,页面进入 spinning 状态,AnimationController在 2 秒内驱动滚动数字持续变化;动画完成后生成 0 到 9 的幸运数字,写入历史列表,并更新数字出现频次。界面会高亮中间数字,同时展示最近生成的结果。

抽奖类小工具的关键不是随机数本身,而是“开始、滚动、完成、展示、历史”这一整条交互链路是否稳定。

图示说明:上图展示 Flutter 页面在移动端的布局组织方式。lucky_number的实际界面由标题、五位滚动数字、幸运数字结果卡片、按钮和历史 Chip 组成。

一、项目定位与功能边界

1.1 应用定位

lucky_number是一个轻量幸运数字生成工具,适合用于抽签、随机演示、Flutter 动画教学和状态流转分析。它没有网络请求,也没有复杂业务依赖,核心逻辑完全由 Dart 随机数和 Flutter 动画系统实现。

项目当前支持:

  • 生成 0 到 9 的幸运数字。
  • 使用 5 个数字格模拟滚动效果。
  • 中间数字作为最终幸运数字展示。
  • 动画期间按钮禁用,防止重复触发。
  • 动画完成后展示结果卡片。
  • 保存最近 10 次生成结果。
  • 页面底部展示最近 5 个历史数字。
  • 维护数字出现频次 Map。

1.2 功能模块

功能模块页面表现源码实现
随机数字0 到 9 的结果math.Random().nextInt(10)
滚动动画五个数字持续变化_controller.addListener(_updateNumbers)
防重复点击转动中按钮禁用_isSpinning
结果展示中间数字高亮index == 2
历史记录最近数字 Chip_history.take(5)
频次统计内部 Map 更新_numberFrequency[result]
生命周期释放动画控制器_controller.dispose()

1.3 技术栈

技术点使用位置价值
Flutter页面、按钮、卡片、Chip构建可交互 UI
Dart随机数、列表、Map实现抽取与统计
Material 3应用主题和组件风格useMaterial3: true
StatefulWidget管理转动状态和历史响应用户点击和动画完成
AnimationController驱动滚动效果形成 2 秒抽取过程

二、工程结构与运行环境

2.1 工程结构

lucky_number是标准 Flutter 工程,核心代码集中在lib/main.dart

文件或目录作用
lib/main.dart应用入口、动画控制、随机数、历史记录和 UI
pubspec.yamlFlutter SDK 与测试依赖声明
test/widget_test.dartWidget 测试入口
ohos/鸿蒙平台工程目录
analysis_options.yamlDart 静态分析规则

2.2 运行命令

flutter doctor flutter pub get flutter run

项目依赖较轻,随机数来自 Dart 标准库dart:math,动画来自 Flutter SDK。

2.3 依赖声明

dependencies:flutter:sdk:fluttercupertino_icons:^1.0.8dev_dependencies:flutter_test:sdk:flutterflutter_lints:^5.0.0

这种结构对鸿蒙适配比较友好,核心逻辑不依赖平台通道,主要验证动画、布局、字体和按钮状态即可。

三、应用入口与主题配置

3.1 main 函数

Flutter 应用从main()进入:

import'package:flutter/material.dart';import'dart:math'asmath;voidmain(){runApp(constLuckyNumberApp());}

dart:math用于生成滚动数字和最终幸运数字。

3.2 根组件

classLuckyNumberAppextendsStatelessWidget{constLuckyNumberApp({super.key});@overrideWidgetbuild(BuildContextcontext){returnMaterialApp(title:'Lucky Number',theme:ThemeData(colorScheme:ColorScheme.fromSeed(seedColor:Colors.amber),useMaterial3:true,),home:constLuckyNumberHomePage(title:'Lucky Number'),);}}

根组件负责配置标题、主题和首页,不保存抽取状态。抽取状态全部由首页 State 管理。

3.3 主题色

colorScheme:ColorScheme.fromSeed(seedColor:Colors.amber)

琥珀色主题和幸运数字的视觉语义比较契合,也用于结果高亮、按钮背景和阴影效果。

四、StatefulWidget 与动画混入

4.1 首页组件

classLuckyNumberHomePageextendsStatefulWidget{constLuckyNumberHomePage({super.key,requiredthis.title});finalStringtitle;@overrideState<LuckyNumberHomePage>createState()=>_LuckyNumberHomePageState();}

首页需要处理按钮点击、动画进度、随机数字、历史记录和结果展示,因此使用StatefulWidget

4.2 SingleTickerProviderStateMixin

class_LuckyNumberHomePageStateextendsState<LuckyNumberHomePage>withSingleTickerProviderStateMixin{// ...}

SingleTickerProviderStateMixin为单个AnimationController提供vsync,避免不必要的动画资源消耗。

4.3 核心状态字段

int _luckyNumber=7;List<int>_spinningNumbers=List.generate(5,(_)=>0);bool _isSpinning=false;lateAnimationController_controller;finalList<int>_history=[];finalMap<int,int>_numberFrequency={};
字段类型作用
_luckyNumberint最终幸运数字,初始为 7
_spinningNumbersList<int>转动过程中显示的 5 个数字
_isSpinningbool是否正在转动
_controllerAnimationController控制 2 秒动画
_historyList<int>最近生成的幸运数字
_numberFrequencyMap<int, int>数字出现次数统计

五、动画初始化与生命周期

5.1 初始化控制器

@overridevoidinitState(){super.initState();_controller=AnimationController(duration:constDuration(milliseconds:2000),vsync:this,);_controller.addListener(_updateNumbers);_controller.addStatusListener((status){if(status==AnimationStatus.completed){_finishSpinning();}});}

动画时长为 2 秒。动画播放期间,每一帧都会触发_updateNumbers();动画完成时调用_finishSpinning()收口。

5.2 监听器职责

监听器触发时机职责
addListener动画每次 tick更新滚动数字
addStatusListener动画状态变化在 completed 时生成最终结果

5.3 释放控制器

@overridevoiddispose(){_controller.dispose();super.dispose();}

动画控制器持有 ticker 资源,页面销毁时必须释放。这是 Flutter 动画页面的基础规范。

六、滚动数字更新逻辑

6.1 _updateNumbers 方法

void_updateNumbers(){finalrandom=math.Random();setState((){_spinningNumbers=List.generate(5,(_)=>random.nextInt(10));});}

动画播放期间,这个方法不断生成 5 个 0 到 9 的随机数字,模拟老虎机式滚动效果。

6.2 数字范围

random.nextInt(10)

nextInt(10)会生成 0 到 9 的整数,不包含 10。

6.3 为什么是 5 个数字

页面使用 5 个数字格,中间位置最终高亮。这样既有滚动氛围,又能明确告诉用户哪个数字是结果。

左侧数字 左中数字 中间结果数字 右中数字 右侧数字

七、开始转动与防重复点击

7.1 _spin 方法

void_spin(){if(_isSpinning)return;setState((){_isSpinning=true;});_controller.forward(from:0);}

如果当前已经在转动,方法直接返回,避免重复启动动画。

7.2 动画从头播放

_controller.forward(from:0);

每次抽取都从动画起点重新开始,这样每次点击都有完整 2 秒滚动过程。

7.3 按钮禁用态

onPressed:_isSpinning?null:_spin

_isSpinning为 true 时,按钮禁用。这比单纯在_spin()里 return 更直观,因为 UI 也会告诉用户当前不能重复点击。

八、完成转动与结果写入

8.1 _finishSpinning 方法

void_finishSpinning(){finalrandom=math.Random();finalresult=random.nextInt(10);setState((){_isSpinning=false;_spinningNumbers=List.generate(5,(_)=>random.nextInt(10));_luckyNumber=result;_history.insert(0,result);if(_history.length>10){_history.removeLast();}_numberFrequency[result]=(_numberFrequency[result]??0)+1;});}

动画结束后会生成最终幸运数字,并更新页面状态。

8.2 状态更新内容

更新项作用
_isSpinning = false结束转动,恢复按钮
_spinningNumbers刷新 5 个显示数字
_luckyNumber = result写入最终结果
_history.insert(0, result)保存最新历史
_history.removeLast()控制历史长度
_numberFrequency[result]统计结果出现次数

8.3 频次统计的真实表现

源码维护了_numberFrequency,但当前 UI 没有把频次统计展示出来。也就是说它已经具备统计数据基础,但还没有形成可见的统计面板。

九、数字滚动 UI

9.1 五个数字格

Row(mainAxisAlignment:MainAxisAlignment.center,children:List.generate(5,(index){finalisHighlighted=!_isSpinning&&index==2;returnContainer(width:50,height:70,margin:constEdgeInsets.symmetric(horizontal:4),child:Center(child:Text(...)),);}),)

五个数字格横向排列,营造抽取滚动效果。

9.2 中间高亮

finalisHighlighted=!_isSpinning&&index==2;

当动画停止后,中间数字格高亮,表示它是最终结果。

9.3 展示逻辑

_isSpinning?_spinningNumbers[index].toString():(index==2?_luckyNumber.toString():_spinningNumbers[index].toString())

转动中展示滚动数字;停止后,中间位置展示_luckyNumber,其他位置仍展示最后一次滚动数字。

十、结果卡片与历史记录

10.1 结果卡片条件

if(!_isSpinning&&_history.isNotEmpty)Card(child:Padding(padding:constEdgeInsets.all(16),child:Column(children:[constText('Your Lucky Number'),Text(_luckyNumber.toString()),],),),)

只有不在转动中且已有历史结果时,才展示幸运数字卡片。

10.2 历史 Chip

if(_history.length>1)Wrap(spacing:4,children:_history.take(5).map((n){returnChip(label:Text(n.toString()),backgroundColor:n==_luckyNumber?Colors.amber:Colors.grey.shade200,);}).toList(),)

页面最多展示最近 5 个历史数字,当前结果会使用琥珀色高亮。

10.3 历史保存策略

历史用途当前实现
数据保存最多 10 个
页面展示最近 5 个
最新位置列表第 0 位
当前结果高亮Chip 背景色

十一、按钮与交互状态

11.1 按钮实现

ElevatedButton.icon(onPressed:_isSpinning?null:_spin,icon:Icon(_isSpinning?Icons.hourglass_empty:Icons.casino),label:Text(_isSpinning?'Spinning...':'Spin for Lucky Number'),style:ElevatedButton.styleFrom(padding:constEdgeInsets.all(20),backgroundColor:Colors.amber,shape:RoundedRectangleBorder(borderRadius:BorderRadius.circular(16),),),)

按钮图标和文案会根据转动状态变化。

11.2 状态表

状态图标文案是否可点击
未转动Icons.casinoSpin for Lucky Number可以
转动中Icons.hourglass_emptySpinning...不可以

11.3 交互反馈

按钮禁用态、滚动数字和结果卡片共同组成完整反馈。用户点击后能明显感知“已经开始、正在进行、已经完成”。

十二、页面布局结构

12.1 Scaffold 骨架

returnScaffold(appBar:AppBar(title:Text(widget.title),backgroundColor:Theme.of(context).colorScheme.inversePrimary,),body:Column(children:[constSizedBox(height:32),constText('Lucky Number Generator'),Row(...),if(!_isSpinning&&_history.isNotEmpty)Card(...),constSpacer(),ElevatedButton.icon(...),if(_history.length>1)Padding(...),],),);

页面采用纵向结构,上方展示标题和数字,中部展示结果,下方放按钮和历史。

12.2 Spacer 的作用

constSpacer()

Spacer把按钮推向页面底部,让结果区域和操作区域形成清晰层级。

12.3 视觉层级

区域作用
标题明确应用主题
数字格展示滚动过程
结果卡片展示最终幸运数字
按钮触发新一轮抽取
历史 Chip回看最近结果

十三、边界场景与真实限制

13.1 防重复点击

_spin()内部和按钮禁用态都处理了重复点击问题。即使用户快速点击按钮,转动过程中也不会重复启动动画。

13.2 历史数量限制

历史列表最多保存 10 条:

if(_history.length>10){_history.removeLast();}

页面只展示最近 5 条,因此历史数据和 UI 展示范围并不完全相同。

13.3 频次统计未展示

_numberFrequency已经在结果完成时更新,但当前页面没有展示频次图表或统计列表。后续可以把它做成数字出现次数面板。

13.4 随机数重复

随机数范围只有 0 到 9,重复出现是正常现象。它更像娱乐抽取工具,不适合用于严肃随机安全场景。

十四、Widget 测试设计

14.1 基础渲染测试

import'package:flutter_test/flutter_test.dart';import'../lib/main.dart';voidmain(){testWidgets('lucky number renders home page',(tester)async{awaittester.pumpWidget(constLuckyNumberApp());expect(find.text('Lucky Number'),findsWidgets);expect(find.text('Spin for Lucky Number'),findsOneWidget);});}

这个测试验证根组件和默认按钮文案。

14.2 点击按钮测试

testWidgets('spin button enters spinning state',(tester)async{awaittester.pumpWidget(constLuckyNumberApp());awaittester.tap(find.text('Spin for Lucky Number'));awaittester.pump();expect(find.text('Spinning...'),findsOneWidget);});

这个测试覆盖点击后_isSpinning状态变化。

14.3 动画完成测试

testWidgets('spin completes and shows result card',(tester)async{awaittester.pumpWidget(constLuckyNumberApp());awaittester.tap(find.text('Spin for Lucky Number'));awaittester.pump(constDuration(milliseconds:2100));expect(find.text('Your Lucky Number'),findsOneWidget);});

测试中推进时间超过 2 秒,可以验证动画完成后的结果展示。

14.4 测试命令

fluttertest

保持测试里的根组件名称与实际源码一致,能避免默认模板测试残留造成编译失败。

十五、鸿蒙适配观察

15.1 适配优势

lucky_number的核心逻辑由 Dart 随机数和 Flutter 动画系统完成,没有复杂原生插件依赖,因此鸿蒙侧主要关注动画、布局和字体显示。

维度当前项目情况鸿蒙侧关注点
随机数math.Random()多端逻辑一致
动画AnimationController2 秒动画流畅度
按钮ElevatedButton.icon禁用态和触控反馈
数字格Row+Container小屏宽度和数字显示
历史Wrap+Chip换行和高亮表现

15.2 构建命令参考

flutter clean flutter pub get flutter build hap

具体构建命令取决于所使用的鸿蒙 Flutter 适配环境。这个项目重点验证动画、按钮状态、数字布局和 Chip 展示。

15.3 运行验证要点

  1. 应用能正常启动到首页。
  2. 点击按钮后进入Spinning...状态。
  3. 转动期间数字持续变化。
  4. 动画结束后中间数字高亮。
  5. 结果卡片能正常显示。
  6. 最近历史 Chip 能正确展示和高亮。

鸿蒙适配时,这类项目的关键是动画帧、按钮禁用态、数字布局和历史 Chip 换行,而不是随机算法本身。

十六、性能与可维护性

16.1 性能特征

项目计算量很小,动画期间主要是 5 个数字的状态刷新。

维度当前表现
动画时长2 秒
每次刷新数字5 个
历史保存10 条
页面展示历史5 条
结果范围0 到 9

16.2 当前结构优点

  • 抽取状态由_isSpinning统一控制。
  • 动画监听与完成监听职责分离。
  • 历史记录有长度限制。
  • 按钮 UI 和状态同步变化。
  • 动画控制器生命周期处理完整。

16.3 可演进方向

可以把结果范围做成可配置项:

intgenerateLuckyNumber({int maxExclusive=10}){returnmath.Random().nextInt(maxExclusive);}

也可以把_numberFrequency展示成统计列表:

List<MapEntry<int,int>>sortedFrequency(Map<int,int>source){finalentries=source.entries.toList();entries.sort((a,b)=>b.value.compareTo(a.value));returnentries;}

这样可以从娱乐工具扩展成带统计信息的小型随机分析页面。

十七、扩展功能思路

17.1 自定义范围

用户可以输入最小值和最大值,生成指定范围内的幸运数字。

intrandomInRange(int min,int max){returnmin+math.Random().nextInt(max-min+1);}

17.2 展示频次统计

当前_numberFrequency已经维护数据,可以新增一个统计卡片展示每个数字出现次数。

_numberFrequency.forEach((number,count){// 构建统计行});

17.3 动画节奏优化

可以让动画前快后慢,增强抽取仪式感。当前项目使用固定时长和监听刷新,后续可以结合曲线或间隔变化优化体验。

十八、常见问题与优化建议

18.1 为什么使用AnimationController

因为项目需要一个明确的 2 秒转动过程,并在动画完成后生成结果。AnimationController可以同时提供播放控制和状态监听。

18.2 为什么_spin()要判断_isSpinning

它可以防止用户连续点击导致多轮动画重叠。按钮禁用是 UI 层保护,_spin()判断是逻辑层保护。

18.3 为什么最终结果只取 0 到 9

源码使用random.nextInt(10),所以结果范围固定为 0 到 9。这个范围适合单数字幸运号码展示。

18.4 为什么历史只展示最近 5 个

页面底部空间有限,展示 5 个 Chip 更紧凑。内部仍保留最多 10 条历史,方便后续扩展。

18.5 为什么频次统计没有出现在页面上

当前源码只维护_numberFrequency,没有对应 UI。它更像为后续统计展示预留的数据基础。

18.6 为什么适合做鸿蒙适配示例

它同时包含动画、按钮禁用、数字布局、Chip、阴影和随机结果展示,能覆盖 Flutter 小型互动页面在鸿蒙侧的多个验证点。

总结

lucky_number用一个 Flutter 页面完成了幸运数字生成器的完整交互闭环:点击按钮开始转动,动画监听持续刷新 5 个数字,动画完成后生成最终结果,写入历史记录并更新频次统计。

从工程角度看,这个项目的结构很适合学习 Flutter 动画状态管理。_isSpinning控制按钮和展示状态,AnimationController控制流程节奏,_history_numberFrequency则为结果追踪提供数据基础。

从鸿蒙适配角度看,项目没有复杂原生依赖,主要验证动画流畅度、按钮禁用态、数字格布局、Chip 展示和字体渲染即可。处理好这些细节后,跨端体验会比较稳定。

如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!


相关资源:

  • Flutter 官方文档
  • Flutter 测试文档
  • OpenHarmony 官网

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询