查看: 91|回复: 0

鸿蒙特效教程08-幸运大转盘抽奖

[复制链接]

1

主题

2

回帖

14

积分

新手上路

积分
14
发表于 2025-4-1 08:32:47 | 显示全部楼层 |阅读模式
鸿蒙特效教程08-幸运大转盘抽奖

本教程将带领大家从零开始,一步步实现一个完整的转盘抽奖效果,包括界面布局、Canvas绘制、动画效果和抽奖逻辑等。
1. 需求分析与整体设计

温馨提醒:本案例有一定难度,建议先收藏起来。
在开始编码前,让我们先明确转盘抽奖的基本需求:
    展示一个可旋转的奖品转盘转盘上有多个奖品区域,每个区域有不同的颜色和奖品名称点击"开始抽奖"按钮后,转盘开始旋转转盘停止后,指针指向的位置即为抽中的奖品每个奖品有不同的中奖概率
整体设计思路:
    使用HarmonyOS的Canvas组件绘制转盘利用动画效果实现转盘旋转根据概率算法确定最终停止位置

2. 基础界面布局

首先,我们创建基础的页面布局,包括标题、转盘区域和结果显示。
  1. @Entry
  2. @Component
  3. struct LuckyWheel {
  4.   build() {
  5.     Column() {
  6.       // 标题
  7.       Text('幸运大转盘')
  8.         .fontSize(28)
  9.         .fontWeight(FontWeight.Bold)
  10.         .fontColor(Color.White)
  11.         .margin({ bottom: 20 })
  12.       // 抽奖结果显示
  13.       Text('点击开始抽奖')
  14.         .fontSize(20)
  15.         .fontColor(Color.White)
  16.         .backgroundColor('#1AFFFFFF')
  17.         .width('90%')
  18.         .textAlign(TextAlign.Center)
  19.         .padding(15)
  20.         .borderRadius(16)
  21.         .margin({ bottom: 30 })
  22.       // 转盘容器(后续会添加Canvas)
  23.       Stack({ alignContent: Alignment.Center }) {
  24.         // 这里稍后会添加Canvas绘制转盘
  25.         // 中央开始按钮
  26.         Button({ type: ButtonType.Circle }) {
  27.           Text('开始\n抽奖')
  28.             .fontSize(18)
  29.             .fontWeight(FontWeight.Bold)
  30.             .textAlign(TextAlign.Center)
  31.             .fontColor(Color.White)
  32.         }
  33.         .width(80)
  34.         .height(80)
  35.         .backgroundColor('#FF6B6B')
  36.       }
  37.       .width('90%')
  38.       .aspectRatio(1)
  39.       .backgroundColor('#0DFFFFFF')
  40.       .borderRadius(16)
  41.       .padding(15)
  42.     }
  43.     .width('100%')
  44.     .height('100%')
  45.     .justifyContent(FlexAlign.Center)
  46.     .backgroundColor(Color.Black)
  47.     .linearGradient({
  48.       angle: 135,
  49.       colors: [
  50.         ['#1A1B25', 0],
  51.         ['#2D2E3A', 1]
  52.       ]
  53.     })
  54.   }
  55. }
复制代码
这个基础布局创建了一个带有标题、结果显示区和转盘容器的页面。转盘容器使用 Stack组件,这样我们可以在转盘上方放置"开始抽奖"按钮。
3. 定义数据结构

接下来,我们需要定义转盘上的奖品数据结构:
  1. // 奖品数据接口
  2. interface PrizesItem {
  3.   name: string     // 奖品名称
  4.   color: string    // 转盘颜色
  5.   probability: number // 概率权重
  6. }
  7. @Entry
  8. @Component
  9. struct LuckyWheel {
  10.   // 奖品数据
  11.   private prizes: PrizesItem[] = [
  12.     { name: '谢谢参与', color: '#FFD8A8', probability: 30 },
  13.     { name: '10积分', color: '#B2F2BB', probability: 20 },
  14.     { name: '5元红包', color: '#D0BFFF', probability: 10 },
  15.     { name: '优惠券', color: '#A5D8FF', probability: 15 },
  16.     { name: '免单券', color: '#FCCFE7', probability: 5 },
  17.     { name: '50积分', color: '#BAC8FF', probability: 15 },
  18.     { name: '会员月卡', color: '#99E9F2', probability: 3 },
  19.     { name: '1元红包', color: '#FFBDBD', probability: 2 }
  20.   ]
  21.   // 状态变量
  22.   @State isSpinning: boolean = false // 是否正在旋转
  23.   @State rotation: number = 0 // 当前旋转角度
  24.   @State result: string = '点击开始抽奖' // 抽奖结果
  25.   // ...其余代码
  26. }
复制代码
这里我们定义了转盘上的8个奖品,每个奖品包含名称、颜色和概率权重。同时定义了三个状态变量来跟踪转盘的状态。
4. 初始化Canvas

现在,让我们初始化Canvas来绘制转盘:
  1. @Entry
  2. @Component
  3. struct LuckyWheel {
  4.   // Canvas 相关设置
  5.   private readonly settings: RenderingContextSettings = new RenderingContextSettings(true); // 启用抗锯齿
  6.   private readonly ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
  7.   // 转盘相关属性
  8.   private canvasWidth: number = 0 // 画布宽度
  9.   private canvasHeight: number = 0 // 画布高度
  10.   // ...其余代码
  11.   build() {
  12.     Column() {
  13.       // ...之前的代码
  14.       // 转盘容器
  15.       Stack({ alignContent: Alignment.Center }) {
  16.         // 使用Canvas绘制转盘
  17.         Canvas(this.ctx)
  18.           .width('100%')
  19.           .height('100%')
  20.           .onReady(() => {
  21.             // 获取Canvas尺寸
  22.             this.canvasWidth = this.ctx.width
  23.             this.canvasHeight = this.ctx.height
  24.             // 初始绘制转盘
  25.             this.drawWheel()
  26.           })
  27.         // 中央开始按钮
  28.         // ...按钮代码
  29.       }
  30.       // ...容器样式
  31.     }
  32.     // ...外层容器样式
  33.   }
  34.   // 绘制转盘(先定义一个空方法,稍后实现)
  35.   private drawWheel(): void {
  36.     // 稍后实现
  37.   }
  38. }
复制代码
这里我们创建了Canvas绘制上下文,并在 onReady回调中获取Canvas尺寸,然后调用 drawWheel方法绘制转盘。
5. 实现转盘绘制

接下来,我们实现 drawWheel方法,绘制转盘:
  1. // 绘制转盘
  2. private drawWheel(): void {
  3.   if (!this.ctx) return
  4.   const centerX = this.canvasWidth / 2
  5.   const centerY = this.canvasHeight / 2
  6.   const radius = Math.min(centerX, centerY) * 0.85
  7.   // 清除画布
  8.   this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
  9.   // 保存当前状态
  10.   this.ctx.save()
  11.   // 移动到中心点
  12.   this.ctx.translate(centerX, centerY)
  13.   // 应用旋转
  14.   this.ctx.rotate((this.rotation % 360) * Math.PI / 180)
  15.   // 绘制转盘扇形
  16.   const anglePerPrize = 2 * Math.PI / this.prizes.length
  17.   for (let i = 0; i < this.prizes.length; i++) {
  18.     const startAngle = i * anglePerPrize
  19.     const endAngle = (i + 1) * anglePerPrize
  20.     this.ctx.beginPath()
  21.     this.ctx.moveTo(0, 0)
  22.     this.ctx.arc(0, 0, radius, startAngle, endAngle)
  23.     this.ctx.closePath()
  24.     // 填充扇形
  25.     this.ctx.fillStyle = this.prizes[i].color
  26.     this.ctx.fill()
  27.     // 绘制边框
  28.     this.ctx.strokeStyle = "#FFFFFF"
  29.     this.ctx.lineWidth = 2
  30.     this.ctx.stroke()
  31.   }
  32.   // 恢复状态
  33.   this.ctx.restore()
  34. }
复制代码
这段代码实现了基本的转盘绘制:
    计算中心点和半径清除画布平移坐标系到转盘中心应用旋转角度绘制每个奖品的扇形区域
运行后,你应该能看到一个彩色的转盘,但还没有文字和指针。
6. 添加奖品文字

继续完善 drawWheel方法,添加奖品文字:
  1. // 绘制转盘扇形
  2. const anglePerPrize = 2 * Math.PI / this.prizes.length
  3. for (let i = 0; i < this.prizes.length; i++) {
  4.   // ...之前的扇形绘制代码
  5.   // 绘制文字
  6.   this.ctx.save()
  7.   this.ctx.rotate(startAngle + anglePerPrize / 2)
  8.   this.ctx.textAlign = 'center'
  9.   this.ctx.textBaseline = 'middle'
  10.   this.ctx.fillStyle = '#333333'
  11.   this.ctx.font = '24px sans-serif'
  12.   // 旋转文字,使其可读性更好
  13.   // 第一象限和第四象限的文字需要额外旋转180度,保证文字朝向
  14.   const needRotate = (i >= this.prizes.length / 4) && (i < this.prizes.length * 3 / 4)
  15.   if (needRotate) {
  16.     this.ctx.rotate(Math.PI)
  17.     this.ctx.fillText(this.prizes[i].name, -radius * 0.6, 0, radius * 0.5)
  18.   } else {
  19.     this.ctx.fillText(this.prizes[i].name, radius * 0.6, 0, radius * 0.5)
  20.   }
  21.   this.ctx.restore()
  22. }
复制代码
这里我们在每个扇形区域添加了奖品文字,并根据位置进行适当旋转,确保文字朝向正确,提高可读性。
7. 添加中心圆盘和指针

继续完善 drawWheel方法,添加中心圆盘和指针:
  1. // 恢复状态
  2. this.ctx.restore()
  3. // 绘制中心圆盘
  4. this.ctx.beginPath()
  5. this.ctx.arc(centerX, centerY, radius * 0.2, 0, 2 * Math.PI)
  6. this.ctx.fillStyle = '#FF8787'
  7. this.ctx.fill()
  8. this.ctx.strokeStyle = '#FFFFFF'
  9. this.ctx.lineWidth = 3
  10. this.ctx.stroke()
  11. // 绘制指针 - 固定在顶部中央
  12. this.ctx.beginPath()
  13. // 三角形指针
  14. this.ctx.moveTo(centerX, centerY - radius - 10)
  15. this.ctx.lineTo(centerX - 15, centerY - radius * 0.8)
  16. this.ctx.lineTo(centerX + 15, centerY - radius * 0.8)
  17. this.ctx.closePath()
  18. this.ctx.fillStyle = '#FF6B6B'
  19. this.ctx.fill()
  20. this.ctx.strokeStyle = '#FFFFFF'
  21. this.ctx.lineWidth = 2
  22. this.ctx.stroke()
  23. // 绘制中心文字
  24. this.ctx.textAlign = 'center'
  25. this.ctx.textBaseline = 'middle'
  26. this.ctx.fillStyle = '#FFFFFF'
  27. this.ctx.font = '18px sans-serif'
  28. // 绘制两行文字
  29. this.ctx.fillText('开始', centerX, centerY - 10)
  30. this.ctx.fillText('抽奖', centerX, centerY + 10)
复制代码
这段代码添加了:
    中心的红色圆盘顶部的三角形指针中心的"开始抽奖"文字
现在转盘的静态部分已经完成。下一步,我们将实现转盘的旋转动画。
8. 实现抽奖逻辑

在实现转盘旋转前,我们需要先实现抽奖逻辑,决定最终奖品:
  1. // 生成随机目标索引(基于概率权重)
  2. private generateTargetIndex(): number {
  3.   const weights = this.prizes.map(prize => prize.probability)
  4.   const totalWeight = weights.reduce((a, b) => a + b, 0)
  5.   const random = Math.random() * totalWeight
  6.   let currentWeight = 0
  7.   for (let i = 0; i < weights.length; i++) {
  8.     currentWeight += weights[i]
  9.     if (random < currentWeight) {
  10.       return i
  11.     }
  12.   }
  13.   return 0
  14. }
复制代码
这个方法根据每个奖品的概率权重生成一个随机索引,概率越高的奖品被选中的机会越大。
9. 实现转盘旋转

现在,让我们实现转盘旋转的核心逻辑:
  1. // 转盘属性
  2. private spinDuration: number = 4000 // 旋转持续时间(毫秒)
  3. private targetIndex: number = 0 // 目标奖品索引
  4. private spinTimer: number = 0 // 旋转定时器
  5. // 开始抽奖
  6. private startSpin(): void {
  7.   if (this.isSpinning) return
  8.   this.isSpinning = true
  9.   this.result = '抽奖中...'
  10.   // 生成目标奖品索引
  11.   this.targetIndex = this.generateTargetIndex()
  12.   console.info(`抽中奖品索引: ${this.targetIndex}, 名称: ${this.prizes[this.targetIndex].name}`)
  13.   // 计算目标角度
  14.   // 每个奖品占据的角度 = 360 / 奖品数量
  15.   const anglePerPrize = 360 / this.prizes.length
  16.   // 因为Canvas中0度是在右侧,顺时针旋转,而指针在顶部(270度位置)
  17.   // 所以需要将奖品旋转到270度位置对应的角度
  18.   // 目标奖品中心点的角度 = 索引 * 每份角度 + 半份角度
  19.   const prizeAngle = this.targetIndex * anglePerPrize + anglePerPrize / 2
  20.   // 需要旋转到270度位置的角度 = 270 - 奖品角度
  21.   // 但由于旋转方向是顺时针,所以需要计算为正向旋转角度
  22.   const targetAngle = (270 - prizeAngle + 360) % 360
  23.   // 获取当前角度的标准化值(0-360范围内)
  24.   const currentRotation = this.rotation % 360
  25.   // 计算从当前位置到目标位置需要旋转的角度(确保是顺时针旋转)
  26.   let deltaAngle = targetAngle - currentRotation
  27.   if (deltaAngle  {
  28.     const elapsed = Date.now() - startTime
  29.     if (elapsed >= this.spinDuration) {
  30.       // 动画结束
  31.       clearInterval(this.spinTimer)
  32.       this.spinTimer = 0
  33.       this.rotation = finalRotation
  34.       this.drawWheel()
  35.       this.isSpinning = false
  36.       this.result = `恭喜获得: ${this.prizes[this.targetIndex].name}`
  37.       return
  38.     }
  39.     // 使用easeOutExpo效果:慢慢减速
  40.     const progress = this.easeOutExpo(elapsed / this.spinDuration)
  41.     this.rotation = initialRotation + progress * (finalRotation - initialRotation)
  42.     // 重绘转盘
  43.     this.drawWheel()
  44.   }, 16) // 大约60fps的刷新率
  45. }
  46. // 缓动函数:指数减速
  47. private easeOutExpo(t: number): number {
  48.   return t === 1 ? 1 : 1 - Math.pow(2, -10 * t)
  49. }
复制代码
这段代码实现了转盘旋转的核心逻辑:
    根据概率生成目标奖品计算目标奖品对应的角度计算需要旋转的总角度(多转几圈再停在目标位置)使用定时器实现转盘的平滑旋转使用缓动函数实现转盘的减速效果旋转结束后显示中奖结果
10. 连接按钮点击事件

现在我们需要将"开始抽奖"按钮与 startSpin方法连接起来:
  1. // 中央开始按钮
  2. Button({ type: ButtonType.Circle }) {
  3.   Text('开始\n抽奖')
  4.     .fontSize(18)
  5.     .fontWeight(FontWeight.Bold)
  6.     .textAlign(TextAlign.Center)
  7.     .fontColor(Color.White)
  8. }
  9. .width(80)
  10. .height(80)
  11. .backgroundColor('#FF6B6B')
  12. .onClick(() => this.startSpin())
  13. .enabled(!this.isSpinning)
  14. .stateEffect(true) // 启用点击效果
复制代码
这里我们给按钮添加了 onClick事件处理器,点击按钮时调用 startSpin方法。同时使用 enabled属性确保在转盘旋转过程中按钮不可点击。
11. 添加资源释放

为了防止内存泄漏,我们需要在页面销毁时清理定时器:
  1. aboutToDisappear() {
  2.   // 清理定时器
  3.   if (this.spinTimer !== 0) {
  4.     clearInterval(this.spinTimer)
  5.     this.spinTimer = 0
  6.   }
  7. }
复制代码
12. 添加底部概率说明(可选)

最后,我们在页面底部添加奖品概率说明:
  1. // 底部说明
  2. Text('奖品说明:概率从高到低排序')
  3.   .fontSize(14)
  4.   .fontColor(Color.White)
  5.   .opacity(0.7)
  6.   .margin({ top: 20 })
  7. // 概率说明
  8. Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Center }) {
  9.   ForEach(this.prizes, (prize: PrizesItem, index) => {
  10.     Text(`${prize.name}: ${prize.probability}%`)
  11.       .fontSize(12)
  12.       .fontColor(Color.White)
  13.       .backgroundColor(prize.color)
  14.       .borderRadius(12)
  15.       .padding({
  16.         left: 10,
  17.         right: 10,
  18.         top: 4,
  19.         bottom: 4
  20.       })
  21.       .margin(4)
  22.   })
  23. }
  24. .width('90%')
  25. .margin({ top: 10 })
复制代码
这段代码在页面底部添加了奖品概率说明,直观展示各个奖品的中奖概率。
13. 美化优化

为了让转盘更加美观,我们可以进一步优化转盘的视觉效果:
  1. // 绘制转盘
  2. private drawWheel(): void {
  3.   // ...之前的代码
  4.   // 绘制转盘外圆边框
  5.   this.ctx.beginPath()
  6.   this.ctx.arc(centerX, centerY, radius + 5, 0, 2 * Math.PI)
  7.   this.ctx.fillStyle = '#2A2A2A'
  8.   this.ctx.fill()
  9.   this.ctx.strokeStyle = '#FFD700' // 金色边框
  10.   this.ctx.lineWidth = 3
  11.   this.ctx.stroke()
  12.   // ...其余绘制代码
  13.   // 给指针添加渐变色和阴影
  14.   let pointerGradient = this.ctx.createLinearGradient(
  15.     centerX, centerY - radius - 15,
  16.     centerX, centerY - radius * 0.8
  17.   )
  18.   pointerGradient.addColorStop(0, '#FF0000')
  19.   pointerGradient.addColorStop(1, '#FF6666')
  20.   this.ctx.fillStyle = pointerGradient
  21.   this.ctx.fill()
  22.   this.ctx.shadowColor = 'rgba(0, 0, 0, 0.5)'
  23.   this.ctx.shadowBlur = 5
  24.   this.ctx.shadowOffsetX = 2
  25.   this.ctx.shadowOffsetY = 2
  26.   // ...其余代码
  27. }
复制代码
完整代码

以下是完整的实现代码:
  1. interface PrizesItem {
  2.   name: string // 奖品名称
  3.   color: string // 转盘颜色
  4.   probability: number // 概率权重
  5. }
  6. @Entry
  7. @Component
  8. struct Index {
  9.   // Canvas 相关设置
  10.   private readonly settings: RenderingContextSettings = new RenderingContextSettings(true); // 启用抗锯齿
  11.   private readonly ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
  12.   // 奖品数据
  13.   private prizes: PrizesItem[] = [
  14.     { name: '谢谢参与', color: '#FFD8A8', probability: 30 },
  15.     { name: '10积分', color: '#B2F2BB', probability: 20 },
  16.     { name: '5元红包', color: '#D0BFFF', probability: 1 },
  17.     { name: '优惠券', color: '#A5D8FF', probability: 15 },
  18.     { name: '免单券', color: '#FCCFE7', probability: 5 },
  19.     { name: '50积分', color: '#BAC8FF', probability: 15 },
  20.     { name: '会员月卡', color: '#99E9F2', probability: 3 },
  21.     { name: '1元红包', color: '#FFBDBD', probability: 2 }
  22.   ]
  23.   // 转盘属性
  24.   @State isSpinning: boolean = false // 是否正在旋转
  25.   @State rotation: number = 0 // 当前旋转角度
  26.   @State result: string = '点击开始抽奖' // 抽奖结果
  27.   private spinDuration: number = 4000 // 旋转持续时间(毫秒)
  28.   private targetIndex: number = 0 // 目标奖品索引
  29.   private spinTimer: number = 0 // 旋转定时器
  30.   private canvasWidth: number = 0 // 画布宽度
  31.   private canvasHeight: number = 0 // 画布高度
  32.   // 生成随机目标索引(基于概率权重)
  33.   private generateTargetIndex(): number {
  34.     const weights = this.prizes.map(prize => prize.probability)
  35.     const totalWeight = weights.reduce((a, b) => a + b, 0)
  36.     const random = Math.random() * totalWeight
  37.     let currentWeight = 0
  38.     for (let i = 0; i < weights.length; i++) {
  39.       currentWeight += weights[i]
  40.       if (random < currentWeight) {
  41.         return i
  42.       }
  43.     }
  44.     return 0
  45.   }
  46.   // 开始抽奖
  47.   private startSpin(): void {
  48.     if (this.isSpinning) {
  49.       return
  50.     }
  51.     this.isSpinning = true
  52.     this.result = '抽奖中...'
  53.     // 生成目标奖品索引
  54.     this.targetIndex = this.generateTargetIndex()
  55.     console.info(`抽中奖品索引: ${this.targetIndex}, 名称: ${this.prizes[this.targetIndex].name}`)
  56.     // 计算目标角度
  57.     // 每个奖品占据的角度 = 360 / 奖品数量
  58.     const anglePerPrize = 360 / this.prizes.length
  59.     // 因为Canvas中0度是在右侧,顺时针旋转,而指针在顶部(270度位置)
  60.     // 所以需要将奖品旋转到270度位置对应的角度
  61.     // 目标奖品中心点的角度 = 索引 * 每份角度 + 半份角度
  62.     const prizeAngle = this.targetIndex * anglePerPrize + anglePerPrize / 2
  63.     // 需要旋转到270度位置的角度 = 270 - 奖品角度
  64.     // 但由于旋转方向是顺时针,所以需要计算为正向旋转角度
  65.     const targetAngle = (270 - prizeAngle + 360) % 360
  66.     // 获取当前角度的标准化值(0-360范围内)
  67.     const currentRotation = this.rotation % 360
  68.     // 计算从当前位置到目标位置需要旋转的角度(确保是顺时针旋转)
  69.     let deltaAngle = targetAngle - currentRotation
  70.     if (deltaAngle  {
  71.       const elapsed = Date.now() - startTime
  72.       if (elapsed >= this.spinDuration) {
  73.         // 动画结束
  74.         clearInterval(this.spinTimer)
  75.         this.spinTimer = 0
  76.         this.rotation = finalRotation
  77.         this.drawWheel()
  78.         this.isSpinning = false
  79.         this.result = `恭喜获得: ${this.prizes[this.targetIndex].name}`
  80.         return
  81.       }
  82.       // 使用easeOutExpo效果:慢慢减速
  83.       const progress = this.easeOutExpo(elapsed / this.spinDuration)
  84.       this.rotation = initialRotation + progress * (finalRotation - initialRotation)
  85.       // 重绘转盘
  86.       this.drawWheel()
  87.     }, 16) // 大约60fps的刷新率
  88.   }
  89.   // 缓动函数:指数减速
  90.   private easeOutExpo(t: number): number {
  91.     return t === 1 ? 1 : 1 - Math.pow(2, -10 * t)
  92.   }
  93.   // 绘制转盘
  94.   private drawWheel(): void {
  95.     if (!this.ctx) {
  96.       return
  97.     }
  98.     const centerX = this.canvasWidth / 2
  99.     const centerY = this.canvasHeight / 2
  100.     const radius = Math.min(centerX, centerY) * 0.85
  101.     // 清除画布
  102.     this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
  103.     // 保存当前状态
  104.     this.ctx.save()
  105.     // 移动到中心点
  106.     this.ctx.translate(centerX, centerY)
  107.     // 应用旋转
  108.     this.ctx.rotate((this.rotation % 360) * Math.PI / 180)
  109.     // 绘制转盘扇形
  110.     const anglePerPrize = 2 * Math.PI / this.prizes.length
  111.     for (let i = 0; i < this.prizes.length; i++) {
  112.       const startAngle = i * anglePerPrize
  113.       const endAngle = (i + 1) * anglePerPrize
  114.       this.ctx.beginPath()
  115.       this.ctx.moveTo(0, 0)
  116.       this.ctx.arc(0, 0, radius, startAngle, endAngle)
  117.       this.ctx.closePath()
  118.       // 填充扇形
  119.       this.ctx.fillStyle = this.prizes[i].color
  120.       this.ctx.fill()
  121.       // 绘制边框
  122.       this.ctx.strokeStyle = "#FFFFFF"
  123.       this.ctx.lineWidth = 2
  124.       this.ctx.stroke()
  125.       // 绘制文字
  126.       this.ctx.save()
  127.       this.ctx.rotate(startAngle + anglePerPrize / 2)
  128.       this.ctx.textAlign = 'center'
  129.       this.ctx.textBaseline = 'middle'
  130.       this.ctx.fillStyle = '#333333'
  131.       this.ctx.font = '30px'
  132.       // 旋转文字,使其可读性更好
  133.       // 第一象限和第四象限的文字需要额外旋转180度,保证文字朝向
  134.       const needRotate = (i >= this.prizes.length / 4) && (i < this.prizes.length * 3 / 4)
  135.       if (needRotate) {
  136.         this.ctx.rotate(Math.PI)
  137.         this.ctx.fillText(this.prizes[i].name, -radius * 0.6, 0, radius * 0.5)
  138.       } else {
  139.         this.ctx.fillText(this.prizes[i].name, radius * 0.6, 0, radius * 0.5)
  140.       }
  141.       this.ctx.restore()
  142.     }
  143.     // 恢复状态
  144.     this.ctx.restore()
  145.     // 绘制中心圆盘
  146.     this.ctx.beginPath()
  147.     this.ctx.arc(centerX, centerY, radius * 0.2, 0, 2 * Math.PI)
  148.     this.ctx.fillStyle = '#FF8787'
  149.     this.ctx.fill()
  150.     this.ctx.strokeStyle = '#FFFFFF'
  151.     this.ctx.lineWidth = 3
  152.     this.ctx.stroke()
  153.     // 绘制指针 - 固定在顶部中央
  154.     this.ctx.beginPath()
  155.     // 三角形指针
  156.     this.ctx.moveTo(centerX, centerY - radius - 10)
  157.     this.ctx.lineTo(centerX - 15, centerY - radius * 0.8)
  158.     this.ctx.lineTo(centerX + 15, centerY - radius * 0.8)
  159.     this.ctx.closePath()
  160.     this.ctx.fillStyle = '#FF6B6B'
  161.     this.ctx.fill()
  162.     this.ctx.strokeStyle = '#FFFFFF'
  163.     this.ctx.lineWidth = 2
  164.     this.ctx.stroke()
  165.     // 绘制中心文字
  166.     this.ctx.textAlign = 'center'
  167.     this.ctx.textBaseline = 'middle'
  168.     this.ctx.fillStyle = '#FFFFFF'
  169.     this.ctx.font = '18px sans-serif'
  170.     // 绘制两行文字
  171.     this.ctx.fillText('开始', centerX, centerY - 10)
  172.     this.ctx.fillText('抽奖', centerX, centerY + 10)
  173.   }
  174.   aboutToDisappear() {
  175.     // 清理定时器
  176.     if (this.spinTimer !== 0) {
  177.       clearInterval(this.spinTimer) // 改成 clearInterval
  178.       this.spinTimer = 0
  179.     }
  180.   }
  181.   build() {
  182.     Column() {
  183.       // 标题
  184.       Text('幸运大转盘')
  185.         .fontSize(28)
  186.         .fontWeight(FontWeight.Bold)
  187.         .fontColor(Color.White)
  188.         .margin({ bottom: 20 })
  189.       // 抽奖结果显示
  190.       Text(this.result)
  191.         .fontSize(20)
  192.         .fontColor(Color.White)
  193.         .backgroundColor('#1AFFFFFF')
  194.         .width('90%')
  195.         .textAlign(TextAlign.Center)
  196.         .padding(15)
  197.         .borderRadius(16)
  198.         .margin({ bottom: 30 })
  199.       // 转盘容器
  200.       Stack({ alignContent: Alignment.Center }) {
  201.         // 使用Canvas绘制转盘
  202.         Canvas(this.ctx)
  203.           .width('100%')
  204.           .height('100%')
  205.           .onReady(() => {
  206.             // 获取Canvas尺寸
  207.             this.canvasWidth = this.ctx.width
  208.             this.canvasHeight = this.ctx.height
  209.             // 初始绘制转盘
  210.             this.drawWheel()
  211.           })
  212.         // 中央开始按钮
  213.         Button({ type: ButtonType.Circle }) {
  214.           Text('开始\n抽奖')
  215.             .fontSize(18)
  216.             .fontWeight(FontWeight.Bold)
  217.             .textAlign(TextAlign.Center)
  218.             .fontColor(Color.White)
  219.         }
  220.         .width(80)
  221.         .height(80)
  222.         .backgroundColor('#FF6B6B')
  223.         .onClick(() => this.startSpin())
  224.         .enabled(!this.isSpinning)
  225.         .stateEffect(true) // 启用点击效果
  226.       }
  227.       .width('90%')
  228.       .aspectRatio(1)
  229.       .backgroundColor('#0DFFFFFF')
  230.       .borderRadius(16)
  231.       .padding(15)
  232.       // 底部说明
  233.       Text('奖品概率说明')
  234.         .fontSize(14)
  235.         .fontColor(Color.White)
  236.         .opacity(0.7)
  237.         .margin({ top: 20 })
  238.       // 概率说明
  239.       Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Center }) {
  240.         ForEach(this.prizes, (prize: PrizesItem) => {
  241.           Text(`${prize.name}: ${prize.probability}%`)
  242.             .fontSize(12)
  243.             .fontColor(Color.White)
  244.             .backgroundColor(prize.color)
  245.             .borderRadius(12)
  246.             .padding({
  247.               left: 10,
  248.               right: 10,
  249.               top: 4,
  250.               bottom: 4
  251.             })
  252.             .margin(4)
  253.         })
  254.       }
  255.       .width('90%')
  256.       .margin({ top: 10 })
  257.     }
  258.     .width('100%')
  259.     .height('100%')
  260.     .justifyContent(FlexAlign.Center)
  261.     .backgroundColor(Color.Black)
  262.     .linearGradient({
  263.       angle: 135,
  264.       colors: [
  265.         ['#1A1B25', 0],
  266.         ['#2D2E3A', 1]
  267.       ]
  268.     })
  269.     .expandSafeArea()
  270.   }
  271. }
复制代码
总结

本教程对 Canvas 的使用有一定难度,建议先点赞收藏。
这个幸运大转盘效果包含以下知识点:
    使用Canvas绘制转盘,支持自定义奖品数量和概率平滑的旋转动画和减速效果基于概率权重的抽奖算法美观的UI设计和交互效果
在实际应用中,你还可以进一步扩展这个组件:
    添加音效实现3D效果添加中奖历史记录连接后端API获取真实抽奖结果添加抽奖次数限制
希望这篇 HarmonyOS Next 教程对你有所帮助,期待您的点赞、评论、收藏。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表