鴻蒙NEXT開發案例:拋硬幣

zhongcx發表於2024-11-11

【1】引言(完整程式碼在最後面)

本專案旨在實現一個簡單的“拋硬幣”功能,使用者可以透過點選螢幕上的地鼠圖示來模擬拋硬幣的過程。應用會記錄並顯示硬幣正面(地鼠面)和反面(數字100面)出現的次數。為了增強使用者體驗,我們還新增了動畫效果,使拋硬幣的過程更加生動有趣。

【2】環境準備

電腦系統:windows 10

開發工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806

工程版本:API 12

真機:mate60 pro

語言:ArkTS、ArkUI

【3】應用結構

應用主要由兩個部分組成:地鼠元件(Hamster)和主頁面元件(CoinTossPage)。

地鼠元件(Hamster)

地鼠元件是應用的核心視覺元素之一,負責展示地鼠的形象。該元件透過@Component裝飾器定義,並接收一個屬性cellWidth,用於控制元件的大小。

主頁面元件(CoinTossPage)

主頁面元件是整個應用的入口點,負責組織和管理各個UI元素。該元件同樣透過@Component裝飾器定義,幷包含多個狀態變數用於跟蹤硬幣的狀態和動畫進度。

【4】功能解析

1. 地鼠元件:

• 透過Stack佈局組合多個圖形元素,建立了一個地鼠的形象。

• 每個圖形元素都設定了具體的尺寸、顏色、邊框等樣式,並透過margin屬性調整位置。

2. 主頁面元件:

• 頂部有一個“拋硬幣”的標題,下方是一個行佈局,用於展示地鼠元件及正反兩面出現的次數。

• 地鼠元件被放置在一個圓形區域內,背景採用線性漸變色。

• 點選地鼠時,會觸發一系列動畫效果,模擬硬幣拋起再落下的過程。

• 透過計算最終的角度,判斷是正面還是反面朝上,並更新相應的計數。

【完整程式碼】

// 定義地鼠元件
@Component
struct Hamster {
  @Prop cellWidth: number // 單元格寬度

  build() {
    Stack() { // 建立一個堆疊佈局
      // 身體
      Text()
        .width(`${this.cellWidth / 2}lpx`)// 寬度為單元格寬度的一半
        .height(`${this.cellWidth / 3 * 2}lpx`)// 高度為單元格高度的2/3
        .backgroundColor("#b49579")// 背景顏色
        .borderRadius({ topLeft: '50%', topRight: '50%' })// 圓角
        .borderColor("#2a272d")// 邊框顏色
        .borderWidth(1) // 邊框寬度
      // 嘴巴
      Ellipse()
        .width(`${this.cellWidth / 4}lpx`)// 嘴巴的寬度
        .height(`${this.cellWidth / 5}lpx`)// 嘴巴的高度
        .fillOpacity(1)// 填充不透明度
        .fill("#e7bad7")// 填充顏色
        .stroke("#563e3f")// 邊框顏色
        .strokeWidth(1)// 邊框寬度
        .margin({ top: `${this.cellWidth / 6}lpx` }) // 上邊距
      // 左眼睛
      Ellipse()
        .width(`${this.cellWidth / 9}lpx`)// 左眼睛的寬度
        .height(`${this.cellWidth / 6}lpx`)// 左眼睛的高度
        .fillOpacity(1)// 填充不透明度
        .fill("#313028")// 填充顏色
        .stroke("#2e2018")// 邊框顏色
        .strokeWidth(1)// 邊框寬度
        .margin({ bottom: `${this.cellWidth / 3}lpx`, right: `${this.cellWidth / 6}lpx` }) // 下邊距和右邊距
      // 右眼睛
      Ellipse()
        .width(`${this.cellWidth / 9}lpx`)// 右眼睛的寬度
        .height(`${this.cellWidth / 6}lpx`)// 右眼睛的高度
        .fillOpacity(1)// 填充不透明度
        .fill("#313028")// 填充顏色
        .stroke("#2e2018")// 邊框顏色
        .strokeWidth(1)// 邊框寬度
        .margin({ bottom: `${this.cellWidth / 3}lpx`, left: `${this.cellWidth / 6}lpx` }) // 下邊距和左邊距
      // 左眼瞳
      Ellipse()
        .width(`${this.cellWidth / 20}lpx`)// 左眼瞳的寬度
        .height(`${this.cellWidth / 15}lpx`)// 左眼瞳的高度
        .fillOpacity(1)// 填充不透明度
        .fill("#fefbfa")// 填充顏色
        .margin({ bottom: `${this.cellWidth / 2.5}lpx`, right: `${this.cellWidth / 6}lpx` }) // 下邊距和右邊距
      // 右眼瞳
      Ellipse()
        .width(`${this.cellWidth / 20}lpx`)// 右眼瞳的寬度
        .height(`${this.cellWidth / 15}lpx`)// 右眼瞳的高度
        .fillOpacity(1)// 填充不透明度
        .fill("#fefbfa")// 填充顏色
        .margin({ bottom: `${this.cellWidth / 2.5}lpx`, left: `${this.cellWidth / 6}lpx` }) // 下邊距和左邊距
    }.width(`${this.cellWidth}lpx`).height(`${this.cellWidth}lpx`) // 設定元件的寬度和高度
  }
}

// 定義頁面元件
@Entry
@Component
struct CoinTossPage {
  @State cellWidth: number = 50 // 單元格寬度
  @State headsCount: number = 0 // 正面朝上的次數
  @State tailsCount: number = 0 // 反面朝上的次數
  @State rotationAngle: number = 0 // 旋轉角度
  @State verticalOffset: number = 0 // 縱向位移
  @State isAnimRun: boolean = false // 動畫是否正在執行

  build() {
    Column() {
      // 頁面標題
      Text('拋硬幣')
        .height(50)// 高度設定為50
        .width('100%')// 寬度設定為100%
        .textAlign(TextAlign.Center)// 文字居中對齊
        .fontColor("#fefefe")// 字型顏色
        .fontSize(20); // 字型大小

      // 顯示地鼠和計數
      Row({ space: 20 }) {
        Stack() {
          Hamster({ cellWidth: this.cellWidth }) // 建立地鼠元件
        }
        .borderRadius('50%') // 設定圓角
        .width(`${this.cellWidth}lpx`) // 設定寬度
        .height(`${this.cellWidth}lpx`) // 設定高度
        .linearGradient({
          // 設定線性漸變背景
          direction: GradientDirection.LeftBottom,
          colors: [['#ebcf2f', 0.0], ['#fef888', 0.5], ['#ebcf2f', 1.0]]
        });

        // 顯示反面朝上的次數
        Text(`${this.tailsCount}`)
          .fontSize(20)
          .fontColor("#fefefe");

        Stack() {
          // 顯示100
          Text("100")
            .fontColor("#9f7606")
            .fontSize(`${this.cellWidth / 2}lpx`);
        }
        .borderRadius('50%') // 設定圓角
        .width(`${this.cellWidth}lpx`) // 設定寬度
        .height(`${this.cellWidth}lpx`) // 設定高度
        .linearGradient({
          // 設定線性漸變背景
          direction: GradientDirection.LeftBottom,
          colors: [['#ebcf2f', 0.0], ['#fef888', 0.5], ['#ebcf2f', 1.0]]
        });

        // 顯示正面朝上的次數
        Text(`${this.headsCount}`)
          .fontSize(20)
          .fontColor("#fefefe");

      }.width('100%').justifyContent(FlexAlign.Center); // 設定寬度和內容居中對齊

      Stack() {
        Stack() {
          // 建立放大版地鼠元件
          Hamster({ cellWidth: this.cellWidth * 3 })
            .visibility(this.isHeadsFaceUp() ? Visibility.Visible : Visibility.Hidden); // 根據狀態顯示或隱藏

          // 顯示100
          Text("100")
            .fontColor("#9f7606")// 字型顏色
            .fontSize(`${this.cellWidth / 2 * 3}lpx`)// 字型大小
            .visibility(!this.isHeadsFaceUp() ? Visibility.Visible : Visibility.Hidden)// 根據狀態顯示或隱藏
            .rotate({
              // 旋轉180度
              x: 1,
              y: 0,
              z: 0,
              angle: 180
            });
        }
        .borderRadius('50%') // 設定圓角
        .width(`${this.cellWidth * 3}lpx`) // 設定寬度
        .height(`${this.cellWidth * 3}lpx`) // 設定高度
        .linearGradient({
          // 設定線性漸變背景
          direction: GradientDirection.LeftBottom,
          colors: [['#ebcf2f', 0.0], ['#fef888', 0.5], ['#ebcf2f', 1.0]]
        })
        .rotate({
          // 根據當前角度旋轉
          x: 1,
          y: 0,
          z: 0,
          angle: this.rotationAngle
        })
        .translate({ x: 0, y: this.verticalOffset }) // 設定縱向位移
        .onClick(() => { // 點選事件處理

          if (this.isAnimRun) {
            return;
          }
          this.isAnimRun = true

          let maxAnimationSteps = 2 * (10 + Math.floor(Math.random() * 10)); // 計算最大動畫次數
          let totalAnimationDuration = 2000; // 動畫總時長

          // 第一次動畫,向上丟擲
          animateToImmediately({
            duration: totalAnimationDuration / 2, // 動畫時長為總時長的一半
            onFinish: () => { // 動畫完成後的回撥
              // 第二次動畫,向下落
              animateToImmediately({
                duration: totalAnimationDuration / 2,
                onFinish: () => {
                  this.rotationAngle = this.rotationAngle % 360; // 確保角度在0到360之間
                  // 判斷當前顯示的面
                  if (this.isHeadsFaceUp()) { // 如果是地鼠面
                    this.tailsCount++; // 反面朝上的次數加1
                  } else { // 如果是反面
                    this.headsCount++; // 正面朝上的次數加1
                  }
                  this.isAnimRun = false
                }
              }, () => {
                this.verticalOffset = 0; // 重置縱向位移
              });
            }
          }, () => {
            // 設定縱向位移,模擬拋硬幣的效果
            this.verticalOffset = -100 * (1 + Math.floor(Math.random() * 5)); // 隨機設定向上的位移
          });

          // 迴圈動畫,增加旋轉效果
          for (let i = 0; i < maxAnimationSteps; i++) {
            animateToImmediately({
              delay: i * totalAnimationDuration / maxAnimationSteps, // 設定每次動畫的延遲
              duration: 100, // 每次動畫的持續時間
              onFinish: () => {
                // 動畫完成後的回撥
              }
            }, () => {
              this.rotationAngle += 90; // 每次增加90度旋轉
            });
          }
        });

      }.width('100%').layoutWeight(1).align(Alignment.Bottom).padding({ bottom: 80 }); // 設定元件的寬度、權重、對齊方式和底部內邊距
    }
    .height('100%') // 設定整個頁面的高度
    .width('100%') // 設定整個頁面的寬度
    .backgroundColor("#0b0d0c"); // 設定背景顏色
  }

  // 判斷當前是否顯示地鼠面
  isHeadsFaceUp() {
    let normalizedAngle = this.rotationAngle % 360; // 規範化角度
    // 判斷角度範圍,確定是否顯示地鼠面
    if (normalizedAngle >= 0 && normalizedAngle < 90 || normalizedAngle >= 270 && normalizedAngle <= 360) {
      return true; // 顯示地鼠面
    }
    return false; // 顯示反面
  }
}

  

相關文章