HarmonyOS应用开发实战:猫猫大作战-Row 横向布局、Spacer 占位、Button 双态样式(暂停灰 / 重新开始红)、底部 paddin

前言

棋盘和点击层搞定后,游戏页还缺一个底部控制栏 ------玩家在游戏中需要随时「暂停」或「重新开始」。这是 ArkUI 里最经典的 Row 双按钮布局:两个 ButtonSpacer 撑开分列两端,整体一行铺满底部。

本篇以「猫猫大作战」游戏页底部控制栏为锚点,把 Row 横向布局Spacer 占位Button 双态样式 (暂停灰 / 重新开始红)、底部 padding 留白 四大要点讲透。读完本篇你将能独立写出:底部操作栏双按钮等距布局按钮色彩语义三种实战套路。

提示:本系列不讲 ArkTS 基础语法与环境搭建,假设你已跟完第 1--17 篇。本篇继续游戏页底部收尾。

一、场景拆解:底部控制栏

打开 entry/src/main/ets/pages/Index.etsGameView,找到最底部的控制栏:

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 等高)

关键SpacerRow 里默认高度等于父容器高度 ,在 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%')
}

关键结构

  1. Column 主容器:从上到下依次是 HUD、棋盘、Spacer、底部栏。
  2. Spacer 在中间:吃掉棋盘下方所有剩余空间,把底部栏推到最底。
  3. 底部栏 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 后:

  • 主循环 gameLoopTimerif (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),计时会「双倍跳」。

九、调试技巧

  1. 临时给 Row 加 border.border({ width: 1, color: Color.Red }),看清底部栏范围。
  2. 按钮点击无响应 :检查 gameState;检查上层 Spacer 是否意外覆盖按钮区域。
  3. 重新开始后猫闪现:打开 DevEco Profiler 看 timer 数量,应该是 3 个,闪现说明有 6 个(旧 3 + 新 3)。
  4. 底部按钮被手势条挡 :增大 padding.bottom 到 32 或 48,避开屏幕底部安全区。

十、性能与最佳实践

  1. 底部栏 = Row + Spacer + 双 Button------最简两端对齐布局。
  2. 圆角 = height / 2------胶囊形按钮的统一公式。
  3. 重新开始前必须 clearTimers------否则新旧定时器并行,状态错乱。
  4. 底部 padding 留够------避开手势条/虚拟导航栏,至少 24vp。
  5. 按钮颜色遵循语义------绿主操作、灰次要、红危险、橙中性。
  6. MVC 分层延续 :底部栏 onClick 只调 pauseGame/startGame,不直接改 gameState

总结

本篇我们从底部控制栏切入,掌握了 Row 横向布局的主轴/交叉轴对齐Spacer 弹性占位撑出两端对齐Button 双态样式(暂停灰 / 重新开始红)+ 胶囊形公式padding 内边距避开底部手势条 四大要点,并给出了底部栏 + pauseGame 的完整代码。核心要点:Row width('100%') + Spacer 实现两端对齐;圆角 = height/2 胶囊形;重新开始必须 clearTimers;底部 padding 留够避开安全区

下一篇我们将继续拆解底部栏里的暂停按钮------Button 颜色与圆角的进阶样式。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

相关推荐
爱写代码的森3 小时前
鸿蒙三方库 | harmony-utils之ObjectUtil对象拷贝与合并详解
华为·harmonyos·鸿蒙·huawei
fiona20263 小时前
HarmonyOS应用开发实战:猫猫大作战-Scroll 容器、Scroller 控制器、滚动监听与性能
harmonyos·鸿蒙
GitCode官方4 小时前
开源鸿蒙跨平台直播| 快手KRN鸿蒙适配与性能优化
华为·开源·harmonyos·atomgit
爱写代码的森4 小时前
鸿蒙三方库 | harmony-utils之TypeUtil类型检查工具详解
华为·harmonyos·鸿蒙·huawei
萌新源4 小时前
【鸿蒙开发实战】HarmonyOS 跨设备分享教程:隔空传送与碰一碰分享
华为·harmonyos
用户0934077735144 小时前
HarmonyOS WPS Open SDK 二开周回顾:从注册到关窗回传怎么串
typescript·harmonyos
程序员黑豆4 小时前
鸿蒙应用开发实战:轻松实现列表上拉加载更多
前端·华为·harmonyos
fiona20265 小时前
HarmonyOS应用《玄象》开发实战:LunarCalendar.ets 农历计算核心:朔望月 + 节气 + 闰月推算
harmonyos·鸿蒙
yaoyaoxingzhe6 小时前
HarmonyOS应用开发实战:猫猫大作战-`$r` 与 `$rawfile` 的区别、资源目录结构、多分辨率适配
harmonyos·鸿蒙