📖 前言

最近在学习鸿蒙原生开发,想着从一个小Demo入手练练手。考虑到日常玩游戏时经常需要掷骰子,就决定开发一个简洁美观的掷骰子应用。本文将详细记录整个开发过程,包括项目创建、UI设计、状态管理、动画实现等核心内容,希望能帮到正在学习鸿蒙开发的小伙伴们!

先看最终效果:

在这里插入图片描述


一、项目概述

1.1 功能需求

这个掷骰子App虽然简单,但我希望它能具备以下功能:

  • ✅ 点击按钮掷骰子
  • ✅ 骰子点数随机变化(1-6点)
  • ✅ 掷骰子动画效果(快速切换显示)
  • ✅ 统计掷骰子次数
  • ✅ 重置计数功能
  • ✅ 简洁美观的UI设计

1.2 技术栈

技术说明
开发框架ArkUI(声明式UI)
开发语言ArkTS(TypeScript扩展)
API版本HarmonyOS Next
开发工具DevEco Studio
项目模型Stage模型

二、项目创建与环境配置

2.1 创建新项目

  1. 打开 DevEco Studio,选择 Create Project
  2. 选择 Empty Ability 模板(适用于纯应用开发)
  3. 填写项目信息:
    • Project name: DiceApp
    • Bundle name: com.example.diceapp
    • Save location: E:\HMproject\Project\DiceApp
    • Compile SDK: 选择最新的 HarmonyOS Next SDK
    • Model: Stage

点击 Finish 完成项目创建。

2.2 项目结构分析

创建完成后,项目目录结构如下:

DiceApp/
├── AppScope/                 # 应用全局配置
│   ├── app.json5            # 应用配置(包名、版本、图标等)
│   └── resources/           # 全局资源
├── entry/                    # 主模块
│   ├── src/main/
│   │   ├── ets/             # ArkTS源码
│   │   │   ├── entryability/
│   │   │   │   └── EntryAbility.ets    # 应用入口
│   │   │   └── pages/
│   │   │       └── Index.ets           # 主页面
│   │   └── resources/       # 资源文件
│   │       ├── base/element/   # 字符串、颜色等
│   │       ├── base/media/     # 图片资源
│   │       └── base/profile/   # 配置文件
│   ├── build-profile.json5  # 构建配置
│   └── module.json5         # 模块配置
├── oh_modules/              # 依赖包
└── build-profile.json5      # 项目构建配置

2.3 核心配置文件解读

app.json5 - 应用全局配置
{
  "app": {
    "bundleName": "com.example.diceapp",    // 应用包名,唯一标识
    "vendor": "example",                     // 开发者名称
    "versionCode": 1000000,                  // 版本号(数值)
    "versionName": "1.0.0",                  // 版本名(字符串)
    "icon": "$media:layered_image",          // 应用图标
    "label": "$string:app_name"              // 应用名称
  }
}
module.json5 - 模块配置

这个文件定义了应用的基本能力(Ability)和页面路由:

{
  "module": {
    "name": "entry",
    "type": "entry",
    "deviceTypes": ["phone"],               // 支持设备类型
    "pages": "$profile:main_pages",         // 页面路由配置
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "exported": true,
        "skills": [
          {
            "entities": ["entity.system.home"],
            "actions": ["ohos.want.action.home"]
          }
        ]
      }
    ]
  }
}

三、UI设计与实现

3.1 设计思路

骰子App的UI设计我采用了以下风格:

  • 暗色主题: 黑色背景,红色骰子,营造高级感
  • 卡片式设计: 骰子作为主体,带有阴影效果
  • 状态反馈: 按钮颜色变化、文字提示等

3.2 骰子点数实现原理

骰子是一个3x3的点阵,每个位置可能有点或无点。我用一个二维数组来表示6种点数:

骰子点阵布局:
[0] [1] [2]
[3] [4] [5]
[6] [7] [8]

对于每个点数(1-6),定义9个位置的显示状态:

private readonly DICE_DOTS: boolean[][] = [
  // 1点:只有中心
  [false, false, false, false, true,  false, false, false, false],
  // 2点:左上和右下
  [false, false, true,  false, false, false, true,  false, false],
  // 3点:对角线
  [false, false, true,  false, true,  false, true,  false, false],
  // 4点:四角
  [true,  false, true,  false, false, false, true,  false, true],
  // 5点:四角+中心
  [true,  false, true,  false, true,  false, true,  false, true],
  // 6点:左右两列
  [true,  false, true,  true,  false, true,  true,  false, true]
]

3.3 页面布局实现

主页面使用 ColumnRow 进行布局,整体结构如下:

@Entry
@Component
struct Index {
  // 状态变量
  @State currentValue: number = 1      // 当前点数
  @State isRolling: boolean = false    // 是否正在掷骰子
  @State rollCount: number = 0         // 掷骰子次数

  build() {
    Scroll() {
      Column() {
        // 1. 顶部标题栏
        Row() {
          Text('🎲 掷骰子')
          Blank()
          Text('共掷 ' + this.rollCount + ' 次')
        }

        // 2. 骰子展示区域
        Column() {
          // 3x3圆点矩阵
        }

        // 3. 点数显示
        Text('点数: ' + this.currentValue)

        // 4. 操作按钮
        Button('🎲 掷骰子')
        Button('🔄 重置计数')
      }
    }
  }
}

3.4 骰子点阵UI实现

使用9个 Circle 组件组成骰子的点阵:

private dotColor(i: number): ResourceColor {
  return this.DICE_DOTS[this.currentValue - 1][i] ? Color.White : Color.Transparent
}

Column() {
  Row() {
    Circle().width(26).height(26).fill(this.dotColor(0))
    Circle().width(26).height(26).fill(this.dotColor(1))
    Circle().width(26).height(26).fill(this.dotColor(2))
  }
  Row() {
    Circle().width(26).height(26).fill(this.dotColor(3))
    Circle().width(26).height(26).fill(this.dotColor(4))
    Circle().width(26).height(26).fill(this.dotColor(5))
  }
  Row() {
    Circle().width(26).height(26).fill(this.dotColor(6))
    Circle().width(26).height(26).fill(this.dotColor(7))
    Circle().width(26).height(26).fill(this.dotColor(8))
  }
}
.width(180).height(180)
.backgroundColor('#FF3B30')          // 红色背景
.borderRadius(24)                   // 圆角
.shadow({                           // 阴影效果
  radius: 20,
  color: '#40FF3B30',
  offsetX: 0,
  offsetY: 8
})

效果说明:

  • 骰子主体为红色背景(#FF3B30)
  • 圆角设计(24vp)更现代
  • 添加阴影增加立体感

四、状态管理与动画实现

4.1 状态变量详解

ArkUI 使用 @State 装饰器声明状态变量,当状态变化时UI会自动更新:

@State currentValue: number = 1      // 当前点数
@State isRolling: boolean = false    // 掷骰子状态
@State rollCount: number = 0         // 掷骰子总次数

4.2 掷骰子动画实现

这是本App的核心功能!通过 setInterval 实现快速切换点数的动画效果:

private rollIntervalId: number = -1

private rollDice(): void {
  if (this.isRolling) return    // 防止重复点击
  
  this.isRolling = true
  let count = 0
  
  this.rollIntervalId = setInterval(() => {
    count++
    this.currentValue = Math.floor(Math.random() * 6) + 1  // 随机1-6
    
    if (count >= 8) {
      clearInterval(this.rollIntervalId)     // 停止动画
      this.rollIntervalId = -1
      this.isRolling = false
      this.rollCount++                       // 累计次数
    }
  }, 100)  // 每100ms切换一次
}

动画参数解析:

  • 切换间隔:100ms(快速闪烁效果)
  • 切换次数:8次(总时长800ms)
  • 随机算法:Math.random() * 6 + 1 生成1-6的随机数

4.3 生命周期管理

很重要!在组件销毁时清除定时器,避免内存泄漏:

aboutToDisappear(): void {
  if (this.rollIntervalId !== -1) {
    clearInterval(this.rollIntervalId)
  }
}

4.4 重置功能

private clearCount(): void {
  this.rollCount = 0
  this.currentValue = 1
}

五、完整代码实现

5.1 主页面完整代码(Index.ets)

@Entry
@Component
struct Index {
  // 骰子点数映射表
  private readonly DICE_DOTS: boolean[][] = [
    [false, false, false, false, true,  false, false, false, false],  // 1
    [false, false, true,  false, false, false, true,  false, false],  // 2
    [false, false, true,  false, true,  false, true,  false, false],  // 3
    [true,  false, true,  false, false, false, true,  false, true],   // 4
    [true,  false, true,  false, true,  false, true,  false, true],   // 5
    [true,  false, true,  true,  false, true,  true,  false, true]    // 6
  ]

  // 状态变量
  @State currentValue: number = 1
  @State isRolling: boolean = false
  @State rollCount: number = 0

  private rollIntervalId: number = -1

  // 生命周期:组件销毁时清理
  aboutToDisappear(): void {
    if (this.rollIntervalId !== -1) {
      clearInterval(this.rollIntervalId)
    }
  }

  // 掷骰子方法
  private rollDice(): void {
    if (this.isRolling) return
    this.isRolling = true
    let count = 0
    this.rollIntervalId = setInterval(() => {
      count++
      this.currentValue = Math.floor(Math.random() * 6) + 1
      if (count >= 8) {
        clearInterval(this.rollIntervalId)
        this.rollIntervalId = -1
        this.isRolling = false
        this.rollCount++
      }
    }, 100)
  }

  // 重置计数
  private clearCount(): void {
    this.rollCount = 0
    this.currentValue = 1
  }

  // 获取点的颜色
  private dotColor(i: number): ResourceColor {
    return this.DICE_DOTS[this.currentValue - 1][i] ? Color.White : Color.Transparent
  }

  build() {
    Scroll() {
      Column() {
        // 顶部标题栏
        Row() {
          Text('🎲 掷骰子')
            .fontSize(28)
            .fontWeight(FontWeight.Bold)
            .fontColor(Color.White)
          Blank()
          Text('共掷 ' + this.rollCount + ' 次')
            .fontSize(14)
            .fontColor('#8E8E93')
        }
        .width('100%')
        .padding(20)

        // 骰子展示
        Column() {
          Row() {
            Circle().width(26).height(26).fill(this.dotColor(0)).margin({ left: 6, right: 6 })
            Circle().width(26).height(26).fill(this.dotColor(1)).margin({ left: 6, right: 6 })
            Circle().width(26).height(26).fill(this.dotColor(2)).margin({ left: 6, right: 6 })
          }.width('100%').justifyContent(FlexAlign.Center)

          Row() {
            Circle().width(26).height(26).fill(this.dotColor(3)).margin({ left: 6, right: 6 })
            Circle().width(26).height(26).fill(this.dotColor(4)).margin({ left: 6, right: 6 })
            Circle().width(26).height(26).fill(this.dotColor(5)).margin({ left: 6, right: 6 })
          }.width('100%').justifyContent(FlexAlign.Center).margin({ top: 6 })

          Row() {
            Circle().width(26).height(26).fill(this.dotColor(6)).margin({ left: 6, right: 6 })
            Circle().width(26).height(26).fill(this.dotColor(7)).margin({ left: 6, right: 6 })
            Circle().width(26).height(26).fill(this.dotColor(8)).margin({ left: 6, right: 6 })
          }.width('100%').justifyContent(FlexAlign.Center).margin({ top: 6 })
        }
        .width(180)
        .height(180)
        .backgroundColor('#FF3B30')
        .borderRadius(24)
        .justifyContent(FlexAlign.Center)
        .shadow({ radius: 20, color: '#40FF3B30', offsetX: 0, offsetY: 8 })
        .margin({ top: 24 })

        // 点数显示
        Text('点数: ' + this.currentValue)
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .fontColor('#8E8E93')
          .margin({ top: 12 })

        // 掷骰子按钮
        Button(this.isRolling ? '🎲 掷动中...' : '🎲 掷骰子')
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .width(220)
          .height(60)
          .borderRadius(30)
          .backgroundColor(this.isRolling ? '#555555' : '#FF9F0A')
          .fontColor(Color.White)
          .enabled(!this.isRolling)
          .margin({ top: 20 })
          .onClick(() => { this.rollDice() })

        // 重置按钮(条件显示)
        if (this.rollCount > 0) {
          Button('🔄 重置计数')
            .fontSize(16)
            .height(44)
            .borderRadius(22)
            .backgroundColor('#333333')
            .fontColor('#8E8E93')
            .margin({ top: 16 })
            .onClick(() => { this.clearCount() })
        }

        // 提示文字(初始状态)
        if (this.rollCount === 0) {
          Text('点击"掷骰子"开始')
            .fontSize(16)
            .fontColor('#8E8E93')
            .margin({ top: 40 })
        }
      }
      .width('100%')
      .backgroundColor('#000000')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#000000')
  }
}

5.2 入口Ability(EntryAbility.ets)

这是应用的生命周期管理类,负责加载主页面:

import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(0x0000, 'DiceApp', 'Ability onCreate');
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        hilog.error(0x0000, 'DiceApp', 'Failed to load content');
        return;
      }
      hilog.info(0x0000, 'DiceApp', 'Succeeded in loading content');
    });
  }

  // 其他生命周期方法...
}

六、运行与调试

6.1 构建项目

在 DevEco Studio 中:

  1. 点击菜单 Build > Build Hap(s)/APP(s) > Build Hap(s)
  2. 或使用快捷键构建项目

6.2 运行应用

方式一:模拟器运行
  1. 点击 Tools > Device Manager
  2. 创建或启动一个 Phone 模拟器
  3. 点击运行按钮(绿色三角形)
方式二:真机运行
  1. 连接华为手机(开启开发者模式)
  2. 在设备列表中选择真机
  3. 点击运行

6.3 运行效果展示

运行成功后,可以看到:

在这里插入图片描述

点击"掷骰子"按钮:

在这里插入图片描述


七、核心知识点总结

7.1 ArkUI 声明式UI

@Entry                    // 页面入口装饰器
@Component               // 组件装饰器
@State                   // 状态变量装饰器

7.2 常用组件

组件用途
Column垂直布局容器
Row水平布局容器
Text文本显示
Button按钮
Circle圆形(用于骰子点)
Scroll可滚动容器

7.3 状态驱动UI

ArkUI 采用状态驱动的方式,当 @State 变量改变时,UI会自动刷新:

@State currentValue: number = 1

// 改变状态
this.currentValue = 5  // UI自动更新为5点

7.4 条件渲染

使用 if 语句控制组件的显示/隐藏:

if (this.rollCount > 0) {
  Button('🔄 重置计数')
}

八、扩展思路

这个Demo还可以继续扩展,比如:

  1. 多种骰子类型 - 添加D20、D12等不同面数的骰子
  2. 多骰子模式 - 同时掷多个骰子
  3. 历史记录 - 记录每次的点数
  4. 音效 - 添加掷骰子的音效
  5. 震动反馈 - 利用手机振动增强体验
  6. 自定义主题 - 允许用户切换骰子颜色、背景等

九、常见问题

Q1: setInterval 为什么不停止?

确保在 aboutToDisappear 生命周期中清理定时器,否则可能导致内存泄漏。

Q2: 按钮连续点击会怎样?

代码中通过 if (this.isRolling) return 防止重复触发,同时按钮设置 enabled(!this.isRolling) 禁用状态。

Q3: 如何修改骰子颜色?

修改 backgroundColor('#FF3B30') 中的颜色值即可。


十、总结

通过这个掷骰子App的开发,我学习了:

✅ HarmonyOS项目结构和配置
✅ ArkUI声明式UI开发模式
✅ 状态管理(@State装饰器)
✅ 定时器实现动画效果
✅ 条件渲染和动态UI
✅ 组件生命周期管理

虽然功能简单,但涵盖了鸿蒙开发的核心概念。希望这篇博文能帮助到正在学习鸿蒙开发的小伙伴们!


参考资料


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

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐