HarmonyOS应用开发实战:猫猫大作战-`ForEach` 的双层嵌套、`keyGenerator` 密钥、`width` 百分比响应式格子

前言

「猫猫大作战」的棋盘是 5 行 × 8 列网格------40 个格子。如果手写 40 个 Stack 组件,代码又长又难维护。ArkUI 的 ForEach 循环渲染就是为这类「重复结构 + 数据驱动 」场景准备的------双层 ForEach 嵌套,外层渲染行、内层渲染列,几十行代码搞定 40 格。

本篇以「猫猫大作战」游戏页棋盘背景网格为锚点,把 ForEach 的双层嵌套、keyGenerator 密钥、width 百分比响应式格子讲透。读完本篇你将能独立写出:双层 ForEach 棋盘响应式格子尺寸数据驱动列表渲染三种实战套路。

提示:本系列不讲 ArkTS 基础语法与环境搭建,假设你已跟完第 1--12 篇。

一、场景拆解:棋盘 5×8 网格

打开 entry/src/main/ets/pages/Index.etsGameView

ts 复制代码
// 来源:entry/src/main/ets/pages/Index.ets  GameView() 第 1 层
Column() {
  ForEach([0, 1, 2, 3, 4], (row: number) => {
    Row() {
      ForEach([0, 1, 2, 3, 4, 1, 2, 3], (col: number) => {
        Stack() {
          Text('').fontSize(20)
        }
        .width(`${100 / COLS}%`).height(`${100 / ROWS}%`)
        .backgroundColor((row + col) % 2 === 0 ? '#F0F4F8' : '#E6EDF3')
      }, (col: number) => `col-${row}-${col}`)
    }
    .width('100%').height(`${100 / ROWS}%`)
  }, (row: number) => `row-${row}`)
}
.width('100%').height('100%')

棋盘规格拆解:

维度 取值 作用
行数 ROWS = 5 外层 ForEach [0,1,2,3,4]
列数 COLS = 8 内层 ForEach [0,1,2,3,4,1,2,3]
格宽 ${100/8}% = '12.5%' 每列 1/8 屏宽
格高 ${100/5}% = '20%' 每行 1/5 棋盘高
棋盘色 (row+col)%2 === 0 ? '#F0F4F8' : '#E6EDF3' 隔行变色

关键观察 :内层 ForEach 的列数组 [0,1,2,3,4,1,2,3] 有重复值(两个 1、两个 2、两个 3)------这是项目早期 bug,正确应该是 [0,1,2,3,4,5,6,7]。后文会讲这种重复值如何让 keyGenerator 失效。

二、ForEach 核心语法速览

参考 ForEach 循环渲染官方指南

ts 复制代码
ForEach(
  arr: Array,                    // 数据数组
  (item: T, index?: number) => { // 渲染每个项的 Builder
    // UI 结构
  },
  (item: T, index?: number) => string  // keyGenerator 生成唯一密钥
)

2.1 三个参数

参数 作用 必填
arr 数据数组
itemBuilder 渲染每项的箭头函数
keyGenerator 生成唯一密钥的箭头函数 ⚠ 强烈推荐

2.2 keyGenerator 的作用

ts 复制代码
ForEach([1, 2, 3], (item: number) => {
  Text(`${item}`)
}, (item: number) => `key-${item}`)   // 密钥 'key-1'/'key-2'/'key-3'

keyGenerator 给每个渲染项生成唯一字符串密钥 ,ArkUI 用它做 diff 追踪------数据变化时只重建密钥变化的项,其余复用。

不写 keyGenerator 的后果 :ArkUI 用 index 做密钥,数组中间插入/删除时所有项重建(因为 index 全变了),性能崩。

2.3 唯一性铁律

ts 复制代码
// ✅ 密钥唯一
(arr: [1, 2, 3], key: `k-${item}`) → 'k-1'/'k-2'/'k-3'

// ❌ 密钥冲突(arr 有重复值)
(arr: [1, 2, 2, 3], key: `k-${item}`) → 'k-1'/'k-2'/'k-2'/'k-3'  ← 'k-2' 冲突!

冲突后果 :ArkUI 只渲染第一个 'k-2',第二个被丢弃------重复值不显示

三、双层 ForEach 嵌套结构

3.1 外层渲染行

ts 复制代码
Column() {
  ForEach([0, 1, 2, 3, 4], (row: number) => {
    Row() { /* 内层渲染列 */ }
    .width('100%').height(`${100 / ROWS}%`)
  }, (row: number) => `row-${row}`)
}
.width('100%').height('100%')
  • 外层 Column:行自上而下排列。
  • 每行 Row :列自左而右排列,高度 ${100/5}% = 20%。
  • 密钥row-${row}row-0/row-1/row-2/row-3/row-4,唯一。

3.2 内层渲染列

ts 复制代码
Row() {
  ForEach([0, 1, 2, 3, 4, 1, 2, 3], (col: number) => {
    Stack() {
      Text('').fontSize(20)
    }
    .width(`${100 / COLS}%`).height(`${100 / ROWS}%`)
    .backgroundColor((row + col) % 2 === 0 ? '#F0F4F8' : '#E6EDF3')
  }, (col: number) => `col-${row}-${col}`)
}
.width('100%').height(`${100 / ROWS}%`)
  • 每格 Stack :宽 ${100/8}% = 12.5%,高 ${100/5}% = 20%。
  • 密钥col-${row}-${col}col-0-0/col-0-1/...,带行号防跨行冲突。
  • 背景色(row+col)%2 隔行变色。

3.3 密钥带行号的关键设计

ts 复制代码
(row: number) => `col-${row}-${col}`   // 注意模板字符串里的 ${row}

为什么不写 col-${col}?因为跨行会有同列号:

  • 第 0 行第 1 列:col-1
  • 第 1 行第 1 列:col-1 ← 冲突!

${row} 后:

  • 第 0 行第 1 列:col-0-1
  • 第 1 行第 1 列:col-1-1 ← 唯一!

双层 ForEach 密钥规则内层密钥必须包含外层变量,否则跨行同列号冲突。

四、width 百分比的响应式格子

ts 复制代码
.width(`${100 / COLS}%`)   // '12.5%'

4.1 模板字符串算百分比

ts 复制代码
`${100 / COLS}%`   // COLS=8 → '12.5%'

关键 :模板字符串内可以做算术,${100/8}%'12.5%'。这样改 COLS 常量,格子宽度自动响应。

4.2 vs 固定 vp 尺寸

ts 复制代码
// 方式 1:百分比(响应式,本项目)
.width(`${100 / COLS}%`)

// 方式 2:固定 vp(不响应屏宽)
.width(48)   // 每格固定 48vp

百分比的优势 :屏宽变化格子自动缩放,5 寸屏和 7 寸屏都适配。 固定 vp 的优势:格子大小一致,棋盘宽度随屏宽变化(屏宽则棋盘宽)。

实战经验棋盘要齔屏用百分比,棋盘要居中固定尺寸用 vp。本项目棋盘齔屏,用百分比。

4.3 height 百分比的同理

ts 复制代码
.height(`${100 / ROWS}%`)   // '20%'

每行高度 ${100/5}% = 20%,5 行正好齔满棋盘高。

五、隔行变色的模运算

ts 复制代码
.backgroundColor((row + col) % 2 === 0 ? '#F0F4F8' : '#E6EDF3')

5.1 (row + col) % 2 的棋盘色逻辑

ts 复制代码
(row + col) % 2 === 0  // 偶数格 → 浅色 #F0F4F8
(row + col) % 2 === 1  // 奇数格 → 深色 #E6EDF3

棋盘色分布示意(5×8):

行\列 0 1 2 3 4 5 6 7
0
1
2
3
4

关键(row+col)%2 让相邻格永远异色------经典棋盘视觉。

5.2 vs row % 2 的行间变色

ts 复制代码
// 方式 1:棋盘格(本项目)
(row + col) % 2   // 相邻格异色

// 方式 2:行间变色
row % 2            // 相邻行异色,同行同色

差异

  • 棋盘格:每格都是独立色块,视觉精致。
  • 行间变色:同行 8 格同色,视觉更朴素。

实战经验棋盘类游戏用 (row+col)%2,列表类用 row%2

六、内层数组重复值 bug 修正

项目源码内层数组:

ts 复制代码
ForEach([0, 1, 2, 3, 4, 1, 2, 3], (col: number) => {
  // ...
}, (col: number) => `col-${row}-${col}`)

问题 :数组 [0,1,2,3,4,1,2,3] 有重复值(两个 1、两个 2、两个 3),共 8 个元素但值范围 0--4。

密钥冲突

  • 第 5 个元素(值 1):col-${row}-1
  • 第 7 个元素(值 1):col-${row}-1 ← 与第 5 个冲突!

后果 :第 5、7 个格子(都值 1)只渲染一个,另一个被丢弃------棋盘少 3 个格子

6.1 修正:用 0--7 唯一值

ts 复制代码
// ✅ 正确:8 个唯一值
ForEach([0, 1, 2, 3, 4, 5, 6, 7], (col: number) => {
  // ...
}, (col: number) => `col-${row}-${col}`)

6.2 或用 Array.from 生成

ts 复制代码
const COLS = 8
const colArr: number[] = Array.from({ length: COLS }, (_, i) => i)   // [0,1,2,3,4,5,6,7]

ForEach(colArr, (col: number) => {
  // ...
}, (col: number) => `col-${row}-${col}`)

关键教训ForEach 数组的值必须唯一(如果 keyGenerator 用值生成密钥),否则密钥冲突丢项。

七、完整代码:棋盘背景网格修正版

ts 复制代码
const ROWS = 5
const COLS = 8
const colArr: number[] = Array.from({ length: COLS }, (_, i) => i)

@Builder
GameView() {
  Stack() {
    // 第 1 层:棋盘背景
    Column() {
      ForEach([0, 1, 2, 3, 4], (row: number) => {
        Row() {
          ForEach(colArr, (col: number) => {
            Stack() {
              Text('').fontSize(20)
            }
            .width(`${100 / COLS}%`).height(`${100 / ROWS}%`)
            .backgroundColor((row + col) % 2 === 0 ? '#F0F4F8' : '#E6EDF3')
          }, (col: number) => `col-${row}-${col}`)
        }
        .width('100%').height(`${100 / ROWS}%`)
      }, (row: number) => `row-${row}`)
    }
    .width('100%').height('100%').zIndex(0)

    // 第 2 层:猫咪层(下篇讲)
    // 第 3 层:顶部 HUD(第 11 篇讲)
  }
  .width('100%').height('100%')
  .alignContent(Alignment.Top)
}

八、ForEach vs LazyForEach 取舍

维度 ForEach LazyForEach
渲染时机 全量立即渲染 按需懒渲染
数据规模 小(< 100 项) 大(≥ 100 项)
接口 arr: Array IDataSource
适用 棋盘、固定网格、短列表 长列表、聊天记录、动态数据

实战经验棋盘 40 格用 ForEach ------数据量小全量渲染更简单。聊天 1000 条用 LazyForEach ------只渲染可见项省内存。本系列第 68 篇会专讲 LazyForEach

九、调试技巧:ForEach 怎么调

  1. console.info 打数据 :Builder 里加 console.info('render cell', row, col),看渲染次数。
  2. 密钥唯一性验证console.info('key', col-{row}-{col}) 打所有密钥,查重复。
  3. 加临时 border :给 Stack 加 .border({ width: 1, color: Color.Red }),看清格子边界。
  4. 真机看棋盘色 :预览器渲染接近真机,但隔行变色可能因屏密度差异,以真机为准

十、性能与最佳实践

  1. keyGenerator 必写:不写用 index 做密钥,数组中间插删时全重建。
  2. 密钥必须唯一 :数据有重复值时,密钥加 index 后缀 key-${item}-${index}
  3. 双层密钥带外层变量col-${row}-${col},防跨行同列号冲突。
  4. 小数据用 ForEach,大数据用 LazyForEach:40 格用 ForEach,1000 条用 LazyForEach。

总结

本篇我们从棋盘背景网格切入,掌握 ForEach 的双层嵌套、keyGenerator 唯一密钥、width 百分比响应式格子、(row+col)%2 隔行变色四大要点,并修正了项目源码内层数组重复值的 bug。核心要点:ForEach 密钥必写且唯一,双层密钥带外层变量,百分比格子响应式,小数据用 ForEach 大数据用 LazyForEach

下一篇我们将继续游戏页,拆解棋盘隔行变色的模运算条件样式。

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


相关资源:

相关推荐
echohelloworld112 小时前
HarmonyOS应用开发实战:猫猫大作战-Row 横向布局、Spacer 占位、Button 双态样式(暂停灰 / 重新开始红)、底部 paddin
harmonyos·鸿蒙
爱写代码的森3 小时前
鸿蒙三方库 | harmony-utils之ObjectUtil对象拷贝与合并详解
华为·harmonyos·鸿蒙·huawei
fiona20264 小时前
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
程序员黑豆5 小时前
鸿蒙应用开发实战:轻松实现列表上拉加载更多
前端·华为·harmonyos
fiona20266 小时前
HarmonyOS应用《玄象》开发实战:LunarCalendar.ets 农历计算核心:朔望月 + 节气 + 闰月推算
harmonyos·鸿蒙