# 鸿蒙 HarmonyOS 应用开发实战(第25期)|骰子(Dice Roller)— Unicode 符号与动画渲染精讲
一、应用概述
骰子(Dice Roller) 是一款经典的休闲娱乐应用,模拟了真实掷骰子的过程。应用投掷两个骰子(六面标准骰),使用 Unicode 骰面符号直观展示每个骰子的点数,并伴有快速滚动的动画效果。每次掷骰后,应用会自动计算点数总和,并根据特殊的点数组合(如蛇眼、Lucky 7、双六)给出趣味描述。应用基于 HarmonyOS 的 ArkTS 语言和声明式 UI 框架构建,完美展示了 Unicode 特殊符号渲染、animation 动画属性、setInterval 定时器、以及 get 计算属性等核心技术的综合运用。
对于鸿蒙开发者而言,骰子应用是一个展示符号渲染与动画技巧的绝佳案例——它让你在约85行代码中,同时掌握Unicode 符号映射、定时器驱动动画、条件描述逻辑、统计追踪四大核心能力,这些能力广泛适用于游戏、抽奖、模拟器等场景。
1.1 功能特性
- 双骰子投掷:模拟两个六面骰子的随机投掷
- Unicode 骰面显示:使用 ⚀⚁⚂⚃⚄⚅ 符号直观展示点数
- 滚动动画:60ms 间隔快速变化,模拟真实骰子滚动
- 点数总和计算:自动计算两个骰子点数和
- 趣味描述:特殊组合(2点/3点/7点/12点)触发趣味文案
- 投掷统计:记录总共投掷次数
- 动画锁定:动画进行中防止重复点击
1.2 适用场景
- 桌游电子化辅助工具
- 聚会活动抽奖环节
- 概率统计教学演示
- 儿童数学启蒙教育
二、技术架构
2.1 整体架构概览
┌─────────────────────────────────────────┐
│ 数据层 │
│ dice1 / dice2 (1~6 整数) │
│ total (点数总和) │
│ isRolling (动画状态) │
│ rollCount (投掷统计) │
├─────────────────────────────────────────┤
│ 状态管理层 │
│ @State 驱动 UI 重新渲染 │
├─────────────────────────────────────────┤
│ 业务逻辑层 │
│ roll() —— 掷骰子 + 动画 │
│ diceFace() —— 数字转 Unicode 符号 │
│ get d1/d2 —— 动画期间的实时随机值 │
│ get description —— 特殊组合描述 │
├─────────────────────────────────────────┤
│ UI 表示层 │
│ 两个骰子显示 / 点数总和 / 趣味描述 / │
│ 投掷统计 / 掷骰按钮 │
└─────────────────────────────────────────┘
2.2 动画状态机
静止状态 (isRolling=false)
│
│ 点击"掷骰子"
▼
动画状态 (isRolling=true)
│
│ setInterval(60ms) × 12次
│ 每次更新 dice1/dice2 为随机值
│ UI 显示实时随机值
▼
判定状态 (count >= 12)
│
│ clearInterval
│ isRolling = false
│ 计算 total / description
│ rollCount++
▼
静止状态 (等待下次投掷)
三、核心代码深度分析
3.1 完整的页面源码
import { router } from '@kit.ArkUI';
@Entry
@Component
struct index25 {
@State dice1: number = 1;
@State dice2: number = 6;
@State total: number = 7;
@State isRolling: boolean = false;
@State rollCount: number = 0;
// 动画期间的实时随机值
get d1(): number { return this.isRolling ? Math.floor(Math.random() * 6) + 1 : this.dice1; }
get d2(): number { return this.isRolling ? Math.floor(Math.random() * 6) + 1 : this.dice2; }
roll() {
if (this.isRolling) return;
this.isRolling = true;
let count = 0;
const intervalId = setInterval(() => {
this.dice1 = Math.floor(Math.random() * 6) + 1;
this.dice2 = Math.floor(Math.random() * 6) + 1;
count++;
if (count >= 12) {
clearInterval(intervalId);
this.isRolling = false;
this.total = this.dice1 + this.dice2;
this.rollCount++;
}
}, 60);
}
// 数字转 Unicode 骰面符号
diceFace(val: number): string {
const faces = ['⚀', '⚁', '⚂', '⚃', '⚄', '⚅'];
return faces[val - 1];
}
// 趣味描述
get description(): string {
const t = this.total;
if (t === 2) return '🐍 蛇眼!';
if (t === 7) return '🎯 Lucky 7!';
if (t === 12) return '🎉 双六!';
if (t === 3) return '😅 霉运';
return '';
}
build() {
Column() {
Row() {
Text('‹ 返回').fontSize(16).fontColor('#0077FF').onClick(() => router.back())
Text('🎲 骰子').fontSize(18).fontWeight(FontWeight.Bold)
Blank()
}.width('92%').padding({ top: 16, bottom: 8 })
Column() {
// 双骰子展示
Row({ space: 20 }) {
Column() {
Text(this.diceFace(this.isRolling ? this.d1 : this.dice1))
.fontSize(80).animation({ duration: 60 })
Text('骰子 1').fontSize(14).fontColor('#888')
}
Column() {
Text(this.diceFace(this.isRolling ? this.d2 : this.dice2))
.fontSize(80).animation({ duration: 60 })
Text('骰子 2').fontSize(14).fontColor('#888')
}
}.margin({ bottom: 16 })
// 点数显示
Text(`点数: ${this.isRolling ? '...' : this.total}`)
.fontSize(36).fontWeight(FontWeight.Bold).fontColor('#F44336')
// 趣味描述
Text(this.description).fontSize(18).fontColor('#FF9800').margin({ top: 4 })
// 投掷统计
Text(`已掷 ${this.rollCount} 次`).fontSize(14).fontColor('#888').margin({ top: 8 })
// 掷骰按钮
Button('🎲 掷骰子').width(200).height(56).borderRadius(28)
.backgroundColor('#4CAF50').fontSize(22).fontWeight(FontWeight.Bold)
.margin({ top: 16 }).onClick(() => this.roll())
}
.layoutWeight(1).justifyContent(FlexAlign.Center)
}
.width('100%').height('100%').backgroundColor('#f8f8f8')
}
}
3.2 Unicode 骰面符号映射
diceFace(val: number): string {
const faces = ['⚀', '⚁', '⚂', '⚃', '⚄', '⚅'];
return faces[val - 1];
}
这是本应用最独特的设计——使用 Unicode 的弥生骰子符号(Miscellaneous Symbols)来展示骰面:
| 数字 | Unicode | 字符 | 描述 |
|---|---|---|---|
| 1 | U+2680 | ⚀ | 白色骰子1 |
| 2 | U+2681 | ⚁ | 白色骰子2 |
| 3 | U+2682 | ⚂ | 白色骰子3 |
| 4 | U+2683 | ⚃ | 白色骰子4 |
| 5 | U+2684 | ⚄ | 白色骰子5 |
| 6 | U+2685 | ⚅ | 白色骰子6 |
这些 Unicode 字符在支持它们的字体中会渲染为带点阵的骰面图案,无需使用图片资源即可获得良好的视觉效果。这种纯文本符号的方案比图片方案具有以下优势:
- 无资源依赖:不需要额外加载图片,减小应用体积
- 矢量缩放:任意放大倍数下保持清晰,无锯齿
- 主题适配:颜色可以通过 CSS 属性自由控制
3.3 动画期间实时随机值
get d1(): number { return this.isRolling ? Math.floor(Math.random() * 6) + 1 : this.dice1; }
get d2(): number { return this.isRolling ? Math.floor(Math.random() * 6) + 1 : this.dice2; }
这是一个精妙的设计——在动画期间,每次 UI 渲染时都实时生成新的随机值,而非等待 setInterval 更新 @State 变量。这样做的好处:
- 更高的刷新率:脱离 60ms 定时器的限制,随 UI 渲染帧率同步更新
- 更流畅的动画:渲染引擎每帧都看到新值,视觉上更加流畅
- 资源节约:无需额外的定时器管理
动画结束后的判定逻辑:
if (count >= 12) {
clearInterval(intervalId);
this.isRolling = false; // 停止实时随机
this.total = this.dice1 + this.dice2;
this.rollCount++;
}
当 isRolling 设为 false 后,get d1() 返回固定的 this.dice1 值(即 setInterval 最后一次设置的值),骰子显示定格。
3.4 animation 动画属性
Text(this.diceFace(...))
.fontSize(80).animation({ duration: 60 })
animation() 是 HarmonyOS 中用于为组件添加过渡动画的属性方法。它的参数是一个 AnimationOptions 对象,包含以下常用属性:
duration:动画持续时间(毫秒),默认1000curve:动画曲线(Curve.EaseInOut等),默认Curve.Easedelay:动画延迟开始的时间(毫秒)iterations:动画重复次数,-1表示无限循环playMode:播放模式(PlayMode.Normal/PlayMode.Reverse/ …)
在本应用中,animation({ duration: 60 }) 设置为60ms的过渡动画。当骰子符号从 ⚀ 变为 ⚅ 时,这个过渡动画让变化过程更加平滑,而非生硬的瞬间切换。
需要注意的是,这里的 animation 属性作用于组件属性变化时的过渡效果。当 diceFace() 的返回值变化时(文字内容变化),动画会尝试在变化前后之间进行过渡。但对于文字内容变化,过渡效果可能不如数值属性(如 width、opacity、rotate 等)明显。
3.5 趣味描述逻辑
get description(): string {
const t = this.total;
if (t === 2) return '🐍 蛇眼!'; // 两个1点
if (t === 7) return '🎯 Lucky 7!'; // 最常见的点数
if (t === 12) return '🎉 双六!'; // 两个6点
if (t === 3) return '😅 霉运'; // 1+2 最小非蛇眼组合
return ''; // 其他组合不显示描述
}
四种特殊组合的判定规则:
- 蛇眼(Snake Eyes):两个骰子都是1点,总和2 —— 骰子中最小的组合,名字源于两个1点看起来像蛇的眼睛
- Lucky 7:总和7 —— 两个骰子点数和为7的概率最高(1/6),被视为幸运数字
- 双六(Boxcars):两个骰子都是6点,总和12 —— 最大的组合
- 霉运:总和3(1+2)—— 仅次于蛇眼的"坏运气"组合
其他的组合(4、5、6、8、9、10、11)不显示趣味描述,保持界面简洁。
四、HarmonyOS 特性深度解析
4.1 get 计算属性的实时求值机制
本应用中使用了三个 get 计算属性(d1、d2、description),它们在每次 UI 渲染时被重新求值。这种机制的关键理解是:
// 在模板中引用 get 属性
Text(this.diceFace(this.isRolling ? this.d1 : this.dice1))
ArkTS 的渲染引擎在每次渲染周期中:
- 检测到
@State变量变化(如isRolling从false变为true) - 触发重新渲染
- 重新求值所有模板表达式
- 计算
this.d1→ 调用get d1()→ 如果是动画状态则生成随机值 - 计算
this.diceFace(随机值)→ 映射为 Unicode 符号 - 更新 UI 显示
这个链条中,get d1() 在动画期间每次渲染都生成不同的随机值,产生了持续的视觉变化。
4.2 setInterval 的正确清理时机
roll() {
if (this.isRolling) return;
this.isRolling = true;
let count = 0;
const intervalId = setInterval(() => {
// ...
if (count >= 12) {
clearInterval(intervalId); // 在回调内部清理
this.isRolling = false;
}
}, 60);
}
定时器的清理发生在回调函数内部(通过 count 计数达到阈值时)。这种自清理模式的好处:
- 确定性清理:清理时机明确且可控
- 无需外部干预:不需要在
aboutToDisappear中额外清理 - 避免内存泄漏:定时器在完成任务后立即释放
相比而言,如果定时器是持续运行的(如时钟应用),则必须在 aboutToDisappear 中清理。
4.3 条件渲染的不同模式
本应用在三个地方使用了条件渲染,展示了三种不同的场景:
// 模式1:三元表达式 - 用于文字内容
Text(`点数: ${this.isRolling ? '...' : this.total}`)
// 模式2:三元表达式 - 用于 get 属性选择
Text(this.diceFace(this.isRolling ? this.d1 : this.dice1))
// 模式3:get 属性条件返回 - 用于描述文字
get description(): string {
if (...) return '...';
return '';
}
这三种模式分别适用于:
- 内联条件:简单的文字内容切换
- 值选择器:根据状态选择不同的数据源
- 计算属性:复杂的多分支逻辑
4.4 绿色主题的按钮设计
Button('🎲 掷骰子').width(200).height(56).borderRadius(28)
.backgroundColor('#4CAF50').fontSize(22).fontWeight(FontWeight.Bold)
按钮使用了 Material Design 的绿色(#4CAF50),在心理学上绿色代表"开始"和"行动"。大尺寸(200×56)、大圆角(28px)和大字体(22px)的设计让按钮成为页面中最重要的视觉焦点,吸引用户点击。
五、UI/UX 设计模式
5.1 页面布局结构
┌─────────────────────────────────┐
│ Row: 导航栏 │
├─────────────────────────────────┤
│ Column: 内容区 (居中) │
│ ├── Row: 两个骰子 │
│ │ ├── 骰子1 (⚀~⚅) │
│ │ └── 骰子2 (⚀~⚅) │
│ ├── Text: 点数总和 (大号红色) │
│ ├── Text: 趣味描述 (橙色) │
│ ├── Text: 投掷次数 (灰色) │
│ └── Button: "🎲 掷骰子" │
└─────────────────────────────────┘
5.2 视觉层次与色彩搭配
| 元素 | 颜色 | 作用 |
|---|---|---|
| 骰子符号 | 默认(黑色) | 核心视觉元素,80px超大字号 |
| 点数总和 | #F44336 红色 |
最重要的结果信息 |
| 趣味描述 | #FF9800 橙色 |
次要的趣味性信息 |
| 掷骰按钮 | #4CAF50 绿色 |
行动召唤(CTA) |
| 统计次数 | #888 灰色 |
辅助信息 |
5.3 信息层级设计
信息按照重要性从上到下排列:
- 骰子符号(最大,最醒目)—— 核心视觉
- 点数总和(36px,红色)—— 核心数据
- 趣味描述(18px,橙色)—— 情感化反馈
- 投掷次数(14px,灰色)—— 辅助统计
每个层级之间通过 margin、字号和颜色差异形成清晰的区分。
六、最佳实践与经验总结
6.1 随机数的正确生成
Math.floor(Math.random() * 6) + 1
这是生成 1~6 范围内随机整数的标准写法。分解如下:
Math.random():生成 [0, 1) 区间的浮点数(含0,不含1)* 6:扩展到 [0, 6) 区间Math.floor():向下取整到 [0, 5] 整数+ 1:偏移到 [1, 6]
重要提示:在鸿蒙开发中,Math.random() 是伪随机数生成器,对于游戏类应用足够了。
6.2 动画期间的防重入
roll() {
if (this.isRolling) return;
this.isRolling = true;
// ...
}
isRolling 标志位实现了按钮防重入。在动画进行中,即使用户多次点击"掷骰子"按钮,roll() 方法也会在入口处直接返回,不会启动新的动画循环。这是交互设计中防止意外操作的关键机制。
6.3 Unicode 符号的兼容性
Unicode 骰面符号(U+2680~U+2685)属于 Unicode 3.0 标准,在现代设备和操作系统中都有良好的支持。但在某些老旧设备或不支持这些符号的字体中,可能会显示为空白或方框。在生产环境中,建议:
- 提供降级方案:检测符号渲染情况,如果无法正常显示则使用数字替代
- 使用自定义字体:打包包含这些符号的字体文件
- 图片备选:在极端情况下回退到图片显示
6.4 代码组织建议
对于更复杂的骰子应用,可以考虑以下优化方向:
- 多种骰子类型:支持4面(D4)、8面(D8)、10面(D10)、12面(D12)、20面(D20)等角色扮演游戏常用骰子
- 投掷历史记录:保存最近几次投掷的结果
- 统计图表:展示点数分布的概率统计
- shake 传感器:利用设备加速度传感器,通过摇动手机触发掷骰
- 联机对战:通过分布式能力实现多人掷骰比大小
七、总结
骰子应用在约85行代码中,精彩展示了 ArkTS 声明式 UI 开发的多个核心概念:
- Unicode 符号映射:使用 ⚀⚁⚂⚃⚄⚅ 纯文本方式渲染骰面
- get 实时随机值:动画期间每次渲染生成新随机数
- setInterval 定时器动画:60ms 间隔的快速滚动效果
- animation 过渡动画:属性变化的平滑过渡
- 防重入机制:isRolling 标志位的按钮锁定
- 条件描述逻辑:特殊点数组合的趣味反馈
这个应用以"掷骰子"这一简单的游戏需求为切入点,展示了纯文本符号渲染和定时器动画的核心技术。它不仅是桌游爱好者的实用工具,更是学习 ArkTS 动画编程和符号处理的优秀教学案例。简洁的代码背后,蕴含着丰富的交互设计智慧和性能优化技巧。
更多推荐

所有评论(0)