鸿蒙开发学习Day02-生肖卡抽奖
知识点核心说明接口定义通过约束列表项数据结构(url+count),提升类型安全性组件装饰器@Entry标记入口组件、@Component声明自定义组件,是 ArkTS 组件开发基础状态管理@State定义响应式变量(如 randomIndex/maskOpacity),状态变更自动刷新 UI通过interface// 约束列表项数据结构:图片地址+计数url: string // 图片资源地址c
·
一、基础信息
| 项目 | 内容 |
|---|---|
| 学习日期 | 2026-02-07 |
| 学习主题 | 基于 ArkTS 实现生肖卡抽奖组件开发 |
| DevEco Studio 版本 | 6.0.0.103 release |
| 鸿蒙 API 版本 | API 20 |
| 运行环境 | Windows 11 |
| 学习目标 | 1. 掌握 ArkTS 中 Grid/Stack 布局的实战用法;2. 理解 @State 状态管理与 UI 联动的核心逻辑;3. 掌握遮罩层、动画效果的移动端实现方式;4. 熟悉 ArkTS 数组操作与类型约束的最佳实践 |
二、核心学习内容
本次学习围绕生肖卡抽奖组件开发展开,核心掌握以下技术点:
- 布局类:Grid 网格布局(2 行 3 列表格排版)、Stack 堆叠布局(多层级遮罩实现)
- 样式类:opacity 透明度控制、scale 缩放效果、zIndex 层级管理
- 交互类:Button 点击事件、Math.random () 随机数生成
- 动效类:animation 动画配置(时长、过渡效果)
- 逻辑类:数组遍历 / 更新、条件渲染、状态驱动 UI 刷新
三、关键知识点总结
3.1 ArkTS 基础语法与装饰器
| 知识点 | 核心说明 |
|---|---|
| 接口定义 | 通过interface ImageCount约束列表项数据结构(url+count),提升类型安全性 |
| 组件装饰器 | @Entry标记入口组件、@Component声明自定义组件,是 ArkTS 组件开发基础 |
| 状态管理 |
|
3.2 布局与基础组件使用
- Stack 堆叠布局:核心用于实现 “底层内容 + 中层抽卡遮罩 + 顶层大奖遮罩” 的层级覆盖,是移动端弹层 / 遮罩的核心布局方案。
- Grid/GridItem 网格布局:通过
columnsTemplate('1fr 1fr 1fr')和rowsTemplate('1fr 1fr')快速实现 2 行 3 列的卡片排版,适配性强且布局灵活。 - 基础组件组合:
Badge:配置位置(RightTop)、大小、颜色,实现卡片右上角计数展示;Image:通过$r()加载本地媒体资源,配合 scale 实现缩放动效;Button/Text:完成交互触发与文本展示,覆盖基础 UI 场景。
3.3 交互与逻辑处理
- 事件处理:
Button的onClick事件串联抽卡、收下卡片、重置游戏全流程,是用户操作与业务逻辑的核心桥梁。 - 随机数生成:
Math.floor(Math.random()*6)生成 0-5 的随机整数,实现抽卡的随机性,符合抽奖场景需求。 - 数组操作规范:
- 遍历渲染:
ForEach遍历images数组渲染 Grid 列表,是 ArkTS 遍历渲染的标准方式; - 数据更新:遵循 “不可变原则”,通过替换整个对象(
this.images[index] = {...})更新数组项,避免 UI 刷新异常; - 逻辑判断:遍历数组判断
count是否均 > 0,通过flag标记 +break提前终止循环,提升执行效率。
- 遍历渲染:
3.4 动画与条件渲染
- 动画效果:通过
animation属性配置时长(如 500ms),结合scale(图片缩放)、opacity(遮罩显隐)实现自然的过渡效果,提升交互体验。 - 条件渲染:
if (this.isGet)按需渲染大奖遮罩层,仅当集齐卡片时加载,减少不必要的 UI 渲染开销。
四、关键技术提炼
- 状态驱动 UI:核心依赖
@State装饰器,所有 UI 变化(遮罩显隐、卡片计数、动画效果)均由状态变量驱动,符合 ArkTS 声明式编程范式。 - 多层级布局实现:Stack 堆叠布局 + zIndex 层级控制,是移动端 “底层内容 + 遮罩层” 弹层的经典实现方案,适配各类弹窗场景。
- 不可变数据更新:数组 / 对象更新时不直接修改属性(如
count++),而是替换整个对象,确保状态变更能被 ArkTS 状态系统检测到。 - 交互动画增强:scale 缩放 + opacity 透明度动画组合,让抽卡操作有直观的视觉反馈,避免交互生硬感。
五、代码优点分析
- 结构清晰,模块化强:代码按 “底层卡片展示→中层抽卡遮罩→顶层大奖遮罩” 拆分,功能模块边界明确,便于理解和后续维护。
- 状态管理规范:所有 UI 相关状态统一通过
@State管理,无零散状态变量,符合声明式编程最佳实践,避免状态混乱。 - 交互体验友好:抽卡图片缩放、遮罩层淡入淡出动画,让每一步操作都有视觉反馈,提升用户体验。
- 逻辑严谨高效:
- 集齐卡片判断时,发现
count=0立即break,减少无效遍历; - 重置功能一键重置
images数组、isGet、prize等状态,保证游戏流程闭环。
- 集齐卡片判断时,发现
- 类型安全保障:通过
interface ImageCount约束数组项类型,避免因数据格式错误导致的运行时异常。
六、部分代码
6.1定义接口
通过interface定义数据结构约束,提升代码类型安全性,避免运行时数据格式错误:
// 约束列表项数据结构:图片地址+计数
interface ImageCount{
url: string // 图片资源地址
count:number // 卡片收集数量
}
6.2状态管理
@State装饰器定义响应式状态变量,变量值变更会自动触发 UI 刷新,是 “状态驱动 UI” 的核心:
@State randomIndex:number = -1 // -1表示还没开始抽
//基于接口,准备数据
@State images:ImageCount[]=[
{url:'app.media.bg_00',count:0},
{url:'app.media.bg_01',count:0},
{url:'app.media.bg_02',count:0},
{url:'app.media.bg_03',count:0},
{url:'app.media.bg_04',count:0},
{url:'app.media.bg_05',count:0}
]
//控制遮罩的显隐
@State maskOpacity:number = 0 //透明度
@State maskZIndex:number = -1 //显示层级
//控制图片的缩放
@State maskImgx : number =0 //水平缩放比
@State maskImgy : number =0 //垂直缩放比
//控制中大奖遮罩的显隐
@State isGet:boolean = false
@State arr: string[]=['pg','hw','xm'] //奖池
@State prize :string = ''//默认没中奖
6.3布局
6.3.1 Stack 堆叠布局
核心用于实现多层级 UI 叠加,本次案例中实现 “底层卡片展示 + 中层抽卡遮罩 + 顶层大奖遮罩” 的层级效果,配合zIndex控制层级优先级(数值越大层级越高)。
6.3.2 Grid/GridItem 网格布局
通过columnsTemplate和rowsTemplate快速实现行列布局,适配性强:
Grid(){
ForEach(this.images,(item:ImageCount,index:number)=>{
GridItem(){ //单个Grid的子组件GrideItem
Badge({
count:item.count,
position: BadgePosition.RightTop,
style:{
badgeSize: 20,
fontSize: 16,
badgeColor: 'fa2a2d'
}
}){
Image($r(item.url)) //设置@State状态变量,实现图片地址的定位
.width(80)
}
}
})
}
.columnsTemplate('1fr 1fr 1fr') // 单行三项
.rowsTemplate('1fr 1fr') // 单列两项
.width('100%')
.height(300)
.margin({top:100})
6.4Badge角标
配置count(显示数值)、position(位置)、style(样式)实现卡片角标计数
//Badge角标设置
Badge({
Count:0, // 角标数值显示
position:BadgePosition.RightTop, //角标位置放置
style:{
badgeSize:20,
fontSize:16,
badgeColor:'fa2a2d'} //style 角标样式设置
})
6.5抽卡按钮
Button('立即抽卡')
.width(200)
.backgroundColor('#ed5b8c')
.margin({top:50})
.onClick(()=>{
//点击时,修改遮罩参数,让遮罩显示
this.maskOpacity = 1
this.maskZIndex = 99
//点击时,图片需要缩放
this.maskImgx = 1
this.maskImgy = 1
//计算随机数 Math.random() [0,1)*(n+1) Math.floor 向下取整
this.randomIndex = Math.floor(Math.random()*6)
})
6.6缩放设置
Text('获得生肖卡')
.fontColor('#f5ebcf')
.fontSize(25)
.fontWeight(FontWeight.Bold)
Image($r(`app.media.img_0${this.randomIndex}`))
.width(200)
//控制元素的缩放
.scale({
x:this.maskImgx,
y:this.maskImgy
}) //通过@State设置maskImgx/y缩放数值
.animation({
duration:500
})//子组件animation设置动画 duration设置动画时间
6.7收卡按钮
Button('开心收下')
.width(200)
.height(50)
.backgroundColor(Color.Transparent)
.border({width:2,color:'#fff9e0'})
.onClick(()=>{
//控制弹层显隐
this.maskOpacity = 0
this.maskZIndex = -1
//图像重置缩放比为0
this.maskImgx = 0
this.maskImgy = 0
// 开心收下,对象数组的情况需要更新,需要修改整个对象
// this.images[this.randomIndex].count++
this.images[this.randomIndex] = {
url: `app.media.img_0${this.randomIndex}`,
count:this.images[this.randomIndex].count+1
}
//每次收完卡片,需要进行简单的检索,判断是否集齐
// 需求:判断数组项的count,是否都大于0,只要有一个等于0,就意味着没集齐
let flag: boolean = true // 假设集齐
for(let item of this.images){
if(item.count ==0){
flag = false // 没集齐
break // 后面的没必要判断了
}
}
this.isGet = flag
//判断是否中奖,如果是 需要抽奖
if(flag){
let randomIndex:number = Math.floor(Math.random()*3)
this.prize = this.arr[randomIndex]
}
})
6.8抽大奖遮罩层
//抽大奖的遮罩层
if (this.isGet) {
Column({ space: 30 }) {
Text('恭喜获得手机一部')
.fontColor('#f5ebcf')
.fontSize(25)
.fontWeight(700)
Image($r(`app.media.${this.prize}`))
.width(300)
Button('再来一次')
.width(200)
.height(50)
.backgroundColor(Color.Transparent)
.border({ width: 2, color: '#fff9e0' })
.onClick(() => {
this.isGet =false
this.prize =''
this.images =[
{url:'app.media.bg_00',count:0},
{url:'app.media.bg_01',count:0},
{url:'app.media.bg_02',count:0},
{url:'app.media.bg_03',count:0},
{url:'app.media.bg_04',count:0},
{url:'app.media.bg_05',count:0}
]
})
}
.justifyContent(FlexAlign.Center)
.height('100%')
.width('100%')
.backgroundColor('#cc000000')
}
6.9生肖卡抽奖代码展示
// 1.定义接口 (每个列表项的数据结构)
interface ImageCount{
url: string
count:number
}
// 0 1 2 3 4 5
//求随机数: Math.random
//向下取整: Math.floor
//console.log('随机数',Math.random()*5);
@Entry
@Component
struct card {
//随机的生肖卡序号 0-5
@State randomIndex:number = -1 // -1表示还没开始抽
//基于接口,准备数据
@State images:ImageCount[]=[
{url:'app.media.bg_00',count:0},
{url:'app.media.bg_01',count:0},
{url:'app.media.bg_02',count:0},
{url:'app.media.bg_03',count:0},
{url:'app.media.bg_04',count:0},
{url:'app.media.bg_05',count:0}
]
//控制遮罩的显隐
@State maskOpacity:number = 0 //透明度
@State maskZIndex:number = -1 //显示层级
//控制图片的缩放
@State maskImgx : number =0 //水平缩放比
@State maskImgy : number =0 //垂直缩放比
//控制中大奖遮罩的显隐
@State isGet:boolean = false
@State arr: string[]=['pg','hw','xm'] //奖池
@State prize :string = ''//默认没中奖
build() {
Stack(){
//初始化的布局结构
Column(){
Grid(){
ForEach(this.images,(item:ImageCount,index:number)=>{
GridItem(){
Badge({
count:item.count,
position: BadgePosition.RightTop,
style:{
badgeSize: 20,
fontSize: 16,
badgeColor: 'fa2a2d'
}
}){
Image($r(item.url))
.width(80)
}
}
})
}
.columnsTemplate('1fr 1fr 1fr')
.rowsTemplate('1fr 1fr')
.width('100%')
.height(300)
.margin({top:100})
Button('立即抽卡')
.width(200)
.backgroundColor('#ed5b8c')
.margin({top:50})
.onClick(()=>{
//点击时,修改遮罩参数,让遮罩显示
this.maskOpacity = 1
this.maskZIndex = 99
//点击时,图片需要缩放
this.maskImgx = 1
this.maskImgy = 1
//计算随机数 Math.random() [0,1)*(n+1)
this.randomIndex = Math.floor(Math.random()*6)
})
}
.width('100%')
.height('100%')
//抽卡遮罩层(弹层)
Column({space:30}){
Text('获得生肖卡')
.fontColor('#f5ebcf')
.fontSize(25)
.fontWeight(FontWeight.Bold)
Image($r(`app.media.img_0${this.randomIndex}`))
.width(200)
//控制元素的缩放
.scale({
x:this.maskImgx,
y:this.maskImgy
})
.animation({
duration:500
})
Button('开心收下')
.width(200)
.height(50)
.backgroundColor(Color.Transparent)
.border({width:2,color:'#fff9e0'})
.onClick(()=>{
//控制弹层显隐
this.maskOpacity = 0
this.maskZIndex = -1
//图像重置缩放比为0
this.maskImgx = 0
this.maskImgy = 0
// 开心收下,对象数组的情况需要更新,需要修改整个对象
// this.images[this.randomIndex].count++
this.images[this.randomIndex] = {
url: `app.media.img_0${this.randomIndex}`,
count:this.images[this.randomIndex].count+1
}
//每次收完卡片,需要进行简单的检索,判断是否集齐
// 需求:判断数组项的count,是否都大于0,只要有一个等于0,就意味着没集齐
let flag: boolean = true // 假设集齐
for(let item of this.images){
if(item.count ==0){
flag = false // 没集齐
break // 后面的没必要判断了
}
}
this.isGet = flag
//判断是否中奖,如果是 需要抽奖
if(flag){
let randomIndex:number = Math.floor(Math.random()*3)
this.prize = this.arr[randomIndex]
}
})
}
.justifyContent(FlexAlign.Center)
.width('100%')
.height('100%')
// 颜色十六进制, 如果是八位,前两位,就是透明度
.backgroundColor('#cc000000')
//设置透明度
.opacity(this.maskOpacity)
.zIndex(this.maskZIndex)
//动画 animation,当我们元素有状态的改变,可以添加animation做动画
.animation({
duration:200
})
//抽大奖的遮罩层
if (this.isGet) {
Column({ space: 30 }) {
Text('恭喜获得手机一部')
.fontColor('#f5ebcf')
.fontSize(25)
.fontWeight(700)
Image($r(`app.media.${this.prize}`))
.width(300)
Button('再来一次')
.width(200)
.height(50)
.backgroundColor(Color.Transparent)
.border({ width: 2, color: '#fff9e0' })
.onClick(() => {
this.isGet =false
this.prize =''
this.images =[
{url:'app.media.bg_00',count:0},
{url:'app.media.bg_01',count:0},
{url:'app.media.bg_02',count:0},
{url:'app.media.bg_03',count:0},
{url:'app.media.bg_04',count:0},
{url:'app.media.bg_05',count:0}
]
})
}
.justifyContent(FlexAlign.Center)
.height('100%')
.width('100%')
.backgroundColor('#cc000000')
}
}
}
}
七、生肖抽奖卡实现




更多推荐




所有评论(0)