【共创季稿事节】鸿蒙原生 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 属性指定跨越的列数:
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 列,视觉上比普通按钮宽一倍。
注意:
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 白色 |
中性辅助 |
代码实现:
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 =,不会丢失中间结果:
- 按下
5→displayText = "5" - 按下
+→previousValue = 5,currentOperator = "+",isNewInput = true - 按下
3→displayText = "3" - 按下
-→ 计算5 + 3 = 8,然后previousValue = 8,currentOperator = "-" - 按下
2→displayText = "2" - 按下
=→ 计算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 布局的核心用法:
- Grid 容器通过
columnsTemplate定义列模板,fr单位让列宽分配灵活直观 - GridItem 子组件自动按行排列,配合
gridSpan支持跨列,适合不规则网格需求 - @Builder 装饰器配合
@State状态管理,实现了 UI 与逻辑的清晰分离 - 数据驱动渲染(
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 等布局组件的实战解析,敬请关注。
更多推荐



所有评论(0)