鸿蒙原生 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 时具有天然优势:

  • 行列精确可控:通过 columnsTemplaterowsTemplate 定义网格模板
  • 自动换行填充:子元素按顺序自动填入网格单元格
  • 跨行跨列支持:子项可通过 gridSpan 属性跨越多个单元格
  • 间距自由设置columnsGaprowsGap 灵活控制行列间距
  • 性能优秀:配合 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 属性指定跨越的列数:

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 定义数据模型

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

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

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

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

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

4.2 声明状态变量

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

@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列)   .    =

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

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 垂直布局作为外壳,将页面分为上下两个区域:

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 布局的完整实现:

@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 列,视觉上比普通按钮宽一倍。

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

columnsGap / rowsGap

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

ForEach 数据驱动渲染

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

layoutWeight 分配垂直空间

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

4.6 按钮颜色与视觉设计

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

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

代码实现:

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';
  }
}

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

.shadow({ radius: 4, color: '#0A000000', offsetX: 0, offsetY: 2 })

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


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

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

5.1 事件分发

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

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 数字输入处理

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 运算符处理与连续运算

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. 按下 5displayText = "5"
  2. 按下 +previousValue = 5, currentOperator = "+", isNewInput = true
  3. 按下 3displayText = "3"
  4. 按下 - → 计算 5 + 3 = 8,然后 previousValue = 8, currentOperator = "-"
  5. 按下 2displayText = "2"
  6. 按下 = → 计算 8 - 2 = 6

5.4 计算结果

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 四则运算引擎

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 构建系统对项目进行编译验证:

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 等布局组件的实战解析,敬请关注。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐