一、基础信息

项目 内容
学习日期 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 数组操作与类型约束的最佳实践

二、核心学习内容

本次学习围绕生肖卡抽奖组件开发展开,核心掌握以下技术点:

  1. 布局类:Grid 网格布局(2 行 3 列表格排版)、Stack 堆叠布局(多层级遮罩实现)
  2. 样式类:opacity 透明度控制、scale 缩放效果、zIndex 层级管理
  3. 交互类:Button 点击事件、Math.random () 随机数生成
  4. 动效类:animation 动画配置(时长、过渡效果)
  5. 逻辑类:数组遍历 / 更新、条件渲染、状态驱动 UI 刷新

三、关键知识点总结

3.1 ArkTS 基础语法与装饰器

知识点 核心说明
接口定义 通过interface ImageCount约束列表项数据结构(url+count),提升类型安全性
组件装饰器 @Entry标记入口组件、@Component声明自定义组件,是 ArkTS 组件开发基础
状态管理

@State定义响应式变量(如 randomIndex/maskOpacity),状态变更自动刷新 UI

3.2 布局与基础组件使用

  • Stack 堆叠布局:核心用于实现 “底层内容 + 中层抽卡遮罩 + 顶层大奖遮罩” 的层级覆盖,是移动端弹层 / 遮罩的核心布局方案。
  • Grid/GridItem 网格布局:通过columnsTemplate('1fr 1fr 1fr')rowsTemplate('1fr 1fr')快速实现 2 行 3 列的卡片排版,适配性强且布局灵活。
  • 基础组件组合
    • Badge:配置位置(RightTop)、大小、颜色,实现卡片右上角计数展示;
    • Image:通过$r()加载本地媒体资源,配合 scale 实现缩放动效;
    • Button/Text:完成交互触发与文本展示,覆盖基础 UI 场景。

3.3 交互与逻辑处理

  1. 事件处理ButtononClick事件串联抽卡、收下卡片、重置游戏全流程,是用户操作与业务逻辑的核心桥梁。
  2. 随机数生成Math.floor(Math.random()*6)生成 0-5 的随机整数,实现抽卡的随机性,符合抽奖场景需求。
  3. 数组操作规范
    • 遍历渲染:ForEach遍历images数组渲染 Grid 列表,是 ArkTS 遍历渲染的标准方式;
    • 数据更新:遵循 “不可变原则”,通过替换整个对象(this.images[index] = {...})更新数组项,避免 UI 刷新异常;
    • 逻辑判断:遍历数组判断count是否均 > 0,通过flag标记 +break提前终止循环,提升执行效率。

3.4 动画与条件渲染

  • 动画效果:通过animation属性配置时长(如 500ms),结合scale(图片缩放)、opacity(遮罩显隐)实现自然的过渡效果,提升交互体验。
  • 条件渲染if (this.isGet)按需渲染大奖遮罩层,仅当集齐卡片时加载,减少不必要的 UI 渲染开销。

四、关键技术提炼

  1. 状态驱动 UI:核心依赖@State装饰器,所有 UI 变化(遮罩显隐、卡片计数、动画效果)均由状态变量驱动,符合 ArkTS 声明式编程范式。
  2. 多层级布局实现:Stack 堆叠布局 + zIndex 层级控制,是移动端 “底层内容 + 遮罩层” 弹层的经典实现方案,适配各类弹窗场景。
  3. 不可变数据更新:数组 / 对象更新时不直接修改属性(如count++),而是替换整个对象,确保状态变更能被 ArkTS 状态系统检测到。
  4. 交互动画增强:scale 缩放 + opacity 透明度动画组合,让抽卡操作有直观的视觉反馈,避免交互生硬感。

五、代码优点分析

  1. 结构清晰,模块化强:代码按 “底层卡片展示→中层抽卡遮罩→顶层大奖遮罩” 拆分,功能模块边界明确,便于理解和后续维护。
  2. 状态管理规范:所有 UI 相关状态统一通过@State管理,无零散状态变量,符合声明式编程最佳实践,避免状态混乱。
  3. 交互体验友好:抽卡图片缩放、遮罩层淡入淡出动画,让每一步操作都有视觉反馈,提升用户体验。
  4. 逻辑严谨高效
    • 集齐卡片判断时,发现count=0立即break,减少无效遍历;
    • 重置功能一键重置images数组、isGetprize等状态,保证游戏流程闭环。
  5. 类型安全保障:通过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 网格布局

通过columnsTemplaterowsTemplate快速实现行列布局,适配性强:

        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')
      }
    }
  }
}

七、生肖抽奖卡实现

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐