前言
「猫猫大作战」的棋盘是 5 行 × 8 列网格——40 个格子。如果手写 40 个Stack组件,代码又长又难维护。ArkUI 的ForEach循环渲染就是为这类「重复结构 + 数据驱动」场景准备的——双层ForEach嵌套,外层渲染行、内层渲染列,几十行代码搞定 40 格。
本篇以「猫猫大作战」游戏页棋盘背景网格为锚点,把ForEach的双层嵌套、keyGenerator密钥、width百分比响应式格子讲透。读完本篇你将能独立写出:双层 ForEach 棋盘、响应式格子尺寸、数据驱动列表渲染三种实战套路。
提示:本系列不讲 ArkTS 基础语法与环境搭建,假设你已跟完第 1–12 篇。
一、场景拆解:棋盘 5×8 网格
打开entry/src/main/ets/pages/Index.ets的GameView:
// 来源: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 循环渲染官方指南。
ForEach( arr: Array, // 数据数组 (item: T, index?: number) => { // 渲染每个项的 Builder // UI 结构 }, (item: T, index?: number) => string // keyGenerator 生成唯一密钥 )2.1 三个参数
| 参数 | 作用 | 必填 |
|---|---|---|
arr | 数据数组 | ✅ |
| itemBuilder | 渲染每项的箭头函数 | ✅ |
| keyGenerator | 生成唯一密钥的箭头函数 | ⚠ 强烈推荐 |
2.2 keyGenerator 的作用
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 唯一性铁律
// ✅ 密钥唯一 (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 外层渲染行
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 内层渲染列
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 密钥带行号的关键设计
(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 百分比的响应式格子
.width(`${100 / COLS}%`) // '12.5%'4.1 模板字符串算百分比
`${100 / COLS}%` // COLS=8 → '12.5%'关键:模板字符串内可以做算术,${100/8}%→'12.5%'。这样改COLS常量,格子宽度自动响应。
4.2 vs 固定 vp 尺寸
// 方式 1:百分比(响应式,本项目) .width(`${100 / COLS}%`) // 方式 2:固定 vp(不响应屏宽) .width(48) // 每格固定 48vp百分比的优势:屏宽变化格子自动缩放,5 寸屏和 7 寸屏都适配。固定 vp 的优势:格子大小一致,棋盘宽度随屏宽变化(屏宽则棋盘宽)。
实战经验:棋盘要齔屏用百分比,棋盘要居中固定尺寸用 vp。本项目棋盘齔屏,用百分比。
4.3 height 百分比的同理
.height(`${100 / ROWS}%`) // '20%'每行高度${100/5}%= 20%,5 行正好齔满棋盘高。
五、隔行变色的模运算
.backgroundColor((row + col) % 2 === 0 ? '#F0F4F8' : '#E6EDF3')5.1(row + col) % 2的棋盘色逻辑
(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 vsrow % 2的行间变色
// 方式 1:棋盘格(本项目) (row + col) % 2 // 相邻格异色 // 方式 2:行间变色 row % 2 // 相邻行异色,同行同色差异:
- 棋盘格:每格都是独立色块,视觉精致。
- 行间变色:同行 8 格同色,视觉更朴素。
实战经验:棋盘类游戏用(row+col)%2,列表类用row%2。
六、内层数组重复值 bug 修正
项目源码内层数组:
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 唯一值
// ✅ 正确:8 个唯一值 ForEach([0, 1, 2, 3, 4, 5, 6, 7], (col: number) => { // ... }, (col: number) => `col-${row}-${col}`)6.2 或用 Array.from 生成
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 用值生成密钥),否则密钥冲突丢项。
七、完整代码:棋盘背景网格修正版
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 怎么调
console.info打数据:Builder 里加console.info('render cell', row, col),看渲染次数。- 密钥唯一性验证:
console.info('key',col-{row}-{col})打所有密钥,查重复。 - 加临时 border:给 Stack 加
.border({ width: 1, color: Color.Red }),看清格子边界。 - 真机看棋盘色:预览器渲染接近真机,但隔行变色可能因屏密度差异,以真机为准。
十、性能与最佳实践
- keyGenerator 必写:不写用 index 做密钥,数组中间插删时全重建。
- 密钥必须唯一:数据有重复值时,密钥加 index 后缀
key-${item}-${index}。 - 双层密钥带外层变量:
col-${row}-${col},防跨行同列号冲突。 - 小数据用 ForEach,大数据用 LazyForEach:40 格用 ForEach,1000 条用 LazyForEach。
总结
本篇我们从棋盘背景网格切入,掌握ForEach的双层嵌套、keyGenerator唯一密钥、width百分比响应式格子、(row+col)%2隔行变色四大要点,并修正了项目源码内层数组重复值的 bug。核心要点:ForEach 密钥必写且唯一,双层密钥带外层变量,百分比格子响应式,小数据用 ForEach 大数据用 LazyForEach。
下一篇我们将继续游戏页,拆解棋盘隔行变色的模运算条件样式。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- 「猫猫大作战」项目源码:本仓库
entry/src/main/ets/pages/Index.ets - ForEach 循环渲染官方指南
- 循环渲染 (ForEach) API 参考
- 线性布局 (Row/Column) 官方指南
- 开源鸿蒙跨平台社区
- HarmonyOS 开发者官方文档首页
- 系列索引:本仓库
articles/INDEX.md