鸿蒙原生 ArkTS 布局之道:Grid 替代多层嵌套布局深度解析

项目演示


目录

  1. 引言:从嵌套地狱到扁平之美
  2. [Grid 布局核心概念](#Grid 布局核心概念)
  3. [Grid vs 传统嵌套布局:全景对比](#Grid vs 传统嵌套布局:全景对比)
  4. [Grid 核心属性详解(API 24 特性)](#Grid 核心属性详解(API 24 特性))
  5. [GridItem 精确定位机制](#GridItem 精确定位机制)
  6. [实战:从 4 层嵌套到 1 层 Grid 的重构之旅](#实战:从 4 层嵌套到 1 层 Grid 的重构之旅)
  7. [Grid 进阶技巧与最佳实践](#Grid 进阶技巧与最佳实践)
  8. [性能优化:为什么 Grid 更快?](#性能优化:为什么 Grid 更快?)
  9. 常见问题与避坑指南
  10. 综合案例:设备控制面板完整实现
  11. 总结与展望
  12. 参考资料

1. 引言:从嵌套地狱到扁平之美

1.1 你是否也曾陷入嵌套地狱?

每一位鸿蒙开发者在入门 ArkUI 声明式 UI 时,都曾使用过 Column 和 Row 进行界面搭建。它们简单直观,几乎可以应对任何线性布局场景。然而,随着界面复杂度的提升,问题逐渐浮现。

想象一个典型的仪表盘界面:顶部标题栏、左侧用户卡片、右侧多个功能按钮网格、底部操作栏。如果用传统的 Column/Row 嵌套方式来实现,代码结构可能如下:

typescript 复制代码
Column() {                        // 第 1 层:主容器
  Row() {                         // 第 2 层:标题栏
    Text('标题')
  }
  Column() {                      // 第 2 层:内容区
    Row() {                       // 第 3 层:上半部分
      Column() {                  // 第 4 层:用户卡片
        Row() { ... }            // 第 5 层:头像 + 信息
        Row() { ... }            // 第 5 层:统计数据
      }
      Column() {                  // 第 4 层:右侧按钮网格
        Row() { ... }            // 第 5 层:第一行按钮
        Row() { ... }            // 第 5 层:第二行按钮
      }
    }
  }
  Row() {                         // 第 2 层:底部操作栏
    Button('确定')
    Button('取消')
  }
}

五层嵌套! 这就是前端开发中常说的"嵌套地狱"。每一层嵌套都会引入额外的布局计算开销,增加代码维护难度,也让界面性能大打折扣。

1.2 为什么要谈 Grid?

Grid(网格布局)是 ArkUI 提供的二维布局容器,它的核心理念是:将页面划分为行和列组成的网格,然后通过坐标精确定位每个 UI 元素。这种方式彻底改变了我们组织界面结构的思路。

使用 Grid 重写上面的仪表盘界面,代码结构变为:

typescript 复制代码
Grid() {
  GridItem() { 标题栏 }          // row 0, column 0-2
  GridItem() { 用户卡片 }        // row 1-2, column 0
  GridItem() { 按钮 1 }          // row 1, column 1
  GridItem() { 按钮 2 }          // row 1, column 2
  GridItem() { 按钮 3 }          // row 2, column 1
  GridItem() { 按钮 4 }          // row 2, column 2
  GridItem() { 底部操作栏 }      // row 3, column 0-2
}

仅一层容器! 所有元素直接作为 Grid 的子组件,通过行列坐标定位。这不仅仅是代码行数的减少,更是布局思维的根本性转变。

1.3 本文目标读者

  • 入门开发者:刚接触 HarmonyOS NEXT,想系统学习 Grid 布局
  • 进阶开发者:已有 Column/Row 使用经验,想深入理解 Grid 的优势
  • 性能优化者:关注界面渲染性能,寻找扁平化布局方案

1.4 技术栈说明

技术 版本 说明
HarmonyOS NEXT API 24 最新稳定版
开发语言 ArkTS 基于 TypeScript 的声明式 UI 语言
UI 框架 ArkUI 全场景应用开发框架
开发工具 DevEco Studio NEXT 官方 IDE

2. Grid 布局核心概念

2.1 什么是 Grid?

Grid 是 ArkUI 提供的二维网格布局容器。它将容器空间划分为行(Row)和列(Column),形成一个由单元格(Cell)组成的网格结构。每个子组件(GridItem)可以占据一个或多个单元格,并精确指定其在网格中的位置。

Grid 的设计灵感来源于 CSS Grid Layout,但在 ArkUI 中做了适配和简化,使其更符合声明式 UI 的范式。

2.2 为什么选择 Grid?

在 ArkUI 中,我们有多种布局容器可选:

布局容器 维度 适用场景
Column 一维(垂直) 垂直排列的线性内容
Row 一维(水平) 水平排列的线性内容
Stack 层叠 组件重叠显示
Flex 一维(可换行) 弹性流式布局
Grid 二维 规则网格、仪表盘、商品展示
List 一维(可滚动) 长列表
WaterFlow 二维(瀑布流) 不规则网格

Grid 的独特优势在于它是 ArkUI 中唯一同时支持精确行列定位和跨单元格扩展的布局容器。这意味着你可以用一个 Grid 容器,完成原本需要多层 Column/Row 嵌套才能实现的复杂布局。

2.3 Grid 与 GridItem 的关系

Grid 是容器,GridItem 是子组件。它们的关系类似于 Column 与内部组件的关系,但 GridItem 额外支持行列定位属性:

typescript 复制代码
Grid() {
  GridItem() {
    Text('Hello Grid')
  }
  .rowStart(0)
  .rowEnd(0)
  .columnStart(0)
  .columnEnd(0)
}
.columnsTemplate('1fr')
.rowsTemplate('1fr')

一个 Grid 可以包含任意数量的 GridItem,每个 GridItem 通过行列坐标确定自身位置和大小。

2.4 基本语法结构

typescript 复制代码
@Entry
@Component
struct GridBasicDemo {
  build() {
    Grid() {
      GridItem() {
        Text('内容')
      }
      .rowStart(0)
      .rowEnd(1)
      .columnStart(0)
      .columnEnd(1)
    }
    .columnsTemplate('1fr 1fr 1fr')
    .rowsTemplate('1fr 1fr')
    .columnsGap(8)
    .rowsGap(8)
    .width('100%')
    .height(300)
  }
}

2.5 关键术语速查

在深入学习之前,让我们明确几个关键术语:

  • 行(Row):网格的水平分割线,从上到下编号为 0, 1, 2, ...
  • 列(Column):网格的垂直分割线,从左到右编号为 0, 1, 2, ...
  • 单元格(Cell):由相邻行列围成的最小单位
  • GridItem:Grid 中的每个子组件,可以占据一个或多个单元格
  • 跨列/跨行:一个 GridItem 占据多个列或行的能力
  • 包含索引:ArkUI 中 rowStart/rowEnd 都是包含的

3. Grid vs 传统嵌套布局:全景对比

3.1 场景定义

为了公平对比,我们选择一个具有代表性的界面场景:设备控制面板。该界面包含:

  1. 顶部标题栏:横跨整个宽度
  2. 左侧用户卡片:占两列高度,显示用户信息和统计数据
  3. 右侧功能区:2x2 的功能按钮网格
  4. 底部操作栏:横跨整个宽度

3.2 方案一:传统 Column/Row 嵌套

typescript 复制代码
@Entry
@Component
struct TraditionalLayout {
  @State wifiOn: boolean = true
  @State bluetoothOn: boolean = false
  @State locationOn: boolean = true
  @State darkModeOn: boolean = false

  build() {
    Column() {
      // ===== 第 1 层:顶部标题栏 =====
      Row() {
        Text('设备控制面板')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.White)
        Text('⚙️')
          .fontSize(24)
          .margin({ left: 8 })
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#4A90D9')

      // ===== 第 1 层:内容区 =====
      Column() {
        // 第 2 层:主内容行
        Row() {
          // 第 3 层:用户卡片列
          Column() {
            Row() {
              Text('👤').fontSize(40).padding(12)
                .backgroundColor('#E8F0FE').borderRadius(30)
              Column() {
                Text('开发者').fontSize(16).fontWeight(FontWeight.Medium)
                Text('高级会员').fontSize(12).fontColor('#FF9800')
              }
              .margin({ left: 12 })
            }
            .width('100%')
            Row() {
              Column() {
                Text('128').fontSize(18).fontWeight(FontWeight.Bold)
                Text('设备').fontSize(11).fontColor('#999999')
              }
              .layoutWeight(1)
              Column() {
                Text('56').fontSize(18).fontWeight(FontWeight.Bold)
                Text('在线').fontSize(11).fontColor('#999999')
              }
              .layoutWeight(1)
            }
            .width('100%').margin({ top: 16 })
          }
          .layoutWeight(1).padding(16)
          .backgroundColor(Color.White).borderRadius(12).margin({ right: 8 })

          // 第 3 层:功能按钮列
          Column() {
            Row() {
              Column() {
                Text('📶').fontSize(24)
                Text('WiFi').fontSize(12)
                Text(this.wifiOn ? '已开启' : '已关闭').fontSize(10)
              }
              .layoutWeight(1).padding(12).backgroundColor(Color.White)
              .borderRadius(12)
              .onClick(() => { this.wifiOn = !this.wifiOn })

              Column() {
                Text('🔵').fontSize(24)
                Text('蓝牙').fontSize(12)
                Text(this.bluetoothOn ? '已开启' : '已关闭').fontSize(10)
              }
              .layoutWeight(1).padding(12).backgroundColor(Color.White)
              .borderRadius(12).margin({ left: 8 })
              .onClick(() => { this.bluetoothOn = !this.bluetoothOn })
            }
            .width('100%')
            Row() {
              Column() {
                Text('📍').fontSize(24)
                Text('定位').fontSize(12)
                Text(this.locationOn ? '已开启' : '已关闭').fontSize(10)
              }
              .layoutWeight(1).padding(12).backgroundColor(Color.White)
              .borderRadius(12).margin({ top: 8 })
              .onClick(() => { this.locationOn = !this.locationOn })

              Column() {
                Text('🌙').fontSize(24)
                Text('深色').fontSize(12)
                Text(this.darkModeOn ? '已开启' : '已关闭').fontSize(10)
              }
              .layoutWeight(1).padding(12).backgroundColor(Color.White)
              .borderRadius(12).margin({ left: 8, top: 8 })
              .onClick(() => { this.darkModeOn = !this.darkModeOn })
            }
            .width('100%')
          }
          .layoutWeight(1)
        }
        .width('100%').layoutWeight(1)
      }
      .width('100%').padding(16)

      // ===== 第 1 层:底部操作栏 =====
      Row() {
        Button('全部开启').layoutWeight(1).backgroundColor('#4A90D9')
        Button('全部关闭').layoutWeight(1).backgroundColor('#FF9800').margin({ left: 12 })
        Button('刷新').layoutWeight(1).backgroundColor('#4CAF50').margin({ left: 12 })
      }
      .width('100%').padding(16).backgroundColor(Color.White)
    }
    .width('100%').height('100%')
  }
}

3.3 方案二:Grid 单层布局

typescript 复制代码
@Entry
@Component
struct GridLayout {
  @State wifiOn: boolean = true
  @State bluetoothOn: boolean = false
  @State locationOn: boolean = true
  @State darkModeOn: boolean = false

  build() {
    Column() {
      Grid() {
        // 顶部标题栏:跨 3 列
        GridItem() {
          Row() {
            Text('设备控制面板')
              .fontSize(20).fontWeight(FontWeight.Bold)
              .fontColor(Color.White)
            Text('⚙️').fontSize(24).margin({ left: 8 })
          }
          .width('100%').height('100%').padding(16)
          .backgroundColor('#4A90D9')
        }
        .rowStart(0).rowEnd(0)
        .columnStart(0).columnEnd(2)

        // 用户卡片:跨 2 行 1 列
        GridItem() {
          Column() {
            Row() {
              Text('👤').fontSize(40).padding(12)
                .backgroundColor('#E8F0FE').borderRadius(30)
              Column() {
                Text('开发者').fontSize(16).fontWeight(FontWeight.Medium)
                Text('高级会员').fontSize(12).fontColor('#FF9800')
              }
              .margin({ left: 12 })
            }
            .width('100%')
            Row() {
              Column() {
                Text('128').fontSize(18).fontWeight(FontWeight.Bold)
                Text('设备').fontSize(11).fontColor('#999999')
              }
              .layoutWeight(1)
              Column() {
                Text('56').fontSize(18).fontWeight(FontWeight.Bold)
                Text('在线').fontSize(11).fontColor('#999999')
              }
              .layoutWeight(1)
            }
            .width('100%').margin({ top: 16 })
          }
          .width('100%').height('100%')
          .padding(16).backgroundColor(Color.White).borderRadius(12)
        }
        .rowStart(1).rowEnd(2)
        .columnStart(0).columnEnd(0)

        // WiFi 按钮
        GridItem() {
          Column() {
            Text('📶').fontSize(24)
            Text('WiFi').fontSize(12)
            Text(this.wifiOn ? '已开启' : '已关闭').fontSize(10)
          }
          .width('100%').height('100%').justifyContent(FlexAlign.Center)
          .backgroundColor(Color.White).borderRadius(12)
          .onClick(() => { this.wifiOn = !this.wifiOn })
        }
        .rowStart(1).rowEnd(1)
        .columnStart(1).columnEnd(1)

        // 蓝牙按钮
        GridItem() {
          Column() {
            Text('🔵').fontSize(24)
            Text('蓝牙').fontSize(12)
            Text(this.bluetoothOn ? '已开启' : '已关闭').fontSize(10)
          }
          .width('100%').height('100%').justifyContent(FlexAlign.Center)
          .backgroundColor(Color.White).borderRadius(12)
          .onClick(() => { this.bluetoothOn = !this.bluetoothOn })
        }
        .rowStart(1).rowEnd(1)
        .columnStart(2).columnEnd(2)

        // 定位按钮
        GridItem() {
          Column() {
            Text('📍').fontSize(24)
            Text('定位').fontSize(12)
            Text(this.locationOn ? '已开启' : '已关闭').fontSize(10)
          }
          .width('100%').height('100%').justifyContent(FlexAlign.Center)
          .backgroundColor(Color.White).borderRadius(12)
          .onClick(() => { this.locationOn = !this.locationOn })
        }
        .rowStart(2).rowEnd(2)
        .columnStart(1).columnEnd(1)

        // 深色模式按钮
        GridItem() {
          Column() {
            Text('🌙').fontSize(24)
            Text('深色').fontSize(12)
            Text(this.darkModeOn ? '已开启' : '已关闭').fontSize(10)
          }
          .width('100%').height('100%').justifyContent(FlexAlign.Center)
          .backgroundColor(Color.White).borderRadius(12)
          .onClick(() => { this.darkModeOn = !this.darkModeOn })
        }
        .rowStart(2).rowEnd(2)
        .columnStart(2).columnEnd(2)

        // 底部操作栏:跨 3 列
        GridItem() {
          Row() {
            Button('全部开启').layoutWeight(1).backgroundColor('#4A90D9')
            Button('全部关闭').layoutWeight(1).backgroundColor('#FF9800').margin({ left: 12 })
            Button('刷新').layoutWeight(1).backgroundColor('#4CAF50').margin({ left: 12 })
          }
          .width('100%').height('100%').padding(16)
          .backgroundColor(Color.White)
        }
        .rowStart(3).rowEnd(3)
        .columnStart(0).columnEnd(2)
      }
      .columnsTemplate('1fr 1fr 1fr')
      .rowsTemplate('60px 1fr 1fr 60px')
      .columnsGap(8)
      .rowsGap(8)
      .padding(16)
      .width('100%')
      .layoutWeight(1)
    }
    .width('100%').height('100%').backgroundColor('#F5F5F5')
  }
}

3.4 深度对比分析

对比维度 传统 Column/Row 嵌套 Grid 单层布局
嵌套层数 4-5 层 1 层
代码行数 ~110 行 ~120 行
布局计算次数 多次递归 单次网格计算
可读性 差:需要逐层理解嵌套关系 好:每个元素直接对应网格坐标
维护成本 高:修改一个区域可能影响其他区域 低:修改局部坐标不影响其他元素
跨区域扩展 困难:需要调整整个嵌套结构 简单:修改 rowStart/rowEnd 即可
性能表现 较差:多层嵌套增加渲染开销 优秀:单层网格高效布局
动态布局 复杂:需要条件渲染嵌套 简洁:仅需调整坐标属性

3.5 为什么开发者容易忽视 Grid?

Grid 虽然强大,但在实际开发中却经常被忽视,原因主要有:

  1. 学习曲线:Grid 的坐标定位概念需要时间适应,不如 Column/Row 直观
  2. 思维惯性:长期使用线性布局形成了思维定势,遇到复杂界面自然想到嵌套
  3. 示例不足:早期文档中 Grid 的示例较少,开发者不知道何时该用 Grid
  4. 误解:有人认为 Grid 只适用于规则网格,不适用于不规则布局

实际上,Grid 的定位能力远超简单网格。通过 rowStart/rowEnd 和 columnStart/columnEnd 的灵活组合,Grid 可以实现几乎任何二维布局,包括不规则的自由布局。


4. Grid 核心属性详解(API 24 特性)

4.1 网格定义属性

4.1.1 columnsTemplate ------ 定义列结构

columnsTemplate 用于定义 Grid 的列数和每列的宽度比例。其值为字符串,由多个段组成,每段代表一列。

基本语法:

typescript 复制代码
Grid() { ... }
.columnsTemplate('1fr 1fr 1fr')  // 3 列等宽

fr 单位说明:

fr 是 fraction(比例)的缩写,表示容器可用空间的比例分配。数字表示该列占几份。

含义 效果
'1fr' 1 列,占满全部宽度
'1fr 1fr' 2 列,各占 50%
'1fr 2fr 1fr' 3 列,比例 1:2:1
'2fr 3fr 2fr 1fr' 4 列,比例 2:3:2:1

固定宽度与比例混合:

typescript 复制代码
.columnsTemplate('100px 1fr 80px')  // 左列固定 100px,右列固定 80px,中间自适应
.columnsTemplate('20% 60% 20%')     // 百分比分配
4.1.2 rowsTemplate ------ 定义行结构

rowsTemplate 用于定义 Grid 的行数和每行的高度比例,用法与 columnsTemplate 完全相同。

typescript 复制代码
Grid() { ... }
.rowsTemplate('80px 1fr 1fr 60px')  // 4 行:顶部 80px,中间自适应,底部 60px

实战技巧:

对于大多数移动应用,建议将 Grid 分为以下几行:

typescript 复制代码
.rowsTemplate('auto 1fr auto')  // 顶部自适应,中间填充,底部自适应

其中 auto 会根据内容自动确定高度,1fr 会占据剩余空间。

4.1.3 动态行列调整

在 API 24 中,columnsTemplaterowsTemplate 支持响应式调整。可以使用 repeat() 函数配合断点动态改变列数:

typescript 复制代码
@Component
struct ResponsiveGrid {
  @State widthBreakpoint: string = 'sm'

  build() {
    Grid() {
      ForEach(items, (item: string) => {
        GridItem() { Text(item) }
      })
    }
    .columnsTemplate(
      `repeat(${new WidthBreakpointType(2, 3, 4, 4).getValue(this.widthBreakpoint)}, 1fr)`
    )
    .rowsTemplate('1fr')
    .columnsGap(12)
    .rowsGap(12)
  }
}

4.2 间距属性

4.2.1 columnsGap ------ 列间距

设置相邻两列之间的间距。

typescript 复制代码
Grid() { ... }
.columnsGap(12)          // 统一间距 12vp
.columnsGap('8px')       // 指定单位
.columnsGap('2%')        // 百分比

注意事项:

  • 设置为负值时按默认值(0)处理
  • 仅影响相邻列之间的距离,不影响 Grid 边缘
  • API 24 中支持间距值参与动画过渡
4.2.2 rowsGap ------ 行间距

设置相邻两行之间的间距,用法与 columnsGap 完全相同。

typescript 复制代码
Grid() { ... }
.rowsGap(12)             // 统一间距 12vp

4.3 滚动相关属性

当 Grid 内容超出可视区域时,可以启用滚动功能:

typescript 复制代码
Grid() { ... }
.scrollBar(BarState.Auto)       // 自动显示滚动条
.scrollBarColor('#888888')      // 滚动条颜色
.scrollBarWidth(4)              // 滚动条宽度 4vp
.edgeEffect(EdgeEffect.Spring)  // 边缘弹簧效果

4.4 GridLayoutOptions ------ 高性能布局选项

API 24 新增了 GridLayoutOptions,用于优化大量 GridItem 场景下的布局性能:

typescript 复制代码
Grid(this.scroller) {
  // 大量 GridItem
}
.columnsTemplate('1fr 1fr 1fr')
.rowsTemplate('1fr')
.gridOptions({
  regularSize: [1, 1]  // 每个单元格大小固定为 1x1
})

适用场景:

  • GridItem 数量超过 50 个
  • 需要通过 scrollToIndex 跳转到指定位置
  • 所有 GridItem 大小规则一致

4.5 API 24 新特性

4.5.1 更高效的懒加载

API 24 优化了 Grid 的懒加载机制。当 Grid 包含 1000+ 个 GridItem 时,不可见项的回收策略更加智能,显著降低了 GPU 渲染压力。

4.5.2 更灵活的单位组合

columnsTemplaterowsTemplate 现在支持混合使用 frpxvp%

typescript 复制代码
.columnsTemplate('100px 1fr 20% 80vp')
4.5.3 GridItem.alignSelf

GridItem 新增了 alignSelf 属性,允许单个格子覆盖容器的默认对齐方式:

typescript 复制代码
GridItem() {
  Text('居右对齐')
}
.rowStart(0).rowEnd(0)
.columnStart(0).columnEnd(0)
.alignSelf(ItemAlign.End)  // 覆盖 Grid 的默认对齐
4.5.4 间距动画支持

columnsGaprowsGap 的变化现在可以参与动画过渡:

typescript 复制代码
@State gapSize: number = 8

Grid() { ... }
.columnsGap(this.gapSize)
.rowsGap(this.gapSize)

Button('增大间距').onClick(() => {
  animateTo({ duration: 300 }, () => {
    this.gapSize = 24
  })
})
4.5.5 无障碍增强

Grid 和 GridItem 自动获得了正确的无障碍焦点遍历顺序,无需手动设置 tabIndex


5. GridItem 精确定位机制

5.1 包含索引规则

在 ArkUI 中,rowStartrowEndcolumnStartcolumnEnd 都是包含索引(Inclusive Index)。这与 CSS Grid 的排他索引(Exclusive Index)不同,是初学者最容易踩坑的地方。

规则说明:

  • rowStart(0).rowEnd(0):仅占据第 0 行
  • rowStart(0).rowEnd(1):同时占据第 0 行和第 1 行
  • rowStart(1).rowEnd(3):占据第 1、2、3 行(共 3 行)

列同理:

  • columnStart(0).columnEnd(0):仅占据第 0 列
  • columnStart(0).columnEnd(2):占据第 0、1、2 列(共 3 列)

5.2 完整跨行列坐标表

假设一个 Grid 有 4 行 3 列:

复制代码
     column 0  column 1  column 2
row 0  [0,0]    [0,1]    [0,2]
row 1  [1,0]    [1,1]    [1,2]
row 2  [2,0]    [2,1]    [2,2]
row 3  [3,0]    [3,1]    [3,2]

常见跨行列场景:

需求 坐标设置 说明
占 1 行 1 列 .rowStart(0).rowEnd(0).columnStart(0).columnEnd(0) 最小单元格
跨全部列 .columnStart(0).columnEnd(2) 3 列网格的全宽
跨全部行 .rowStart(0).rowEnd(3) 4 行网格的全高
跨 2 行 1 列 .rowStart(1).rowEnd(2).columnStart(0).columnEnd(0) 左侧大块
跨 2 列 2 行 .rowStart(1).rowEnd(2).columnStart(1).columnEnd(2) 右下角大块

5.3 实战验证方法

为了验证坐标理解是否正确,可以在开发阶段为每个 GridItem 添加不同的背景色:

typescript 复制代码
@Entry
@Component
struct CoordinateDemo {
  build() {
    Grid() {
      GridItem() { Text('A') }
        .rowStart(0).rowEnd(0).columnStart(0).columnEnd(0)
        .backgroundColor(Color.Red)
      GridItem() { Text('B') }
        .rowStart(0).rowEnd(0).columnStart(1).columnEnd(2)
        .backgroundColor(Color.Blue)
      GridItem() { Text('C') }
        .rowStart(1).rowEnd(2).columnStart(0).columnEnd(0)
        .backgroundColor(Color.Green)
      GridItem() { Text('D') }
        .rowStart(1).rowEnd(1).columnStart(1).columnEnd(1)
        .backgroundColor(Color.Yellow)
      GridItem() { Text('E') }
        .rowStart(1).rowEnd(2).columnStart(2).columnEnd(2)
        .backgroundColor(Color.Orange)
      GridItem() { Text('F') }
        .rowStart(2).rowEnd(2).columnStart(1).columnEnd(1)
        .backgroundColor(Color.Purple)
    }
    .columnsTemplate('1fr 1fr 1fr')
    .rowsTemplate('80px 80px 80px')
    .columnsGap(4)
    .rowsGap(4)
    .width('100%')
  }
}

5.4 GridItem 其他属性

5.4.1 selected 和 selectable
typescript 复制代码
GridItem() { Text('可选项') }
.selected(true)          // 设置选中状态
.selectable(true)        // 允许选中
.onSelect((isSelected: boolean) => {
  // 选中状态变化回调
})
5.4.2 GridItemOptions(API 11+)

构造函数参数,用于设置初始属性:

typescript 复制代码
GridItem({
  rowStart: 0,
  rowEnd: 1,
  columnStart: 0,
  columnEnd: 2,
  style: GridItemStyle.Default
}) {
  Text('带初始配置')
}

5.5 坐标越界处理

如果 GridItem 的坐标超出了 Grid 定义的行列范围,会发生什么?

  • 超出列数 :如果 Grid 有 3 列(索引 0-2),设置 columnEnd(3) 是错误的,会导致布局异常
  • 超出行数:同理,超出行数会导致 GridItem 不可见或溢出
  • 坐标重叠:两个 GridItem 占据相同的单元格时,后渲染的会覆盖先渲染的

正确做法: 始终确保 rowEnd 和 columnEnd 不超过行列数减一。


6. 实战:从 4 层嵌套到 1 层 Grid 的重构之旅

6.1 原始代码分析

假设我们有一个商品详情页面,使用了 4 层嵌套:

复制代码
Column {                    // 第 1 层
  Row { 标题 }             // 第 2 层
  Column {                  // 第 2 层
    Row {                   // 第 3 层
      Column { 图片区 }     // 第 4 层
      Column { 信息区 }     // 第 4 层
    }
    Row {                   // 第 3 层
      Column { 规格 }       // 第 4 层
      Column { 价格 }       // 第 4 层
      Column { 按钮 }       // 第 4 层
    }
  }
}

问题:

  • 嵌套过深,修改困难
  • 多个 layoutWeight 的交互增加调试难度
  • 如果想让价格区域跨两列,需要重构整个下半区

6.2 重构为 Grid

第一步:定义网格结构

分析布局需求,确定行列划分:

复制代码
+--------------------------+
|       标题(跨 3 列)      |  row 0
+-------------+------------+
|             |   信息区     |  row 1
|  图片区      +------------+
|  (跨 2 行)   |   规格区     |  row 2
+-------------+------------+
|   价格       | 加入购物车   | 立即购买  |  row 3
+-------------+------------+

对应 Grid 配置:

  • :3 列(图片区占 1 列,右侧占 2 列)
  • :4 行
typescript 复制代码
.columnsTemplate('1fr 1fr 1fr')
.rowsTemplate('auto 1fr 1fr auto')
第二步:映射每个区域的坐标
区域 rowStart rowEnd columnStart columnEnd
标题 0 0 0 2
图片区 1 2 0 0
信息区 1 1 1 2
规格区 2 2 1 2
价格 3 3 0 0
加入购物车 3 3 1 1
立即购买 3 3 2 2
第三步:编写 Grid 代码
typescript 复制代码
@Entry
@Component
struct ProductDetailPage {
  build() {
    Column() {
      Grid() {
        // 标题:跨全部 3 列
        GridItem() {
          Row() {
            Text('商品详情')
              .fontSize(18).fontWeight(FontWeight.Bold)
          }
          .width('100%').justifyContent(FlexAlign.Center)
          .padding(12).backgroundColor(Color.White)
        }
        .rowStart(0).rowEnd(0).columnStart(0).columnEnd(2)

        // 图片区:跨 2 行 1 列
        GridItem() {
          Column() {
            Text('🖼️').fontSize(80).margin({ bottom: 12 })
            Text('商品图片').fontSize(14).fontColor('#999999')
          }
          .width('100%').height('100%').justifyContent(FlexAlign.Center)
          .backgroundColor('#F5F5F5').borderRadius(8).margin({ right: 8 })
        }
        .rowStart(1).rowEnd(2).columnStart(0).columnEnd(0)

        // 信息区:跨 1 行 2 列
        GridItem() {
          Column() {
            Text('鸿蒙智能手表 Pro Max')
              .fontSize(16).fontWeight(FontWeight.Bold)
            Text('一款颠覆想象的智能穿戴设备...')
              .fontSize(12).fontColor('#666666').margin({ top: 8 })
          }
          .width('100%').height('100%').padding(8)
          .backgroundColor(Color.White).borderRadius(8)
        }
        .rowStart(1).rowEnd(1).columnStart(1).columnEnd(2)

        // 规格区:跨 1 行 2 列
        GridItem() {
          Column() {
            Text('规格参数').fontSize(14).fontWeight(FontWeight.Medium)
            Row() {
              Text('颜色:黑色').fontSize(11).fontColor('#666666')
              Text('尺寸:45mm').fontSize(11).fontColor('#666666')
                .margin({ left: 12 })
            }
            .margin({ top: 6 })
          }
          .width('100%').height('100%').padding(8)
          .backgroundColor(Color.White).borderRadius(8).margin({ top: 8 })
        }
        .rowStart(2).rowEnd(2).columnStart(1).columnEnd(2)

        // 价格
        GridItem() {
          Text('¥1299').fontSize(18).fontWeight(FontWeight.Bold)
            .fontColor('#FF4444')
        }
        .rowStart(3).rowEnd(3).columnStart(0).columnEnd(0)

        // 加入购物车
        GridItem() {
          Button('加入购物车').width('100%').backgroundColor('#FF9800')
        }
        .rowStart(3).rowEnd(3).columnStart(1).columnEnd(1)

        // 立即购买
        GridItem() {
          Button('立即购买').width('100%').backgroundColor('#FF4444')
        }
        .rowStart(3).rowEnd(3).columnStart(2).columnEnd(2)
      }
      .columnsTemplate('1fr 1fr 1fr')
      .rowsTemplate('auto 1fr 1fr auto')
      .columnsGap(8).rowsGap(8)
      .padding(12).width('100%').layoutWeight(1)
    }
    .width('100%').height('100%').backgroundColor('#F0F0F0')
  }
}
第四步:对比结果
指标 原始嵌套 Grid 重构
嵌套层数 4 层 1 层
修改价格区域 需修改 Row + Column 结构 仅修改 rowStart/columnStart
添加新区域 复杂:需插入 Column/Row 简单:新增 GridItem
代码可读性 差:需要逐层缩进 好:每个区域对应明确坐标

6.3 重构心得

从嵌套到 Grid 的重构过程可以总结为以下步骤:

  1. 分析布局:画出界面的网格划分图,确定行列数
  2. 定义模板 :根据分析结果设置 columnsTemplaterowsTemplate
  3. 分配坐标:为每个区域指定 rowStart/rowEnd/columnStart/columnEnd
  4. 迁移内容:将原来嵌套的内容移入对应的 GridItem
  5. 调试样式:调整间距、背景色等视觉属性
  6. 验证交互:确保所有点击事件、状态管理正常工作

7. Grid 进阶技巧与最佳实践

7.1 使用 @Builder 拆分 GridItem

当 GridItem 内容较复杂时,使用 @Builder 装饰器将其拆分为独立的 UI 构建函数,可以大幅提升代码可读性。

typescript 复制代码
@Entry
@Component
struct GridBuilderDemo {
  @State items: string[] = ['A', 'B', 'C', 'D', 'E', 'F']

  @Builder
  ItemCard(content: string, color: string) {
    Column() {
      Text(content)
        .fontSize(24).fontWeight(FontWeight.Bold).fontColor(Color.White)
    }
    .width('100%').height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor(color).borderRadius(8)
  }

  build() {
    Grid() {
      ForEach(this.items, (item: string, index: number) => {
        GridItem() {
          this.ItemCard(item, this.getColor(index))
        }
        .rowStart(Math.floor(index / 3))
        .rowEnd(Math.floor(index / 3))
        .columnStart(index % 3)
        .columnEnd(index % 3)
      })
    }
    .columnsTemplate('1fr 1fr 1fr')
    .rowsTemplate('1fr 1fr')
    .columnsGap(8).rowsGap(8)
    .width('100%').height(300)
  }

  private getColor(index: number): string {
    const colors: string[] = ['#E57373', '#81C784', '#64B5F6',
      '#FFD54F', '#BA68C8', '#4DB6AC']
    return colors[index % colors.length]
  }
}

7.2 动态网格调整

根据屏幕尺寸或用户偏好动态改变 Grid 的行列配置:

typescript 复制代码
@Entry
@Component
struct DynamicGrid {
  @State columns: number = 3
  @State items: string[] = ['A', 'B', 'C', 'D', 'E', 'F']

  build() {
    Column() {
      Row() {
        Button('2 列').onClick(() => { this.columns = 2 })
        Button('3 列').onClick(() => { this.columns = 3 }).margin({ left: 8 })
        Button('4 列').onClick(() => { this.columns = 4 }).margin({ left: 8 })
      }
      .margin({ bottom: 12 })

      Grid() {
        ForEach(this.items, (item: string, index: number) => {
          GridItem() {
            Text(item).fontSize(20).fontWeight(FontWeight.Bold)
          }
          .rowStart(Math.floor(index / this.columns))
          .rowEnd(Math.floor(index / this.columns))
          .columnStart(index % this.columns)
          .columnEnd(index % this.columns)
          .backgroundColor('#E3F2FD').borderRadius(8)
        })
      }
      .columnsTemplate(this.generateColumnsTemplate())
      .rowsTemplate('1fr').columnsGap(8).rowsGap(8)
      .width('100%').layoutWeight(1)
    }
    .width('100%').height('100%').padding(16)
  }

  private generateColumnsTemplate(): string {
    let template: string = ''
    for (let i: number = 0; i < this.columns; i++) {
      template += '1fr '
    }
    return template.trim()
  }
}

7.3 Grid + LazyForEach

当 GridItem 数量很大(如商品列表、图片画廊)时,使用 LazyForEach 可以实现懒加载:

typescript 复制代码
class GridDataSource implements IDataSource {
  private dataList: string[] = []
  private listener: DataChangeListener | null = null

  constructor(data: string[]) {
    this.dataList = data
  }

  totalCount(): number {
    return this.dataList.length
  }

  getData(index: number): string {
    return this.dataList[index]
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    this.listener = listener
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    this.listener = null
  }
}

@Entry
@Component
struct LazyGridDemo {
  private dataSource: GridDataSource = new GridDataSource(
    Array.from({ length: 1000 }, (_, i) => `Item ${i}`)
  )

  build() {
    Grid() {
      LazyForEach(this.dataSource, (item: string, index: number) => {
        GridItem() {
          Text(item).fontSize(14).fontColor('#333333')
        }
        .rowStart(Math.floor(index / 3))
        .rowEnd(Math.floor(index / 3))
        .columnStart(index % 3)
        .columnEnd(index % 3)
        .backgroundColor('#F5F5F5').borderRadius(8)
      }, (item: string) => item)
    }
    .columnsTemplate('1fr 1fr 1fr')
    .rowsTemplate('1fr').columnsGap(8).rowsGap(8)
    .width('100%').height('100%')
  }
}

7.4 响应式 Grid

结合窗口宽度实现不同屏幕尺寸的自适应布局:

typescript 复制代码
@Entry
@Component
struct ResponsiveGrid {
  @State windowWidth: number = 0

  build() {
    Column() {
      Text('窗口宽度: ' + this.windowWidth + 'vp')
        .fontSize(14).fontColor('#666666').margin({ bottom: 12 })

      Grid() {
        ForEach(['1', '2', '3', '4', '5', '6', '7', '8', '9'],
          (item: string, index: number) => {
            GridItem() {
              Text(item).fontSize(24).fontWeight(FontWeight.Bold)
            }
            .rowStart(Math.floor(index / this.getColumnCount()))
            .rowEnd(Math.floor(index / this.getColumnCount()))
            .columnStart(index % this.getColumnCount())
            .columnEnd(index % this.getColumnCount())
            .backgroundColor('#E3F2FD').borderRadius(8)
          }
        )
      }
      .columnsTemplate(this.getColumnsTemplate())
      .rowsTemplate('1fr').columnsGap(8).rowsGap(8)
      .width('100%').layoutWeight(1)
    }
    .width('100%').height('100%').padding(16)
  }

  private getColumnCount(): number {
    if (this.windowWidth < 320) return 2
    if (this.windowWidth < 600) return 3
    if (this.windowWidth < 840) return 4
    return 5
  }

  private getColumnsTemplate(): string {
    let template: string = ''
    const count: number = this.getColumnCount()
    for (let i: number = 0; i < count; i++) {
      template += '1fr '
    }
    return template.trim()
  }
}

7.5 状态管理与 Grid

Grid 与 ArkUI 的状态管理机制完美配合。使用 @State 装饰器管理 GridItem 的选中状态:

typescript 复制代码
interface ItemData {
  id: number
  name: string
  isSelected: boolean
}

@Entry
@Component
struct StateManagedGrid {
  @State items: ItemData[] = [
    { id: 1, name: '商品 A', isSelected: false },
    { id: 2, name: '商品 B', isSelected: true },
    { id: 3, name: '商品 C', isSelected: false },
    { id: 4, name: '商品 D', isSelected: false },
  ]

  build() {
    Grid() {
      ForEach(this.items, (item: ItemData, index: number) => {
        GridItem() {
          Column() {
            Text(item.name)
              .fontSize(14)
              .fontColor(item.isSelected ? '#FF4444' : '#333333')
            Text(item.isSelected ? '已选中' : '未选中')
              .fontSize(11)
              .fontColor(item.isSelected ? '#FF4444' : '#999999')
              .margin({ top: 4 })
          }
          .width('100%').height('100%').justifyContent(FlexAlign.Center)
          .backgroundColor(item.isSelected ? '#FFEBEE' : '#F5F5F5')
          .borderRadius(8)
          .onClick(() => {
            item.isSelected = !item.isSelected
          })
        }
        .rowStart(Math.floor(index / 2))
        .rowEnd(Math.floor(index / 2))
        .columnStart(index % 2)
        .columnEnd(index % 2)
      }, (item: ItemData) => item.id.toString())
    }
    .columnsTemplate('1fr 1fr')
    .rowsTemplate('1fr').columnsGap(8).rowsGap(8)
    .width('100%').height(300)
  }
}

8. 性能优化:为什么 Grid 更快?

8.1 布局计算模型

ArkUI 的布局过程分为三个阶段:测量(Measure)、布局(Layout)、绘制(Draw)

Column/Row 嵌套的布局过程:

复制代码
主 Column 测量
  └─ 子 Row 测量
       └─ 子 Column 测量
            └─ 子 Row 测量
                 └─ ...(递归)
主 Column 布局
  └─ 子 Row 布局
       └─ 子 Column 布局
            └─ ...(递归)

每一层嵌套都会增加一次完整的测量-布局循环。对于 4 层嵌套,需要进行 4 轮递归计算。

Grid 的布局过程:

复制代码
Grid 统一测量所有 GridItem
Grid 统一布局所有 GridItem

Grid 将所有子组件的测量和布局合并为一次计算,避免了递归开销。

8.2 实际性能数据

以下数据来自 HarmonyOS 官方性能测试(API 24,华为 Mate 60 Pro):

布局方式 嵌套层数 布局时间 帧率
Column/Row 嵌套 4 层 12.3ms 58 FPS
Column/Row 嵌套 3 层 9.8ms 59 FPS
Grid 单层 1 层 4.2ms 60 FPS

虽然在简单界面上差异不明显,但在复杂界面(如包含 20+ 组件的仪表盘)上,Grid 的性能优势会非常突出。

8.3 Grid 性能优势总结

  1. 减少布局计算次数:单层 Grid 只需一次网格计算,而多层嵌套需要多次递归
  2. 避免冗余容器:每个 Column/Row 都会创建一个容器节点,Grid 减少了节点数量
  3. 更高效的绘制:扁平化的组件树减少了绘制遍历的深度
  4. API 24 优化:新的懒加载和回收机制进一步降低了内存和 GPU 压力

8.4 通用布局优化原则

除了使用 Grid,以下原则也能提升布局性能:

  1. 控制嵌套深度:尽量不超过 3 层,超过时考虑 Grid 或扁平化
  2. 避免冗余容器:如果内层容器与外层容器布局方向相同,考虑合并
  3. 合理使用 @Builder:将复杂 UI 片段拆分为独立 Builder,减少重复代码
  4. 选择合适的组件:简单网格用 Grid,长列表用 List,瀑布流用 WaterFlow
  5. 使用 LazyForEach:大量数据时务必使用懒加载

9. 常见问题与避坑指南

9.1 包含索引 vs 排他索引

问题描述: 设置 rowStart(0).rowEnd(1) 预期占 1 行,但实际占了 2 行。

原因: ArkUI 使用包含索引,即 rowEnd 是包含的。

正确做法:

  • 占 1 行:.rowStart(n).rowEnd(n)
  • 占 2 行:.rowStart(n).rowEnd(n + 1)
  • 占全部行:.rowStart(0).rowEnd(总行数 - 1)

9.2 columnsGap 和 rowsGap 不生效

问题描述: 设置了 .columnsGap(12) 但列之间没有间距。

可能原因:

  1. Grid 没有设置 columnsTemplate
  2. columnsTemplate 只有一列(多列才有间距)
  3. GridItem 没有正确占据单元格

排查方法:

typescript 复制代码
Grid() {
  GridItem() { Text('A') }.rowStart(0).rowEnd(0).columnStart(0).columnEnd(0)
  GridItem() { Text('B') }.rowStart(0).rowEnd(0).columnStart(1).columnEnd(1)
}
.columnsTemplate('1fr 1fr')  // 必须有至少 2 列
.columnsGap(12)

9.3 GridItem 内容不显示

问题描述: GridItem 设置正确但内容不可见。

可能原因:

  1. Grid 没有设置 height(内容被压缩为 0)
  2. GridItem 的坐标超出范围
  3. GridItem 没有设置背景色或内容透明

解决方案:

typescript 复制代码
Grid() { ... }
.height(300)  // 或 .layoutWeight(1)

9.4 fr 单位使用误区

问题描述: .columnsTemplate('1fr') 但 Grid 没有宽度。

原因: fr 是比例单位,依赖容器的实际尺寸。如果 Grid 本身没有确定宽度,1fr 无法计算。

正确做法:

typescript 复制代码
Grid() { ... }
.width('100%')  // 或固定宽度
.columnsTemplate('1fr 1fr')

9.5 混用固定宽度和 fr

问题描述: .columnsTemplate('100px 1fr') 中 1fr 计算异常。

注意事项: 固定宽度会先被分配空间,剩余空间再按 fr 比例分配。如果固定宽度之和超过容器宽度,Grid 会溢出。

typescript 复制代码
.columnsTemplate('100px 1fr')     // 正确:1fr = 剩余空间
.columnsTemplate('100px 200px')   // 正确:固定宽度
.columnsTemplate('1fr 50%')       // 正确:fr 和 % 可以混用

9.6 @Builder 中不能声明 @State

错误示例:

typescript 复制代码
@Builder
ItemCard() {
  @State isSelected: boolean = false  // 错误
}

正确做法:

typescript 复制代码
@Component
struct ItemCardComponent {
  @State isSelected: boolean = false

  build() {
    // UI
  }
}

@Builder
ItemCard() {
  ItemCardComponent()
}

9.7 滚动冲突

问题描述: Grid 嵌套在 Scroll 中,滚动行为异常。

原因: Grid 自身可以滚动,Scroll 也可以滚动,两者冲突。

解决方案:

  • 如果 Grid 内容不超出,移除外层 Scroll
  • 如果需要滚动,让 Grid 自己滚动,移除外层 Scroll
  • 不要同时让 Grid 和外层容器滚动

9.8 Dark Mode 适配

问题描述: Grid 的背景色在深色模式下不协调。

注意事项: 使用资源引用替代硬编码颜色值:

typescript 复制代码
.backgroundColor($r('app.color.card_background'))

resources/element/color.jsonresources/dark/element/color.json 中分别定义浅色和深色值。


10. 综合案例:设备控制面板完整实现

10.1 需求分析

实现一个智能设备控制面板,包含:

  • 顶部渐变标题栏(跨 3 列)
  • 左侧用户卡片(跨 2 行 1 列)
  • 右侧 4 个功能按钮(2x2 网格)
  • 底部操作栏(跨 3 列)
  • 所有交互均可点击

10.2 完整代码

typescript 复制代码
@Entry
@Component
struct DeviceControlPanel {
  // 用户信息
  @State userName: string = '鸿蒙开发者'
  @State userLevel: string = '高级会员'
  @State deviceCount: number = 128
  @State onlineCount: number = 56

  // 功能开关状态
  @State wifiEnabled: boolean = true
  @State bluetoothEnabled: boolean = false
  @State locationEnabled: boolean = true
  @State darkModeEnabled: boolean = false

  // 操作反馈
  @State feedbackText: string = '欢迎使用设备控制面板'

  @Builder
  TitleBar() {
    Row() {
      Column() {
        Text('设备控制面板')
          .fontSize(22).fontWeight(FontWeight.Bold).fontColor(Color.White)
        Text('用 Grid 替代多层嵌套布局')
          .fontSize(12).fontColor('#E0E0E0').margin({ top: 4 })
      }
      .alignItems(HorizontalAlign.Start).layoutWeight(1)

      Text('⚙️').fontSize(28).padding(8)
        .backgroundColor('rgba(255,255,255,0.2)').borderRadius(20)
    }
    .width('100%').height('100%').padding({ left: 16, right: 16 })
    .linearGradient({
      direction: GradientDirection.Right,
      colors: [['#4A90D9', 0], ['#357ABD', 1]]
    })
    .borderRadius({ bottomLeft: 16, bottomRight: 16 })
  }

  @Builder
  UserCard() {
    Column() {
      Row() {
        Text('👤').fontSize(40).padding(12)
          .backgroundColor('#E8F0FE').borderRadius(30)
        Column() {
          Text(this.userName).fontSize(18).fontWeight(FontWeight.Medium)
          Text(this.userLevel).fontSize(12).fontColor('#FF9800')
            .margin({ top: 2 })
        }
        .alignItems(HorizontalAlign.Start).margin({ left: 12 }).layoutWeight(1)
      }
      .width('100%')

      Row() {
        Column() {
          Text(this.deviceCount.toString())
            .fontSize(20).fontWeight(FontWeight.Bold).fontColor('#4A90D9')
          Text('设备').fontSize(11).fontColor('#999999')
        }.layoutWeight(1)
        Column() {
          Text(this.onlineCount.toString())
            .fontSize(20).fontWeight(FontWeight.Bold).fontColor('#4CAF50')
          Text('在线').fontSize(11).fontColor('#999999')
        }.layoutWeight(1)
      }
      .width('100%').margin({ top: 16 })
    }
    .width('100%').height('100%').padding(16)
    .backgroundColor(Color.White).borderRadius(12)
    .shadow({ radius: 4, color: 'rgba(0,0,0,0.08)', offsetY: 2 })
  }

  @Builder
  QuickSettingCard(icon: string, label: string, enabled: boolean,
                   onToggle: () => void) {
    Column() {
      Text(icon).fontSize(28)
      Text(label).fontSize(13).fontColor('#333333').margin({ top: 8 })
      Row() {
        Text(enabled ? '●' : '○').fontSize(10)
          .fontColor(enabled ? '#4CAF50' : '#CCCCCC')
        Text(enabled ? '已开启' : '已关闭').fontSize(10)
          .fontColor(enabled ? '#4CAF50' : '#999999').margin({ left: 4 })
      }
      .margin({ top: 4 })
    }
    .width('100%').height('100%').justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center).backgroundColor(Color.White)
    .borderRadius(12)
    .shadow({ radius: 4, color: 'rgba(0,0,0,0.08)', offsetY: 2 })
    .onClick(() => { onToggle() })
  }

  @Builder
  BottomBar() {
    Row() {
      Button() { Text('全部开启').fontSize(14).fontColor(Color.White) }
        .type(ButtonType.Capsule).backgroundColor('#4A90D9')
        .layoutWeight(1).height(40)
        .onClick(() => {
          this.wifiEnabled = true
          this.bluetoothEnabled = true
          this.locationEnabled = true
          this.darkModeEnabled = true
          this.feedbackText = '已开启所有功能'
        })

      Button() { Text('全部关闭').fontSize(14).fontColor(Color.White) }
        .type(ButtonType.Capsule).backgroundColor('#FF9800')
        .layoutWeight(1).height(40).margin({ left: 12 })
        .onClick(() => {
          this.wifiEnabled = false
          this.bluetoothEnabled = false
          this.locationEnabled = false
          this.darkModeEnabled = false
          this.feedbackText = '已关闭所有功能'
        })

      Button() { Text('刷新').fontSize(14).fontColor(Color.White) }
        .type(ButtonType.Capsule).backgroundColor('#4CAF50')
        .layoutWeight(1).height(40).margin({ left: 12 })
        .onClick(() => {
          this.feedbackText = '数据已刷新'
        })
    }
    .width('100%').height('100%').padding({ left: 16, right: 16 })
    .backgroundColor(Color.White)
    .borderRadius({ topLeft: 16, topRight: 16 })
    .shadow({ radius: 4, color: 'rgba(0,0,0,0.08)', offsetY: -2 })
  }

  build() {
    Column() {
      Grid() {
        // Row 0: 顶部标题栏(跨 3 列)
        GridItem() { this.TitleBar() }
        .rowStart(0).rowEnd(0).columnStart(0).columnEnd(2)

        // Row 1-2: 用户卡片(跨 2 行 1 列)
        GridItem() { this.UserCard() }
        .rowStart(1).rowEnd(2).columnStart(0).columnEnd(0)

        // Row 1, Column 1: WiFi
        GridItem() {
          this.QuickSettingCard('📶', 'WiFi', this.wifiEnabled, () => {
            this.wifiEnabled = !this.wifiEnabled
            this.feedbackText = 'WiFi ' + (this.wifiEnabled ? '已开启' : '已关闭')
          })
        }
        .rowStart(1).rowEnd(1).columnStart(1).columnEnd(1)

        // Row 1, Column 2: 蓝牙
        GridItem() {
          this.QuickSettingCard('🔵', '蓝牙', this.bluetoothEnabled, () => {
            this.bluetoothEnabled = !this.bluetoothEnabled
            this.feedbackText = '蓝牙 ' + (this.bluetoothEnabled ? '已开启' : '已关闭')
          })
        }
        .rowStart(1).rowEnd(1).columnStart(2).columnEnd(2)

        // Row 2, Column 1: 定位
        GridItem() {
          this.QuickSettingCard('📍', '定位', this.locationEnabled, () => {
            this.locationEnabled = !this.locationEnabled
            this.feedbackText = '定位 ' + (this.locationEnabled ? '已开启' : '已关闭')
          })
        }
        .rowStart(2).rowEnd(2).columnStart(1).columnEnd(1)

        // Row 2, Column 2: 深色模式
        GridItem() {
          this.QuickSettingCard('🌙', '深色', this.darkModeEnabled, () => {
            this.darkModeEnabled = !this.darkModeEnabled
            this.feedbackText = '深色模式 ' + (this.darkModeEnabled ? '已开启' : '已关闭')
          })
        }
        .rowStart(2).rowEnd(2).columnStart(2).columnEnd(2)

        // Row 3: 底部操作栏(跨 3 列)
        GridItem() { this.BottomBar() }
        .rowStart(3).rowEnd(3).columnStart(0).columnEnd(2)
      }
      .columnsTemplate('1fr 1fr 1fr')
      .rowsTemplate('60px 1fr 1fr 56px')
      .columnsGap(8)
      .rowsGap(8)
      .padding({ left: 16, right: 16 })
      .width('100%')
      .layoutWeight(1)

      Text(this.feedbackText)
        .fontSize(12).fontColor('#666666')
        .width('100%').textAlign(TextAlign.Center)
        .padding(8).backgroundColor('#FFFFFF')
    }
    .width('100%').height('100%').backgroundColor('#F5F5F5')
  }
}

10.3 布局结构说明

复制代码
+----------------------------+
|       TitleBar (跨 3 列)     |  row 0
+-------------+--------------+
|             |   WiFi   | 蓝牙  |  row 1
|  UserCard    +--------------+
|  (跨 2 行)   |   定位   | 深色  |  row 2
+-------------+--------------+
|    BottomBar (跨 3 列)      |  row 3
+----------------------------+

10.4 关键实现要点

  1. @Builder 封装:将标题栏、用户卡片、快捷设置、底部操作栏分别封装为独立的 Builder
  2. 状态管理 :所有开关状态通过 @State 管理,实现响应式更新
  3. 跨行列定位:UserCard 跨 2 行 1 列,TitleBar 和 BottomBar 跨全部 3 列
  4. 布局比例 :使用 1fr 实现自适应,配合固定值控制头部和底部高度
  5. 交互反馈:每次点击更新反馈文本,提升用户体验

11. 总结与展望

11.1 核心观点

本文详细介绍了如何使用 HarmonyOS ArkTS 的 Grid 布局替代传统的多层嵌套布局。通过对比分析和实战案例,我们可以得出以下核心观点:

  1. Grid 是扁平布局的利器:通过将多层嵌套转化为单层 Grid,可以显著降低布局复杂度
  2. 包含索引是关键:正确理解 rowStart/rowEnd 和 columnStart/columnEnd 的包含含义是使用 Grid 的前提
  3. 性能优势明显:在复杂界面上,Grid 的布局性能优于多层嵌套的 Column/Row
  4. 适用场景广泛:Grid 不仅适用于规则网格,也能实现自由布局

11.2 学习路径建议

对于想要深入掌握 Grid 的开发者,建议按以下路径学习:

  1. 基础入门:学习 columnsTemplate、rowsTemplate、columnsGap、rowsGap 等基础属性
  2. 坐标系统:通过彩色方块练习理解包含索引的含义
  3. 实战练习:尝试用 Grid 重构 2-3 个现有页面
  4. 进阶技巧:学习 @Builder、LazyForEach、响应式布局等高级用法
  5. 性能优化:在实践中体会 Grid 的性能优势,学习 GridLayoutOptions 等优化手段

11.3 未来展望

随着 HarmonyOS 的不断演进,Grid 布局也在持续增强。可以预见的未来方向包括:

  1. 更智能的布局算法:AI 辅助生成 Grid 布局代码
  2. 更强的动画支持:Grid 的行列变化可以参与更丰富的动画
  3. 跨端适配:Grid 在手机、平板、折叠屏、车机等多端的自适应能力将更强
  4. 声明式配置:通过 JSON/DSL 配置生成 Grid 布局,降低开发门槛

12. 参考资料

官方文档

社区资源

示例项目


感谢阅读! 希望本文能帮助你在 HarmonyOS 开发中更好地使用 Grid 布局。如有任何问题或建议,欢迎在评论区交流讨论。

相关推荐
●VON5 小时前
鸿蒙 PC Markdown 编辑器可靠性设计:未保存内容的崩溃恢复闭环
华为·中间件·编辑器·harmonyos·鸿蒙
●VON6 小时前
鸿蒙 PC Markdown 编辑器导出架构:安全 HTML 与系统 PDF
华为·架构·编辑器·harmonyos·鸿蒙
信仰8746 小时前
HCIA-华为数通基础理论与实践08
网络·笔记·华为
不羁的木木6 小时前
HarmonyOS技术精讲-Connectivity Kit(短距通信服务):初识统一短距通信框架
华为·wpf·harmonyos
爸爸6196 小时前
UIAbility 生命周期在日记 App 中的实践
后端·华为·harmonyos·鸿蒙系统
tyqtyq2215 小时前
旅行打包清单 App — HarmonyOS AI 应用开发技术博客
人工智能·学习·华为·生活·harmonyos
HONG````15 小时前
Lazy Import 动态加载:减少 HarmonyOS 应用冷启动时间
harmonyos
条tiao条19 小时前
Navigation页面跳转教学博客
ui·华为·harmonyos·鸿蒙·页面跳转
ZZZMMM.zip19 小时前
竞品分析框架-基于鸿蒙的AI竞品分析应用开发实践
人工智能·华为·harmonyos·鸿蒙系统