鴻蒙NEXT開發案例:轉盤

zhongcx發表於2024-11-10

【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"); // 設定元件背景顏色
  }
}

  

相關文章