

前言
棋盘和点击层搞定后,游戏页还缺一个底部控制栏 ------玩家在游戏中需要随时「暂停」或「重新开始」。这是 ArkUI 里最经典的 Row 双按钮布局:两个 Button 用 Spacer 撑开分列两端,整体一行铺满底部。
本篇以「猫猫大作战」游戏页底部控制栏为锚点,把 Row 横向布局 、Spacer 占位 、Button 双态样式 (暂停灰 / 重新开始红)、底部 padding 留白 四大要点讲透。读完本篇你将能独立写出:底部操作栏 、双按钮等距布局 、按钮色彩语义三种实战套路。
提示:本系列不讲 ArkTS 基础语法与环境搭建,假设你已跟完第 1--17 篇。本篇继续游戏页底部收尾。
一、场景拆解:底部控制栏
打开 entry/src/main/ets/pages/Index.ets 的 GameView,找到最底部的控制栏:
ts
// 来源: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,圆角 22 |
Spacer() |
弹性占位 | 把两按钮推向左右两端 |
Button('重新开始') |
红色按钮 | 100×44vp,圆角 22 |
.padding({...}) |
内边距 | 左右 24、底 24、顶 12 |
关键经验 :Row + Spacer 是「两端对齐」的最简组合 ------Spacer 吃掉中间所有剩余空间,把两按钮顶到两端。
二、Row 横向布局
2.1 Row 的核心属性
ts
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 控制子组件在交叉轴(垂直方向)的对齐:
ts
Row() {
Button('暂停').height(44)
Spacer()
Button('重新开始').height(44)
}
.alignItems(VerticalAlign.Center) // 两按钮垂直居中
| VerticalAlign 值 | 视觉 |
|---|---|
Top |
顶部对齐 |
Center |
垂直居中(推荐) |
Bottom |
底部对齐 |
提示:两按钮高度一致时
alignItems几乎无视觉差异 ,但保留Center是好习惯------后续按钮高度变化时不会错位。
三、Spacer 弹性占位
3.1 Spacer 的本质
ts
Row() {
Button('暂停')
Spacer() // 占据两按钮之间的所有剩余空间
Button('重新开始')
}
.width('100%')
Spacer 是一个弹性空白组件 ------它会自动撑开,吃掉父容器中所有剩余空间。在 Row 里它把后续组件推向右端,实现「两端对齐」。
3.2 Spacer 高度的坑
ts
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
「两端对齐」有两种写法:
ts
// 写法 1:Spacer(本项目)
Row() {
Button('暂停'); Spacer(); Button('重新开始')
}
// 写法 2:layoutWeight(第 23 篇会讲)
Row() {
Button('暂停').layoutWeight(0)
Blank().layoutWeight(1) // 占满剩余
Button('重新开始').layoutWeight(0)
}
| 写法 | 优点 | 缺点 |
|---|---|---|
Spacer |
简洁直观 | 只能撑开,不能按比例 |
layoutWeight |
可按比例分配 | 语法稍繁 |
实战经验 :两端对齐用 Spacer,按比例分宽用 layoutWeight。本系列第 22 篇会专讲 Blank 占位,第 23 篇专讲 layoutWeight。
3.4 多 Spacer 实现等距分布
ts
Row() {
Spacer()
Button('A')
Spacer()
Button('B')
Spacer()
Button('C')
Spacer()
}
.width('100%')
// 4 个 Spacer 平分剩余空间,3 个按钮等距分布
关键 :Spacer 平分剩余空间------4 个 Spacer 就把剩余空间 4 等分,3 个按钮自然等距。
四、Button 双态样式
4.1 暂停按钮(灰色次要操作)
ts
Button('暂停')
.width(100)
.height(44)
.fontSize(15)
.fontColor('#FFFFFF')
.backgroundColor('#95A5A6') // 灰色
.borderRadius(22) // 高度的一半 = 胶囊形
.onClick(() => {
this.pauseGame();
})
色彩语义:
| 颜色 | hex | 含义 |
|---|---|---|
灰 #95A5A6 |
中性灰 | 次要操作(暂停) |
红 #E74C3C |
警告红 | 危险操作(重新开始会丢进度) |
绿 #2ECC71 |
成功绿 | 主操作(开始游戏) |
橙 #F39C12 |
提醒橙 | 中性重置 |
提示:按钮颜色就是用户引导------绿色「开始」最显眼,灰色「暂停」温和,红色「重新开始」警示。本系列第 19 篇会专讲暂停按钮样式。
4.2 重新开始按钮(红色危险操作)
ts
Button('重新开始')
.width(100)
.height(44)
.fontSize(15)
.fontColor('#FFFFFF')
.backgroundColor('#E74C3C') // 红色
.borderRadius(22) // 胶囊形
.onClick(() => {
this.clearTimers();
this.startGame();
})
点击逻辑拆解:
| 步骤 | 代码 | 作用 |
|---|---|---|
| 1 | this.clearTimers() |
清掉旧的 3 个定时器 |
| 2 | this.startGame() |
重置引擎 + 启动新定时器 |
关键经验 :重新开始前必须 clearTimers ------否则旧的 gameLoopTimer/spawnTimer/timeTimer 还在跑,会与新循环冲突,导致猫「闪现」或计时「跳变」。
4.3 圆角 22 = 胶囊形
ts
.borderRadius(22) // height(44) 的一半 → 胶囊形
胶囊形公式 :borderRadius = height / 2。本项目按钮高 44vp,圆角 22vp,左右两端是半圆,整体呈胶囊形。
| 圆角值 | 视觉 | 适用 |
|---|---|---|
| 0 | 直角 | 表格行 |
| 8 | 小圆角 | 卡片 |
| 12 | 中圆角 | 棋盘背景 |
| 22(=height/2) | 胶囊 | 按钮(本项目) |
| 999 | 超大圆角 | 头像 |
五、padding 内边距
5.1 底部栏 padding 配置
ts
Row() { /* ... */ }
.width('100%')
.padding({ left: 24, right: 24, bottom: 24, top: 12 })
| 边 | 值 | 原因 |
|---|---|---|
| left / right | 24 | 两端按钮离屏幕边缘留 24vp 呼吸空间 |
| bottom | 24 | 底部留白,避开手势条/虚拟导航栏 |
| top | 12 | 顶部留白少一点,紧凑感 |
关键经验 :底部栏的 bottom padding 必须够大------HarmonyOS 设备屏幕底部有手势条或虚拟导航条,padding 太小按钮会被遮挡。
5.2 padding vs margin
ts
// padding:内边距,在容器内部撑开空白
Row() { /* ... */ }.padding({ left: 24 })
// margin:外边距,在容器外部撑开空白
Row() { /* ... */ }.margin({ left: 24 })
| 属性 | 位置 | 影响布局 |
|---|---|---|
padding |
容器内 | 子组件被「挤」向中心 |
margin |
容器外 | 容器整体被推开 |
本项目底部栏用 padding------Row 铺满 100% 后,内部左右各留 24vp,让按钮不贴屏幕边缘。
六、底部栏与上方棋盘的联动
6.1 整体 GameView 结构
ts
@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 实现
ts
// 来源: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 篇)。
七、完整代码:底部控制栏
ts
// 来源: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 不铺满宽度导致按钮挤一起
ts
// ❌ 错误:Row 没设 width,默认 wrap_content
Row() {
Button('暂停'); Spacer(); Button('重新开始')
}
// Row 宽度 = 两按钮宽 + Spacer 默认宽,挤在左侧
// ✅ 正确:Row width('100%')
Row() { /* ... */ }.width('100%')
8.2 Spacer 忘记写导致两按钮紧贴
ts
// ❌ 错误:没 Spacer,两按钮紧贴在 Row 左侧
Row() {
Button('暂停')
Button('重新开始')
}.width('100%')
// ✅ 正确:Spacer 撑开
Row() {
Button('暂停')
Spacer()
Button('重新开始')
}.width('100%')
8.3 重新开始忘清定时器
ts
// ❌ 错误:直接 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.ets - Row 横向布局官方指南
- Button 组件官方指南
- padding/margin 边距官方指南
- ArkUI 布局性能最佳实践
- 开源鸿蒙跨平台社区
- HarmonyOS 开发者官方文档首页
- 系列索引:本仓库
articles/INDEX.md