从零构建HarmonyOS趣味抽签小程序:API 24全栈开发指南
项目演示



一、项目背景与技术选型
1.1 项目背景
在数字化时代,娱乐类应用成为人们日常生活中不可或缺的一部分。趣味抽签作为一种古老而又充满乐趣的互动形式,通过现代技术的加持,焕发出新的活力。本项目旨在开发一款基于HarmonyOS的趣味抽签小程序,为用户提供轻松愉快的抽签体验。
1.2 技术选型
| 技术维度 | 选择 | 理由 |
|---|---|---|
| 开发框架 | HarmonyOS ArkTS | 华为官方推荐,性能优异,生态完善 |
| API版本 | API 24 | 最新稳定版本,功能丰富,兼容性好 |
| 开发工具 | DevEco Studio 4.1 | 官方IDE,集成开发环境,调试便捷 |
| 构建工具 | Hvigor | HarmonyOS官方构建工具,自动化构建流程 |
1.3 项目目标
- 实现完整的抽签功能,包括摇签动画和签文展示
- 设计美观的UI界面,提升用户体验
- 支持多次抽签,提供娱乐价值
- 代码结构清晰,易于维护和扩展
二、开发环境搭建
2.1 DevEco Studio安装
- 下载DevEco Studio: 访问华为开发者官网,下载最新版本的DevEco Studio 4.1
- 安装DevEco Studio: 运行安装程序,按照向导完成安装
- 配置SDK: 打开DevEco Studio,进入Settings > Appearance & Behavior > System Settings > HarmonyOS SDK,下载API 24相关组件
2.2 项目创建流程
- 打开DevEco Studio: 启动DevEco Studio,点击Start a new HarmonyOS project
- 选择模板: 在模板选择界面,选择Empty Ability模板
- 配置项目:
- Project name: MyApplication6
- Bundle name: com.example.myapplication6
- Save location: 选择合适的存储路径
- Language: ArkTS
- API Level: 24
- 完成创建: 点击Finish按钮,等待项目初始化完成
2.3 项目结构分析
MyApplication6/
├── AppScope/ # 应用全局配置和资源
│ ├── resources/ # 全局资源文件
│ │ └── base/
│ │ ├── element/ # 全局样式元素
│ │ │ └── string.json # 全局字符串资源
│ │ └── media/ # 全局媒体资源
│ └── app.json5 # 应用配置文件
├── entry/ # 主应用模块
│ ├── src/main/
│ │ ├── ets/ # ArkTS源码目录
│ │ │ ├── entryability/ # 应用入口Ability
│ │ │ │ └── EntryAbility.ets
│ │ │ ├── entrybackupability/ # 备份Ability
│ │ │ │ └── EntryBackupAbility.ets
│ │ │ └── pages/ # 页面目录
│ │ │ └── Index.ets # 主页面
│ │ ├── resources/ # 模块资源
│ │ │ ├── base/
│ │ │ │ ├── element/
│ │ │ │ │ ├── color.json # 颜色资源
│ │ │ │ │ ├── float.json # 浮点数值资源
│ │ │ │ │ └── string.json # 字符串资源
│ │ │ │ ├── media/ # 媒体资源
│ │ │ │ └── profile/ # 配置文件
│ │ │ │ ├── backup_config.json
│ │ │ │ └── main_pages.json # 页面路由配置
│ │ │ └── dark/ # 深色模式资源
│ │ │ └── element/
│ │ │ └── color.json
│ │ └── module.json5 # 模块配置文件
│ ├── .preview/ # 预览构建产物
│ ├── build-profile.json5 # 模块构建配置
│ ├── hvigorfile.ts # 模块构建脚本
│ ├── obfuscation-rules.txt # 混淆规则
│ └── oh-package.json5 # 模块依赖配置
├── .hvigor/ # Hvigor构建缓存
├── build-profile.json5 # 项目构建配置
├── hvigorfile.ts # 项目构建脚本
├── oh-package.json5 # 项目依赖配置
├── local.properties # 本地配置
├── code-linter.json5 # 代码检查配置
└── .idea/ # IDE配置
2.4 配置文件详解
AppScope/app.json5:
{
"app": {
"bundleName": "com.example.myapplication6",
"vendor": "example",
"version": {
"code": 1000000,
"name": "1.0.0"
},
"apiVersion": {
"compatible": 24,
"target": 24,
"releaseType": "Release"
}
}
}
entry/build-profile.json5:
{
"apiType": "stageMode",
"buildOption": {
"resOptions": {
"copyCodeResource": {
"enable": false
}
}
},
"buildOptionSet": [
{
"name": "release",
"arkOptions": {
"obfuscation": {
"ruleOptions": {
"enable": false,
"files": ["./obfuscation-rules.txt"]
}
}
}
}
],
"targets": [
{ "name": "default" },
{ "name": "ohosTest" }
]
}
entry/src/main/resources/base/profile/main_pages.json:
{
"pages": [
"pages/Index"
]
}
三、核心功能设计与实现
3.1 功能需求分析
3.1.1 核心功能
- 抽签功能: 用户点击按钮触发抽签流程,随机获取一条签文
- 摇签动画: 签筒摇晃动画效果,增强仪式感和趣味性
- 签文展示: 展示签文等级、内容和星级评分
- 重复抽签: 支持多次抽签,可随时重新抽取
3.1.2 用户体验需求
- 视觉效果: 美观的界面设计,温暖舒适的配色方案
- 交互反馈: 清晰的操作反馈,按钮状态变化
- 动画效果: 流畅的动画效果,提升体验品质
3.2 数据结构设计
3.2.1 签文数据接口
interface SignInfo {
level: string // 签文等级:大吉/中吉/小吉/末吉/凶
content: string // 签文内容,简短通俗,娱乐向
luck: number // 幸运值:1-5星
}
3.2.2 签文数据实现
private signs: SignInfo[] = [
// 大吉(8条)- 最高等级,寓意极好
{ level: '大吉', content: '今天适合摸鱼,老板看不见你', luck: 5 },
{ level: '大吉', content: '出门会遇到贵人,请你喝奶茶', luck: 5 },
{ level: '大吉', content: '彩票中奖概率提升100倍,快去买', luck: 5 },
{ level: '大吉', content: '暗恋的人今天会主动联系你', luck: 5 },
{ level: '大吉', content: '今天的饭特别香,多吃一碗', luck: 5 },
{ level: '大吉', content: '加班?不存在的,准时下班', luck: 5 },
{ level: '大吉', content: '刷视频必刷到超好看的内容', luck: 5 },
{ level: '大吉', content: '请人吃饭必被请回,稳赚', luck: 5 },
// 中吉(8条)- 中高等级,寓意较好
{ level: '中吉', content: '今天运气不错,适合表白', luck: 4 },
{ level: '中吉', content: '快递今天必到,拆箱快乐', luck: 4 },
{ level: '中吉', content: '今天的奶茶会少糖也很好喝', luck: 4 },
{ level: '中吉', content: '开会会提前结束,开心', luck: 4 },
{ level: '中吉', content: '路上会遇到可爱的小动物', luck: 4 },
{ level: '中吉', content: '今天的头发特别柔顺', luck: 4 },
{ level: '中吉', content: '玩游戏会遇到神队友', luck: 4 },
{ level: '中吉', content: '今天适合买买买,钱包不心疼', luck: 4 },
// 小吉(8条)- 中等级别,寓意一般
{ level: '小吉', content: '今天天气不错,心情也好', luck: 3 },
{ level: '小吉', content: '外卖会比预计时间早到', luck: 3 },
{ level: '小吉', content: '今天会听到好听的歌', luck: 3 },
{ level: '小吉', content: '午睡睡得特别香', luck: 3 },
{ level: '小吉', content: '今天穿的衣服很合身', luck: 3 },
{ level: '小吉', content: '会收到有趣的消息', luck: 3 },
{ level: '小吉', content: '今天适合整理房间', luck: 3 },
{ level: '小吉', content: '喝白开水都觉得甜', luck: 3 },
// 末吉(4条)- 中低等级,寓意平平
{ level: '末吉', content: '今天平平淡淡才是真', luck: 2 },
{ level: '末吉', content: '今天适合宅家追剧', luck: 2 },
{ level: '末吉', content: '今天的烦恼都会过去', luck: 2 },
{ level: '末吉', content: '虽然普通但很充实', luck: 2 },
// 凶(6条)- 最低等级,寓意不佳
{ level: '凶', content: '今天不宜冲动消费', luck: 1 },
{ level: '凶', content: '今天适合少说多做', luck: 1 },
{ level: '凶', content: '今天的奶茶可能会踩雷', luck: 1 },
{ level: '凶', content: '开会可能会被点名', luck: 1 },
{ level: '凶', content: '今天不适合做重大决定', luck: 1 },
{ level: '凶', content: '出门记得带伞,可能下雨', luck: 1 }
]
3.3 状态管理设计
3.3.1 状态变量定义
@State isShaking: boolean = false // 是否正在摇晃
@State hasResult: boolean = false // 是否有抽签结果
@State currentSign: SignInfo = { // 当前签文信息
level: '',
content: '',
luck: 0
}
@State shakeCount: number = 0 // 摇晃次数计数
@State rotateAngle: number = 0 // 签筒旋转角度
3.3.2 状态管理原理
在ArkTS中,@State是最基础也是最重要的状态装饰器,其工作原理如下:
- 响应式更新: 当
@State装饰的变量发生变化时,框架会自动触发组件的重新渲染 - 依赖追踪: 框架会追踪哪些UI组件依赖于哪些状态变量
- 局部更新: 只更新依赖变化状态的UI部分,而不是整个组件
- 性能优化: 这种机制避免了不必要的渲染,提高了应用性能
3.4 UI组件实现
3.4.1 主布局结构
build() {
Column() {
// 标题区域
Text('趣味抽签')
.fontSize(32)
.fontWeight(FontWeight.Bold)
.margin({ top: 60, bottom: 40 })
.textAlign(TextAlign.Center)
// 签筒或结果区域
if (!this.hasResult) {
// 签筒展示
this.buildSignContainer()
} else {
// 结果展示
this.buildResultCard()
}
// 抽签按钮
this.buildDrawButton()
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#FFF5E6')
}
3.4.2 签筒容器组件
private buildSignContainer(): void {
Stack({ alignContent: Alignment.Center }) {
Column() {
Text('摇一摇')
.fontSize(24)
.fontWeight(FontWeight.Medium)
.margin({ bottom: 10 })
Text('抽取今日运势')
.fontSize(16)
.opacity(0.6)
}
}
.width(180)
.height(240)
.backgroundColor('#8B4513')
.borderRadius(16)
.shadow({ radius: 10, color: '#8B4513', offsetY: 5 })
.rotate({ angle: this.rotateAngle })
.onClick(() => {
this.shake()
})
.margin({ bottom: 40 })
}
组件设计思路:
- 使用
Stack容器实现签筒的立体效果 - 内部使用
Column垂直排列文字提示 - 设置圆角和阴影增强视觉效果
- 绑定点击事件触发摇晃动画
3.4.3 签文结果卡片组件
private buildResultCard(): void {
Column() {
// 签文等级
Text(this.currentSign.level)
.fontSize(28)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 20 })
.fontColor(this.getLevelColor(this.currentSign.level))
// 签文内容
Text(this.currentSign.content)
.fontSize(20)
.textAlign(TextAlign.Center)
.lineHeight(32)
.margin({ bottom: 30 })
.padding({ left: 20, right: 20 })
// 星级评分
Row() {
ForEach(
this.getLuckStars(this.currentSign.luck),
(_: number, index: number) => {
Text('★')
.fontSize(24)
.fontColor('#FFD700')
.margin({ right: index < this.currentSign.luck - 1 ? 5 : 0 })
},
(_: number, index: number) => index.toString()
)
}
}
.width('80%')
.backgroundColor('#FFF8DC')
.borderRadius(20)
.padding(30)
.shadow({ radius: 15, color: '#D2B48C', offsetY: 8 })
.animation({
duration: 500,
curve: Curve.EaseOut
})
.margin({ bottom: 40 })
}
组件设计思路:
- 使用
Column垂直排列等级、内容和星级 - 等级文字根据等级显示不同颜色
- 内容文字居中显示,设置适当行高
- 星级评分使用
ForEach循环渲染 - 添加入场动画效果增强仪式感
3.4.4 抽签按钮组件
private buildDrawButton(): void {
Button(this.hasResult ? '再抽一签' : '点击抽签')
.width(200)
.height(50)
.fontSize(18)
.backgroundColor('#FF6B6B')
.fontColor('#FFFFFF')
.borderRadius(25)
.shadow({ radius: 8, color: '#FF6B6B', offsetY: 3 })
.onClick(() => {
this.shake()
})
.enabled(!this.isShaking)
}
组件设计思路:
- 根据状态动态改变按钮文字
- 设置圆角和阴影增强视觉效果
- 摇晃时禁用按钮防止重复点击
- 绑定点击事件触发抽签流程
3.5 交互逻辑实现
3.5.1 摇晃动画逻辑
private shake(): void {
if (this.isShaking) return
this.isShaking = true
this.hasResult = false
this.shakeCount = 0
this.rotateAngle = 0
const shakeInterval: number = setInterval(() => {
this.shakeCount++
this.rotateAngle = this.shakeCount % 2 === 0 ? 10 : -10
if (this.shakeCount >= 15) {
clearInterval(shakeInterval)
this.isShaking = false
this.rotateAngle = 0
this.drawSign()
}
}, 80)
}
动画设计原理:
- 初始化状态: 设置
isShaking为true,hasResult为false,重置计数器和角度 - 定时摇晃: 使用
setInterval每80毫秒更新一次旋转角度 - 交替旋转: 通过取模运算实现左右交替摇晃效果(±10度)
- 结束动画: 摇晃15次后停止,重置角度,调用抽签方法
3.5.2 抽签逻辑
private drawSign(): void {
const randomIndex: number = Math.floor(Math.random() * this.signs.length)
this.currentSign = this.signs[randomIndex]
this.hasResult = true
}
抽签算法:
- 生成随机索引: 使用
Math.random()生成0到1之间的随机数,乘以数组长度后向下取整 - 获取签文: 根据随机索引从签文数组中获取对应的签文
- 更新状态: 设置
hasResult为true,触发UI更新显示结果
3.5.3 辅助方法
private getLevelColor(level: string): string {
switch (level) {
case '大吉':
return '#FFD700' // 金色
case '中吉':
return '#FF8C00' // 橙色
case '小吉':
return '#FF6347' // 番茄色
case '末吉':
return '#8B8B8B' // 灰色
case '凶':
return '#4A4A4A' // 深灰色
default:
return '#333333' // 默认黑色
}
}
private getLuckStars(luck: number): number[] {
return Array.from({ length: luck })
}
辅助方法作用:
getLevelColor: 根据签文等级返回对应的颜色值getLuckStars: 根据幸运值生成对应长度的数组,用于渲染星星
四、API 24核心特性详解
4.1 @State状态装饰器
4.1.1 基本用法
@State是ArkTS中最常用的状态装饰器,用于管理组件内部状态:
@State count: number = 0
@State message: string = 'Hello'
@State isActive: boolean = false
4.1.2 工作机制
- 状态绑定:
@State装饰的变量会与组件的UI建立绑定关系 - 响应式更新: 当变量值发生变化时,框架会自动更新依赖该变量的UI部分
- 单向数据流: 状态变化只能由组件内部触发,外部无法直接修改
4.1.3 使用场景
@State count: number = 0
build() {
Column() {
Text(`计数: ${this.count}`)
.fontSize(20)
Button('增加')
.onClick(() => {
this.count++
})
}
}
4.2 @Entry和@Component装饰器
4.2.1 @Entry
@Entry标识一个组件作为页面的入口,每个页面只能有一个@Entry组件:
@Entry
@Component
struct Index {
build() {
Column() {
Text('首页')
}
}
}
4.2.2 @Component
@Component标识一个可复用的组件,可以在其他组件中引用:
@Component
struct MyButton {
build() {
Button('点击我')
}
}
4.3 Column布局容器
4.3.1 基本用法
Column用于垂直排列子组件:
Column() {
Text('第一行')
Text('第二行')
Text('第三行')
}
4.3.2 常用属性
| 属性 | 类型 | 说明 |
|---|---|---|
| width | string | number | 宽度 |
| height | string | number | 高度 |
| justifyContent | FlexAlign | 主轴对齐方式 |
| alignItems | ItemAlign | 交叉轴对齐方式 |
| backgroundColor | ResourceColor | 背景色 |
| padding | Padding | 内边距 |
| margin | Margin | 外边距 |
4.3.3 对齐方式
Column() {
Text('内容')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center) // 垂直居中
.alignItems(ItemAlign.Center) // 水平居中
FlexAlign取值:
FlexAlign.Start: 起始位置对齐FlexAlign.Center: 居中对齐FlexAlign.End: 结束位置对齐FlexAlign.SpaceBetween: 两端对齐,间距均匀FlexAlign.SpaceAround: 间距均匀分布FlexAlign.SpaceEvenly: 间距相等
4.4 Row布局容器
4.4.1 基本用法
Row用于水平排列子组件:
Row() {
Text('左侧')
Text('右侧')
}
4.4.2 常用属性
与Column类似,但对齐方向不同:
Row() {
Text('内容')
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween) // 水平两端对齐
.alignItems(ItemAlign.Center) // 垂直居中
4.5 Stack布局容器
4.5.1 基本用法
Stack用于堆叠子组件,后添加的组件会覆盖在前面组件之上:
Stack() {
Text('底层')
.width(200)
.height(200)
.backgroundColor(Color.Red)
Text('顶层')
.width(100)
.height(100)
.backgroundColor(Color.Blue)
}
4.5.2 对齐方式
Stack({ alignContent: Alignment.Center }) {
Text('居中')
}
Alignment取值:
Alignment.TopStart: 左上角Alignment.TopCenter: 顶部居中Alignment.TopEnd: 右上角Alignment.CenterStart: 左侧居中Alignment.Center: 正中心Alignment.CenterEnd: 右侧居中Alignment.BottomStart: 左下角Alignment.BottomCenter: 底部居中Alignment.BottomEnd: 右下角
4.6 Text组件
4.6.1 基本用法
Text用于显示文本内容:
Text('Hello World')
4.6.2 常用属性
| 属性 | 类型 | 说明 |
|---|---|---|
| fontSize | number | 字体大小 |
| fontWeight | number | FontWeight | 字体粗细 |
| fontColor | ResourceColor | 字体颜色 |
| textAlign | TextAlign | 文本对齐方式 |
| lineHeight | number | 行高 |
| opacity | number | 透明度(0-1) |
| maxLines | number | 最大行数 |
| decoration | TextDecoration | 文字装饰 |
4.6.3 字体粗细
Text('粗体文字')
.fontWeight(FontWeight.Bold)
Text('中等粗细')
.fontWeight(FontWeight.Medium)
Text('自定义粗细')
.fontWeight(500)
FontWeight枚举:
FontWeight.Lighter: 300FontWeight.Normal: 400FontWeight.Medium: 500FontWeight.Bold: 700FontWeight.Bolder: 800
4.7 Button组件
4.7.1 基本用法
Button用于创建可点击的按钮:
Button('点击我')
4.7.2 常用属性
| 属性 | 类型 | 说明 |
|---|---|---|
| width | string | number | 宽度 |
| height | string | number | 高度 |
| fontSize | number | 文字大小 |
| fontColor | ResourceColor | 文字颜色 |
| backgroundColor | ResourceColor | 背景色 |
| borderRadius | number | 圆角大小 |
| enabled | boolean | 是否可用 |
| stateEffect | boolean | 是否开启按压效果 |
4.7.3 状态管理
@State isEnabled: boolean = true
build() {
Button('点击我')
.enabled(this.isEnabled)
.onClick(() => {
this.isEnabled = false
})
}
4.8 ForEach循环渲染
4.8.1 基本用法
ForEach用于循环渲染列表数据:
ForEach(
['苹果', '香蕉', '橙子'],
(item: string) => {
Text(item)
}
)
4.8.2 完整参数
ForEach(
this.dataList, // 数据源
(item: string, index: number) => { // 生成函数
Text(`${index}: ${item}`)
},
(item: string, index: number) => { // key生成函数
return `${index}-${item}`
}
)
4.8.3 key生成的重要性
在ArkTS中,ForEach的第三个参数(key生成函数)非常重要,它用于标识每个元素的唯一性,帮助框架优化列表更新性能。如果不提供key生成函数,框架可能会错误地复用组件实例,导致渲染错误。
4.9 rotate变换
4.9.1 基本用法
rotate用于旋转组件:
Text('旋转文字')
.rotate({ angle: 45 })
4.9.2 RotateOptions接口
interface RotateOptions {
angle: number // 旋转角度(度)
centerX?: number // 旋转中心X坐标
centerY?: number // 旋转中心Y坐标
}
4.9.3 API 24注意事项
在API 24中,rotate方法必须使用RotateOptions对象,且angle属性是必需的:
// 正确写法
.rotate({ angle: this.rotateAngle })
// 错误写法(API 24不支持)
.rotate({ z: this.rotateAngle })
.rotate(this.rotateAngle)
4.10 shadow阴影效果
4.10.1 基本用法
shadow用于为组件添加阴影效果:
Text('带阴影的文字')
.shadow({ radius: 5, color: Color.Gray })
4.10.2 ShadowOptions接口
interface ShadowOptions {
radius: number // 阴影模糊半径
color?: ResourceColor // 阴影颜色
offsetX?: number // X轴偏移量
offsetY?: number // Y轴偏移量
}
4.10.3 阴影效果示例
Button('带阴影的按钮')
.width(150)
.height(50)
.borderRadius(25)
.backgroundColor('#FF6B6B')
.shadow({
radius: 8,
color: '#FF6B6B',
offsetX: 0,
offsetY: 3
})
4.11 animation动画效果
4.11.1 基本用法
animation用于为组件添加动画效果:
Text('带动画的文字')
.animation({
duration: 500,
curve: Curve.EaseOut
})
4.11.2 AnimationOptions接口
interface AnimationOptions {
duration?: number // 动画持续时间(毫秒)
curve?: Curve // 动画曲线
delay?: number // 延迟时间(毫秒)
iterations?: number // 重复次数(-1表示无限循环)
playMode?: PlayMode // 播放模式
}
4.11.3 动画曲线
Text('动画示例')
.animation({
duration: 1000,
curve: Curve.EaseOut // 缓出效果
})
Curve枚举:
Curve.Linear: 线性曲线Curve.Ease: 缓入缓出Curve.EaseIn: 缓入Curve.EaseOut: 缓出Curve.EaseInOut: 缓入缓出Curve.FastOutSlowIn: 快速开始慢速结束Curve.LinearOutSlowIn: 线性开始慢速结束
4.11.4 播放模式
Text('循环动画')
.animation({
duration: 500,
iterations: -1,
playMode: PlayMode.Alternate // 交替播放
})
PlayMode枚举:
PlayMode.Normal: 正常播放PlayMode.Alternate: 交替播放(正向→反向→正向…)PlayMode.Reverse: 反向播放
4.12 onClick事件
4.12.1 基本用法
onClick用于绑定点击事件:
Button('点击我')
.onClick(() => {
console.log('按钮被点击了')
})
4.12.2 事件参数
Button('点击我')
.onClick((event: ClickEvent) => {
console.log(`点击位置: (${event.x}, ${event.y})`)
})
ClickEvent属性:
x: 点击位置的X坐标y: 点击位置的Y坐标timestamp: 点击时间戳
五、UI设计与用户体验优化
5.1 色彩设计方案
5.1.1 主色调选择
本项目采用温暖舒适的配色方案,营造轻松愉快的氛围:
| 元素 | 颜色 | 十六进制 | 说明 |
|---|---|---|---|
| 背景色 | 奶油色 | #FFF5E6 | 温暖舒适,适合娱乐应用 |
| 签筒色 | 棕色 | #8B4513 | 模拟木质签筒,复古风格 |
| 按钮色 | 珊瑚色 | #FF6B6B | 醒目吸引,易于点击 |
| 卡片色 | 玉米色 | #FFF8DC | 温馨复古,签文展示 |
5.1.2 等级颜色映射
不同签文等级使用不同颜色,帮助用户快速识别:
| 等级 | 颜色 | 十六进制 | 寓意 |
|---|---|---|---|
| 大吉 | 金色 | #FFD700 | 吉祥富贵,最高等级 |
| 中吉 | 橙色 | #FF8C00 | 积极向上,中高等级 |
| 小吉 | 番茄色 | #FF6347 | 活力四射,中等级别 |
| 末吉 | 灰色 | #8B8B8B | 平淡普通,中低等级 |
| 凶 | 深灰色 | #4A4A4A | 谨慎提醒,最低等级 |
5.1.3 颜色心理学
颜色选择并非随意,而是基于颜色心理学的原理:
- 金色: 象征财富、好运、尊贵,适合表示大吉
- 橙色: 象征热情、活力、温暖,适合表示中吉
- 红色: 象征能量、激情、喜庆,适合表示小吉
- 灰色: 象征平静、稳重、中性,适合表示末吉
- 深灰色: 象征严肃、谨慎、提醒,适合表示凶
5.2 动画效果设计
5.2.1 摇晃动画参数
| 参数 | 值 | 说明 |
|---|---|---|
| 旋转角度 | ±10度 | 模拟真实摇签筒的幅度 |
| 摇晃次数 | 15次 | 足够的摇晃次数增强仪式感 |
| 间隔时间 | 80毫秒 | 适中的频率,不过快也不过慢 |
| 总时长 | 1.2秒 | 合理的动画时长 |
5.2.2 结果出现动画
| 参数 | 值 | 说明 |
|---|---|---|
| 持续时间 | 500毫秒 | 快速但平滑的入场效果 |
| 动画曲线 | EaseOut | 缓出效果,自然流畅 |
5.2.3 动画实现原理
摇晃动画使用setInterval实现,每次改变旋转角度:
const shakeInterval: number = setInterval(() => {
this.shakeCount++
this.rotateAngle = this.shakeCount % 2 === 0 ? 10 : -10
if (this.shakeCount >= 15) {
clearInterval(shakeInterval)
this.isShaking = false
this.rotateAngle = 0
this.drawSign()
}
}, 80)
动画原理:
- 使用取模运算实现左右交替摇晃
- 每次间隔80毫秒,频率适中
- 摇晃15次后停止,确保足够的仪式感
- 停止后重置角度并调用抽签方法
5.3 用户体验优化
5.3.1 按钮状态管理
Button(this.hasResult ? '再抽一签' : '点击抽签')
.enabled(!this.isShaking)
优化点:
- 摇晃时禁用按钮,防止重复点击
- 根据状态动态改变按钮文字
- 禁用状态下按钮自动变灰,提供视觉反馈
5.3.2 视觉反馈
Text(this.currentSign.level)
.fontColor(this.getLevelColor(this.currentSign.level))
优化点:
- 签文等级使用不同颜色,一目了然
- 星级评分使用金色星星,直观展示幸运程度
- 签文卡片添加阴影和圆角,增强层次感
5.3.3 操作引导
Stack({ alignContent: Alignment.Center }) {
Column() {
Text('摇一摇')
.fontSize(24)
.fontWeight(FontWeight.Medium)
.margin({ bottom: 10 })
Text('抽取今日运势')
.fontSize(16)
.opacity(0.6)
}
}
优化点:
- 清晰的文字提示引导用户操作
- 主标题和副标题层次分明
- 适当的透明度区分主次
六、常见问题与解决方案
6.1 构建错误:backgroundGradient不存在
6.1.1 错误信息
Property 'backgroundGradient' does not exist on type 'StackAttribute'.
Property 'backgroundGradient' does not exist on type 'ButtonAttribute'.
Property 'backgroundGradient' does not exist on type 'ColumnAttribute'.
6.1.2 问题原因
在API 24中,Stack、Button、Column等组件不支持backgroundGradient属性。这是因为HarmonyOS在不同API版本中对组件属性的支持有所变化,backgroundGradient可能在更高版本或更低版本中支持,但在API 24中不可用。
6.1.3 解决方案
改用backgroundColor纯色背景替代渐变背景:
// 错误写法
.backgroundGradient({
direction: GradientDirection.BottomTop,
colors: ['#8B4513', '#D2691E']
})
// 正确写法
.backgroundColor('#8B4513')
6.1.4 替代方案
如果需要渐变效果,可以使用LinearGradient组件作为背景:
Stack() {
LinearGradient({
colors: ['#8B4513', '#D2691E'],
direction: GradientDirection.Bottom
})
.width('100%')
.height('100%')
// 内容组件
Column() {
Text('摇一摇')
}
}
6.2 构建错误:GradientDirection.BottomTop不存在
6.2.1 错误信息
Property 'BottomTop' does not exist on type 'typeof GradientDirection'.
Did you mean 'Bottom'?
6.2.2 问题原因
API 24中GradientDirection枚举的取值与其他版本不同,BottomTop在API 24中不存在。
6.2.3 解决方案
查看API 24支持的GradientDirection枚举值:
// API 24支持的枚举值
GradientDirection.Left
GradientDirection.Right
GradientDirection.Top
GradientDirection.Bottom
6.3 构建错误:rotate参数类型错误
6.3.1 错误信息
No overload matches this call.
Overload 1 of 3, '(value: RotateOptions): StackAttribute', gave the following error.
Argument of type '{ z: number; }' is not assignable to parameter of type 'RotateOptions'.
Property 'angle' is missing in type '{ z: number; }' but required in type 'RotateOptions'.
6.3.2 问题原因
API 24中rotate方法必须使用RotateOptions对象,且必须包含angle属性。使用{ z: angle }或直接传入数字都是不正确的。
6.3.3 解决方案
使用正确的RotateOptions格式:
// 错误写法
.rotate({ z: this.rotateAngle })
.rotate(this.rotateAngle)
// 正确写法
.rotate({ angle: this.rotateAngle })
6.4 构建错误:any/unknown类型问题
6.4.1 错误信息
Use explicit types instead of "any", "unknown" (arkts-no-any-unknown)
6.4.2 问题原因
ArkTS是一种强类型语言,要求所有变量和参数都必须显式声明类型。当ForEach的回调函数参数没有声明类型时,编译器会默认推断为any类型,这在严格模式下会报错。
6.4.3 解决方案
为ForEach的回调参数添加显式类型声明:
// 错误写法
ForEach(this.getLuckStars(...), (_, index) => {
Text('★')
})
// 正确写法
ForEach(
this.getLuckStars(...),
(_: number, index: number) => {
Text('★')
},
(_: number, index: number) => index.toString()
)
6.5 构建缓存问题
6.5.1 问题现象
修改代码后构建仍然报旧错误,或者修改不生效。
6.5.2 问题原因
Hvigor构建系统会缓存构建产物,当缓存没有正确更新时,会导致旧的编译结果仍然被使用。
6.5.3 解决方案
方法一:使用DevEco Studio清理
- 点击Build > Clean Project
- 等待清理完成后重新构建
方法二:手动清理缓存
删除以下目录:
E:\MyApplication6\.hvigor\cacheE:\MyApplication6\entry\.preview
方法三:使用命令行清理
hvigorw clean
6.6 动画效果不生效
6.6.1 问题现象
摇晃动画或结果出现动画没有效果。
6.6.2 问题原因
- 状态变量没有正确更新
- 动画配置不正确
- 组件没有正确绑定动画
6.6.3 解决方案
检查状态更新:
// 确保状态变量被正确更新
private shake(): void {
this.isShaking = true
// ...
this.rotateAngle = this.shakeCount % 2 === 0 ? 10 : -10
}
检查动画配置:
// 确保动画配置正确
.animation({
duration: 500,
curve: Curve.EaseOut
})
检查组件绑定:
// 确保动画绑定在正确的组件上
Column() {
// ...
}
.animation({ duration: 500, curve: Curve.EaseOut })
七、代码优化与最佳实践
7.1 代码组织优化
7.1.1 方法拆分
将复杂的构建逻辑拆分为独立方法,提高代码可读性:
build() {
Column() {
this.buildTitle()
if (!this.hasResult) {
this.buildSignContainer()
} else {
this.buildResultCard()
}
this.buildDrawButton()
}
// ...
}
private buildTitle(): void {
Text('趣味抽签')
.fontSize(32)
.fontWeight(FontWeight.Bold)
.margin({ top: 60, bottom: 40 })
.textAlign(TextAlign.Center)
}
private buildSignContainer(): void {
// 签筒容器实现
}
private buildResultCard(): void {
// 结果卡片实现
}
private buildDrawButton(): void {
// 按钮实现
}
7.1.2 状态分组
将相关状态变量分组,提高代码组织性:
// 动画相关状态
@State isShaking: boolean = false
@State shakeCount: number = 0
@State rotateAngle: number = 0
// 结果相关状态
@State hasResult: boolean = false
@State currentSign: SignInfo = { level: '', content: '', luck: 0 }
7.2 性能优化
7.2.1 减少重复渲染
使用@State最小化渲染范围,只更新必要的UI部分:
@State count: number = 0
build() {
Column() {
Text(`计数: ${this.count}`) // 只更新此部分
Text('固定文本') // 不更新
}
}
7.2.2 优化动画性能
使用硬件加速的动画效果,避免过度绘制:
// 使用transform属性而非改变布局属性
.rotate({ angle: this.rotateAngle }) // 硬件加速
// 避免
.width(this.dynamicWidth) // 触发重排
7.2.3 优化列表渲染
为ForEach提供高效的key生成函数:
ForEach(
this.items,
(item) => Text(item.name),
(item) => item.id // 使用唯一ID作为key
)
7.3 代码规范
7.3.1 命名规范
使用驼峰命名法,变量名语义化:
// 正确
@State isShaking: boolean = false
@State rotateAngle: number = 0
// 错误
@State a: boolean = false
@State b: number = 0
7.3.2 格式规范
统一缩进风格,适当的空行分隔:
// 正确
build() {
Column() {
Text('标题')
.fontSize(20)
Button('点击')
.onClick(() => {
// 逻辑
})
}
}
// 错误(缺少空行)
build() {
Column() {
Text('标题').fontSize(20)
Button('点击').onClick(() => {})
}
}
7.3.3 类型规范
所有变量必须显式声明类型,避免使用any:
// 正确
const name: string = '张三'
const age: number = 25
// 错误
const name = '张三' // 隐式推断
const data: any = {} // 使用any
7.4 错误处理
7.4.1 边界检查
在关键逻辑中添加边界检查:
private drawSign(): void {
if (this.signs.length === 0) {
console.error('签文数据为空')
return
}
const randomIndex: number = Math.floor(Math.random() * this.signs.length)
this.currentSign = this.signs[randomIndex]
this.hasResult = true
}
7.4.2 异常捕获
使用try-catch捕获可能的异常:
private shake(): void {
try {
if (this.isShaking) return
this.isShaking = true
this.hasResult = false
this.shakeCount = 0
this.rotateAngle = 0
const shakeInterval: number = setInterval(() => {
this.shakeCount++
this.rotateAngle = this.shakeCount % 2 === 0 ? 10 : -10
if (this.shakeCount >= 15) {
clearInterval(shakeInterval)
this.isShaking = false
this.rotateAngle = 0
this.drawSign()
}
}, 80)
} catch (error) {
console.error('摇晃动画失败:', error)
this.isShaking = false
}
}
八、部署与发布
8.1 构建HAP包
8.1.1 构建Debug版本
- 打开DevEco Studio
- 点击Build > Build HAP(s) / APP(s) > Build Debug HAP
- 等待构建完成
- 在
entry/build/outputs/default/debug目录下找到生成的HAP包
8.1.2 构建Release版本
- 点击Build > Build HAP(s) / APP(s) > Build Release HAP
- 等待构建完成
- 在
entry/build/outputs/default/release目录下找到生成的HAP包
8.2 签名配置
8.2.1 创建签名配置
- 打开Project Structure(快捷键Ctrl+Alt+Shift+S)
- 选择Modules > Signing Configs
- 点击+按钮创建签名配置
- 填写配置信息:
- Name: release
- Key store path: 选择或创建密钥库文件
- Key alias: 密钥别名
- Key store password: 密钥库密码
- Key password: 密钥密码
8.2.2 配置构建类型
- 在Project Structure中选择Modules > Build Types
- 选择release构建类型
- 在Signing Config下拉菜单中选择创建的签名配置
- 点击OK保存配置
8.2.3 手动配置签名
在entry/build-profile.json5中手动配置签名:
{
"apiType": "stageMode",
"buildOption": {
"signingConfig": {
"storeFile": "keystore.jks",
"storePassword": "password",
"keyAlias": "alias",
"keyPassword": "password"
}
}
}
8.3 应用发布
8.3.1 准备发布材料
- 应用图标: 准备不同尺寸的应用图标
- 应用截图: 准备应用界面截图
- 应用描述: 编写应用功能描述和使用说明
- 隐私声明: 编写隐私政策声明
8.3.2 上传到应用市场
- 访问HarmonyOS应用市场
- 登录开发者账号
- 点击"发布应用"
- 填写应用信息:
- 应用名称
- 应用描述
- 应用分类
- 目标用户
- 上传应用包(HAP文件)
- 上传应用截图和图标
- 提交审核
8.3.3 审核流程
- 自动审核: 系统自动检查应用包的安全性和合规性
- 人工审核: 审核人员检查应用内容和功能
- 审核结果: 审核通过或拒绝,并给出反馈
- 发布上线: 审核通过后应用上线发布
九、功能扩展与进阶优化
9.1 功能扩展
9.1.1 历史记录功能
使用Preferences存储用户的抽签历史:
import { Preferences } from '@kit.AppPreferencesKit'
private preferences: Preferences | null = null
async initPreferences(): Promise<void> {
this.preferences = await Preferences.getPreferences(this.context, 'sign_history')
}
async saveSignHistory(sign: SignInfo): Promise<void> {
if (!this.preferences) return
const history: SignInfo[] = await this.preferences.get('history', [])
history.unshift({ ...sign, timestamp: Date.now() })
// 只保留最近20条记录
if (history.length > 20) {
history.pop()
}
await this.preferences.put('history', history)
await this.preferences.flush()
}
9.1.2 分享功能
使用系统分享能力分享签文:
import { share } from '@kit.ShareKit'
private shareSign(sign: SignInfo): void {
share({
title: '趣味抽签',
content: `我抽到了【${sign.level}】:${sign.content}`,
shareType: ShareType.Text
})
}
9.1.3 语音播报功能
使用语音合成功能播报签文:
import { speechSynthesis } from '@kit.SpeechKit'
private speakSign(sign: SignInfo): void {
speechSynthesis.speak({
text: `恭喜你抽到了${sign.level},签文内容是:${sign.content}`,
language: 'zh-CN',
rate: 1.0
})
}
9.1.4 主题切换功能
支持多种主题风格:
@State currentTheme: string = 'warm'
private themes: Record<string, ThemeConfig> = {
warm: {
background: '#FFF5E6',
container: '#8B4513',
button: '#FF6B6B',
card: '#FFF8DC'
},
cool: {
background: '#E6F7FF',
container: '#1890FF',
button: '#52C41A',
card: '#E6FFFB'
},
dark: {
background: '#1F1F1F',
container: '#4A4A4A',
button: '#FF6B6B',
card: '#3A3A3A'
}
}
private applyTheme(): void {
const theme = this.themes[this.currentTheme]
// 应用主题颜色
}
9.2 技术进阶
9.2.1 状态管理进阶
使用@Provide和@Consume进行跨组件状态管理:
// 父组件
@Provide currentSign: SignInfo = { level: '', content: '', luck: 0 }
// 子组件
@Consume currentSign: SignInfo
build() {
Text(`当前签文:${this.currentSign.content}`)
}
9.2.2 路由导航
添加页面路由和导航功能:
import { router } from '@kit.CoreRouterKit'
private navigateToHistory(): void {
router.pushUrl({
url: 'pages/History'
})
}
9.2.3 网络请求
从服务器获取签文数据:
import { http } from '@kit.NetworkKit'
async fetchSigns(): Promise<void> {
try {
const response = await http.request({
method: http.RequestMethod.GET,
url: 'https://api.example.com/signs'
})
if (response.responseCode === 200) {
const data = JSON.parse(response.result as string)
this.signs = data.signs
}
} catch (error) {
console.error('获取签文失败:', error)
}
}
十、总结与展望
10.1 项目总结
本项目成功实现了一个基于HarmonyOS API 24的趣味抽签小程序,主要完成了以下工作:
- 项目架构设计: 设计了清晰的组件结构和数据结构
- 核心功能实现: 实现了签文数据、摇晃动画、抽签逻辑和结果展示
- UI设计: 采用温暖舒适的配色方案,设计了美观的界面
- 用户体验优化: 添加了动画效果、状态管理和操作引导
- 问题解决: 解决了API 24兼容性问题和构建错误
10.2 技术收获
通过本项目的开发,深入学习了以下技术:
- ArkTS基础: 掌握了ArkTS的基本语法和组件使用
- 状态管理: 理解了
@State装饰器的工作原理 - 布局系统: 熟练使用
Column、Row、Stack布局容器 - 动画效果: 掌握了
rotate、animation等动画API - 构建系统: 了解了Hvigor构建工具的使用和常见问题
10.3 未来展望
本项目还有很多可以扩展和优化的地方:
- 功能扩展: 添加历史记录、分享、语音播报等功能
- 技术升级: 使用更高级的状态管理和网络请求
- 性能优化: 进一步优化动画性能和渲染效率
- 用户体验: 添加更多交互效果和主题切换
附录:完整代码
以下是趣味抽签小程序的完整代码:
[Index.ets](file:///E:/MyApplication6/entry/src/main/ets/pages/Index.ets)
@Entry
@Component
struct Index {
@State isShaking: boolean = false
@State hasResult: boolean = false
@State currentSign: SignInfo = { level: '', content: '', luck: 0 }
@State shakeCount: number = 0
@State rotateAngle: number = 0
private signs: SignInfo[] = [
{ level: '大吉', content: '今天适合摸鱼,老板看不见你', luck: 5 },
{ level: '大吉', content: '出门会遇到贵人,请你喝奶茶', luck: 5 },
{ level: '大吉', content: '彩票中奖概率提升100倍,快去买', luck: 5 },
{ level: '大吉', content: '暗恋的人今天会主动联系你', luck: 5 },
{ level: '大吉', content: '今天的饭特别香,多吃一碗', luck: 5 },
{ level: '大吉', content: '加班?不存在的,准时下班', luck: 5 },
{ level: '大吉', content: '刷视频必刷到超好看的内容', luck: 5 },
{ level: '大吉', content: '请人吃饭必被请回,稳赚', luck: 5 },
{ level: '中吉', content: '今天运气不错,适合表白', luck: 4 },
{ level: '中吉', content: '快递今天必到,拆箱快乐', luck: 4 },
{ level: '中吉', content: '今天的奶茶会少糖也很好喝', luck: 4 },
{ level: '中吉', content: '开会会提前结束,开心', luck: 4 },
{ level: '中吉', content: '路上会遇到可爱的小动物', luck: 4 },
{ level: '中吉', content: '今天的头发特别柔顺', luck: 4 },
{ level: '中吉', content: '玩游戏会遇到神队友', luck: 4 },
{ level: '中吉', content: '今天适合买买买,钱包不心疼', luck: 4 },
{ level: '小吉', content: '今天天气不错,心情也好', luck: 3 },
{ level: '小吉', content: '外卖会比预计时间早到', luck: 3 },
{ level: '小吉', content: '今天会听到好听的歌', luck: 3 },
{ level: '小吉', content: '午睡睡得特别香', luck: 3 },
{ level: '小吉', content: '今天穿的衣服很合身', luck: 3 },
{ level: '小吉', content: '会收到有趣的消息', luck: 3 },
{ level: '小吉', content: '今天适合整理房间', luck: 3 },
{ level: '小吉', content: '喝白开水都觉得甜', luck: 3 },
{ level: '末吉', content: '今天平平淡淡才是真', luck: 2 },
{ level: '末吉', content: '今天适合宅家追剧', luck: 2 },
{ level: '末吉', content: '今天的烦恼都会过去', luck: 2 },
{ level: '末吉', content: '虽然普通但很充实', luck: 2 },
{ level: '凶', content: '今天不宜冲动消费', luck: 1 },
{ level: '凶', content: '今天适合少说多做', luck: 1 },
{ level: '凶', content: '今天的奶茶可能会踩雷', luck: 1 },
{ level: '凶', content: '开会可能会被点名', luck: 1 },
{ level: '凶', content: '今天不适合做重大决定', luck: 1 },
{ level: '凶', content: '出门记得带伞,可能下雨', luck: 1 }
]
build() {
Column() {
Text('趣味抽签')
.fontSize(32)
.fontWeight(FontWeight.Bold)
.margin({ top: 60, bottom: 40 })
.textAlign(TextAlign.Center)
if (!this.hasResult) {
Stack({ alignContent: Alignment.Center }) {
Column() {
Text('摇一摇')
.fontSize(24)
.fontWeight(FontWeight.Medium)
.margin({ bottom: 10 })
Text('抽取今日运势')
.fontSize(16)
.opacity(0.6)
}
}
.width(180)
.height(240)
.backgroundColor('#8B4513')
.borderRadius(16)
.shadow({ radius: 10, color: '#8B4513', offsetY: 5 })
.rotate({ angle: this.rotateAngle })
.onClick(() => {
this.shake()
})
.margin({ bottom: 40 })
}
if (this.hasResult) {
Column() {
Text(this.currentSign.level)
.fontSize(28)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 20 })
.fontColor(this.getLevelColor(this.currentSign.level))
Text(this.currentSign.content)
.fontSize(20)
.textAlign(TextAlign.Center)
.lineHeight(32)
.margin({ bottom: 30 })
.padding({ left: 20, right: 20 })
Row() {
ForEach(
this.getLuckStars(this.currentSign.luck),
(_: number, index: number) => {
Text('★')
.fontSize(24)
.fontColor('#FFD700')
.margin({ right: index < this.currentSign.luck - 1 ? 5 : 0 })
},
(_: number, index: number) => index.toString()
)
}
}
.width('80%')
.backgroundColor('#FFF8DC')
.borderRadius(20)
.padding(30)
.shadow({ radius: 15, color: '#D2B48C', offsetY: 8 })
.animation({
duration: 500,
curve: Curve.EaseOut
})
.margin({ bottom: 40 })
}
Button(this.hasResult ? '再抽一签' : '点击抽签')
.width(200)
.height(50)
.fontSize(18)
.backgroundColor('#FF6B6B')
.fontColor('#FFFFFF')
.borderRadius(25)
.shadow({ radius: 8, color: '#FF6B6B', offsetY: 3 })
.onClick(() => {
this.shake()
})
.enabled(!this.isShaking)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#FFF5E6')
}
private shake(): void {
if (this.isShaking) return
this.isShaking = true
this.hasResult = false
this.shakeCount = 0
this.rotateAngle = 0
const shakeInterval: number = setInterval(() => {
this.shakeCount++
this.rotateAngle = this.shakeCount % 2 === 0 ? 10 : -10
if (this.shakeCount >= 15) {
clearInterval(shakeInterval)
this.isShaking = false
this.rotateAngle = 0
this.drawSign()
}
}, 80)
}
private drawSign(): void {
const randomIndex: number = Math.floor(Math.random() * this.signs.length)
this.currentSign = this.signs[randomIndex]
this.hasResult = true
}
private getLevelColor(level: string): string {
switch (level) {
case '大吉':
return '#FFD700'
case '中吉':
return '#FF8C00'
case '小吉':
return '#FF6347'
case '末吉':
return '#8B8B8B'
case '凶':
return '#4A4A4A'
default:
return '#333333'
}
}
private getLuckStars(luck: number): number[] {
return Array.from({ length: luck })
}
}
interface SignInfo {
level: string
content: string
luck: number
}
参考文献
更多推荐



所有评论(0)