【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; // 顯示反面 } }