鸿蒙特效教程07-九宫格幸运抽奖

鸿蒙特效教程07-九宫格幸运抽奖

在移动应用中,抽奖功能是一种常见且受欢迎的交互方式,能够有效提升用户粘性。本教程将带领大家从零开始,逐步实现一个九宫格抽奖效果,适合HarmonyOS开发的初学者阅读。

最终效果预览

我们将实现一个经典的九宫格抽奖界面,包含以下核心功能:

  • 3×3网格布局展示奖品
  • 点击中间按钮启动抽奖
  • 高亮格子循环移动的动画效果
  • 动态变速,模拟真实抽奖过程
  • 预设中奖结果的展示

实现步骤

步骤一:创建基本结构和数据模型

首先,我们需要创建一个基础页面结构和定义数据模型。通过定义奖品的数据结构,为后续的九宫格布局做准备。

typescript 复制代码
// 定义奖品项的接口
interface PrizeItem {
  id: number
  name: string
  icon: ResourceStr
  color: string
}

@Entry
@Component
struct LuckyDraw {
  // 基本页面结构
  build() {
    Column() {
      Text('幸运抽奖')
        .fontSize(24)
        .fontColor(Color.White)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#121212')
  }
}

在这一步,我们定义了PrizeItem接口来规范奖品的数据结构,并创建了一个基本的页面结构,只包含一个标题。

步骤二:创建奖品数据和状态管理

接下来,我们添加具体的奖品数据,并定义抽奖功能所需的状态变量。

typescript 复制代码
@Entry
@Component
struct LuckyDraw {
  // 定义奖品数组
  @State prizes: PrizeItem[] = [
    { id: 1, name: '谢谢参与', icon: $r('app.media.startIcon'), color: '#FF9500' },
    { id: 2, name: '10积分', icon: $r('app.media.startIcon'), color: '#34C759' },
    { id: 3, name: '优惠券', icon: $r('app.media.startIcon'), color: '#007AFF' },
    { id: 8, name: '1元红包', icon: $r('app.media.startIcon'), color: '#FF3B30' },
    { id: 0, name: '开始\n抽奖', icon: $r('app.media.startIcon'), color: '#FF2D55' },
    { id: 4, name: '5元红包', icon: $r('app.media.startIcon'), color: '#5856D6' },
    { id: 7, name: '免单券', icon: $r('app.media.startIcon'), color: '#E73C39' },
    { id: 6, name: '50积分', icon: $r('app.media.startIcon'), color: '#38B0DE' },
    { id: 5, name: '会员卡', icon: $r('app.media.startIcon'), color: '#39A5DC' },
  ]
  
  // 当前高亮的奖品索引
  @State currentIndex: number = -1
  
  // 是否正在抽奖
  @State isRunning: boolean = false
  
  // 中奖结果
  @State result: string = '点击开始抽奖'
  
  build() {
    // 页面结构保持不变
  }
}

在这一步,我们添加了以下内容:

  1. 创建了一个包含9个奖品的数组,每个奖品都有id、名称、图标和颜色属性
  2. 添加了三个状态变量:
    • currentIndex:跟踪当前高亮的奖品索引
    • isRunning:标记抽奖是否正在进行
    • result:记录并显示抽奖结果

步骤三:实现九宫格布局

现在我们来实现九宫格的基本布局,使用Grid组件和ForEach循环遍历奖品数组。

typescript 复制代码
build() {
  Column({ space: 30 }) {
    // 标题
    Text('幸运抽奖')
      .fontSize(24)
      .fontWeight(FontWeight.Bold)
      .fontColor(Color.White)
    
    // 结果显示区域
    Column() {
      Text(this.result)
        .fontSize(20)
        .fontColor(Color.White)
    }
    .width('90%')
    .padding(15)
    .backgroundColor('#0DFFFFFF')
    .borderRadius(16)
    
    // 九宫格抽奖区域
    Grid() {
      ForEach(this.prizes, (prize: PrizeItem, index) => {
        GridItem() {
          Column() {
            if (index === 4) {
              // 中间的开始按钮
              Button({ type: ButtonType.Capsule }) {
                Text(prize.name)
                  .fontSize(18)
                  .fontWeight(FontWeight.Bold)
                  .textAlign(TextAlign.Center)
                  .fontColor(Color.White)
              }
              .width('90%')
              .height('90%')
              .backgroundColor(prize.color)
            } else {
              // 普通奖品格子
              Image(prize.icon)
                .width(40)
                .height(40)
              Text(prize.name)
                .fontSize(14)
                .fontColor(Color.White)
                .margin({ top: 8 })
                .textAlign(TextAlign.Center)
            }
          }
          .width('100%')
          .height('100%')
          .justifyContent(FlexAlign.Center)
          .alignItems(HorizontalAlign.Center)
          .backgroundColor(prize.color)
          .borderRadius(12)
          .padding(10)
        }
      })
    }
    .columnsTemplate('1fr 1fr 1fr')
    .rowsTemplate('1fr 1fr 1fr')
    .columnsGap(10)
    .rowsGap(10)
    .width('90%')
    .aspectRatio(1)
    .backgroundColor('#0DFFFFFF')
    .borderRadius(16)
    .padding(10)
  }
  .width('100%')
  .height('100%')
  .justifyContent(FlexAlign.Center)
  .backgroundColor(Color.Black)
  .linearGradient({
    angle: 135,
    colors: [
      ['#121212', 0],
      ['#242424', 1]
    ]
  })
}

在这一步,我们实现了以下内容:

  1. 创建了整体的页面布局,包括标题、结果显示区域和九宫格区域
  2. 使用 Grid 组件创建3×3的网格布局
  3. 使用 ForEach 遍历奖品数组,为每个奖品创建一个格子
  4. 根据索引判断,为中间位置创建"开始抽奖"按钮,其他位置显示奖品信息
  5. 为每个格子设置了合适的样式和背景色

步骤四:实现高亮效果和点击事件

接下来,我们要实现格子的高亮效果,并添加点击事件处理。

typescript 复制代码
build() {
  Column({ space: 30 }) {
    // 前面的代码保持不变...
    
    // 九宫格抽奖区域
    Grid() {
      ForEach(this.prizes, (prize: PrizeItem, index) => {
        GridItem() {
          Column() {
            if (index === 4) {
              // 中间的开始按钮
              Button({ type: ButtonType.Capsule }) {
                Text(prize.name)
                  .fontSize(18)
                  .fontWeight(FontWeight.Bold)
                  .textAlign(TextAlign.Center)
                  .fontColor(Color.White)
              }
              .width('90%')
              .height('90%')
              .backgroundColor(prize.color)
              .onClick(() => this.startLottery()) // 添加点击事件
            } else {
              // 普通奖品格子
              Image(prize.icon)
                .width(40)
                .height(40)
              Text(prize.name)
                .fontSize(14)
                .fontColor(index === this.currentIndex ? prize.color : Color.White) // 高亮时修改文字颜色
                .margin({ top: 8 })
                .textAlign(TextAlign.Center)
            }
          }
          .width('100%')
          .height('100%')
          .justifyContent(FlexAlign.Center)
          .alignItems(HorizontalAlign.Center)
          .backgroundColor(index === this.currentIndex && index !== 4 ? Color.White : prize.color) // 高亮时切换背景色
          .borderRadius(12)
          .padding(10)
          .animation({ // 添加动画效果
            duration: 200,
            curve: Curve.EaseInOut
          })
        }
      })
    }
    // Grid的其他属性保持不变...
  }
  // Column的属性保持不变...
}

// 添加开始抽奖的空方法
startLottery() {
  // 在下一步实现
}

在这一步,我们:

  1. 为中间的"开始抽奖"按钮添加了点击事件处理方法startLottery()
  2. 实现了格子高亮效果:
    • 当格子被选中时(index === this.currentIndex),背景色变为白色,文字颜色变为奖品颜色
    • 添加了动画效果,使高亮切换更加平滑
  3. 预定义了startLottery()方法,暂时为空实现

步骤五:实现抽奖动画逻辑

现在我们来实现抽奖动画的核心逻辑,包括循环高亮、速度变化和结果控制。

typescript 复制代码
@Entry
@Component
struct LuckyDraw {
  // 前面的状态变量保持不变...
  
  // 添加动画控制相关变量
  private timer: number = 0
  private speed: number = 100
  private totalRounds: number = 30
  private currentRound: number = 0
  private targetIndex: number = 2 // 假设固定中奖"优惠券"
  
  // 开始抽奖
  startLottery() {
    if (this.isRunning) {
      return // 防止重复点击
    }
    
    this.isRunning = true
    this.result = '抽奖中...'
    this.currentRound = 0
    this.speed = 100
    
    // 启动动画
    this.runLottery()
  }
  
  // 运行抽奖动画
  runLottery() {
    if (this.timer) {
      clearTimeout(this.timer)
    }
    
    this.timer = setTimeout(() => {
      // 更新当前高亮的格子
      this.currentIndex = (this.currentIndex + 1) % 9
      if (this.currentIndex === 4) { // 跳过中间的"开始抽奖"按钮
        this.currentIndex = 5
      }
      
      this.currentRound++
      
      // 增加速度变化,模拟减速效果
      if (this.currentRound > this.totalRounds * 0.7) {
        this.speed += 10 // 大幅减速
      } else if (this.currentRound > this.totalRounds * 0.5) {
        this.speed += 5 // 小幅减速
      }
      
      // 结束条件判断
      if (this.currentRound >= this.totalRounds && this.currentIndex === this.targetIndex) {
        // 抽奖结束
        this.isRunning = false
        this.result = `恭喜获得: ${this.prizes[this.targetIndex].name}`
      } else {
        // 继续动画
        this.runLottery()
      }
    }, this.speed)
  }
  
  // 组件销毁时清除定时器
  aboutToDisappear() {
    if (this.timer) {
      clearTimeout(this.timer)
      this.timer = 0
    }
  }
  
  // build方法保持不变...
}

在这一步,我们实现了抽奖动画的核心逻辑:

  1. 添加了动画控制相关变量:

    • timer:用于存储定时器ID
    • speed:控制动画速度
    • totalRounds:总共旋转的轮数
    • currentRound:当前已旋转的轮数
    • targetIndex:预设的中奖索引
  2. 实现了startLottery()方法:

    • 防止重复点击
    • 初始化抽奖状态
    • 调用runLottery()开始动画
  3. 实现了runLottery()方法:

    • 使用setTimeout创建循环动画
    • 更新高亮格子的索引,并跳过中间的开始按钮
    • 根据进度增加延迟时间,模拟减速效果
    • 根据条件判断是否结束动画
    • 递归调用自身形成动画循环
  4. 添加了aboutToDisappear()生命周期方法,确保在组件销毁时清除定时器,避免内存泄漏

完整代码

最后,我们对代码进行完善和优化,确保抽奖功能正常工作并提升用户体验。

完整的代码如下:

typescript 复制代码
interface PrizeItem {
  id: number
  name: string
  icon: ResourceStr
  color: string
}

@Entry
@Component
struct LuckyDraw {
  // 定义奖品数组
  @State prizes: PrizeItem[] = [
    {
      id: 1,
      name: '谢谢参与',
      icon: $r('app.media.startIcon'),
      color: '#FF9500'
    },
    {
      id: 2,
      name: '10积分',
      icon: $r('app.media.startIcon'),
      color: '#34C759'
    },
    {
      id: 3,
      name: '优惠券',
      icon: $r('app.media.startIcon'),
      color: '#007AFF'
    },
    {
      id: 8,
      name: '1元红包',
      icon: $r('app.media.startIcon'),
      color: '#FF3B30'
    },
    {
      id: 0,
      name: '开始\n抽奖',
      icon: $r('app.media.startIcon'),
      color: '#FF2D55'
    },
    {
      id: 4,
      name: '5元红包',
      icon: $r('app.media.startIcon'),
      color: '#5856D6'
    },
    {
      id: 7,
      name: '免单券',
      icon: $r('app.media.startIcon'),
      color: '#E73C39'
    },
    {
      id: 6,
      name: '50积分',
      icon: $r('app.media.startIcon'),
      color: '#38B0DE'
    },
    {
      id: 5,
      name: '会员卡',
      icon: $r('app.media.startIcon'),
      color: '#39A5DC'
    },
  ]
  // 当前高亮的奖品索引
  @State currentIndex: number = -1
  // 是否正在抽奖
  @State isRunning: boolean = false
  // 中奖结果
  @State result: string = '点击下方按钮开始抽奖'
  // 动画定时器
  private timer: number = 0
  // 动画速度控制
  private speed: number = 100
  private totalRounds: number = 30
  private currentRound: number = 0
  // 预设中奖索引(可以根据概率随机生成)
  private targetIndex: number = 2 // 假设固定中奖"优惠券"

  // 开始抽奖
  startLottery() {
    if (this.isRunning) {
      return
    }

    this.isRunning = true
    this.result = '抽奖中...'
    this.currentRound = 0
    this.speed = 100

    // 启动动画
    this.runLottery()
  }

  // 运行抽奖动画
  runLottery() {
    if (this.timer) {
      clearTimeout(this.timer)
    }

    this.timer = setTimeout(() => {
      // 更新当前高亮的格子
      this.currentIndex = (this.currentIndex + 1) % 9
      if (this.currentIndex === 4) { // 跳过中间的"开始抽奖"按钮
        this.currentIndex = 5
      }

      this.currentRound++

      // 增加速度变化,模拟减速效果
      if (this.currentRound > this.totalRounds * 0.7) {
        this.speed += 10
      } else if (this.currentRound > this.totalRounds * 0.5) {
        this.speed += 5
      }

      // 结束条件判断
      if (this.currentRound >= this.totalRounds && this.currentIndex === this.targetIndex) {
        // 抽奖结束
        this.isRunning = false
        this.result = `恭喜获得: ${this.prizes[this.targetIndex].name}`
      } else {
        // 继续动画
        this.runLottery()
      }
    }, this.speed)
  }

  // 组件销毁时清除定时器
  aboutToDisappear() {
    if (this.timer) {
      clearTimeout(this.timer)
      this.timer = 0
    }
  }

  build() {
    Column({ space: 30 }) {
      // 标题
      Text('幸运抽奖')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor(Color.White)

      // 结果显示
      Column() {
        Text(this.result)
          .fontSize(20)
          .fontColor(Color.White)
      }
      .width('90%')
      .padding(15)
      .backgroundColor('#0DFFFFFF')
      .borderRadius(16)

      // 九宫格抽奖区域
      Grid() {
        ForEach(this.prizes, (prize: PrizeItem, index) => {
          GridItem() {
            Column() {
              if (index === 4) {
                // 中间的开始按钮
                Button({ type: ButtonType.Capsule }) {
                  Text(prize.name)
                    .fontSize(18)
                    .fontWeight(FontWeight.Bold)
                    .textAlign(TextAlign.Center)
                    .fontColor(Color.White)
                }
                .width('90%')
                .height('90%')
                .backgroundColor(prize.color)
                .onClick(() => this.startLottery())
              } else {
                // 普通奖品格子
                Image(prize.icon)
                  .width(40)
                  .height(40)
                Text(prize.name)
                  .fontSize(14)
                  .fontColor(index === this.currentIndex && index !== 4 ? prize.color : Color.White)
                  .margin({ top: 8 })
                  .textAlign(TextAlign.Center)
              }
            }
            .width('100%')
            .height('100%')
            .justifyContent(FlexAlign.Center)
            .alignItems(HorizontalAlign.Center)
            .backgroundColor(index === this.currentIndex && index !== 4 ? Color.White : prize.color)
            .borderRadius(12)
            .padding(10)
            .animation({
              duration: 200,
              curve: Curve.EaseInOut
            })
          }
        })
      }
      .columnsTemplate('1fr 1fr 1fr')
      .rowsTemplate('1fr 1fr 1fr')
      .columnsGap(10)
      .rowsGap(10)
      .width('90%')
      .aspectRatio(1)
      .backgroundColor('#0DFFFFFF')
      .borderRadius(16)
      .padding(10)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor(Color.Black)
    .linearGradient({
      angle: 135,
      colors: [
        ['#121212', 0],
        ['#242424', 1]
      ]
    })
    .expandSafeArea() // 颜色扩展到安全区域
  }
}

核心概念解析

1. Grid组件

Grid组件是实现九宫格布局的核心,它具有以下重要属性:

  • columnsTemplate:定义网格的列模板。'1fr 1fr 1fr'表示三列等宽布局。
  • rowsTemplate:定义网格的行模板。'1fr 1fr 1fr'表示三行等高布局。
  • columnsGaprowsGap:设置列和行之间的间距。
  • aspectRatio:设置宽高比,确保网格是正方形。

2. 动画实现原理

抽奖动画的核心是通过定时器和状态更新实现的:

  1. 循环高亮 :通过setTimeout定时更新currentIndex状态,实现格子的循环高亮。
  2. 动态速度 :随着循环轮数的增加,逐渐增加延迟时间(this.speed += 10),实现减速效果。
  3. 结束条件 :当满足两个条件时停止动画:
    • 已完成设定的总轮数(this.currentRound >= this.totalRounds
    • 当前高亮的格子是目标奖品(this.currentIndex === this.targetIndex

3. 高亮效果

格子的高亮效果是通过条件样式实现的:

typescript 复制代码
.backgroundColor(index === this.currentIndex && index !== 4 ? Color.White : prize.color)

当格子被选中时(index === this.currentIndex),背景色变为白色,文字颜色变为奖品颜色,产生对比鲜明的高亮效果。

4. 资源清理

在组件销毁时,我们需要清除定时器以避免内存泄漏:

typescript 复制代码
aboutToDisappear() {
  if (this.timer) {
    clearTimeout(this.timer)
    this.timer = 0
  }
}

进阶优化思路

完成基本功能后,可以考虑以下优化方向:

1. 随机中奖结果

目前中奖结果是固定的,可以实现一个随机算法,根据概率分配不同奖品:

typescript 复制代码
// 根据概率生成中奖索引
generatePrizeIndex() {
  // 定义各奖品的概率权重
  const weights = [50, 10, 5, 3, 0, 2, 1, 8, 20]; // 数字越大概率越高
  const totalWeight = weights.reduce((a, b) => a + b, 0);
  
  // 生成随机数
  const random = Math.random() * totalWeight;
  
  // 根据权重决定中奖索引
  let currentWeight = 0;
  for (let i = 0; i < weights.length; i++) {
    if (i === 4) continue; // 跳过中间的"开始抽奖"按钮
    
    currentWeight += weights[i];
    if (random < currentWeight) {
      return i;
    }
  }
  
  return 0; // 默认返回第一个奖品
}

2. 抽奖音效

添加音效可以提升用户体验:

typescript 复制代码
// 播放抽奖音效
playSound(type: 'start' | 'running' | 'end') {
  // 根据不同阶段播放不同音效
}

3. 振动反馈

在抽奖开始和结束时添加振动反馈:

typescript 复制代码
// 导入振动模块
import { vibrator } from '@kit.SensorServiceKit';

// 触发振动
triggerVibration() {
  vibrator.vibrate(50); // 振动50毫秒
}

4. 抽奖次数限制

添加抽奖次数限制和剩余次数显示:

typescript 复制代码
@State remainingTimes: number = 3; // 剩余抽奖次数

startLottery() {
  if (this.isRunning || this.remainingTimes <= 0) {
    return;
  }
  
  this.remainingTimes--;
  // 其他抽奖逻辑...
}

总结

本教程从零开始,一步步实现了九宫格抽奖效果,涵盖了以下关键内容:

  1. 数据结构定义和状态管理
  2. 网格布局和循环渲染
  3. 条件样式和动画效果
  4. 定时器控制和动态速度
  5. 生命周期管理和资源清理

希望这篇 HarmonyOS Next 教程对你有所帮助,期待您的点赞、评论、收藏。

相关推荐
万少1 小时前
2025中了 聊一聊程序员为什么都要做自己的产品
前端·harmonyos
网络小白不怕黑2 小时前
华为设备 QoS 流分类与流标记深度解析及实验脚本
网络·华为
网络小白不怕黑2 小时前
华为交换机堆叠与集群技术深度解析附带脚本
网络·华为
幽蓝计划14 小时前
HarmonyOS NEXT仓颉开发语言实战案例:动态广场
华为·harmonyos
万少20 小时前
第五款 HarmonyOS 上架作品 奇趣故事匣 来了
前端·harmonyos·客户端
幽蓝计划20 小时前
HarmonyOS NEXT仓颉开发语言实战案例:电影App
华为·harmonyos
HMS Core1 天前
HarmonyOS免密认证方案 助力应用登录安全升级
安全·华为·harmonyos
生如夏花℡1 天前
HarmonyOS学习记录3
学习·ubuntu·harmonyos
伍哥的传说1 天前
鸿蒙系统(HarmonyOS)应用开发之手势锁屏密码锁(PatternLock)
前端·华为·前端框架·harmonyos·鸿蒙
funnycoffee1231 天前
Huawei 6730 Switch software upgrade example版本升级
java·前端·华为