写点什么

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

作者:苏杰豪
  • 2025-03-26
    北京
  • 本文字数:8430 字

    阅读完需:约 28 分钟

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

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

最终效果预览

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


  • 3×3 网格布局展示奖品

  • 点击中间按钮启动抽奖

  • 高亮格子循环移动的动画效果

  • 动态变速,模拟真实抽奖过程

  • 预设中奖结果的展示


实现步骤

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

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


// 定义奖品项的接口interface PrizeItem {  id: number  name: string  icon: ResourceStr  color: string}
@Entry@Componentstruct LuckyDraw { // 基本页面结构 build() { Column() { Text('幸运抽奖') .fontSize(24) .fontColor(Color.White) } .width('100%') .height('100%') .backgroundColor('#121212') }}
复制代码


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

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

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


@Entry@Componentstruct 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. 添加了三个状态变量:

  3. currentIndex:跟踪当前高亮的奖品索引

  4. isRunning:标记抽奖是否正在进行

  5. result:记录并显示抽奖结果

步骤三:实现九宫格布局

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


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. 为每个格子设置了合适的样式和背景色

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

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


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. 实现了格子高亮效果:

  3. 当格子被选中时(index === this.currentIndex),背景色变为白色,文字颜色变为奖品颜色

  4. 添加了动画效果,使高亮切换更加平滑

  5. 预定义了startLottery()方法,暂时为空实现

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

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


@Entry@Componentstruct 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. 添加了动画控制相关变量:

  2. timer:用于存储定时器 ID

  3. speed:控制动画速度

  4. totalRounds:总共旋转的轮数

  5. currentRound:当前已旋转的轮数

  6. targetIndex:预设的中奖索引

  7. 实现了startLottery()方法:

  8. 防止重复点击

  9. 初始化抽奖状态

  10. 调用runLottery()开始动画

  11. 实现了runLottery()方法:

  12. 使用setTimeout创建循环动画

  13. 更新高亮格子的索引,并跳过中间的开始按钮

  14. 根据进度增加延迟时间,模拟减速效果

  15. 根据条件判断是否结束动画

  16. 递归调用自身形成动画循环

  17. 添加了aboutToDisappear()生命周期方法,确保在组件销毁时清除定时器,避免内存泄漏

完整代码

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


完整的代码如下:


interface PrizeItem {  id: number  name: string  icon: ResourceStr  color: string}
@Entry@Componentstruct 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. 结束条件:当满足两个条件时停止动画:

  4. 已完成设定的总轮数(this.currentRound >= this.totalRounds

  5. 当前高亮的格子是目标奖品(this.currentIndex === this.targetIndex

3. 高亮效果

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


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


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

4. 资源清理

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


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

进阶优化思路

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

1. 随机中奖结果

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


// 根据概率生成中奖索引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. 抽奖音效

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


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

3. 振动反馈

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


// 导入振动模块import { vibrator } from '@kit.SensorServiceKit';
// 触发振动triggerVibration() { vibrator.vibrate(50); // 振动50毫秒}
复制代码

4. 抽奖次数限制

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


@State remainingTimes: number = 3; // 剩余抽奖次数
startLottery() { if (this.isRunning || this.remainingTimes <= 0) { return; } this.remainingTimes--; // 其他抽奖逻辑...}
复制代码

总结

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


  1. 数据结构定义和状态管理

  2. 网格布局和循环渲染

  3. 条件样式和动画效果

  4. 定时器控制和动态速度

  5. 生命周期管理和资源清理


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

发布于: 1 小时前阅读数: 10
用户头像

苏杰豪

关注

鸿蒙很开门~ 2019-03-30 加入

传智教育、黑马程序员课程研究员

评论

发布
暂无评论
鸿蒙特效教程07-九宫格幸运抽奖_鸿蒙_苏杰豪_InfoQ写作社区