【共创季稿事节】鸿蒙原生 ArkTS Grid 布局实战:从零构建 Emoji Picker 表情面板
鸿蒙原生 ArkTS Grid 布局实战:从零构建 Emoji Picker 表情面板
一、为什么选择 Grid 布局实现表情面板?
在即时通讯、社交评论、笔记编辑等应用中,表情选择器(Emoji Picker) 几乎是标配功能。用户期望它响应迅速、分类清晰、视觉舒适。而实现这样一个多行多列、支持分类切换和选中高亮的面板,恰好是 Grid 布局 的典型应用场景。
传统的线性布局(Column + Row 嵌套)虽然在简单场景下够用,但当表情数量增长到数百个时,会遇到以下问题:
- 代码冗余 —— 每个表情需手动排列,难以维护;
- 行列不均 —— 表情长度不同可能导致参差不齐;
- 性能损耗 —— 深层嵌套降低渲染效率;
- 扩展困难 —— 增删表情或调整列数需大改布局。
Grid 布局通过 行列模板(rowsTemplate / columnsTemplate) 以声明式方式描述网格结构,配合 GridItem 子组件自动填充,大幅降低布局复杂度。HarmonyOS NEXT 的 Grid 组件更是深度优化了滚动复用和懒加载,能够轻松承载数百个表情节点而不掉帧。
二、HarmonyOS NEXT(API 24)ArkUI 布局概览
在深入代码之前,有必要先了解 HarmonyOS NEXT 在 API 24 版本中 ArkUI 布局体系的一些关键变化:
2.1 全局 API 无需 import
与早期版本不同,API 24 的 ArkUI 将核心布局组件(Column、Row、Grid、GridItem、Scroll、Text、Divider 等)以及装饰器(@Entry、@Component、@State 等)全部设计为 全局内置 API。这意味着开发者无需在文件顶部写任何 import 语句即可直接使用这些组件与装饰器。这不仅简化了代码结构,也降低了新手的学习门槛。
2.2 声明式 UI 与状态响应
ArkTS 采用 声明式 + 响应式 编程范式,其核心思想是:UI 是状态的函数。开发者只需描述"数据长什么样,UI 就长什么样",框架自动追踪状态变化并最小化更新渲染树。
具体来说,通过 @State 装饰器标记的成员变量一旦发生变化,框架便会自动重新执行 build() 方法中依赖该变量的 UI 子树。这比命令式的手动刷新(如 setState 或 notifyDataChange)更高效、更不易出错。
2.3 布局体系分层
HarmonyOS NEXT 的 ArkUI 布局体系可归纳为三层:
| 层级 | 典型组件 | 职责 |
|---|---|---|
| 容器层 | Column, Row, Flex, Stack, Grid | 定义子组件的排列方式 |
| 滚动层 | Scroll, List, Grid(内置滚动) | 处理内容溢出时的滚动交互 |
| 装饰层 | Divider, Blank, Badge | 提供视觉辅助与修饰 |
表情选择器恰好需要这三层的协同配合,是一个绝佳的综合性学习案例。
三、表情面板架构设计
下面是我们即将构建的 Emoji Picker 的整体架构设计,从数据结构到 UI 层级逐一拆解。
3.1 数据模型设计
首先定义表情分类的数据模型。每一个类别包含名称、图标和表情列表:
interface EmojiCategory {
/** 类别名称(如"笑脸""动物""美食") */
name: string;
/** 类别图标(用一个代表性 emoji 字符) */
icon: string;
/** 该类别下所有表情的字符串数组 */
emojis: string[];
}
这种模型极简且可扩展。如果未来需要添加"国旗"或"符号"分类,只需向数组中追加一个新的 EmojiCategory 对象即可,UI 层自动适配,无需修改任何布局代码。
3.2 分类数据概览
本案例内置了 6 个常用分类,覆盖日常生活中最常用的表情:
| 分类 | 图标 | 表情数量 | 典型表情 |
|---|---|---|---|
| 笑脸 | 😊 | 71 个 | 😀😂😍🤩😭 |
| 动物 | 🐱 | 56 个 | 🐶🐱🐼🐨🦊 |
| 美食 | 🍔 | 64 个 | 🍕🍔🍟🌮🍣 |
| 活动 | ⚽ | 64 个 | ⚽🏀🎮🎸🏆 |
| 旅行 | 🌍 | 56 个 | ✈️🏖️🗽🗼🌅 |
| 物品 | 💡 | 64 个 | 📱💻⌚📷💡 |
总计约 375 个表情,涵盖 Unicode Emoji 15.0 的大部分常用字符。每个类别的表情数量并不要求完全一致,Grid 会根据 columnsTemplate 自动换行对齐,这正是网格布局的优势所在。
3.3 UI 层级结构
整个页面的 UI 树以 Column 为根容器,从上到下依次排列五个区域:
Column(全屏根容器)
├── 标题栏 高度 48vp
│ └── Text("😊 表情选择器")
├── 选中预览区 高度 64vp
│ ├── Text(选中的表情) fontSize: 56
│ └── Text("点击上面的表情即可选中")
├── Divider 分割线
├── 分类 Tab 栏 高度 48vp(可横向滚动)
│ └── Scroll → Row → ForEach → Column[icon + name]
├── Divider 分割线
└── 表情网格面板 高度 400vp(可纵向滚动)
└── Scroll → Grid → ForEach → GridItem[Text(emoji)]
每一层的职责清晰、解耦良好,便于单独测试和复用。
四、Grid 核心属性深度解析
Grid 是 ArkUI 中最强大的布局容器之一,理解其核心属性是高效使用它的关键。
4.1 columnsTemplate —— 列模板
columnsTemplate 定义网格的列数及各列的宽度分配策略,其值是一个以空格分隔的字符串:
"1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr"
这里的 fr 是"fraction"(份数)的单位,类似于 CSS Grid 中的 fr 单位。1fr 表示等分剩余空间的一个份额。上面的字符串表示:将 Grid 总宽度均分为 8 等份,每份宽度相等,即 8 列等宽布局。
除了 fr,还支持以下单位:
| 单位 | 含义 | 示例 |
|---|---|---|
fr |
弹性份数,按比例分配剩余空间 | "1fr 2fr" → 第一列占 1/3 |
vp |
虚拟像素(逻辑像素) | "80vp 80vp 80vp" |
px |
物理像素 | "100px auto" |
% |
百分比 | "30% 30% 40%" |
auto |
根据内容自适应 | "auto 1fr" |
4.2 rowsTemplate —— 行模板
rowsTemplate 的语法与 columnsTemplate 完全一致,用于控制行高的分配。例如 "1fr 1fr 1fr" 表示三行等高。
需要注意的是,如果 Grid 不设置固定高度,行数会自动延伸;如果设置了固定高度且行模板使用 fr 单位,则行高会按比例撑满固定高度。在本例中,Grid 高度设为 400vp,因此行高会在此范围内按 1fr 均分。
4.3 rowsGap 与 columnsGap —— 行列间距
这两个属性控制网格项之间的间距:
.rowsGap(4) // 行间距 4vp
.columnsGap(4) // 列间距 4vp
适度的间距避免表情之间挤在一起,提升触控选择时的精准度和视觉舒适度。
4.4 GridItem —— 网格项
每个 GridItem 代表网格中的一个单元格。子组件(如 Text)的宽高默认充满 GridItem。你可以通过 .width() 和 .height() 链式调用精细控制每个格子的大小。
GridItem() {
Text(emoji)
.fontSize(32) // 表情字号
.width('100%') // 撑满格子宽度
.height('100%') // 撑满格子高度
.textAlign(TextAlign.Center)
}
4.5 Scroll + Grid 组合的滚动复用
当 Grid 的内容超出设定高度时,Grid 本身支持纵向滚动,但我们使用了 Scroll 包裹 Grid。这是因为:
- 外层的 Scroll 提供弹簧回弹效果(
edgeEffect(EdgeEffect.Spring)); - Grid 负责布局和子项复用——当 GridItem 滚出视口时,框架会自动回收并复用,保持流畅滚动性能。
五、响应式状态管理
ArkTS 的响应式系统围绕装饰器展开。本案例使用了两个 @State 变量:
5.1 @State selectedEmoji
@State selectedEmoji: string = '😊';
- 作用:记录当前用户选中的表情。
- 关联 UI:预览区的 Text(大号展示)和网格中每个 GridItem 的背景色(高亮判断)。
- 变化触发:当用户点击任一表情时更新,UI 自动刷新两处依赖点。
5.2 @State activeCategoryIndex
@State activeCategoryIndex: number = 0;
- 作用:记录当前激活的分类 Tab 索引。
- 关联 UI:Tab 的选中背景色(半透明蓝 ↔ 透明)和下方 Grid 显示的内容(通过
categories[activeCategoryIndex].emojis切换)。 - 变化触发:用户点击分类 Tab 时更新,Grid 内容整个切换为另一组表情。
5.3 响应式数据流图
用户点击 Tab
↓
activeCategoryIndex 变化
↓
Tab 栏高亮更新 → Grid 切换 emoji 源数组
↓
用户点击表情
↓
selectedEmoji 变化
↓
预览区文字刷新 → 网格中对应项背景高亮
整个过程无需手动调用 any update() 或 refresh() 方法,框架自动追踪依赖并增量更新,这正是声明式 UI 框架的核心价值。
六、完整代码逐段解读
下面逐段剖析 Index.ets 的核心逻辑。
6.1 文件头与数据模型
/**
* EmojiPicker —— 基于 Grid 布局的表情选择器(表情面板)
*
* 【核心技术】Grid + Emoji
* 【布局要点】
* 1. 使用 Grid() 容器实现多行多列的宫格布局
* 2. rowsTemplate / columnsTemplate 控制行列数量与比例
* 3. 结合 Scroll 实现可滚动分类导航
* 4. 通过 @State 响应式数据驱动界面刷新
* 5. 支持按类别切换表情面板
*
* 【关于 import】
* 在 HarmonyOS NEXT(API 24)中,ArkUI 内置组件以及装饰器
* 都是全局 API,无需显式 import。因此本文件顶部无 import 语句。
*/
interface EmojiCategory {
name: string;
icon: string;
emojis: string[];
}
这里没有 import 语句,因为所有 ArkUI 组件在 API 24 中都是全局可用的。EmojiCategory 接口定义了数据结构规范,类型安全地约束后续数据。
6.2 结构体与响应式状态
@Entry
@Component
struct Index {
@State selectedEmoji: string = '😊';
@State activeCategoryIndex: number = 0;
private categories: EmojiCategory[] = [ /* 6 个分类,共约 375 个表情 */ ];
private readonly GRID_COLUMNS: number = 8;
private readonly SELECTED_EMOJI_SIZE: number = 56;
private readonly EMOJI_SIZE: number = 32;
private readonly TAB_HEIGHT: number = 48;
private readonly PANEL_HEIGHT: number = 400;
@Entry标记该组件为应用入口页面@Component声明为可复用自定义组件@State赋予变量状态驱动能力private readonly常量用于集中管理尺寸参数,方便后续视觉调整
6.3 build() —— UI 构建逻辑
标题栏、预览区、分割线的实现直截了当,这里重点看 Tab 栏 和 Grid 表情面板。
Tab 栏:水平可滚动分类导航
Scroll() {
Row() {
ForEach(this.categories, (category: EmojiCategory, index: number) => {
Column() {
Text(category.icon).fontSize(22)
Text(category.name).fontSize(11).margin({ top: 2 })
}
.width(64)
.height(this.TAB_HEIGHT)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.backgroundColor(
this.activeCategoryIndex === index
? '#1A007AFF' // 选中态:半透明蓝背景
: Color.Transparent
)
.borderRadius(8)
.onClick(() => {
this.activeCategoryIndex = index;
})
})
}
.width('100%')
.height(this.TAB_HEIGHT)
.padding({ left: 8, right: 8 })
.alignItems(VerticalAlign.Center)
}
.width('100%')
.height(this.TAB_HEIGHT)
.scrollBar(BarState.Off)
设计要点:
- 外层
Scroll包裹Row,当分类增加(如添加"国旗""符号"等)时,用户可横向滑动查看更多 Tab; - 每个 Tab 是
Column(图标 + 文字垂直排列),宽度固定 64vp,确保触控 Hit 面积足够大; backgroundColor通过三元表达式根据activeCategoryIndex动态切换,实现选中高亮效果;- 隐藏 ScrollBar 让界面更整洁。
Grid 表情面板:多行多列可滚动网格
Scroll() {
Grid() {
ForEach(
this.categories[this.activeCategoryIndex].emojis,
(emoji: string) => {
GridItem() {
Text(emoji)
.fontSize(this.EMOJI_SIZE)
.width('100%')
.height('100%')
.textAlign(TextAlign.Center)
.backgroundColor(
this.selectedEmoji === emoji
? '#33007AFF'
: Color.Transparent
)
.borderRadius(6)
.onClick(() => {
this.selectedEmoji = emoji;
})
}
}
)
}
.columnsTemplate("1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr")
.rowsTemplate("1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr")
.rowsGap(4)
.columnsGap(4)
.width('100%')
.height(this.PANEL_HEIGHT)
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
}
.width('100%')
.height(this.PANEL_HEIGHT)
.scrollBar(BarState.Auto)
.edgeEffect(EdgeEffect.Spring)
设计要点:
ForEach遍历当前分类的emojis数组,动态生成GridItem,每一个 GridItem 内部是一个 Text 组件显示单个 emoji 字符;columnsTemplate("1fr x 8")声明 8 列等宽(实际代码中用generateRepeatTemplate方法动态拼接);- 每个表情点击后更新
selectedEmoji,同个表情的 background-color 立即变为浅蓝色高亮,实现了选中状态的即时视觉反馈; edgeEffect(EdgeEffect.Spring)在滚动到边界时产生弹性回弹效果,提升操作手感的细腻度。
6.4 辅助方法
private generateRepeatTemplate(count: number, unit: string): string {
const parts: string[] = [];
for (let i = 0; i < count; i++) {
parts.push(unit);
}
return parts.join(' ');
}
这个辅助方法将 count 和 unit 参数拼接成 Grid 所需的 template 字符串。例如 generateRepeatTemplate(8, '1fr') 返回 "1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr"。如果后续想从 8 列改为 10 列,只需修改 GRID_COLUMNS 常量即可。
七、API 24 的新特性应用
本案例代码也可以在 API 24(HarmonyOS NEXT 7.0.0)下编译运行。与 API 23 相比,API 24 带来了一些值得关注的改进:
7.1 全局组件注册
在 API 24 中,ArkUI 的所有布局组件和装饰器都预先注册在全局命名空间中,无需手动 import。这不仅减少了文件顶部的样板代码,还避免了因 import 遗漏导致的编译错误。
7.2 更强的类型推断
ArkTS 编译器在 API 24 中增强了类型推断能力。例如 ForEach 中回调参数的类型可以自动从数组类型推断:
ForEach(this.categories, (category, index) => {
// category 自动推断为 EmojiCategory,无需手动标注
})
这使得代码更简洁且类型安全。
7.3 更完善的属性链式调用
API 24 对所有链式调用的返回类型进行了精细化处理,使得 IDE 的代码补全更准确,错误提示更精确。例如 Text().fontSize().fontColor() 每个步骤都能精确感知当前上下文。
八、最佳实践与性能优化
在实际工程中构建表情选择器时,以下实践经验可以帮助你写出更高质量的应用:
8.1 数据与视图分离
将表情数据定义在单独的文件(如 EmojiData.ts)中,与 Index.ets 的 UI 逻辑解耦。这样不仅方便多人协作,也便于未来从远程配置中心动态拉取表情包。
8.2 使用 LazyForEach 处理超长列表
如果单分类的表情数量超过 200 个(例如完整 Unicode Emoji 库超过 1,800 个),建议改用 LazyForEach 替代 ForEach。LazyForEach 只在项目即将进入视口时才渲染对应的 GridItem,大幅降低首屏加载时间。
8.3 适当使用 cachedCount
对于 Grid 内频繁切换分类的场景,可以通过 cachedCount 属性保留当前分类在视口外的部分 GridItem 缓存,切换回来时无需重新创建:
Grid()
.cachedCount(4) // 缓存上下各 4 行的 GridItem
8.4 考虑滑动方向设置
由于表情面板以纵向滑动为主,可以显式设置 Grid 的滑动方向,帮助框架做进一步的性能优化:
Scroll() {
Grid()
.scrollBar(BarState.Auto)
}
.edgeEffect(EdgeEffect.Spring)
8.5 字体图标 vs emoji 字符
本例直接使用 Unicode emoji 字符(如 '😊'),优点是简单直接,无需额外资源文件。但在跨平台场景下,不同设备的 emoji 渲染样式可能不一致。如果追求品牌统一性,可以考虑使用 自定义字体图标 方式,将设计团队设计的表情以字体文件形式嵌入应用。
九、扩展与改进方向
一个完整的产品级表情选择器往往还需要以下增强:
9.1 搜索功能
增加顶部搜索栏,允许用户输入关键词(如"笑"“动物”)快速过滤表情。可以搭配自定义 Pipe(过滤器)或使用 Array.filter() 实现。
9.2 最近使用
记录用户频繁使用的表情,在分类栏的最左侧增加"最近使用"分类,将该数据持久化到本地 Preferences 或数据库。
9.3 表情肤色选择
对于支持多种肤色的表情(如挥手 👋、握手 🤝),长按弹出肤色选择面板(Fitzpatrick 肤色量表)。这需要额外维护一个"基础 emoji → 肤色变体"的映射数据结构。
9.4 与 TextInput 联动
在实际聊天应用中,点击表情后应将选中的 emoji 插入到 TextInput 或 TextArea 的当前光标位置。这需要借助 TextInputController 和 CaretOffset API 实现。
9.5 暗色模式适配
通过 @Styles 和自定义主题变量,为表情面板提供两种配色方案,响应系统的深色/浅色模式切换:
@Styles backgroundColor() {
.backgroundColor(this.isDarkMode ? '#FF1C1C1E' : '#FFF5F5F5')
}
十、小结
通过本教程,我们以 Emoji Picker 表情选择器 为例,完整实践了 HarmonyOS NEXT(API 24)中 ArkTS Grid 布局的核心用法:
- Grid + GridItem 多行多列布局的声明式写法;
- Scroll + Grid 组合 实现可滚动的密集网格面板;
- @State 响应式变量 驱动 UI 状态变化;
- ForEach 动态遍历 生成网格列表项;
- Tab 分类切换 与表情选中高亮的交互设计;
- 合理的 常量管理 和 辅助函数 提升代码可维护性。
Grid 布局不仅仅是"九宫格"那么简单,它在表情选择器、商品展示、图片画廊、图标面板等场景中都有着广泛的应用。掌握 Grid 的 columnsTemplate、rowsTemplate 和 GridItem 的配合使用,能够让你在面对各类宫格类 UI 需求时更得心应手。
希望这篇博客能帮助你在 HarmonyOS 原生开发的路上更进一步,用 Grid 布局创作出更多优秀的应用体验。
附录:完整源码
完整的项目源码位于 entry/src/main/ets/pages/Index.ets,总代码量 369 行,涵盖 6 个分类、约 375 个表情。可直接在 DevEco Studio 中打开并运行。
技术栈:HarmonyOS NEXT · ArkTS · ArkUI · Grid · Scroll
开发工具:DevEco Studio 5.0+
最低兼容:API 24(HarmonyOS NEXT 7.0.0)
更多推荐




所有评论(0)