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

简介: 本教程将带领大家从零开始,一步步实现一个完整的转盘抽奖效果,包括界面布局、Canvas绘制、动画效果和抽奖逻辑等。

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

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

开发环境准备

  • DevEco Studio 5.0.3
  • HarmonyOS Next API 15

下载代码仓库

1. 需求分析与整体设计

温馨提醒:本案例有一定难度,建议先收藏起来。

在开始编码前,让我们先明确转盘抽奖的基本需求:

  • 展示一个可旋转的奖品转盘
  • 转盘上有多个奖品区域,每个区域有不同的颜色和奖品名称
  • 点击"开始抽奖"按钮后,转盘开始旋转
  • 转盘停止后,指针指向的位置即为抽中的奖品
  • 每个奖品有不同的中奖概率

整体设计思路:

  • 使用HarmonyOS的Canvas组件绘制转盘
  • 利用动画效果实现转盘旋转
  • 根据概率算法确定最终停止位置

特效08-幸运大转盘-效果.gif

2. 基础界面布局

首先,我们创建基础的页面布局,包括标题、转盘区域和结果显示。

@Entry
@Component
struct LuckyWheel {
   
  build() {
   
    Column() {
   
      // 标题
      Text('幸运大转盘')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .fontColor(Color.White)
        .margin({
    bottom: 20 })

      // 抽奖结果显示
      Text('点击开始抽奖')
        .fontSize(20)
        .fontColor(Color.White)
        .backgroundColor('#1AFFFFFF')
        .width('90%')
        .textAlign(TextAlign.Center)
        .padding(15)
        .borderRadius(16)
        .margin({
    bottom: 30 })

      // 转盘容器(后续会添加Canvas)
      Stack({
    alignContent: Alignment.Center }) {
   
        // 这里稍后会添加Canvas绘制转盘

        // 中央开始按钮
        Button({
    type: ButtonType.Circle }) {
   
          Text('开始\n抽奖')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .textAlign(TextAlign.Center)
            .fontColor(Color.White)
        }
        .width(80)
        .height(80)
        .backgroundColor('#FF6B6B')
      }
      .width('90%')
      .aspectRatio(1)
      .backgroundColor('#0DFFFFFF')
      .borderRadius(16)
      .padding(15)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor(Color.Black)
    .linearGradient({
   
      angle: 135,
      colors: [
        ['#1A1B25', 0],
        ['#2D2E3A', 1]
      ]
    })
  }
}
AI 代码解读

这个基础布局创建了一个带有标题、结果显示区和转盘容器的页面。转盘容器使用Stack组件,这样我们可以在转盘上方放置"开始抽奖"按钮。

3. 定义数据结构

接下来,我们需要定义转盘上的奖品数据结构:

// 奖品数据接口
interface PrizesItem {
   
  name: string     // 奖品名称
  color: string    // 转盘颜色
  probability: number // 概率权重
}

@Entry
@Component
struct LuckyWheel {
   
  // 奖品数据
  private prizes: PrizesItem[] = [
    {
    name: '谢谢参与', color: '#FFD8A8', probability: 30 },
    {
    name: '10积分', color: '#B2F2BB', probability: 20 },
    {
    name: '5元红包', color: '#D0BFFF', probability: 10 },
    {
    name: '优惠券', color: '#A5D8FF', probability: 15 },
    {
    name: '免单券', color: '#FCCFE7', probability: 5 },
    {
    name: '50积分', color: '#BAC8FF', probability: 15 },
    {
    name: '会员月卡', color: '#99E9F2', probability: 3 },
    {
    name: '1元红包', color: '#FFBDBD', probability: 2 }
  ]

  // 状态变量
  @State isSpinning: boolean = false // 是否正在旋转
  @State rotation: number = 0 // 当前旋转角度
  @State result: string = '点击开始抽奖' // 抽奖结果

  // ...其余代码
}
AI 代码解读

这里我们定义了转盘上的8个奖品,每个奖品包含名称、颜色和概率权重。同时定义了三个状态变量来跟踪转盘的状态。

4. 初始化Canvas

现在,让我们初始化Canvas来绘制转盘:

@Entry
@Component
struct LuckyWheel {
   
  // Canvas 相关设置
  private readonly settings: RenderingContextSettings = new RenderingContextSettings(true); // 启用抗锯齿
  private readonly ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);

  // 转盘相关属性
  private canvasWidth: number = 0 // 画布宽度
  private canvasHeight: number = 0 // 画布高度

  // ...其余代码

  build() {
   
    Column() {
   
      // ...之前的代码

      // 转盘容器
      Stack({
    alignContent: Alignment.Center }) {
   
        // 使用Canvas绘制转盘
        Canvas(this.ctx)
          .width('100%')
          .height('100%')
          .onReady(() => {
   
            // 获取Canvas尺寸
            this.canvasWidth = this.ctx.width
            this.canvasHeight = this.ctx.height
            // 初始绘制转盘
            this.drawWheel()
          })

        // 中央开始按钮
        // ...按钮代码
      }
      // ...容器样式
    }
    // ...外层容器样式
  }

  // 绘制转盘(先定义一个空方法,稍后实现)
  private drawWheel(): void {
   
    // 稍后实现
  }
}
AI 代码解读

这里我们创建了Canvas绘制上下文,并在onReady回调中获取Canvas尺寸,然后调用drawWheel方法绘制转盘。

5. 实现转盘绘制

接下来,我们实现drawWheel方法,绘制转盘:

// 绘制转盘
private drawWheel(): void {
   
  if (!this.ctx) return

  const centerX = this.canvasWidth / 2
  const centerY = this.canvasHeight / 2
  const radius = Math.min(centerX, centerY) * 0.85

  // 清除画布
  this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)

  // 保存当前状态
  this.ctx.save()

  // 移动到中心点
  this.ctx.translate(centerX, centerY)
  // 应用旋转
  this.ctx.rotate((this.rotation % 360) * Math.PI / 180)

  // 绘制转盘扇形
  const anglePerPrize = 2 * Math.PI / this.prizes.length
  for (let i = 0; i < this.prizes.length; i++) {
   
    const startAngle = i * anglePerPrize
    const endAngle = (i + 1) * anglePerPrize

    this.ctx.beginPath()
    this.ctx.moveTo(0, 0)
    this.ctx.arc(0, 0, radius, startAngle, endAngle)
    this.ctx.closePath()

    // 填充扇形
    this.ctx.fillStyle = this.prizes[i].color
    this.ctx.fill()

    // 绘制边框
    this.ctx.strokeStyle = "#FFFFFF"
    this.ctx.lineWidth = 2
    this.ctx.stroke()
  }

  // 恢复状态
  this.ctx.restore()
}
AI 代码解读

这段代码实现了基本的转盘绘制:

  1. 计算中心点和半径
  2. 清除画布
  3. 平移坐标系到转盘中心
  4. 应用旋转角度
  5. 绘制每个奖品的扇形区域

运行后,你应该能看到一个彩色的转盘,但还没有文字和指针。

6. 添加奖品文字

继续完善drawWheel方法,添加奖品文字:

// 绘制转盘扇形
const anglePerPrize = 2 * Math.PI / this.prizes.length
for (let i = 0; i < this.prizes.length; i++) {
   
  // ...之前的扇形绘制代码

  // 绘制文字
  this.ctx.save()
  this.ctx.rotate(startAngle + anglePerPrize / 2)
  this.ctx.textAlign = 'center'
  this.ctx.textBaseline = 'middle'
  this.ctx.fillStyle = '#333333'
  this.ctx.font = '24px sans-serif'

  // 旋转文字,使其可读性更好
  // 第一象限和第四象限的文字需要额外旋转180度,保证文字朝向
  const needRotate = (i >= this.prizes.length / 4) && (i < this.prizes.length * 3 / 4)
  if (needRotate) {
   
    this.ctx.rotate(Math.PI)
    this.ctx.fillText(this.prizes[i].name, -radius * 0.6, 0, radius * 0.5)
  } else {
   
    this.ctx.fillText(this.prizes[i].name, radius * 0.6, 0, radius * 0.5)
  }

  this.ctx.restore()
}
AI 代码解读

这里我们在每个扇形区域添加了奖品文字,并根据位置进行适当旋转,确保文字朝向正确,提高可读性。

7. 添加中心圆盘和指针

继续完善drawWheel方法,添加中心圆盘和指针:

// 恢复状态
this.ctx.restore()

// 绘制中心圆盘
this.ctx.beginPath()
this.ctx.arc(centerX, centerY, radius * 0.2, 0, 2 * Math.PI)
this.ctx.fillStyle = '#FF8787'
this.ctx.fill()
this.ctx.strokeStyle = '#FFFFFF'
this.ctx.lineWidth = 3
this.ctx.stroke()

// 绘制指针 - 固定在顶部中央
this.ctx.beginPath()
// 三角形指针
this.ctx.moveTo(centerX, centerY - radius - 10)
this.ctx.lineTo(centerX - 15, centerY - radius * 0.8)
this.ctx.lineTo(centerX + 15, centerY - radius * 0.8)
this.ctx.closePath()
this.ctx.fillStyle = '#FF6B6B'
this.ctx.fill()
this.ctx.strokeStyle = '#FFFFFF'
this.ctx.lineWidth = 2
this.ctx.stroke()

// 绘制中心文字
this.ctx.textAlign = 'center'
this.ctx.textBaseline = 'middle'
this.ctx.fillStyle = '#FFFFFF'
this.ctx.font = '18px sans-serif'

// 绘制两行文字
this.ctx.fillText('开始', centerX, centerY - 10)
this.ctx.fillText('抽奖', centerX, centerY + 10)
AI 代码解读

这段代码添加了:

  1. 中心的红色圆盘
  2. 顶部的三角形指针
  3. 中心的"开始抽奖"文字

现在转盘的静态部分已经完成。下一步,我们将实现转盘的旋转动画。

8. 实现抽奖逻辑

在实现转盘旋转前,我们需要先实现抽奖逻辑,决定最终奖品:

// 生成随机目标索引(基于概率权重)
private generateTargetIndex(): number {
   
  const weights = this.prizes.map(prize => prize.probability)
  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++) {
   
    currentWeight += weights[i]
    if (random < currentWeight) {
   
      return i
    }
  }
  return 0
}
AI 代码解读

这个方法根据每个奖品的概率权重生成一个随机索引,概率越高的奖品被选中的机会越大。

9. 实现转盘旋转

现在,让我们实现转盘旋转的核心逻辑:

// 转盘属性
private spinDuration: number = 4000 // 旋转持续时间(毫秒)
private targetIndex: number = 0 // 目标奖品索引
private spinTimer: number = 0 // 旋转定时器

// 开始抽奖
private startSpin(): void {
   
  if (this.isSpinning) return

  this.isSpinning = true
  this.result = '抽奖中...'

  // 生成目标奖品索引
  this.targetIndex = this.generateTargetIndex()

  console.info(`抽中奖品索引: ${
     this.targetIndex}, 名称: ${
     this.prizes[this.targetIndex].name}`)

  // 计算目标角度
  // 每个奖品占据的角度 = 360 / 奖品数量
  const anglePerPrize = 360 / this.prizes.length

  // 因为Canvas中0度是在右侧,顺时针旋转,而指针在顶部(270度位置)
  // 所以需要将奖品旋转到270度位置对应的角度
  // 目标奖品中心点的角度 = 索引 * 每份角度 + 半份角度
  const prizeAngle = this.targetIndex * anglePerPrize + anglePerPrize / 2

  // 需要旋转到270度位置的角度 = 270 - 奖品角度
  // 但由于旋转方向是顺时针,所以需要计算为正向旋转角度
  const targetAngle = (270 - prizeAngle + 360) % 360

  // 获取当前角度的标准化值(0-360范围内)
  const currentRotation = this.rotation % 360

  // 计算从当前位置到目标位置需要旋转的角度(确保是顺时针旋转)
  let deltaAngle = targetAngle - currentRotation
  if (deltaAngle <= 0) {
   
    deltaAngle += 360
  }

  // 最终旋转角度 = 当前角度 + 5圈 + 到目标的角度差
  const finalRotation = this.rotation + 360 * 5 + deltaAngle

  console.info(`当前角度: ${
     currentRotation}°, 奖品角度: ${
     prizeAngle}°, 目标角度: ${
     targetAngle}°, 旋转量: ${
     deltaAngle}°, 最终角度: ${
     finalRotation}°`)

  // 使用基于帧动画的方式旋转,确保视觉上平滑旋转
  let startTime = Date.now()
  let initialRotation = this.rotation

  // 清除可能存在的定时器
  if (this.spinTimer) {
   
    clearInterval(this.spinTimer)
  }

  // 创建新的动画定时器
  this.spinTimer = setInterval(() => {
   
    const elapsed = Date.now() - startTime

    if (elapsed >= this.spinDuration) {
   
      // 动画结束
      clearInterval(this.spinTimer)
      this.spinTimer = 0
      this.rotation = finalRotation
      this.drawWheel()
      this.isSpinning = false
      this.result = `恭喜获得: ${
     this.prizes[this.targetIndex].name}`
      return
    }

    // 使用easeOutExpo效果:慢慢减速
    const progress = this.easeOutExpo(elapsed / this.spinDuration)
    this.rotation = initialRotation + progress * (finalRotation - initialRotation)

    // 重绘转盘
    this.drawWheel()
  }, 16) // 大约60fps的刷新率
}

// 缓动函数:指数减速
private easeOutExpo(t: number): number {
   
  return t === 1 ? 1 : 1 - Math.pow(2, -10 * t)
}
AI 代码解读

这段代码实现了转盘旋转的核心逻辑:

  1. 根据概率生成目标奖品
  2. 计算目标奖品对应的角度
  3. 计算需要旋转的总角度(多转几圈再停在目标位置
  4. 使用定时器实现转盘的平滑旋转
  5. 使用缓动函数实现转盘的减速效果
  6. 旋转结束后显示中奖结果

10. 连接按钮点击事件

现在我们需要将"开始抽奖"按钮与startSpin方法连接起来:

// 中央开始按钮
Button({
    type: ButtonType.Circle }) {
   
  Text('开始\n抽奖')
    .fontSize(18)
    .fontWeight(FontWeight.Bold)
    .textAlign(TextAlign.Center)
    .fontColor(Color.White)
}
.width(80)
.height(80)
.backgroundColor('#FF6B6B')
.onClick(() => this.startSpin())
.enabled(!this.isSpinning)
.stateEffect(true) // 启用点击效果
AI 代码解读

这里我们给按钮添加了onClick事件处理器,点击按钮时调用startSpin方法。同时使用enabled属性确保在转盘旋转过程中按钮不可点击。

11. 添加资源释放

为了防止内存泄漏,我们需要在页面销毁时清理定时器:

aboutToDisappear() {
   
  // 清理定时器
  if (this.spinTimer !== 0) {
   
    clearInterval(this.spinTimer)
    this.spinTimer = 0
  }
}
AI 代码解读

12. 添加底部概率说明(可选)

最后,我们在页面底部添加奖品概率说明:

// 底部说明
Text('奖品说明:概率从高到低排序')
  .fontSize(14)
  .fontColor(Color.White)
  .opacity(0.7)
  .margin({
    top: 20 })

// 概率说明
Flex({
    wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Center }) {
   
  ForEach(this.prizes, (prize: PrizesItem, index) => {
   
    Text(`${
     prize.name}: ${
     prize.probability}%`)
      .fontSize(12)
      .fontColor(Color.White)
      .backgroundColor(prize.color)
      .borderRadius(12)
      .padding({
   
        left: 10,
        right: 10,
        top: 4,
        bottom: 4
      })
      .margin(4)
  })
}
.width('90%')
.margin({
    top: 10 })
AI 代码解读

这段代码在页面底部添加了奖品概率说明,直观展示各个奖品的中奖概率。

13. 美化优化

为了让转盘更加美观,我们可以进一步优化转盘的视觉效果:

// 绘制转盘
private drawWheel(): void {
   
  // ...之前的代码

  // 绘制转盘外圆边框
  this.ctx.beginPath()
  this.ctx.arc(centerX, centerY, radius + 5, 0, 2 * Math.PI)
  this.ctx.fillStyle = '#2A2A2A'
  this.ctx.fill()
  this.ctx.strokeStyle = '#FFD700' // 金色边框
  this.ctx.lineWidth = 3
  this.ctx.stroke()

  // ...其余绘制代码

  // 给指针添加渐变色和阴影
  let pointerGradient = this.ctx.createLinearGradient(
    centerX, centerY - radius - 15,
    centerX, centerY - radius * 0.8
  )
  pointerGradient.addColorStop(0, '#FF0000')
  pointerGradient.addColorStop(1, '#FF6666')
  this.ctx.fillStyle = pointerGradient
  this.ctx.fill()

  this.ctx.shadowColor = 'rgba(0, 0, 0, 0.5)'
  this.ctx.shadowBlur = 5
  this.ctx.shadowOffsetX = 2
  this.ctx.shadowOffsetY = 2

  // ...其余代码
}
AI 代码解读

完整代码

以下是完整的实现代码:

interface PrizesItem {
   
  name: string // 奖品名称
  color: string // 转盘颜色
  probability: number // 概率权重
}

@Entry
@Component
struct Index {
   
  // Canvas 相关设置
  private readonly settings: RenderingContextSettings = new RenderingContextSettings(true); // 启用抗锯齿
  private readonly ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
  // 奖品数据
  private prizes: PrizesItem[] = [
    {
    name: '谢谢参与', color: '#FFD8A8', probability: 30 },
    {
    name: '10积分', color: '#B2F2BB', probability: 20 },
    {
    name: '5元红包', color: '#D0BFFF', probability: 1 },
    {
    name: '优惠券', color: '#A5D8FF', probability: 15 },
    {
    name: '免单券', color: '#FCCFE7', probability: 5 },
    {
    name: '50积分', color: '#BAC8FF', probability: 15 },
    {
    name: '会员月卡', color: '#99E9F2', probability: 3 },
    {
    name: '1元红包', color: '#FFBDBD', probability: 2 }
  ]
  // 转盘属性
  @State isSpinning: boolean = false // 是否正在旋转
  @State rotation: number = 0 // 当前旋转角度
  @State result: string = '点击开始抽奖' // 抽奖结果
  private spinDuration: number = 4000 // 旋转持续时间(毫秒)
  private targetIndex: number = 0 // 目标奖品索引
  private spinTimer: number = 0 // 旋转定时器
  private canvasWidth: number = 0 // 画布宽度
  private canvasHeight: number = 0 // 画布高度

  // 生成随机目标索引(基于概率权重)
  private generateTargetIndex(): number {
   
    const weights = this.prizes.map(prize => prize.probability)
    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++) {
   
      currentWeight += weights[i]
      if (random < currentWeight) {
   
        return i
      }
    }
    return 0
  }

  // 开始抽奖
  private startSpin(): void {
   
    if (this.isSpinning) {
   
      return
    }

    this.isSpinning = true
    this.result = '抽奖中...'

    // 生成目标奖品索引
    this.targetIndex = this.generateTargetIndex()

    console.info(`抽中奖品索引: ${
     this.targetIndex}, 名称: ${
     this.prizes[this.targetIndex].name}`)

    // 计算目标角度
    // 每个奖品占据的角度 = 360 / 奖品数量
    const anglePerPrize = 360 / this.prizes.length

    // 因为Canvas中0度是在右侧,顺时针旋转,而指针在顶部(270度位置)
    // 所以需要将奖品旋转到270度位置对应的角度
    // 目标奖品中心点的角度 = 索引 * 每份角度 + 半份角度
    const prizeAngle = this.targetIndex * anglePerPrize + anglePerPrize / 2

    // 需要旋转到270度位置的角度 = 270 - 奖品角度
    // 但由于旋转方向是顺时针,所以需要计算为正向旋转角度
    const targetAngle = (270 - prizeAngle + 360) % 360

    // 获取当前角度的标准化值(0-360范围内)
    const currentRotation = this.rotation % 360

    // 计算从当前位置到目标位置需要旋转的角度(确保是顺时针旋转)
    let deltaAngle = targetAngle - currentRotation
    if (deltaAngle <= 0) {
   
      deltaAngle += 360
    }

    // 最终旋转角度 = 当前角度 + 5圈 + 到目标的角度差
    const finalRotation = this.rotation + 360 * 5 + deltaAngle

    console.info(`当前角度: ${
     currentRotation}°, 奖品角度: ${
     prizeAngle}°, 目标角度: ${
     targetAngle}°, 旋转量: ${
     deltaAngle}°, 最终角度: ${
     finalRotation}°`)

    // 使用基于帧动画的方式旋转,确保视觉上平滑旋转
    let startTime = Date.now()
    let initialRotation = this.rotation

    // 清除可能存在的定时器
    if (this.spinTimer) {
   
      clearInterval(this.spinTimer)
    }

    // 创建新的动画定时器
    this.spinTimer = setInterval(() => {
   
      const elapsed = Date.now() - startTime

      if (elapsed >= this.spinDuration) {
   
        // 动画结束
        clearInterval(this.spinTimer)
        this.spinTimer = 0
        this.rotation = finalRotation
        this.drawWheel()
        this.isSpinning = false
        this.result = `恭喜获得: ${
     this.prizes[this.targetIndex].name}`
        return
      }

      // 使用easeOutExpo效果:慢慢减速
      const progress = this.easeOutExpo(elapsed / this.spinDuration)
      this.rotation = initialRotation + progress * (finalRotation - initialRotation)

      // 重绘转盘
      this.drawWheel()
    }, 16) // 大约60fps的刷新率
  }

  // 缓动函数:指数减速
  private easeOutExpo(t: number): number {
   
    return t === 1 ? 1 : 1 - Math.pow(2, -10 * t)
  }

  // 绘制转盘
  private drawWheel(): void {
   
    if (!this.ctx) {
   
      return
    }

    const centerX = this.canvasWidth / 2
    const centerY = this.canvasHeight / 2
    const radius = Math.min(centerX, centerY) * 0.85

    // 清除画布
    this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)

    // 保存当前状态
    this.ctx.save()

    // 移动到中心点
    this.ctx.translate(centerX, centerY)
    // 应用旋转
    this.ctx.rotate((this.rotation % 360) * Math.PI / 180)

    // 绘制转盘扇形
    const anglePerPrize = 2 * Math.PI / this.prizes.length
    for (let i = 0; i < this.prizes.length; i++) {
   
      const startAngle = i * anglePerPrize
      const endAngle = (i + 1) * anglePerPrize

      this.ctx.beginPath()
      this.ctx.moveTo(0, 0)
      this.ctx.arc(0, 0, radius, startAngle, endAngle)
      this.ctx.closePath()

      // 填充扇形
      this.ctx.fillStyle = this.prizes[i].color
      this.ctx.fill()

      // 绘制边框
      this.ctx.strokeStyle = "#FFFFFF"
      this.ctx.lineWidth = 2
      this.ctx.stroke()

      // 绘制文字
      this.ctx.save()
      this.ctx.rotate(startAngle + anglePerPrize / 2)
      this.ctx.textAlign = 'center'
      this.ctx.textBaseline = 'middle'
      this.ctx.fillStyle = '#333333'
      this.ctx.font = '30px'

      // 旋转文字,使其可读性更好
      // 第一象限和第四象限的文字需要额外旋转180度,保证文字朝向
      const needRotate = (i >= this.prizes.length / 4) && (i < this.prizes.length * 3 / 4)
      if (needRotate) {
   
        this.ctx.rotate(Math.PI)
        this.ctx.fillText(this.prizes[i].name, -radius * 0.6, 0, radius * 0.5)
      } else {
   
        this.ctx.fillText(this.prizes[i].name, radius * 0.6, 0, radius * 0.5)
      }

      this.ctx.restore()
    }

    // 恢复状态
    this.ctx.restore()

    // 绘制中心圆盘
    this.ctx.beginPath()
    this.ctx.arc(centerX, centerY, radius * 0.2, 0, 2 * Math.PI)
    this.ctx.fillStyle = '#FF8787'
    this.ctx.fill()
    this.ctx.strokeStyle = '#FFFFFF'
    this.ctx.lineWidth = 3
    this.ctx.stroke()

    // 绘制指针 - 固定在顶部中央
    this.ctx.beginPath()
    // 三角形指针
    this.ctx.moveTo(centerX, centerY - radius - 10)
    this.ctx.lineTo(centerX - 15, centerY - radius * 0.8)
    this.ctx.lineTo(centerX + 15, centerY - radius * 0.8)
    this.ctx.closePath()
    this.ctx.fillStyle = '#FF6B6B'
    this.ctx.fill()
    this.ctx.strokeStyle = '#FFFFFF'
    this.ctx.lineWidth = 2
    this.ctx.stroke()

    // 绘制中心文字
    this.ctx.textAlign = 'center'
    this.ctx.textBaseline = 'middle'
    this.ctx.fillStyle = '#FFFFFF'
    this.ctx.font = '18px sans-serif'

    // 绘制两行文字
    this.ctx.fillText('开始', centerX, centerY - 10)
    this.ctx.fillText('抽奖', centerX, centerY + 10)
  }

  aboutToDisappear() {
   
    // 清理定时器
    if (this.spinTimer !== 0) {
   
      clearInterval(this.spinTimer) // 改成 clearInterval
      this.spinTimer = 0
    }
  }

  build() {
   
    Column() {
   
      // 标题
      Text('幸运大转盘')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .fontColor(Color.White)
        .margin({
    bottom: 20 })

      // 抽奖结果显示
      Text(this.result)
        .fontSize(20)
        .fontColor(Color.White)
        .backgroundColor('#1AFFFFFF')
        .width('90%')
        .textAlign(TextAlign.Center)
        .padding(15)
        .borderRadius(16)
        .margin({
    bottom: 30 })

      // 转盘容器
      Stack({
    alignContent: Alignment.Center }) {
   
        // 使用Canvas绘制转盘
        Canvas(this.ctx)
          .width('100%')
          .height('100%')
          .onReady(() => {
   
            // 获取Canvas尺寸
            this.canvasWidth = this.ctx.width
            this.canvasHeight = this.ctx.height
            // 初始绘制转盘
            this.drawWheel()
          })

        // 中央开始按钮
        Button({
    type: ButtonType.Circle }) {
   
          Text('开始\n抽奖')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .textAlign(TextAlign.Center)
            .fontColor(Color.White)
        }
        .width(80)
        .height(80)
        .backgroundColor('#FF6B6B')
        .onClick(() => this.startSpin())
        .enabled(!this.isSpinning)
        .stateEffect(true) // 启用点击效果
      }
      .width('90%')
      .aspectRatio(1)
      .backgroundColor('#0DFFFFFF')
      .borderRadius(16)
      .padding(15)

      // 底部说明
      Text('奖品概率说明')
        .fontSize(14)
        .fontColor(Color.White)
        .opacity(0.7)
        .margin({
    top: 20 })

      // 概率说明
      Flex({
    wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Center }) {
   
        ForEach(this.prizes, (prize: PrizesItem) => {
   
          Text(`${
     prize.name}: ${
     prize.probability}%`)
            .fontSize(12)
            .fontColor(Color.White)
            .backgroundColor(prize.color)
            .borderRadius(12)
            .padding({
   
              left: 10,
              right: 10,
              top: 4,
              bottom: 4
            })
            .margin(4)
        })
      }
      .width('90%')
      .margin({
    top: 10 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor(Color.Black)
    .linearGradient({
   
      angle: 135,
      colors: [
        ['#1A1B25', 0],
        ['#2D2E3A', 1]
      ]
    })
    .expandSafeArea()
  }
}
AI 代码解读

总结

本教程对 Canvas 的使用有一定难度,建议先点赞收藏。

这个幸运大转盘效果包含以下知识点:

  1. 使用Canvas绘制转盘,支持自定义奖品数量和概率
  2. 平滑的旋转动画和减速效果
  3. 基于概率权重的抽奖算法
  4. 美观的UI设计和交互效果

在实际应用中,你还可以进一步扩展这个组件:

  • 添加音效
  • 实现3D效果
  • 添加中奖历史记录
  • 连接后端API获取真实抽奖结果
  • 添加抽奖次数限制

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

megasu
+关注
目录
打赏
0
0
0
0
70
分享
相关文章
鸿蒙特效教程01-哔哩哔哩点赞与一键三连效果实现教程
本教程面向HarmonyOS初学者,详细讲解如何实现类似哔哩哔哩APP中的点赞与一键三连效果。内容涵盖基础布局、状态切换、点击动画、长按手势识别、旋转缩放动画以及粒子爆炸效果的实现。通过ArkUI布局系统、状态管理、手势处理和动画技术,逐步完成从简单到复杂的交互设计。最终效果包括图标变色、缩放、旋转及粒子动画,为用户提供流畅生动的体验。适合希望掌握HarmonyOS开发技巧的开发者学习参考。
124 67
鸿蒙特效教程09-深入学习animateTo动画
本教程将带领大家从零开始,一步步讲解如何讲解 animateTo 动画,并实现按钮交互效果,使新手也能轻松掌握。
39 6
|
4天前
|
鸿蒙特效教程07-九宫格幸运抽奖
在移动应用中,抽奖功能是一种常见且受欢迎的交互方式,能够有效提升用户粘性。本教程将带领大家从零开始,逐步实现一个九宫格抽奖效果,适合HarmonyOS开发的初学者阅读。
69 2
|
4天前
|
鸿蒙特效教程06-可拖拽网格实现教程
本教程适合 HarmonyOS Next 初学者,通过简单到复杂的步骤,一步步实现类似桌面APP中的可拖拽编辑效果。
71 1
鸿蒙特效教程04-直播点赞动画效果实现教程
本教程适合HarmonyOS初学者,通过简单到复杂的步骤,通过HarmonyOS的Canvas组件,一步步实现时下流行的点赞动画效果。
72 1
【HarmonyOS开发】ArkTS基础语法及使用(鸿蒙开发基础教程)
【HarmonyOS开发】ArkTS基础语法及使用(鸿蒙开发基础教程)
569 4
|
3天前
|
鸿蒙特效教程03-水波纹动画效果实现教程
本教程适合HarmonyOS初学者,通过简单到复杂的步骤,一步步实现漂亮的水波纹动画效果。
70 0
|
4天前
|
鸿蒙特效教程02-微信语音录制动画效果实现教程
本教程适合HarmonyOS初学者,通过简单到复杂的步骤,一步步实现类似微信APP中的语音录制动画效果。
73 0
HarmonyOS ArkTS声明式UI开发实战教程
本文深入探讨了ArkTS作为HarmonyOS生态中新一代声明式UI开发框架的优势与应用。首先对比了声明式与命令式开发的区别,展示了ArkTS如何通过直观高效的代码提升可维护性。接着分析了其核心三要素:数据驱动、组件化和状态管理,并通过具体案例解析布局体系、交互组件开发技巧及复杂状态管理方案。最后,通过构建完整TODO应用实战,结合调试优化指南,帮助开发者掌握声明式UI设计精髓,感受ArkTS的独特魅力。文章鼓励读者通过“破坏性实验”建立声明式编程思维,共同推动HarmonyOS生态发展。
62 3
|
3月前
|
「Mac畅玩鸿蒙与硬件36」UI互动应用篇13 - 数字滚动抽奖器
本篇将带你实现一个简单的数字滚动抽奖器。用户点击按钮后,屏幕上的数字会以滚动动画的形式随机变动,最终显示一个抽奖数字。这个项目展示了如何结合定时器、状态管理和动画实现一个有趣的互动应用。
123 23
「Mac畅玩鸿蒙与硬件36」UI互动应用篇13 - 数字滚动抽奖器