鸿蒙NEXT开发案例:转盘

zhongcx
发布于 2024-12-1 10:05
浏览
0收藏

鸿蒙NEXT开发案例:转盘-鸿蒙开发者社区

【1】引言
在鸿蒙NEXT系统中,开发一个有趣且实用的转盘应用不仅可以提升用户体验,还能展示鸿蒙系统的强大功能。本文将介绍如何使用鸿蒙NEXT系统开发一个转盘应用,涵盖从组件定义到用户交互的过程。
【2】环境准备
电脑系统:windows 10
开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806
工程版本:API 12
真机:mate60 pro
语言:ArkTS、ArkUI
【3】难点分析

  1. 扇形路径的计算
    难点:创建扇形的路径需要精确计算起始点、结束点和弧线参数。尤其是涉及到三角函数的使用,初学者可能会对如何将角度转换为坐标感到困惑。
    解决方案:可以通过绘制简单的示意图来帮助理解扇形的构造,并在代码中添加详细注释,解释每一步的计算过程。
  2. 动态角度计算
    难点:在转盘旋转时,需要根据单元格的比例动态计算每个单元格的角度和旋转角度。这涉及到累加和比例计算,可能会导致逻辑错误。
    解决方案:使用数组的 reduce 方法来计算总比例,并在计算每个单元格的角度时,确保逻辑清晰。可以通过单元测试来验证每个单元格的角度是否正确。
  3. 动画效果的实现
    难点:实现转盘的旋转动画需要对动画的持续时间、曲线和结束后的状态进行管理。初学者可能会对如何控制动画的流畅性和效果感到困惑。
    解决方案:可以参考鸿蒙NEXT的动画文档,了解不同的动画效果和参数设置。通过逐步调试,观察动画效果并进行调整。
  4. 用户交互的处理
    难点:处理用户点击事件,尤其是在动画进行时,如何禁用按钮以防止重复点击,可能会导致状态管理的复杂性。
    解决方案:在按钮的点击事件中,使用状态变量(如 isAnimating)来控制按钮的可用性,并在动画结束后恢复按钮的状态。
  5. 组件的状态管理
    难点:在多个组件之间传递状态(如当前选中的单元格、转盘的角度等)可能会导致状态管理混乱。
    解决方案:使用状态管理工具(如 @State 和 @Trace)来确保状态的统一管理,并在需要的地方进行状态更新,保持组件之间的解耦。
    【完整代码】
import { CounterComponent, CounterType } from '@kit.ArkUI'; // 导入计数器组件和计数器类型

// 定义扇形组件
@Component
struct Sector {
  @Prop radius: number; // 扇形的半径
  @Prop angle: number; // 扇形的角度
  @Prop color: string; // 扇形的颜色

  // 创建扇形路径的函数
  createSectorPath(radius: number, angle: number): string {
    const centerX = radius / 2; // 计算扇形中心的X坐标
    const centerY = radius / 2; // 计算扇形中心的Y坐标
    const startX = centerX; // 扇形起始点的X坐标
    const startY = centerY - radius; // 扇形起始点的Y坐标
    const halfAngle = angle / 4; // 计算半个角度

    // 计算扇形结束点1的坐标
    const endX1 = centerX + radius * Math.cos((halfAngle * Math.PI) / 180);
    const endY1 = centerY - radius * Math.sin((halfAngle * Math.PI) / 180);

    // 计算扇形结束点2的坐标
    const endX2 = centerX + radius * Math.cos((-halfAngle * Math.PI) / 180);
    const endY2 = centerY - radius * Math.sin((-halfAngle * Math.PI) / 180);

    // 判断是否为大弧
    const largeArcFlag = angle / 2 > 180 ? 1 : 0;
    const sweepFlag = 1; // 设置弧线方向为顺时针

    // 生成SVG路径命令
    const pathCommands =
      `M${startX} ${startY} A${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${endX1} ${endY1} L${centerX} ${centerY} L${endX2} ${endY2} A${radius} ${radius} 0 ${largeArcFlag} ${1 -
        sweepFlag} ${startX} ${startY} Z`;
    return pathCommands; // 返回路径命令
  }

  // 构建扇形组件
  build() {
    Stack() {
      // 创建第一个扇形路径
      Path()
        .width(`${this.radius}px`) // 设置宽度为半径
        .height(`${this.radius}px`) // 设置高度为半径
        .commands(this.createSectorPath(this.radius, this.angle)) // 设置路径命令
        .fillOpacity(1) // 设置填充透明度
        .fill(this.color) // 设置填充颜色
        .strokeWidth(0) // 设置边框宽度为0
        .rotate({ angle: this.angle / 4 - 90 }); // 旋转扇形

      // 创建第二个扇形路径
      Path()
        .width(`${this.radius}px`) // 设置宽度为半径
        .height(`${this.radius}px`) // 设置高度为半径
        .commands(this.createSectorPath(this.radius, this.angle)) // 设置路径命令
        .fillOpacity(1) // 设置填充透明度
        .fill(this.color) // 设置填充颜色
        .strokeWidth(0) // 设置边框宽度为0
        .rotate({ angle: 180 - (this.angle / 4 - 90) }); // 旋转扇形
    }
  }
}

// 定义单元格类
@ObservedV2
class Cell {
  @Trace angle: number = 0; // 扇形的角度
  @Trace title: string; // 当前格子的标题
  @Trace color: string; // 背景颜色
  @Trace rotate: number = 0; // 在转盘要旋转的角度
  angleStart: number = 0; // 轮盘所在区间的起始
  angleEnd: number = 0; // 轮盘所在区间的结束
  proportion: number = 0; // 所占比例

  // 构造函数
  constructor(proportion: number, title: string, color: string) {
    this.proportion = proportion; // 设置比例
    this.title = title; // 设置标题
    this.color = color; // 设置颜色
  }
}

// 定义转盘组件
@Entry
@Component
struct Wheel {
  @State cells: Cell[] = []; // 存储单元格的数组
  @State wheelWidth: number = 600; // 转盘的宽度
  @State currentAngle: number = 0; // 当前转盘的角度
  @State selectedName: string = ""; // 选中的名称
  isAnimating: boolean = false; // 动画状态
  colorIndex: number = 0; // 颜色索引
  colorPalette: string[] = [ // 颜色调色板
    "#26c2ff",
    "#978efe",
    "#c389fe",
    "#ff85bd",
    "#ff7051",
    "#fea800",
    "#ffcf18",
    "#a9c92a"
  ];

  // 组件即将出现时调用
  aboutToAppear(): void {
    // 初始化单元格
    this.cells.push(new Cell(1, "跑步", this.colorPalette[this.colorIndex++ % this.colorPalette.length]));
    this.cells.push(new Cell(2, "跳绳", this.colorPalette[this.colorIndex++ % this.colorPalette.length]));
    this.cells.push(new Cell(1, "唱歌", this.colorPalette[this.colorIndex++ % this.colorPalette.length]));
    this.cells.push(new Cell(4, "跳舞", this.colorPalette[this.colorIndex++ % this.colorPalette.length]));

    this.calculateAngles(); // 计算角度
  }

  // 计算每个单元格的角度
  private calculateAngles() {
    // 根据比例计算总比例
    const totalProportion = this.cells.reduce((sum, cell) => sum + cell.proportion, 0);
    this.cells.forEach(cell => {
      cell.angle = (cell.proportion * 360) / totalProportion; // 计算每个单元格的角度
    });

    let cumulativeAngle = 0; // 累计角度
    this.cells.forEach(cell => {
      cell.angleStart = cumulativeAngle; // 设置起始角度
      cumulativeAngle += cell.angle; // 更新累计角度
      cell.angleEnd = cumulativeAngle; // 设置结束角度
      cell.rotate = cumulativeAngle - (cell.angle / 2); // 计算旋转角度
    });
  }

  // 构建转盘组件
  build() {
    Column() {
      Row() {
        Text('转盘').fontSize(20).fontColor("#0b0e15"); // 显示转盘标题
      }.width('100%').height(44).justifyContent(FlexAlign.Center); // 设置行的宽度和高度

      // 显示当前状态
      Text(this.isAnimating ? '旋转中' : `${this.selectedName}`).fontSize(20).fontColor("#0b0e15").height(40);

      Stack() {
        Stack() {
          // 遍历每个单元格并绘制扇形
          ForEach(this.cells, (cell: Cell) => {
            Stack() {
              Sector({ radius: lpx2px(this.wheelWidth) / 2, angle: cell.angle, color: cell.color }); // 创建扇形
              Text(cell.title).fontColor(Color.White).margin({ bottom: `${this.wheelWidth / 1.4}lpx` }); // 显示单元格标题
            }.width('100%').height('100%').rotate({ angle: cell.rotate }); // 设置宽度和高度,并旋转
          });
        }
        .borderRadius('50%') // 设置圆角
        .backgroundColor(Color.Gray) // 设置背景颜色
        .width(`${this.wheelWidth}lpx`) // 设置转盘宽度
        .height(`${this.wheelWidth}lpx`) // 设置转盘高度
        .rotate({ angle: this.currentAngle }); // 旋转转盘

        // 创建指针
        Polygon({ width: 20, height: 10 })
          .points([[0, 0], [10, -20], [20, 0]]) // 设置指针的点
          .fill("#d72b0b") // 设置指针颜色
          .height(20) // 设置指针高度
          .margin({ bottom: '140lpx' }); // 设置指针底部边距

        // 创建开始按钮
        Button('开始')
          .fontColor("#c53a2c") // 设置按钮字体颜色
          .borderWidth(10) // 设置按钮边框宽度
          .borderColor("#dd2218") // 设置按钮边框颜色
          .backgroundColor("#fde427") // 设置按钮背景颜色
          .width('200lpx') // 设置按钮宽度
          .height('200lpx') // 设置按钮高度
          .borderRadius('50%') // 设置按钮为圆形
          .clickEffect({ level: ClickEffectLevel.LIGHT }) // 设置点击效果
          .onClick(() => { // 点击按钮时的回调函数
            if (this.isAnimating) { // 如果正在动画中,返回
              return;
            }
            this.selectedName = ""; // 清空选中的名称
            this.isAnimating = true; // 设置动画状态为正在动画
            animateTo({ // 开始动画
              duration: 5000, // 动画持续时间为5000毫秒
              curve: Curve.EaseInOut, // 动画曲线为缓入缓出
              onFinish: () => { // 动画完成后的回调
                this.currentAngle %= 360; // 保持当前角度在0到360之间
                for (const cell of this.cells) { // 遍历每个单元格
                  // 检查当前角度是否在单元格的角度范围内
                  if (360 - this.currentAngle >= cell.angleStart && 360 - this.currentAngle <= cell.angleEnd) {
                    this.selectedName = cell.title; // 设置选中的名称为当前单元格的标题
                    break; // 找到后退出循环
                  }
                }
                this.isAnimating = false; // 设置动画状态为未动画
              },
            }, () => { // 动画进行中的回调
              this.currentAngle += (360 * 5 + Math.floor(Math.random() * 360)); // 更新当前角度,增加随机旋转
            });
          });
      }

      // 创建滚动区域
      Scroll() {
        Column() {
          // 遍历每个单元格,创建输入框和计数器
          ForEach(this.cells, (item: Cell, index: number) => {
            Row() {
              // 创建文本输入框,显示单元格标题
              TextInput({ text: item.title })
                .layoutWeight(1) // 设置输入框占据剩余空间
                .onChange((value) => { // 输入框内容变化时的回调
                  item.title = value; // 更新单元格标题
                });
              // 创建计数器组件
              CounterComponent({
                options: {
                  type: CounterType.COMPACT, // 设置计数器类型为紧凑型
                  numberOptions: {
                    label: `当前占比`, // 设置计数器标签
                    value: item.proportion, // 设置计数器初始值
                    min: 1, // 设置最小值
                    max: 100, // 设置最大值
                    step: 1, // 设置步长
                    onChange: (value: number) => { // 计数器值变化时的回调
                      item.proportion = value; // 更新单元格的比例
                      this.calculateAngles(); // 重新计算角度
                    }
                  }
                }
              });
              // 创建删除按钮
              Button('删除').onClick(() => {
                this.cells.splice(index, 1); // 从单元格数组中删除当前单元格
                this.calculateAngles(); // 重新计算角度
              });
            }.width('100%').justifyContent(FlexAlign.SpaceBetween) // 设置行的宽度和内容对齐方式
            .padding({ left: 40, right: 40 }); // 设置左右内边距
          });
        }.layoutWeight(1); // 设置滚动区域占据剩余空间
      }.layoutWeight(1) // 设置滚动区域占据剩余空间
      .margin({ top: 20, bottom: 20 }) // 设置上下外边距
      .align(Alignment.Top); // 设置对齐方式为顶部对齐

      // 创建添加新内容按钮
      Button('添加新内容').onClick(() => {
        // 向单元格数组中添加新单元格
        this.cells.push(new Cell(1, "新内容", this.colorPalette[this.colorIndex++ % this.colorPalette.length]));
        this.calculateAngles(); // 重新计算角度
      }).margin({ top: 20, bottom: 20 }); // 设置按钮的上下外边距
    }
    .height('100%') // 设置组件高度为100%
    .width('100%') // 设置组件宽度为100%
    .backgroundColor("#f5f8ff"); // 设置组件背景颜色
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
  • 170.
  • 171.
  • 172.
  • 173.
  • 174.
  • 175.
  • 176.
  • 177.
  • 178.
  • 179.
  • 180.
  • 181.
  • 182.
  • 183.
  • 184.
  • 185.
  • 186.
  • 187.
  • 188.
  • 189.
  • 190.
  • 191.
  • 192.
  • 193.
  • 194.
  • 195.
  • 196.
  • 197.
  • 198.
  • 199.
  • 200.
  • 201.
  • 202.
  • 203.
  • 204.
  • 205.
  • 206.
  • 207.
  • 208.
  • 209.
  • 210.
  • 211.
  • 212.
  • 213.
  • 214.
  • 215.
  • 216.
  • 217.
  • 218.
  • 219.
  • 220.
  • 221.
  • 222.
  • 223.
  • 224.
  • 225.
  • 226.
  • 227.
  • 228.
  • 229.
  • 230.
  • 231.
  • 232.
  • 233.
  • 234.
  • 235.
  • 236.
  • 237.
  • 238.
  • 239.
  • 240.
  • 241.
  • 242.
  • 243.
  • 244.
  • 245.
  • 246.
  • 247.
  • 248.
  • 249.
  • 250.
  • 251.
  • 252.
  • 253.

分类
标签
收藏
回复
举报


回复
    相关推荐