鸿蒙NEXT开发案例:抛硬币

【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. 主页面组件:
• 顶部有一个“抛硬币”的标题,下方是一个行布局,用于展示地鼠组件及正反两面出现的次数。
• 地鼠组件被放置在一个圆形区域内,背景采用线性渐变色。
• 点击地鼠时,会触发一系列动画效果,模拟硬币抛起再落下的过程。
• 通过计算最终的角度,判断是正面还是反面朝上,并更新相应的计数。
【完整代码】
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 | // 定义地鼠组件 @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 ; // 显示反面 } } |
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库