HarmonyOS应用开发实战:猫猫大作战-Row 横向布局、Spacer 占位、Button 双态样式(暂停灰 / 重新开始红)、底部 paddin 前言棋盘和点击层搞定后游戏页还缺一个底部控制栏——玩家在游戏中需要随时「暂停」或「重新开始」。这是 ArkUI 里最经典的Row双按钮布局两个Button用Spacer撑开分列两端整体一行铺满底部。本篇以「猫猫大作战」游戏页底部控制栏为锚点把Row 横向布局、Spacer 占位、Button 双态样式暂停灰 / 重新开始红、底部 padding 留白四大要点讲透。读完本篇你将能独立写出底部操作栏、双按钮等距布局、按钮色彩语义三种实战套路。提示本系列不讲 ArkTS 基础语法与环境搭建假设你已跟完第 1–17 篇。本篇继续游戏页底部收尾。一、场景拆解底部控制栏打开entry/src/main/ets/pages/Index.ets的GameView找到最底部的控制栏// 来源entry/src/main/ets/pages/Index.ets GameView() 底部 Row() { Button(暂停) .width(100) .height(44) .fontSize(15) .fontColor(#FFFFFF) .backgroundColor(#95A5A6) .borderRadius(22) .onClick(() { this.pauseGame(); }) Spacer() Button(重新开始) .width(100) .height(44) .fontSize(15) .fontColor(#FFFFFF) .backgroundColor(#E74C3C) .borderRadius(22) .onClick(() { this.clearTimers(); this.startGame(); }) } .width(100%) .padding({ left: 24, right: 24, bottom: 24, top: 12 })这条底部栏规格拆解元素类型作用Row外壳横向容器width(100%)铺满底部Button(暂停)灰色按钮100×44vp圆角 22Spacer()弹性占位把两按钮推向左右两端Button(重新开始)红色按钮100×44vp圆角 22.padding({...})内边距左右 24、底 24、顶 12关键经验RowSpacer是「两端对齐」的最简组合——Spacer吃掉中间所有剩余空间把两按钮顶到两端。二、Row 横向布局2.1 Row 的核心属性Row() { /* 子组件横向排列 */ } .width(100%) // 容器宽度 .height(60) // 容器高度可选 .justifyContent(FlexAlign.SpaceBetween) // 主轴对齐 .alignItems(VerticalAlign.Center) // 交叉轴对齐 .padding({ left: 24, right: 24 }) // 内边距Row 的两个轴轴方向对齐属性主轴水平justifyContent(FlexAlign.xxx)交叉轴垂直alignItems(VerticalAlign.xxx)2.2 主轴对齐方式justifyContent控制子组件在主轴水平方向的排布FlexAlign 值视觉效果适用场景Start靠左排列左对齐组Center居中排列居中按钮组End靠右排列右对齐组SpaceBetween两端对齐中间不留白两端按钮推荐SpaceAround每个按钮左右各留半个间距等距分布SpaceEvenly任意两按钮间距相等含端点均匀分布本项目底部栏用了Spacer()手动撑开所以Row没显式设justifyContent——Spacer已经把两按钮顶到两端了。2.3 交叉轴对齐alignItems控制子组件在交叉轴垂直方向的对齐Row() { Button(暂停).height(44) Spacer() Button(重新开始).height(44) } .alignItems(VerticalAlign.Center) // 两按钮垂直居中VerticalAlign 值视觉Top顶部对齐Center垂直居中推荐Bottom底部对齐提示两按钮高度一致时alignItems几乎无视觉差异但保留Center是好习惯——后续按钮高度变化时不会错位。三、Spacer 弹性占位3.1 Spacer 的本质Row() { Button(暂停) Spacer() // 占据两按钮之间的所有剩余空间 Button(重新开始) } .width(100%)Spacer是一个弹性空白组件——它会自动撑开吃掉父容器中所有剩余空间。在Row里它把后续组件推向右端实现「两端对齐」。3.2 Spacer 高度的坑Row() { Button(暂停).height(44) Spacer() // 默认 height 父高度 Button(重新开始).height(44) } .height(60) // Row 高度 60 // Spacer 自动占满 60 高度与 Row 等高关键Spacer在Row里默认高度等于父容器高度在Column里默认宽度等于父容器宽度。本项目Row没设height由内部 Button 的 44vp 撑开Spacer 高度自然也是 44vp。3.3 Spacer vs layoutWeight「两端对齐」有两种写法// 写法 1Spacer本项目 Row() { Button(暂停); Spacer(); Button(重新开始) } // 写法 2layoutWeight第 23 篇会讲 Row() { Button(暂停).layoutWeight(0) Blank().layoutWeight(1) // 占满剩余 Button(重新开始).layoutWeight(0) }写法优点缺点Spacer简洁直观只能撑开不能按比例layoutWeight可按比例分配语法稍繁实战经验两端对齐用 Spacer按比例分宽用 layoutWeight。本系列第 22 篇会专讲 Blank 占位第 23 篇专讲 layoutWeight。3.4 多 Spacer 实现等距分布Row() { Spacer() Button(A) Spacer() Button(B) Spacer() Button(C) Spacer() } .width(100%) // 4 个 Spacer 平分剩余空间3 个按钮等距分布关键Spacer平分剩余空间——4 个 Spacer 就把剩余空间 4 等分3 个按钮自然等距。四、Button 双态样式4.1 暂停按钮灰色次要操作Button(暂停) .width(100) .height(44) .fontSize(15) .fontColor(#FFFFFF) .backgroundColor(#95A5A6) // 灰色 .borderRadius(22) // 高度的一半 胶囊形 .onClick(() { this.pauseGame(); })色彩语义颜色hex含义灰#95A5A6中性灰次要操作暂停红#E74C3C警告红危险操作重新开始会丢进度绿#2ECC71成功绿主操作开始游戏橙#F39C12提醒橙中性重置提示按钮颜色就是用户引导——绿色「开始」最显眼灰色「暂停」温和红色「重新开始」警示。本系列第 19 篇会专讲暂停按钮样式。4.2 重新开始按钮红色危险操作Button(重新开始) .width(100) .height(44) .fontSize(15) .fontColor(#FFFFFF) .backgroundColor(#E74C3C) // 红色 .borderRadius(22) // 胶囊形 .onClick(() { this.clearTimers(); this.startGame(); })点击逻辑拆解步骤代码作用1this.clearTimers()清掉旧的 3 个定时器2this.startGame()重置引擎 启动新定时器关键经验重新开始前必须 clearTimers——否则旧的gameLoopTimer/spawnTimer/timeTimer还在跑会与新循环冲突导致猫「闪现」或计时「跳变」。4.3 圆角 22 胶囊形.borderRadius(22) // height(44) 的一半 → 胶囊形胶囊形公式borderRadius height / 2。本项目按钮高 44vp圆角 22vp左右两端是半圆整体呈胶囊形。圆角值视觉适用0直角表格行8小圆角卡片12中圆角棋盘背景22height/2胶囊按钮本项目999超大圆角头像五、padding 内边距5.1 底部栏 padding 配置Row() { /* ... */ } .width(100%) .padding({ left: 24, right: 24, bottom: 24, top: 12 })边值原因left / right24两端按钮离屏幕边缘留 24vp 呼吸空间bottom24底部留白避开手势条/虚拟导航栏top12顶部留白少一点紧凑感关键经验底部栏的 bottom padding 必须够大——HarmonyOS 设备屏幕底部有手势条或虚拟导航条padding 太小按钮会被遮挡。5.2 padding vs margin// padding内边距在容器内部撑开空白 Row() { /* ... */ }.padding({ left: 24 }) // margin外边距在容器外部撑开空白 Row() { /* ... */ }.margin({ left: 24 })属性位置影响布局padding容器内子组件被「挤」向中心margin容器外容器整体被推开本项目底部栏用padding——Row铺满 100% 后内部左右各留 24vp让按钮不贴屏幕边缘。六、底部栏与上方棋盘的联动6.1 整体 GameView 结构Builder GameView() { Column() { this.GameHUD() // 顶部 HUD第 11 篇 Column() { Row() { /* 预告区 */ } // 预告区第 12 篇 Stack() { // 游戏棋盘第 13-17 篇 /* 背景 猫咪 点击层 */ } .width(BOARD_W * CELL_SIZE) .height(BOARD_H * CELL_SIZE) } Spacer() // 关键弹性占位把底部栏推到最底 Row() { /* 暂停 / 重新开始 */ } // 底部控制栏本篇 .padding({ left: 24, right: 24, bottom: 24, top: 12 }) } .width(100%).height(100%) }关键结构Column 主容器从上到下依次是 HUD、棋盘、Spacer、底部栏。Spacer 在中间吃掉棋盘下方所有剩余空间把底部栏推到最底。底部栏 padding bottom 24避开屏幕底部手势条。6.2 pauseGame 实现// 来源entry/src/main/ets/pages/Index.ets pauseGame() { if (this.gameState GameState.PLAYING) { this.gameState GameState.PAUSED; } }逻辑仅在PLAYING状态切到PAUSED。切 PAUSED 后主循环gameLoopTimer里if (gameState ! PLAYING) return→物理暂停。spawnTimer 同理 →生成暂停。timeTimer 里if (gameState PLAYING) gameTime→计时暂停。build() 检测gameState PAUSED→ 渲染PauseOverlay本系列第 93 篇。七、完整代码底部控制栏// 来源entry/src/main/ets/pages/Index.ets GameView() 底部 Builder GameView() { Column() { this.GameHUD() Column() { // 预告区 Row() { /* ... */ } // 游戏棋盘 Stack() { /* 3 层叠 */ } .width(GameConfig.BOARD_WIDTH * GameConfig.CELL_SIZE) .height(GameConfig.BOARD_HEIGHT * GameConfig.CELL_SIZE) .borderRadius(12) .clip(true) .backgroundColor(#D6EEF5) } .alignItems(HorizontalAlign.Center) Spacer() // 弹性占位推底部栏到最底 // 底部控制栏本篇重点 Row() { Button(暂停) .width(100) .height(44) .fontSize(15) .fontColor(#FFFFFF) .backgroundColor(#95A5A6) .borderRadius(22) .onClick(() { this.pauseGame(); }) Spacer() // 撑开两按钮到两端 Button(重新开始) .width(100) .height(44) .fontSize(15) .fontColor(#FFFFFF) .backgroundColor(#E74C3C) .borderRadius(22) .onClick(() { this.clearTimers(); this.startGame(); }) } .width(100%) .padding({ left: 24, right: 24, bottom: 24, top: 12 }) } .width(100%).height(100%) .linearGradient({ direction: GradientDirection.Bottom, colors: [[#E8F4F8, 0.0], [#D6EEF5, 0.5], [#C9E8F2, 1.0]] }) .alignItems(HorizontalAlign.Center) } pauseGame() { if (this.gameState GameState.PLAYING) { this.gameState GameState.PAUSED; } }八、踩坑提示8.1 Row 不铺满宽度导致按钮挤一起// ❌ 错误Row 没设 width默认 wrap_content Row() { Button(暂停); Spacer(); Button(重新开始) } // Row 宽度 两按钮宽 Spacer 默认宽挤在左侧 // ✅ 正确Row width(100%) Row() { /* ... */ }.width(100%)8.2 Spacer 忘记写导致两按钮紧贴// ❌ 错误没 Spacer两按钮紧贴在 Row 左侧 Row() { Button(暂停) Button(重新开始) }.width(100%) // ✅ 正确Spacer 撑开 Row() { Button(暂停) Spacer() Button(重新开始) }.width(100%)8.3 重新开始忘清定时器// ❌ 错误直接 startGame旧定时器还在跑 Button(重新开始).onClick(() { this.startGame(); // 旧的 gameLoopTimer 还在跑 }) // ✅ 正确先 clearTimers 再 startGame Button(重新开始).onClick(() { this.clearTimers(); this.startGame(); })后果不清旧定时器新游戏开始后会有两套主循环并行——猫会「闪现」两个循环都改 cats计时会「双倍跳」。九、调试技巧临时给 Row 加 border.border({ width: 1, color: Color.Red })看清底部栏范围。按钮点击无响应检查gameState检查上层Spacer是否意外覆盖按钮区域。重新开始后猫闪现打开 DevEco Profiler 看 timer 数量应该是 3 个闪现说明有 6 个旧 3 新 3。底部按钮被手势条挡增大padding.bottom到 32 或 48避开屏幕底部安全区。十、性能与最佳实践底部栏 Row Spacer 双 Button——最简两端对齐布局。圆角 height / 2——胶囊形按钮的统一公式。重新开始前必须 clearTimers——否则新旧定时器并行状态错乱。底部 padding 留够——避开手势条/虚拟导航栏至少 24vp。按钮颜色遵循语义——绿主操作、灰次要、红危险、橙中性。MVC 分层延续底部栏 onClick 只调pauseGame/startGame不直接改gameState。总结本篇我们从底部控制栏切入掌握了Row 横向布局的主轴/交叉轴对齐、Spacer 弹性占位撑出两端对齐、Button 双态样式暂停灰 / 重新开始红 胶囊形公式、padding 内边距避开底部手势条四大要点并给出了底部栏 pauseGame 的完整代码。核心要点Row width(‘100%’) Spacer 实现两端对齐圆角 height/2 胶囊形重新开始必须 clearTimers底部 padding 留够避开安全区。下一篇我们将继续拆解底部栏里的暂停按钮——Button 颜色与圆角的进阶样式。如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力相关资源「猫猫大作战」项目源码本仓库entry/src/main/ets/pages/Index.etsRow 横向布局官方指南Button 组件官方指南padding/margin 边距官方指南ArkUI 布局性能最佳实践开源鸿蒙跨平台社区HarmonyOS 开发者官方文档首页系列索引本仓库articles/INDEX.md