一、引言
我们每天都在无意识中做随机选择。午餐吃什么、今晚看哪部电影、周末去哪里玩——这些决策有一个共同特点:选项太多,但决策本身不重要,“选哪个都行”。这时候,把决策权交给随机数生成器反而能打破犹豫不决的心理僵局。
随机选择器的技术核心是Math.random()——JavaScript 的内置伪随机数函数。它能生成 0 到 1 之间的一个浮点数,所有后续的随机操作(硬币正反面、骰子点数、范围内整数、列表中选取)都是对这一随机源的变换和映射。
但一个好的随机选择器不止于数字生成。它需要有一个旋转动画——用户在按下按钮后,看到数字或图标快速变化,经过约 0.6 秒后才定格在最终结果。这个动画不是多余的装饰,而是一种心理暗示:它让用户感受到"随机过程正在发生",增强了对随机性的信任感。如果按下按钮后结果立刻出现,用户的大脑会怀疑"是不是程序早就定好了"。
本文将用 ArkUI 从零构建一个随机选择器,包含四种随机模式:硬币抛掷(正反面)、骰子滚动(1-6)、数字范围(任意区间)、自定义列表抽取。每种模式都带有旋转动画和最近结果记录。
阅读完本文,你将能够:
- 使用
Math.random()变换生成多种随机结果 - 用
setInterval实现旋转定格动画(600ms 快速循环 + 定格) - 管理动画状态防止重复点击
- 构建四模式 tab 切换界面
二、随机数变换
2.1 Math.random() 的基础
Math.random()返回 [0, 1) 区间的伪随机浮点数。这个看似简单的函数是所有随机操作的基础:
// 硬币:二选一constface=Math.random()<0.5?'🪙 正面':'🪙 反面';// 骰子:六选一constdice=Math.floor(Math.random()*6)+1;// 范围内整数:[min, max] 区间constrange=max-min+1;constnum=min+Math.floor(Math.random()*range);// 列表中随机选取constitem=items[Math.floor(Math.random()*items.length)];每种变换的核心公式:
| 操作 | 公式 | 解释 |
|---|---|---|
| 布尔二选一 | Math.random() < 0.5 | 随机数 < 0.5 的概率恰好是 50% |
| 1-6 骰子 | floor(random * 6) + 1 | 0→0, 0.99→5.94→5, +1→[1,6] |
| 范围整数 | min + floor(random * range) | 0→min, 0.99→min+range-1, →[min, max] |
| 数组选取 | arr[floor(random * len)] | 索引范围 [0, len-1] |
2.2 为什么不用 crypto.getRandomValues()
密码生成器(上一篇)使用了Math.random(),本文也使用Math.random(),但对于不同的目的。Math.random()是伪随机数生成器(PRNG)——由确定性算法根据种子生成的数字序列。对于密码安全,PRNG 不够(攻击者可以通过已知输出推断下一个输出),但对于硬币抛掷、骰子滚动、午饭吃什么这些场景,PRNG 已经足够。
如果需要密码学安全的随机数,应使用crypto.getRandomValues(),但随机选择器不在这个范畴内。
三、旋转定格动画
3.1 动画原理
旋转动画的核心思路简单:在约 600ms 的时间内快速连续显示随机结果,然后定格在最终值。用户看到的是数字/图标快速变化的视觉效果,产生"随机正在发生"的感觉。
privatespinCount:number=0;flipCoin():void{if(this.spinning)return;// 防止重复点击this.spinning=true;this.spinCount=0;this.spinTimer=setInterval(()=>{this.spinCount++;this.coinFace=Math.random()<0.5?'🪙 正面':'🪙 反面';// 快速变化if(this.spinCount>=12){// 12 × 50ms = 600ms 后结束this.stopSpin();this.coinFace=Math.random()<0.5?'🪙 正面':'🪙 反面';// 定格consth=[this.coinFace,...this.coinHistory];if(h.length>5)h.pop();this.coinHistory=h;}},50);}四个阶段:
- 防重复:
if (this.spinning) return。动画期间禁止再次点击,防止多个动画同时运行。 - 快速变化:每 50ms 更新一次显示值(20fps),连续 12 次 = 600ms。用户在这 0.6 秒内看到硬币在正反面之间快速切换。
- 定格:12 次循环后清除定时器,最后一次随机决定最终结果。
- 记录历史:最终结果加入历史列表(最多保留 5 条)。
3.2 骰子的动画
骰子使用相同的动画结构,但随机值的变换不同:
rollDice():void{if(this.spinning)return;this.spinning=true;this.spinCount=0;this.spinTimer=setInterval(()=>{this.spinCount++;this.diceResult=Math.floor(Math.random()*6)+1;if(this.spinCount>=12){this.stopSpin();this.diceResult=Math.floor(Math.random()*6)+1;consth=[this.diceResult,...this.diceHistory];if(h.length>5)h.pop();this.diceHistory=h;}},50);}骰子结果展示使用 Unicode 骰子符号:⚀(1)、⚁(2)、⚂(3)、⚃(4)、⚄(5)、⚅(6)。这些是正式的 Unicode 字符,在任何平台上都能正常显示:
diceEmoji(n:number):string{constfaces=['','⚀','⚁','⚂','⚃','⚄','⚅'];returnfaces[n];}80sp 大号骰子符号配合下方的数字文字,既有视觉冲击力又清晰无误——光看符号可能把 ⚁ 和 ⚂ 弄混(尤其是在动画快速闪过时),但下方的阿拉伯数字消除了任何歧义。
3.3 随机数的动画
数字范围内的随机值是三段动画中视觉变化最剧烈的——因为数字可以从个位跳到百位,视觉跨度大:
generateNum():void{if(this.spinning)return;this.minVal=parseInt(this.minText)||1;this.maxVal=parseInt(this.maxText)||100;if(this.minVal>this.maxVal){consttmp=this.minVal;this.minVal=this.maxVal;this.maxVal=tmp;// 自动修正:最小值 > 最大值时交换}this.spinning=true;this.spinCount=0;constrange=this.maxVal-this.minVal+1;this.spinTimer=setInterval(()=>{this.spinCount++;this.numResult=this.minVal+Math.floor(Math.random()*range);if(this.spinCount>=12){this.stopSpin();this.numResult=this.minVal+Math.floor(Math.random()*range);consth=[this.numResult,...this.numHistory];if(h.length>5)h.pop();this.numHistory=h;}},50);}一个用户友好的设计:如果用户输入 min > max(比如 min=100, max=1),程序自动交换两个值。这避免了"生成失败"的错误提示,因为用户的意图很明确——“我想要 1 到 100 之间的随机数”,输入顺序不重要。
3.4 列表抽取的动画
列表抽取的动画比前三种更有趣——文本在变化时给人"滑轮在旋转"的感觉:
pickFromList():void{if(this.spinning||this.listItems.length===0)return;this.spinning=true;this.spinCount=0;this.spinTimer=setInterval(()=>{this.spinCount++;this.listResult=this.listItems[Math.floor(Math.random()*this.listItems.length)];if(this.spinCount>=16){// 16 × 50ms = 800ms,比其他模式稍长this.stopSpin();this.listResult=this.listItems[Math.floor(Math.random()*this.listItems.length)];consth=[this.listResult,...this.listHistory];if(h.length>5)h.pop();this.listHistory=h;}},50);}列表抽取使用 16 次循环(800ms)而非 12 次(600ms),动画稍长。原因是列表项的文本变化需要更多时间让用户感知"名字在滚动"——相较于数字,人的大脑需要更长时间来处理文本识别。
3.5 动画状态管理
所有四种模式共享同一个动画锁——this.spinning。这意味着在硬币动画期间,不能点击骰子或数字生成。这是一个有意的设计——同时运行多个动画会造成视觉混乱和状态冲突。
动画期间,按钮变为灰色(#CCCCCC),提供视觉上的"暂时不可用"反馈:
.backgroundColor(this.spinning?'#CCCCCC':'#1677FF')stopSpin()同时清理定时器和动画状态:
stopSpin():void{if(this.spinTimer!==-1){clearInterval(this.spinTimer);this.spinTimer=-1;}this.spinning=false;}aboutToDisappear()生命周期中调用stopSpin(),防止页面离开后定时器继续运行。
四、UI 设计
4.1 四模式 Tab 切换
四种模式通过顶部 tab 栏切换:
[🪙 硬币] [🎲 骰子] [🔢 数字] [📋 列表]每个 tab 显示图标 + 文字,当前选中的为白字加粗,未选中的为半透明白字(#FFFFFF66)。Tab 栏整合在深色标题栏下方,形成视觉上的连续性。
内容区使用if (this.activeTab === N)条件渲染对应模式的内容,互斥显示。
4.2 硬币模式
硬币模式内容极简——一个 64sp 的大号结果文字(“🪙 正面"或"🪙 反面”)+ 一个品红色(#EB2F96)抛掷按钮 + 最近结果列表。
品红色的选择是有意为之:硬币抛掷带有轻微的游戏/娱乐属性,品红比蓝色更活泼、更适合这个场景。而用户点击时,动画赋予硬币一种"命运正在被决定"的仪式感。
最近结果以小卡片形式展示,显示前 5 次抛掷记录。
4.3 骰子模式
骰子模式展示 80sp 的骰子符号(⚀⚁⚂⚃⚄⚅)和下方的阿拉伯数字。用户在动画结束后能通过两个独立信息通道确认结果——图形和文字。
最近结果以水平排列的小卡片展示,每张卡片包含骰子符号(32sp)和数字。这个布局比垂直列表更紧凑——在 360dp 宽度下,5 个 56vp 宽的小卡片恰好排成一行。
4.4 数字模式
数字模式有两个TextInput(最小值 / 最大值,键盘类型为InputType.Number)+ 绿色"生成随机数"按钮 + 结果显示。
56sp 等宽字体(monospace)显示结果数字,与秒表的显示风格一致。结果下方是最近数字的横向排列(52vp 宽的白色卡片)。
4.5 列表模式
列表模式是四个中最复杂的:
- 添加输入:
TextInput+ "添加"按钮(蓝色),输入后点添加或回车 - 选项列表:白色卡片展示所有选项,右侧 × 按钮删除单个
- 抽取按钮:品红色"🎯 抽取一个"按钮,带旋转动画
- 结果展示:大号文字 + 🎯 图标展示抽中结果
- 历史记录:最近 5 次抽取结果
添加按钮在输入为空时灰色(#CCCCCC),输入不为空时蓝色(#1677FF),这与待办清单和倒数日的表单验证模式一致。
五、完整代码结构
RandomPickerPage ├── 数据定义 │ └── TABS[] — 四个 tab 标签 ├── 状态变量 │ ├── @State activeTab — 当前模式 │ ├── @State spinning — 动画锁 │ ├── 硬币:@State coinFace + coinHistory[] │ ├── 骰子:@State diceResult + diceHistory[] │ ├── 数字:@State minVal/maxVal/minText/maxText + numResult + numHistory[] │ └── 列表:@State listInput + listItems[] + listResult + listHistory[] ├── 动画引擎 │ ├── flipCoin() — 硬币动画(12 帧 × 50ms) │ ├── rollDice() — 骰子动画(12 帧 × 50ms) │ ├── generateNum() — 数字动画(12 帧 × 50ms,自动交换 min/max) │ ├── pickFromList() — 列表抽取动画(16 帧 × 50ms) │ └── stopSpin() — 清理定时器 + 重置 spinning ├── 列表管理 │ ├── addListItem() — 添加选项 │ └── deleteListItem() — 删除选项 ├── 视图(四个 if 分支) │ ├── Tab 0:硬币 — 大号结果 + 抛掷按钮 + 历史列表 │ ├── Tab 1:骰子 — 骰子符号 + 数字 + 掷骰子按钮 + 横向历史 │ ├── Tab 2:数字 — 范围输入 + 结果 + 生成按钮 + 横向历史 │ └── Tab 3:列表 — 输入添加 + 选项列表 + 抽取按钮 + 结果 + 历史 └── 生命周期 └── aboutToDisappear() — 清除动画定时器六、总结
本文从零构建了一个随机选择器。与前十一篇的数据管理和工具类应用不同,随机选择器的核心是随机数变换 + 旋转定格动画——没有持久化数据(仅内存中的历史记录),没有复杂列表,只有四种随机模式和一个共享的动画引擎。
核心要点回顾:
四种随机变换:
Math.random()映射到布尔二选一(硬币)、1-6 整数(骰子) 、[min, max] 范围(数字)、数组随机索引(列表)。每个变换都是对同一随机源的重新解释。旋转定格动画:50ms 定时器每帧更新随机值,12-16 帧后定格在最终结果。600-800ms 的动画时长让用户感知"随机正在发生",增强了结果的可信度。动画锁(
spinning)防止重复点击。共享动画引擎:四种模式共用
spinning、spinCount、spinTimer三个私有变量和stopSpin()清理方法。这种共享设计减少了代码重复的同时保证了状态一致性——不可能出现两个动画同时运行的情况。骰子的 Unicode 符号:⚀⚁⚂⚃⚄⚅ 是正式的 Unicode 字符,在所有平台可正常显示。80sp 大号图形 + 下方阿拉伯数字双通道显示,消除识别歧义。
自动交换 min/max:数字模式下,用户输入 min > max 时程序自动交换两值,不做错误提示。这是一种用户友好的容错设计——用户的意图明确,不需要被纠正。
按钮颜色语义:硬币按钮品红(
#EB2F96)= 游戏/娱乐,骰子按钮蓝(#1677FF)= 标准操作,数字按钮绿(#52C41A)= 生成/产出,列表中抽取按钮品红(#EB2F96)= 随机抽取。四种按钮使用三种颜色,形成微妙的视觉层次——硬币和列表共用品红因为它们都是"选择"操作,骰子是中性游戏,数字是工具。
随机选择器是一个"小而有用"的工具——四种模式、一个Math.random()、一段旋转动画。它是决策疲劳的解药,也是随机数应用的一个完整示例。