【共创季稿事节】鸿蒙原生 ArkTS 布局实战:用 Grid 组件构建精美计算器数字键盘

鸿蒙原生 ArkTS 布局实战:用 Grid 组件构建精美计算器数字键盘


一、前言

HarmonyOS NEXT 自 API 24 起全面去除了 Android AOSP 代码,标志着鸿蒙系统迈入纯血原生时代。与此同时,ArkTS(Ark TypeScript)作为鸿蒙原生应用开发的首选语言,结合 ArkUI 声明式 UI 框架,为开发者提供了一套高效、简洁、类型安全的界面构建方案。

在移动应用开发中,数字键盘 是一类极其常见的 UI 场景------无论是计算器、拨号盘、支付密码输入,还是各种表单的数字录入,都离不开它。而要实现规整、响应式的键盘布局,Grid(网格)容器无疑是最合适的选择。

本文将从一个完整的计算器应用出发,手把手带你掌握鸿蒙 ArkTS 的 Grid 布局,从组件设计、数据驱动渲染到交互逻辑实现,逐一拆解。


二、认识 HarmonyOS Grid 布局

2.1 什么是 Grid

Grid 是 ArkUI 提供的网格布局容器,允许开发者将子组件按照行(Row)和列(Column)的二维网格进行排列。与传统的线性布局(Column / Row / Flex)相比,Grid 在处理「表格状」UI 时具有天然优势:

  • 行列精确可控 :通过 columnsTemplate 和 rowsTemplate 定义网格模板
  • 自动换行填充:子元素按顺序自动填入网格单元格
  • 跨行跨列支持 :子项可通过 gridSpan 属性跨越多个单元格
  • 间距自由设置 :columnsGap 和 rowsGap 灵活控制行列间距
  • 性能优秀 :配合 LazyForEach 可支撑大量数据的懒加载渲染

2.2 Grid 的核心属性

属性 类型 说明
columnsTemplate string 列模板,如 "1fr 1fr 1fr" 表示三等分列
rowsTemplate string 行模板,可选,不设置则自动撑开
columnsGap Length 列间距(vp)
rowsGap Length 行间距(vp)
editable boolean 是否允许拖拽编辑(API 24+)
supportAnimation boolean 是否支持拖拽动画

其中 columnsTemplate 是最核心的属性,它采用 CSS Grid 类似的 fr 单位语法:

  • "1fr 1fr 1fr 1fr" ------ 4 列等宽,每列各占 1 份可用空间
  • "100px 1fr 2fr" ------ 第 1 列固定 100vp,后两列按 1:2 分配剩余空间
  • "repeat(3, 1fr)" ------ 3 列等宽(简写语法)

2.3 GridItem 的跨列能力

每个 GridItem 子组件可以通过 gridSpan 属性指定跨越的列数:

typescript 复制代码
GridItem() {
  Button('0')
    // ...
}
.gridSpan(2)    // 跨 2 列

这在数字键盘中尤其重要------底部的「0」键通常比其它数字键宽一倍。


三、项目结构概览

在开始编码前,我们先看一下示例工程的文件结构:

复制代码
entry/
├── src/
│   ├── main/
│   │   ├── ets/
│   │   │   ├── entryability/
│   │   │   │   └── EntryAbility.ets    // 应用入口 Ability
│   │   │   └── pages/
│   │   │       └── Index.ets           // ★ 主页面(我们的代码)
│   │   ├── module.json5                // 模块配置
│   │   └── resources/                  // 资源文件(字符串、颜色等)
│   └── ohosTest/                       // 单元测试
├── build-profile.json5                 // 构建配置
└── hvigorfile.ts                       // Hvigor 构建脚本

所有布局和业务逻辑都集中在 Index.ets 中,单文件即可完整展示 Grid 布局的威力。


四、从零搭建:计算器数字键盘

4.1 定义数据模型

首先我们需要明确:每个键盘按钮都有哪些属性?

typescript 复制代码
/**
 * 键盘按钮数据结构
 * 每个按钮包含显示文本、颜色样式和点击处理标识
 */
interface KeyItem {
  label: string;       // 按钮显示的文本
  type: KeyType;       // 按钮类型(数字、运算符、功能)
  colSpan?: number;    // 跨列数(可选,默认 1,0 按钮跨 2 列)
}

同时定义按钮类型的枚举,方便后续进行样式分发和事件分发:

typescript 复制代码
enum KeyType {
  NUMBER,      // 数字键(0-9)
  OPERATOR,    // 运算符键(+ - × ÷)
  FUNCTION,    // 功能键(AC、DEL、%、=、.、(、))
}

设计理念:通过数据模型驱动 UI 渲染,而不是在模板中逐个手写 19 个按钮。这样做的好处是------如果要修改键盘布局,只需修改数据数组,UI 自动响应变化。

4.2 声明状态变量

对于交互类组件,状态管理是核心。ArkTS 使用 @State 装饰器标记响应式变量:

typescript 复制代码
@State private displayText: string = '0';        // 显示屏当前文本
@State private previousValue: number = 0;        // 上一个操作数
@State private currentOperator: string = '';      // 当前运算符
@State private isNewInput: boolean = true;        // 是否为新输入
@State private lastResult: number = 0;            // 上一次计算结果

每当 @State 变量的值发生变化时,ArkUI 会自动重新渲染关联的 UI 组件,无需手动操作 DOM。

4.3 设计按钮数据集

标准计算器采用 4 列 × 5 行 的键盘布局:

复制代码
第1行: AC   (    )    ÷
第2行: 7    8    9    ×
第3行: 4    5    6    -
第4行: 1    2    3    +
第5行: 0   (跨2列)   .    =

在代码中,我们用数组来描述这个布局:

typescript 复制代码
private readonly keys: KeyItem[] = [
  // 第1行:功能键区
  { label: 'AC', type: KeyType.FUNCTION },
  { label: '(',  type: KeyType.FUNCTION },
  { label: ')',  type: KeyType.FUNCTION },
  { label: '÷',  type: KeyType.OPERATOR },
  // 第2行
  { label: '7',  type: KeyType.NUMBER },
  { label: '8',  type: KeyType.NUMBER },
  { label: '9',  type: KeyType.NUMBER },
  { label: '×',  type: KeyType.OPERATOR },
  // 第3行
  { label: '4',  type: KeyType.NUMBER },
  { label: '5',  type: KeyType.NUMBER },
  { label: '6',  type: KeyType.NUMBER },
  { label: '-',  type: KeyType.OPERATOR },
  // 第4行
  { label: '1',  type: KeyType.NUMBER },
  { label: '2',  type: KeyType.NUMBER },
  { label: '3',  type: KeyType.NUMBER },
  { label: '+',  type: KeyType.OPERATOR },
  // 第5行:0 跨2列 + 小数点 + 等号
  { label: '0',  type: KeyType.NUMBER, colSpan: 2 },
  { label: '.',  type: KeyType.FUNCTION },
  { label: '=',  type: KeyType.FUNCTION },
];

这里的「0」按钮特意标记了 colSpan: 2,表示它将跨越 2 列------这在视觉上模拟了真实计算器的宽键。

4.4 构建 UI 界面(build 方法)

build() 方法是 ArkTS 组件的入口。这里采用 Column 垂直布局作为外壳,将页面分为上下两个区域:

typescript 复制代码
build() {
  Column() {
    // 显示屏区域(上半部分)
    this.buildDisplay()

    // Grid 键盘区域(下半部分)
    this.buildKeypad()
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#1C1C1E')   // 深色背景,模拟真实计算器
}

为了控制上下区域的高度比例,我们在 @Builder 内部使用 layoutWeight:

  • 显示屏占据 layoutWeight(1)(1 份权重)
  • 键盘区域占据 layoutWeight(3)(3 份权重)
  • 最终比例为 1:3,键盘区域更高,符合真实计算器的人体工学设计

4.5 ★ 核心:Grid 键盘(@Builder buildKeypad)

这是本文的核心------Grid 布局的完整实现:

typescript 复制代码
@Builder
buildKeypad() {
  Grid() {
    ForEach(this.keys, (item: KeyItem) => {
      GridItem() {
        Button(item.label)
          .width('100%')
          .height('100%')
          .borderRadius(12)
          .fontSize(28)
          .fontColor(this.getTextColor(item))
          .backgroundColor(this.getButtonColor(item))
          .shadow({ radius: 4, color: '#0A000000',
                    offsetX: 0, offsetY: 2 })
          .onClick(() => { this.onKeyPress(item); })
      }
      .gridSpan(item.colSpan ?? 1)   // ★ 关键:设置跨列数
    }, (item: KeyItem) => item.label)
  }
  .columnsTemplate('1fr 1fr 1fr 1fr')   // ★ 核心:4 列等宽
  .columnsGap(8)                         // 列间距
  .rowsGap(8)                            // 行间距
  .padding({ left: 12, right: 12, bottom: 24 })
  .width('100%')
  .layoutWeight(3)
  .height('100%')
}

布局要点逐条解析:

① columnsTemplate('1fr 1fr 1fr 1fr')

这是 Grid 布局的灵魂配置 。1fr 是 fraction(分数)的缩写,表示将容器的可用宽度按比例分配。写成 1fr 1fr 1fr 1fr 意味着 4 列平分总宽度,每列占 25%。

如果希望第 1 列固定 80vp,其余 3 列等分剩余空间,可以写成 "80vp 1fr 1fr 1fr"。

② gridSpan 实现跨列

对于「0」按钮,我们在 GridItem 上调用 .gridSpan(2),告诉 Grid 这个子项应该占据 2 列。由于「0」位于第 5 行的第 1 个位置,它从第 1 列跨越到第 2 列,视觉上比普通按钮宽一倍。

注意 :gridSpan 是 GridItem 的属性,不是 Button 的属性。一定要链式调用在 GridItem() 之后。

③ columnsGap / rowsGap

这两个属性分别控制列与列之间、行与行之间的间距。设置为 8vp 在手机上大约是 2px 左右的物理间距,既保证按键间有清晰的分隔,又不会显得松散。

④ ForEach 数据驱动渲染

不同于在模板中逐个编写 GridItem,这里使用 ForEach 遍历 keys 数组,为每个 KeyItem 动态创建一个 GridItem。第二参数 (item: KeyItem) => item.label 是键生成函数,帮助框架高效识别和跟踪每个列表项的变化。

⑤ layoutWeight 分配垂直空间

显示屏和键盘区域通过 layoutWeight 在 Column 中按 1:3 的比例分配垂直空间。与 Flex 布局中的 flexWeight 类似,layoutWeight 让容器内的子组件按权重瓜分剩余空间。

4.6 按钮颜色与视觉设计

为了让计算器更有质感,我们对三类按钮做了不同的配色:

按钮类型 背景色 文字色 说明
数字键(0-9) #333333 深灰 #FFFFFF 白色 沉稳低调
运算符(+ - × ÷) #FF9500 橙黄 #FFFFFF 白色 醒目突出
功能键(AC / =) #D4D4D2 浅灰 #1C1C1E 深色 次级操作
括号 / 小数点 #505050 中灰 #FFFFFF 白色 中性辅助

代码实现:

typescript 复制代码
getButtonColor(item: KeyItem): ResourceStr {
  switch (item.type) {
    case KeyType.NUMBER:  return '#333333';
    case KeyType.OPERATOR: return '#FF9500';
    case KeyType.FUNCTION:
      if (item.label === 'AC' || item.label === '=') return '#D4D4D2';
      return '#505050';
    default: return '#333333';
  }
}

此外还为每个按钮添加了微弱的阴影效果:

typescript 复制代码
.shadow({ radius: 4, color: '#0A000000', offsetX: 0, offsetY: 2 })

阴影半径 4vp,偏移量向下 2vp,透明度很低(#0A),营造「浮起」的立体感。


五、交互逻辑:让计算器「算得对」

漂亮的 UI 只是第一步。一个计算器必须能正确运算。本节展示如何使用 ArkTS 的 @State 和函数方法来组织交互逻辑。

5.1 事件分发

点击任何按钮都会触发 onKeyPress 方法,它根据 KeyType 将事件分发给对应的处理函数:

typescript 复制代码
onKeyPress(item: KeyItem): void {
  switch (item.type) {
    case KeyType.NUMBER:   this.onNumberInput(item.label);   break;
    case KeyType.OPERATOR: this.onOperatorInput(item.label);  break;
    case KeyType.FUNCTION: this.onFunctionInput(item.label);  break;
  }
}

这种策略模式的设计让事件处理逻辑清晰可维护,新增按钮类型只需增加一个 case 分支。

5.2 数字输入处理

typescript 复制代码
onNumberInput(num: string): void {
  if (this.isNewInput) {
    this.displayText = num;
    this.isNewInput = false;
  } else {
    if (this.displayText === '0') {
      this.displayText = num;
    } else {
      this.displayText += num;
    }
  }
}

关键细节:

  • 刚按下运算符后,isNewInput 为 true,此时输入数字会替换显示屏内容,而不是追加
  • 避免「007」这类不合法的前导零------当前是 "0" 时直接替换

5.3 运算符处理与连续运算

typescript 复制代码
onOperatorInput(op: string): void {
  const currentValue = parseFloat(this.displayText);

  if (this.currentOperator !== '' && !this.isNewInput) {
    // 连续运算:前值 + 当前值 按上一次运算符计算
    this.previousValue = this.calculate(
      this.previousValue, currentValue, this.currentOperator
    );
    this.displayText = String(this.previousValue);
  } else {
    this.previousValue = currentValue;
  }

  this.currentOperator = op;
  this.isNewInput = true;
}

这段代码支持了连续运算 ------比如依次按下 5 + 3 - 2 =,不会丢失中间结果:

  1. 按下 5 → displayText = "5"
  2. 按下 + → previousValue = 5, currentOperator = "+", isNewInput = true
  3. 按下 3 → displayText = "3"
  4. 按下 - → 计算 5 + 3 = 8,然后 previousValue = 8, currentOperator = "-"
  5. 按下 2 → displayText = "2"
  6. 按下 = → 计算 8 - 2 = 6

5.4 计算结果

typescript 复制代码
onFunctionInput(fn: string): void {
  switch (fn) {
    case '=':
      const currentValue = parseFloat(this.displayText);
      const result = this.calculate(
        this.previousValue, currentValue, this.currentOperator
      );
      this.displayText = String(result);
      // ... 重置状态
      break;
    // ...
  }
}

5.5 四则运算引擎

typescript 复制代码
calculate(a: number, b: number, operator: string): number {
  switch (operator) {
    case '+': result = a + b; break;
    case '-': result = a - b; break;
    case '×': result = a * b; break;
    case '÷':
      if (b === 0) {
        this.displayText = 'Error';
        // ... 重置状态
        return 0;
      }
      result = a / b; break;
  }
  // 保留 10 位小数防浮点溢出
  return Math.round(result * 1e10) / 1e10;
}

浮点精度处理 :JavaScript/ArkTS 的浮点数运算存在精度问题(如 0.1 + 0.2 !== 0.3)。通过在结果上 Math.round(result * 1e10) / 1e10,我们保留 10 位有效小数,截断了微小的浮点误差。

除零保护:当除数为 0 时,显示屏显示 "Error" 并重置所有运算符状态,防止连锁错误。

5.6 AC 清除与小数点

  • AC 键 :一键重置所有状态,回到初始状态 displayText = "0"
  • 小数点:智能判断------如果当前已是新输入状态,显示 "0.";否则检查是否已包含小数点,避免输入 "1.2.3"

六、编译与运行验证

我们使用 HarmonyOS 的 Hvigor 构建系统对项目进行编译验证:

bash 复制代码
cd app016
hvigorw assembleHap --no-daemon

成功输出如下:

复制代码
BUILD SUCCESSFUL in 8s

编译通过后会在 entry/build/default/outputs 目录生成 .hap 包,可以安装到 HarmonyOS NEXT 模拟器或真机上运行。

运行效果直观展示了一个深色主题的完整计算器:

  • 深色背景(#1C1C1E)配灰色/橙色按钮
  • 显示屏白色大字显示输入和计算结果
  • 所有 19 个按键布局整齐,4 列等宽
  • 「0」按钮横跨 2 列
  • 点击按键有交互反馈,计算逻辑正确

七、Grid 布局的最佳实践与常见问题

7.1 何时选择 Grid 而非 Flex?

场景 推荐布局 原因
表格状 UI(键盘、日历、图库) Grid 行列对齐天然精确
一维排列(列表、导航栏) Flex / Column / Row 语法更简洁
不规则布局(瀑布流) Grid + rowsTemplate 跨行跨列自如
简单行内排列 Flex Wrap 无需额外容器

7.2 性能考量

  • 对于少量数据(如 20 个按键),直接使用 ForEach 即可
  • 对于大量数据(如日历的 365 天),推荐使用 LazyForEach 实现按需渲染,大幅减少首帧创建开销
  • Grid 的 cachedCount 属性可以控制上下文缓存数量,平衡滚动流畅度和内存占用

7.3 常见错误排查

问题 原因 解决
按钮超出 Grid 范围 columnsTemplate 列数不足 检查列模板的 fr 段数
GridItem 不显示 缺少 GridItem() 包裹 确保子元素都被 GridItem() 包围
gridSpan 无效 链式调用位置错误 确保 .gridSpan() 在 GridItem() 之后调用
布局未充满 缺少宽高设置 为 Grid 设置 .width('100%').height('100%')
间距不一致 Gap 属性拼写错误 API 24 使用 columnsGap/rowsGap

八、扩展思路:从计算器到更多场景

Grid 布局的数字键盘不仅适用于计算器,稍加改造就能适配多种场景:

8.1 拨号键盘

将按钮数据集替换为:

复制代码
1  2  3
4  5  6
7  8  9
*  0  #

增加拨号/挂断按钮,就可以成为一个完整的拨号器。

8.2 密码输入键盘

使用 Grid 布局重排数字顺序(如随机排列),配合 @State inputPassword 和 6 个密码圆点指示器,打造成安全支付键盘。

8.3 单位换算器

保留数字键盘,在显示屏下方增加单位选择区域。输入数字 -> 选择单位 -> 实时换算,一个 Grid 键盘 + Flex 单位选择的组合就能完成。

8.4 自定义键盘融合 Grid 与 Flex

上半部分用 Grid 展示候选词(3 列 × N 行),下半部分用 Grid 展示字母键盘(10 列 × 3 行),再配合 Flex 布局的功能按钮(空格、删除、回车),构成一个完整的输入法键盘原型。


九、总结

通过本文的实战演练,我们从零构建了一个功能完整的计算器应用 ,并深入理解了 HarmonyOS NEXT(API 24)中 Grid 布局的核心用法:

  1. Grid 容器 通过 columnsTemplate 定义列模板,fr 单位让列宽分配灵活直观
  2. GridItem 子组件 自动按行排列,配合 gridSpan 支持跨列,适合不规则网格需求
  3. @Builder 装饰器 配合 @State 状态管理,实现了 UI 与逻辑的清晰分离
  4. 数据驱动渲染 (ForEach + KeyItem 数组)让布局修改只需改数据,无需动模板

鸿蒙原生开发正处于高速发展期,API 24 带来的 ArkTS 能力已经相当成熟------丰富的容器组件(Grid、Flex、RelativeContainer、List)、完善的响应式状态管理、以及类型安全的编译期检查,都让开发体验直追甚至超越主流移动开发框架。

希望这篇博客能帮助你快速上手 HarmonyOS NEXT 的 Grid 布局,为你的鸿蒙应用开发之路添砖加瓦。


附录:完整源代码

完整代码位于 entry/src/main/ets/pages/Index.ets,已在 HarmonyOS NEXT API 24 环境下编译通过。如需完整工程源码,可参考文末说明获取。

关键代码片段速查

复制代码
├── 数据模型定义       →  KeyItem 接口 + KeyType 枚举
├── 状态管理            →  @State 装饰的 5 个响应式变量
├── 按钮数据集          →  keys 数组(19 项,4×5 布局)
├── UI 构建入口         →  build() → Column + @Builder
├── 显示屏              →  buildDisplay()  @Builder
├── Grid 键盘           →  buildKeypad()  @Builder  ★
├── 按钮样式            →  getTextColor() + getButtonColor()
├── 事件分发            →  onKeyPress()
├── 数字输入逻辑        →  onNumberInput()
├── 运算符逻辑          →  onOperatorInput()
├── 功能键逻辑          →  onFunctionInput()
└── 计算引擎            →  calculate()

作者 :鸿蒙原生开发实践

版本 :HarmonyOS NEXT API 24

语言 :ArkTS 5.0

构建工具 :Hvigor

许可 :MIT License

更新日期:2026-06-23

本文为鸿蒙原生 ArkTS 布局系列的第一篇,后续将继续带来 Flex、RelativeContainer、List 等布局组件的实战解析,敬请关注。