HarmonyOS应用<民族图鉴>开发第16篇:首页快捷入口——功能网格布局深度解析

📖 引言
打开支付宝首页,最醒目的是什么?——是那一排又一排的功能入口:扫一扫、付款、出行、电影票、充值中心…… 九宫格、十二宫格,密密麻麻的图标,每个都是一个功能入口。
为什么各大 App 都这么喜欢"功能入口网格"?因为它有几个不可替代的优势:
- 信息密度高:一屏能放很多功能入口,用户一目了然
- 查找效率高:图标 + 文字,扫一眼就能找到想要的
- 拓展性好:想加新功能?往里塞一个图标就行
- 用户习惯:大家都这么做,用户一看就知道"点这里进功能"
「民族图鉴」的首页也有一个快捷入口区域,一共 6 个入口,分两行三列:
- 🧠 灵魂测试 → 跳转到测验页
- 🏛️ 民族图鉴 → 跳转到个人页的图鉴入口
- 🌍 民族分布 → 跳转到地图页
- 🎵 民族音乐 → 音乐馆页面
- 🤖 AI助手 → AI 问答页面
- 🖼️ 图片工具 → 图片工具箱
每个入口都是一个图标 + 一行文字的卡片,整齐排列在网格里。
这一篇,我们就从这个快捷入口网格入手,深入讲解 Grid 网格布局的使用、图标按钮的设计、以及功能入口的组织方式。Grid 是一个非常强大的布局组件,掌握了它,你就能轻松搞定各种宫格布局、表格布局、不规则网格。
🎯 学习目标
完成本文后,你将能够:
- ✅ 掌握 Grid 网格布局的核心概念与使用方法
- ✅ 理解 rowsTemplate / columnsTemplate 的用法
- ✅ 学会 GridItem 的布局控制(rowStart/columnStart)
- ✅ 掌握图标按钮的设计规范
- ✅ 理解 Grid 和 Row/Column 嵌套的区别与选择
- ✅ 了解响应式网格的实现(不同屏幕不同列数)
- ✅ 写出整齐、美观、可扩展的功能入口网格
💡 需求分析
功能入口网格的核心需求
| 需求点 | 说明 | 为什么重要 |
|---|---|---|
| 整齐排列 | 所有入口大小一致,行列对齐 | 视觉整齐,专业感强 |
| 图标+文字 | 每个入口有图标有文字 | 直观好认,记忆成本低 |
| 点击跳转 | 点击进入对应功能页面 | 最基本的交互 |
| 可扩展 | 加新功能不用大改布局 | 产品迭代快,经常加功能 |
| 视觉层级 | 重要的入口更醒目 | 引导用户使用核心功能 |
| 多端适配 | 不同屏幕大小都好看 | 手机、平板、折叠屏都要适配 |
「民族图鉴」快捷入口设计
🎯 快捷入口
┌──────────┬──────────┬──────────┐
│ 🧠 │ 🏛️ │ 🌍 │
│ 灵魂测试 │ 民族图鉴 │ 民族分布 │
├──────────┼──────────┼──────────┤
│ 🎵 │ 🤖 │ 🖼️ │
│ 民族音乐 │ AI助手 │ 图片工具 │
└──────────┴──────────┴──────────┘
3列 × 2行,共6个入口
设计细节:
| 设计要素 | 值 | 说明 |
|---|---|---|
| 列数 | 3 列 | 手机上 3 列最合适 |
| 行数 | 2 行 | 6个入口,2行就够了 |
| 入口卡片 | 96vp 高 | 图标 + 文字,高度适中 |
| 图标大小 | 44vp | 圆形图标,手指好点 |
| 图标颜色 | 6种不同颜色 | 每个功能一种颜色,辨识度高 |
| 文字大小 | 12fp | 入口名称,小而清晰 |
| 横向间距 | 12vp | 卡片之间的距离 |
| 纵向间距 | 12vp | 行之间的距离 |
| 卡片圆角 | 16vp | 大圆角,现代感 |
网格布局的几种实现方式
在鸿蒙里,做网格布局有好几种方式,该选哪个?
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Row + Column 嵌套 | 简单直观,容易理解 | 写起来啰嗦,不方便动态控制 | 固定的少量入口(2-3行) |
| Grid 组件 | 专业网格,功能强大,性能好 | 概念稍多,需要学习 | 大量网格、不规则网格、动态数据 |
| Flex 布局 | 灵活,自动换行 | 对齐不如 Grid 精准 | 瀑布流、标签云等不规则布局 |
「民族图鉴」的快捷入口只有 6 个,Row + Column 也能做,但我们还是用 Grid——因为 Grid 更专业、扩展性更好,以后加新功能直接加数据就行,不用改布局结构。
🛠️ 核心实现
步骤1:Grid 组件基础
Grid 是专门用来做网格布局的容器组件。它的核心概念很简单:
- 行(Row):横向的一行
- 列(Column):纵向的一列
- 单元格(Cell):行和列交叉的格子
1.1 最简单的 Grid
Grid() {
GridItem() { Text('1').textAlign(TextAlign.Center) }
GridItem() { Text('2').textAlign(TextAlign.Center) }
GridItem() { Text('3').textAlign(TextAlign.Center) }
GridItem() { Text('4').textAlign(TextAlign.Center) }
GridItem() { Text('5').textAlign(TextAlign.Center) }
GridItem() { Text('6').textAlign(TextAlign.Center) }
}
.columnsTemplate('1fr 1fr 1fr') // 3 列,每列等宽
.rowsTemplate('1fr 1fr') // 2 行,每行等高
.rowsGap(12) // 行间距
.columnsGap(12) // 列间距
.width('100%')
.height(200)
关键属性:
| 属性 | 类型 | 说明 |
|---|---|---|
columnsTemplate |
string |
列模板,定义每列的宽度 |
rowsTemplate |
string |
行模板,定义每行的高度 |
columnsGap |
number |
列之间的间距 |
rowsGap |
number |
行之间的间距 |
onClick |
(event: GridItemClickEvent) => void |
点击事件 |
1.2 模板语法:fr 是什么?
columnsTemplate('1fr 1fr 1fr') 里的 fr 是什么意思?
fr 是 fraction(分数/比例)的意思。1fr 1fr 1fr 就是"三等分,每列占 1 份"。
┌─────────┬─────────┬─────────┐
│ 1fr │ 1fr │ 1fr │ = 3 列,等宽
└─────────┴─────────┴─────────┘
再比如 2fr 1fr 1fr 就是"第一列占 2 份,第二第三列各占 1 份",第一列是其他列的两倍宽。
┌─────────────┬─────────┬─────────┐
│ 2fr │ 1fr │ 1fr │ = 3 列,第一列两倍宽
└─────────────┴─────────┴─────────┘
也可以混合固定值和 fr:
.columnsTemplate('100vp 1fr 2fr')
// 第一列固定 100vp,剩下的空间分成 3 份,第二列 1 份,第三列 2 份
💡 fr 和 Flex 的 layoutWeight 很像。Flex 布局里的
layoutWeight(1)就是占剩余空间的比例,Grid 里的1fr也是占剩余空间的比例。原理是一样的,只是写法不同。
1.3 「民族图鉴」快捷入口的 Grid 实现
// pages/Index.ets
@Builder
buildQuickEntryGrid(): void {
Column({ space: $r('app.float.spacing_md') }) {
// ---- 标题 ----
Row() {
Text('\u{1F3AF}') // 🎯 靶心
.fontSize($r('app.float.icon_size_md'))
Text($r('app.string.home_quick_access')) // "快捷入口"
.fontSize($r('app.float.font_size_lg'))
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.text_primary'))
}
.width('100%')
// ---- 入口网格 ----
Grid() {
GridItem() {
this.buildQuickEntryItem(
'\u{1F9E0}', // 🧠
this.getLocalizedText('灵魂测试', 'Soul Test'),
$r('app.color.primary_color'),
() => { this.currentIndex = 3; } // 跳转到测验Tab
)
}
GridItem() {
this.buildQuickEntryItem(
'\u{1F3DB}\u{FE0F}', // 🏛️
this.getLocalizedText('民族图鉴', 'Collection'),
$r('app.color.legendary_color'),
() => { this.currentIndex = 4; } // 跳转到我的Tab
)
}
GridItem() {
this.buildQuickEntryItem(
'\u{1F30D}', // 🌍
this.getLocalizedText('民族分布', 'Distribution'),
$r('app.color.accent_color'),
() => { this.currentIndex = 2; } // 跳转到地图Tab
)
}
GridItem() {
this.buildQuickEntryItem(
'\u{1F3B5}', // 🎵
this.getLocalizedText('民族音乐', 'Music'),
'#E74C3C',
() => { router.pushUrl({ url: 'pages/MusicPage' }); }
)
}
GridItem() {
this.buildQuickEntryItem(
'\u{1F916}', // 🤖
this.getLocalizedText('AI助手', 'AI Helper'),
'#9B59B6',
() => { router.pushUrl({ url: 'pages/AIChatPage' }); }
)
}
GridItem() {
this.buildQuickEntryItem(
'\u{1F5BC}\u{FE0F}', // 🖼️
this.getLocalizedText('图片工具', 'Image Tools'),
'#1ABC9C',
() => { router.pushUrl({ url: 'pages/ImageToolPage' }); }
)
}
}
.columnsTemplate('1fr 1fr 1fr') // 3 列,等宽
.rowsGap($r('app.float.spacing_sm'))
.columnsGap($r('app.float.spacing_sm'))
.width('100%')
}
.width('90%')
}
用 Grid 的好处是:如果以后要加第 7 个、第 8 个入口,只要多加几个 GridItem 就行了,Grid 会自动排列到第三行、第四行。布局代码完全不用改。如果用 Row + Column,加一行就要多套一层 Row,麻烦多了。
步骤2:入口卡片设计——图标 + 文字
每个功能入口都是一个"圆形图标 + 文字标签"的卡片,结构很简单,但要做得好看也有讲究。
2.1 入口卡片实现
// pages/Index.ets
/**
* 构建单个快捷入口项
* @param icon 图标(emoji)
* @param label 标签文字
* @param iconColor 图标背景色
* @param onClickAction 点击回调
*/
@Builder
buildQuickEntryItem(
icon: string,
label: string,
iconColor: ResourceStr,
onClickAction: () => void
): void {
Column({ space: 8 }) {
// ---- 图标 ----
Text(icon)
.fontSize(24)
.fontColor('#FFFFFF')
.width(44)
.height(44)
.borderRadius(22)
.backgroundColor(iconColor)
.textAlign(TextAlign.Center)
// ---- 文字标签 ----
Text(label)
.fontSize($r('app.float.font_size_xs'))
.fontColor($r('app.color.text_primary'))
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.layoutWeight(1)
.height(96)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.backgroundColor($r('app.color.card_background'))
.borderRadius($r('app.float.radius_lg'))
.border({ width: 1, color: $r('app.color.border_color') })
.onClick(onClickAction)
}
卡片结构:
Column(整个入口卡片,高度96vp)
├── Text(圆形图标,44vp圆,emoji图标)
└── Text(文字标签,12fp,最多1行)
设计要点:
- 圆形图标:44vp 的正圆,刚好是最小触控目标的尺寸,点起来舒服
- 彩色背景:每个入口一个颜色,辨识度高,视觉也丰富
- 文字在下面:图标在上,文字在下,符合用户的认知习惯
- 卡片化:整个入口是一个白色圆角卡片,有边框,有点击区域
- 居中对齐:图标和文字都居中,整齐好看
2.2 颜色搭配的艺术
6 个入口 6 种颜色,怎么选颜色?有几个原则:
| 原则 | 说明 | 例子 |
|---|---|---|
| 功能语义 | 颜色要和功能的"感觉"匹配 | 音乐用红色(热情)、AI用紫色(科技感) |
| 饱和度适中 | 不要太艳也不要太灰 | 主色调是调整后的,不是纯红纯蓝 |
| 明度接近 | 所有颜色的亮度差不多 | 不会有的特别亮有的特别暗 |
| 数量控制 | 不要太多颜色,5-6 种够了 | 太多了显得乱 |
「民族图鉴」的 6 个入口颜色:
| 功能 | 颜色 | 色值 | 感觉 |
|---|---|---|---|
| 灵魂测试 | 主色红 | #E74C3C | 热情、活力 |
| 民族图鉴 | 金色 | #F39C12 | 珍贵、收藏 |
| 民族分布 | 绿色 | #27AE60 | 地理、自然 |
| 民族音乐 | 橙色红 | #E74C3C | 音乐、热情 |
| AI助手 | 紫色 | #9B59B6 | 科技、神秘 |
| 图片工具 | 青色 | #1ABC9C | 创意、艺术 |
💡 颜色搭配小技巧:
- 去 Dribbble、Behance 上看优秀设计,直接"借鉴"它们的配色
- 用 Adobe Color、Coolors 等配色工具生成和谐的配色方案
- 记住几个经典配色:马卡龙色、莫兰迪色、北欧风
- 同一个 App 里的颜色,饱和度和明度要统一,不然会像拼凑的
步骤3:动态数据——用 ForEach 渲染网格
如果入口是从数据来的(而不是写死的),用 ForEach 更方便。以后加新入口,只要往数组里加一条数据就行。
3.1 数据模型
// models/QuickEntry.ts
export interface QuickEntryItem {
id: string; // 唯一标识
icon: string; // 图标(emoji 或 图片)
labelZh: string; // 中文标签
labelEn: string; // 英文标签
color: string; // 图标背景色
targetType: 'tab' | 'page'; // 跳转类型:切Tab / 跳页面
targetValue: string; // 跳转目标:tab索引 或 页面url
sortOrder: number; // 排序
}
3.2 数据配置
// constants/QuickEntryConfig.ts
import { QuickEntryItem } from '../models/QuickEntry';
export const QUICK_ENTRIES: QuickEntryItem[] = [
{
id: 'quiz',
icon: '\u{1F9E0}',
labelZh: '灵魂测试',
labelEn: 'Soul Test',
color: '#E74C3C',
targetType: 'tab',
targetValue: '3',
sortOrder: 1
},
{
id: 'collection',
icon: '\u{1F3DB}\u{FE0F}',
labelZh: '民族图鉴',
labelEn: 'Collection',
color: '#F39C12',
targetType: 'tab',
targetValue: '4',
sortOrder: 2
},
// ... 更多
];
3.3 用 ForEach 渲染
Grid() {
ForEach(this.quickEntries, (entry: QuickEntryItem) => {
GridItem() {
QuickEntryCard({ entry: entry })
}
}, (entry: QuickEntryItem) => entry.id)
}
.columnsTemplate('1fr 1fr 1fr')
.rowsGap(12)
.columnsGap(12)
.width('100%')
这样布局代码就很简洁了——只管布局,不管具体有多少个入口。入口数据在配置文件里管理,职责分离。
💡 数据和视图分离的好处:
- 好维护:改入口名称、颜色、排序,去数据文件改,不用动布局代码
- 好扩展:加新入口,往数组里加一条就行
- 好测试:数据可以单独测试
- 好复用:同样的数据可以在不同地方展示
步骤4:Grid 进阶——不规则网格
Grid 最强大的地方不是整齐的网格,而是——可以做不规则网格。一个单元格可以跨多行、跨多列。
4.1 跨列(columnStart / columnEnd)
Grid() {
// 第一个占 2 列
GridItem() {
Text('大卡片')
.width('100%')
.height('100%')
.backgroundColor('#FF6B6B')
}
.columnStart(1) // 从第1列开始
.columnEnd(2) // 到第2列结束(占2列)
// 后面的正常排
GridItem() { Text('2').backgroundColor('#4ECDC4') }
GridItem() { Text('3').backgroundColor('#45B7D1') }
GridItem() { Text('4').backgroundColor('#96CEB4') }
GridItem() { Text('5').backgroundColor('#FFEAA7') }
}
.columnsTemplate('1fr 1fr 1fr')
.rowsTemplate('1fr 1fr')
效果:
┌─────────────────────┬──────────┐
│ 大卡片 │ 2 │
├──────────┬──────────┼──────────┤
│ 3 │ 4 │ 5 │
└──────────┴──────────┴──────────┘
4.2 跨行(rowStart / rowEnd)
Grid() {
// 第一个占 2 行
GridItem() {
Text('侧边栏')
.width('100%')
.height('100%')
.backgroundColor('#FF6B6B')
}
.rowStart(1)
.rowEnd(2)
// 右边正常排
GridItem() { Text('2').backgroundColor('#4ECDC4') }
GridItem() { Text('3').backgroundColor('#45B7D1') }
GridItem() { Text('4').backgroundColor('#96CEB4') }
}
.columnsTemplate('1fr 1fr 1fr')
.rowsTemplate('1fr 1fr')
效果:
┌──────────┬──────────┬──────────┐
│ │ 2 │ 3 │
│ 侧边栏 ├──────────┼──────────┤
│ │ 4 │ │
└──────────┴──────────┴──────────┘
4.3 实战案例:首页 Banner 布局
比如首页顶部有个大 Banner,旁边两个小入口:
Grid() {
// 大 Banner,占 2 列 2 行
GridItem() {
Image('banner.jpg')
.width('100%')
.height('100%')
.objectFit(ImageFit.Cover)
}
.columnStart(1)
.columnEnd(2)
.rowStart(1)
.rowEnd(2)
// 右上角小卡片
GridItem() {
SmallCard({ title: '活动1' })
}
.columnStart(3)
.rowStart(1)
// 右下角小卡片
GridItem() {
SmallCard({ title: '活动2' })
}
.columnStart(3)
.rowStart(2)
}
.columnsTemplate('1fr 1fr 1fr')
.rowsTemplate('1fr 1fr')
.height(200)
效果:
┌─────────────────────┬──────────┐
│ │ 活动1 │
│ Banner 大图 ├──────────┤
│ │ 活动2 │
└─────────────────────┴──────────┘
这就是 Grid 比 Row/Column 强大的地方——不规则布局用 Grid 特别方便,用 Row/Column 嵌套的话,得写好多层,还容易乱。
步骤5:响应式网格——不同屏幕不同列数
手机上 3 列合适,但平板呢?平板屏幕宽,3 列的话每个卡片太大了,5-6 列才合适。折叠屏展开了更宽,可能要 8 列。
这就是响应式布局——根据屏幕宽度,动态调整列数。
5.1 实现思路
- 获取屏幕宽度
- 根据宽度判断当前是什么设备(手机/平板/大屏)
- 动态设置 columnsTemplate 的列数
5.2 代码实现
@Entry
@Component
struct QuickEntryGrid {
@State columns: number = 3; // 默认 3 列(手机)
aboutToAppear(): void {
this.updateColumns();
}
onPageShow(): void {
this.updateColumns(); // 页面显示时也检查一下(折叠屏展开/折叠)
}
private updateColumns(): void {
const screenWidth = display.getWindowProperties().windowRect.width;
if (screenWidth > 840) {
this.columns = 6; // 大屏/折叠屏展开:6列
} else if (screenWidth > 600) {
this.columns = 4; // 平板:4列
} else {
this.columns = 3; // 手机:3列
}
}
// 生成 columnsTemplate 字符串
private getColumnsTemplate(): string {
return Array(this.columns).fill('1fr').join(' ');
// 比如 3 列:'1fr 1fr 1fr'
// 比如 5 列:'1fr 1fr 1fr 1fr 1fr'
}
build() {
Grid() {
ForEach(this.entries, (entry) => {
GridItem() {
QuickEntryCard({ entry: entry })
}
}, e => e.id)
}
.columnsTemplate(this.getColumnsTemplate()) // 动态列数
.rowsGap(12)
.columnsGap(12)
.width('100%')
}
}
断点设计(Breakpoint):
| 屏幕宽度 | 列数 | 设备类型 |
|---|---|---|
| < 600vp | 3 列 | 手机(竖屏) |
| 600-840vp | 4 列 | 小平板 / 手机横屏 |
| > 840vp | 6 列 | 大平板 / 折叠屏展开 |
这样在任何尺寸的屏幕上,入口网格都能以合适的列数显示,不会太挤也不会太松。
💡 响应式设计的核心思想:不是为每个设备单独做一套界面,而是用一套代码,根据屏幕尺寸自动调整布局。Grid + 动态列数是实现响应式网格最简单有效的方式。
⚠️ 常见问题与解决方案
问题1:Grid 里的内容大小不一致,对不齐
现象:
Grid 里每个 GridItem 的内容高度不一样,有的高有的矮,网格看起来歪歪扭扭的。
原因:
GridItem 默认是"内容撑大"的,内容高的格子就高,内容矮的格子就矮。
解决方案:
设置 GridItem 的高度为 100%,让它填满单元格
GridItem() {
Column() {
// 内容
}
.width('100%')
.height('100%') // 填满整个 GridItem
}
或者直接设置 Grid 的行高为固定值或 1fr:
Grid() {
// ...
}
.rowsTemplate('1fr 1fr') // 每行等高,各占一半
1fr 的行就是"所有行平分高度",每行一样高,GridItem 默认会填满行高,自然就对齐了。
问题2:Grid 的点击事件怎么获取点击了哪个
现象:
想给整个 Grid 加一个点击事件,知道用户点了第几项,但是不知道怎么获取索引。
解决方案:
方案1:给每个 GridItem 单独加 onClick(推荐)
ForEach(this.items, (item: Item, index: number) => {
GridItem() {
ItemCard({ item: item })
}
.onClick(() => {
console.info('点击了第 ' + index + ' 个');
this.handleItemClick(item);
})
}, item => item.id)
每个 GridItem 自己处理自己的点击事件,最直观,最灵活。
方案2:用 Grid 的 onItemClick
Grid() {
ForEach(this.items, (item) => {
GridItem() { ItemCard({ item: item }) }
}, item => item.id)
}
.onItemClick((event: GridItemClickEvent) => {
const rowIndex = event.rowIndex; // 行索引
const columnIndex = event.columnIndex; // 列索引
console.info(`点击了第${rowIndex}行,第${columnIndex}列`);
})
onItemClick 可以拿到行号和列号,适合做一些全局的处理。
问题3:Grid 嵌套 Scroll,滚动方向冲突
现象:
Grid 放在一个纵向滚动的页面里,Grid 内容多了想让它纵向滚,但是页面也在纵向滚,冲突了。
解决方案:
大多数情况下,Grid 不要滚动,让外面的 Scroll 滚动
Grid 本身是布局容器,不是滚动容器。如果内容很多,用 Scroll 包裹 Grid:
Scroll() {
Grid() {
// 很多很多 GridItem
}
.columnsTemplate('1fr 1fr 1fr')
.width('100%')
}
.scrollable(ScrollDirection.Vertical)
外面的 Scroll 负责滚动,Grid 负责布局,各司其职。
Grid 默认是不滚动的,它只是一个布局容器。如果你想让 Grid 本身滚动,需要设置
scrollBar和相关属性,但一般不推荐——用 Scroll 包裹更灵活,也更符合组件职责分离的原则。
问题4:入口图标太小,点不到
现象:
快捷入口的图标太小(比如 24vp),文字也小,很难点中,经常误触。
解决方案:
保证触控区域至少 44×44vp
这是移动端设计的黄金法则——所有可点击元素,触控目标至少 44×44vp(dp/pt)。
我们的设计是:
- 圆形图标:44vp(刚好满足最小触控目标)
- 整个卡片:宽度自适应,高度 96vp(更大,更好点)
整个卡片都是可点击的,不只是图标那一点。用户点图标周围的空白区域,也能触发点击。这样就好点多了。
// 整个 Column 都加 onClick,不是只给 Text 加
Column() {
Text(icon) // 图标只是视觉上的
.width(44)
.height(44)
Text(label)
}
.height(96) // 整个卡片高度 96
.width('100%') // 宽度充满
.onClick(() => { // 整个卡片可点击
// ...
})
记住:用户看到的是图标,但点击的是整个卡片。视觉上可以小,但点击区域一定要够大。这是 UI 和交互的区别——UI 管好看,交互管好⽤。
问题5:功能太多,一屏放不下
现象:
功能入口越来越多,从 6 个加到 12 个、20 个,一屏放不下了。
解决方案:
方案1:分页 + "更多"按钮(推荐)
第一页放最常用的 8-10 个,最后放一个"更多"按钮,点进去看全部。
// 前 5 个显示在首页
const displayEntries = allEntries.slice(0, 5);
// 第 6 个是"更多"
const moreEntry = {
icon: '···',
label: '更多',
onClick: () => { router.pushUrl({ url: 'pages/AllEntriesPage' }); }
};
好处:首页不拥挤,核心功能一眼能看到;更多功能藏在二级页面,想用的人自己找。
方案2:横向滚动的网格
做成可以左右滑动的网格,一行 4 个,一共 3 行,可以翻页。
Scroll() {
Grid() {
// 很多入口
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsTemplate('1fr 1fr 1fr')
.width('200%') // 宽度设大,让它能横滑
}
.scrollable(ScrollDirection.Horizontal)
好处:可以放很多入口,又不占纵向空间;用户左右滑就能看更多。
方案3:分类 Tab
入口太多就分类——“常用”、“生活服务”、“金融理财”、“交通出行”…… 上面一排 Tab 切换分类。
支付宝就是这么做的——首页一排分类 Tab,每个分类下面是对应的功能入口。
💡 功能入口的"二八定律":
80% 的用户只会用 20% 的功能。所以把最核心的 20% 放在最显眼的位置,剩下的 80% 藏起来,想用的人能找到就行。不要把所有功能都堆在首页,那样看起来很杂乱,核心功能反而被淹没了。
🧠 进阶拓展:宫格导航的深度设计
6.1 宫格导航的设计原则
宫格导航(也叫"九宫格"、“快捷入口”)是移动端最常见的导航形式之一。但要做好并不简单——不是把图标往格子里一放就行。
6.1.1 数量原则:多少个入口最合适?
| 数量 | 排列方式 | 适用场景 | 优缺点 |
|---|---|---|---|
| 4个 | 1×4 或 2×2 | 核心功能很少,极简产品 | ✅ 每个都很醒目 ❌ 太少了,不够用 |
| 6个 | 2×3 | 中等功能数量 | ✅ 刚好一屏,不挤不松 ❌ 一般不太够 |
| 8个 | 2×4 或 4×2 | 功能较多 | ✅ 数量适中,大部分产品够用 ❌ 两排,需要稍微往下看 |
| 9个 | 3×3 | 功能很多 | ✅ 经典九宫格,信息密度高 ❌ 太多了,找起来费劲 |
| 12个 | 3×4 | 超级APP | ✅ 功能多 ❌ 太拥挤,视觉负担重 |
「民族图鉴」的选择:8个(2行×4列)
为什么是8个?因为我们有8个主要功能模块:
- 🧠 灵魂测试
- 🏛️ 民族图鉴
- 🌍 民族分布
- 🎵 民族音乐
- 🤖 AI助手
- 🖼️ 图片工具
- 📚 文化学堂
- 🎮 知识问答
8个不多不少,刚好两排,用户一眼能扫完,又能覆盖主要功能。
💡 二八定律在宫格里的应用:
20% 的功能承担了 80% 的使用量。把最常用的 2-3 个功能放在最显眼的位置(通常是第一行前几个),其他功能按重要程度排列。不要平均分配注意力。
6.1.2 排列原则:顺序怎么定?
入口的排列顺序不是随便排的,有讲究:
1. 使用频率排序(最常用)
- 用得越多的,放得越靠前
- 符合用户习惯,找起来快
2. 用户认知排序(心智模型)
- 按用户对功能的认知分类排列
- 相关的功能放在一起
3. 产品目标排序(运营导向)
- 想推什么功能,就放前面
- 新功能、重要功能放显眼位置
「民族图鉴」的排序是混合策略:
- 灵魂测试放第一个——因为这是我们的特色功能,想让用户第一眼就看到
- 民族图鉴放第二个——这是核心功能,用户最常用
- 然后按功能类型排列:工具类(分布、音乐)、互动类(AI、图片)、学习类(学堂、问答)
6.1.3 文字规范:入口名称怎么起?
入口下面的文字标签,看起来简单,其实很有学问:
| 原则 | 说明 | 反例 |
|---|---|---|
| 字数统一 | 最好都是 2-4 个字,长短一致 | 一个叫"测试",一个叫"民族文化知识问答" |
| 语义清晰 | 一看就知道是什么功能 | “探索”、“发现”——啥意思? |
| 词性统一 | 都是名词,或都是动词 | 有的叫"音乐"(名词),有的叫"去测试"(动词) |
| 避免歧义 | 不要有多种理解 | “工具”——什么工具? |
「民族图鉴」的入口名称:
- 都是4个字:灵魂测试、民族图鉴、民族分布、民族音乐……
- 都是"名词+名词"或"形容词+名词"结构
- 一看就知道是什么功能
别小看这几个字。用户打开 App,扫一眼宫格,0.5 秒内要判断出"我要的功能在哪"。名字起得不好,用户找不到,功能再好用也白搭。
6.2 图标的设计规范
图标是宫格的灵魂——好的图标,用户一眼就知道是什么功能;不好的图标,用户看了文字才明白,甚至看完文字还不明白。
6.2.1 图标风格的选择
| 风格 | 特点 | 适用场景 | 代表产品 |
|---|---|---|---|
| 线性图标 | 只有线条,没有填充 | 简约、现代、轻量 | iOS、微信 |
| 填充图标 | 实心填充,有重量感 | 醒目、有力量感 | Material Design |
| 扁平图标 | 纯色 + 简单形状 | 简洁、现代 | 大部分 App |
| 拟物图标 | 仿真物体,有阴影有质感 | 真实、有温度 | 早期 iOS |
| 插画图标 | 手绘风格,有细节 | 有个性、有温度 | 小众精品 App |
「民族图鉴」的选择:扁平圆形图标 + emoji 符号
为什么用 emoji?
- 开发快:不用找设计师画图标,直接用 emoji
- 风格统一:所有 emoji 都是一个风格,不会乱
- 辨识度高:emoji 大家都认识,学习成本低
- 有温度:emoji 比纯图标更活泼、更有趣
当然,如果有设计师资源,还是建议画一套专属图标——更有品牌感。但在项目初期,emoji 是性价比很高的选择。
6.2.2 图标的颜色设计
图标颜色不是随便选的,要考虑:
1. 颜色的语义
不同颜色会给人不同的心理感受:
- 🔴 红色:热情、危险、重要
- 🟠 橙色:活力、温暖、美食
- 🟡 黄色:快乐、阳光、警告
- 🟢 绿色:自然、健康、成功
- 🔵 蓝色:科技、信任、冷静
- 🟣 紫色:神秘、高贵、创意
- 🟤 棕色:传统、质朴、土地
「民族图鉴」的图标颜色:
- 灵魂测试:紫色(神秘、探索)
- 民族图鉴:蓝色(知识、信任)
- 民族分布:绿色(自然、地理)
- 民族音乐:橙色(活力、艺术)
- AI助手:青色(科技、未来)
- 图片工具:粉色(创意、美学)
- 文化学堂:棕色(传统、学问)
- 知识问答:红色(挑战、热情)
2. 颜色的统一性
虽然每个图标颜色不一样,但要保持统一性:
- 饱和度一致:不要有的特别鲜艳,有的特别灰
- 明度一致:不要有的特别亮,有的特别暗
- 数量控制:颜色种类不要太多,6-8种就够了
💡 配色小技巧:
如果你不知道怎么选颜色,可以去 Dribbble、Pinterest 上搜"app icon color",看看别人是怎么配的。找一套你喜欢的,直接拿来用——配色这东西,不要自己瞎琢磨,站在巨人的肩膀上。
6.2.3 图标的大小与留白
图标不是越大越好——周围要有留白,不然会显得拥挤。
| 元素 | 大小 | 说明 |
|---|---|---|
| 图标尺寸 | 44vp | 最小触控目标 |
| 图标外圆形 | 56vp | 比图标大一圈,有呼吸感 |
| 图标与圆形间距 | 6vp | 四周留白 |
| 图标与文字间距 | 8vp | 图文间距 |
| 整个卡片高度 | 96vp | 图标 + 文字 + 上下留白 |
留白不是浪费空间——留白是为了让内容更突出、更易读。
6.3 点击反馈与微动效
用户点了一下入口,要不要给点反馈?——要的!而且反馈的质量直接影响用户对 App 的感受。
6.3.1 为什么需要点击反馈?
想象一下:你点了一个按钮,什么反应都没有。你会怎么想?
- “我点到了吗?”
- “是不是卡了?”
- “这个按钮是不是坏的?”
- 然后你又点了一下
没有反馈的按钮,用户会困惑、会重复点击、会失去信任感。
好的点击反馈,要满足三个条件:
- 即时:点下去立刻有反应,不能有延迟
- 明显:用户能清楚地看到变化
- 自然:动效要自然,不能太夸张
6.3.2 三种常见的点击反馈
1. 透明度变化(最简单)
按下的时候透明度变低,松开恢复。最简单,也最常用。
@State isPressed: boolean = false;
Column() {
// 图标 + 文字
}
.opacity(this.isPressed ? 0.7 : 1)
.animation({ duration: 100 })
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Down) {
this.isPressed = true;
} else if (event.type === TouchType.Up ||
event.type === TouchType.Cancel) {
this.isPressed = false;
}
})
优点:实现简单,效果不错
缺点:比较平淡,没什么特色
2. 缩放效果(最有质感)
按下的时候缩小一点,松开弹回来。像按一个真实的按钮一样,很有手感。
.scale({ x: this.isPressed ? 0.92 : 1, y: this.isPressed ? 0.92 : 1 })
.animation({ duration: 100, curve: Curve.EaseInOut })
优点:手感好,有质感
缺点:如果周围有其他元素,缩放可能会导致位置变化
3. 背景色变化(最清晰)
按下的时候背景色变深或变灰。最适合按钮、卡片。
.backgroundColor(this.isPressed ? '#F0F0F0' : '#FFFFFF')
.animation({ duration: 100 })
优点:反馈清晰,不影响布局
缺点:需要背景色,透明图标不适用
「民族图鉴」的选择:缩放 + 透明度,双重反馈
我们把缩放和透明度结合起来——按下时缩到 95% + 透明度 80%,既有手感又有层次感。
.opacity(this.isPressed ? 0.8 : 1)
.scale({ x: this.isPressed ? 0.95 : 1, y: this.isPressed ? 0.95 : 1 })
.animation({ duration: 120, curve: Curve.EaseInOut })
💡 动效时长的秘密:
点击反馈的动画时长一定要短——100-150 毫秒就够了。太长了会觉得"迟饨"、“慢”。记住:用户按下的那一瞬间就要有反应,不能等。
6.3.3 入口点击后的跳转动效
用户点了入口,跳转到新页面,这个过程也要有动效——不然会觉得突兀。
常见的页面转场动效:
| 动效 | 说明 | 适用场景 |
|---|---|---|
| 右侧滑入 | 新页面从右边滑进来 | 层级导航(大部分场景) |
| 底部滑入 | 新页面从底部推上来 | 弹窗、模态页 |
| 淡入淡出 | 新页面渐渐出现 | 平级切换、Tab切换 |
| 缩放淡入 | 新页面从小变大,同时淡入 | 点击图标/卡片跳转 |
「民族图鉴」用的是系统默认的右侧滑入——这是最经典、用户最熟悉的转场方式。
如果你想做的更有特色一点,可以做"图标展开"的动效——点击哪个图标,新页面就从那个图标的位置展开出来。这种动效很有空间感,但实现起来也更复杂。
6.4 宫格的多种布局形式
3×3 九宫格只是最基础的形式。宫格布局还有很多变化。
6.4.1 不同列数的布局
3列布局(手机端最常用)
┌───┬───┬───┐
│ │ │ │
├───┼───┼───┤
│ │ │ │
└───┴───┴───┘
- 每个格子比较宽,图标可以大一点
- 文字空间充足,可以放 4 个字
- 适合功能不多(6-9个)的产品
4列布局(功能较多时)
┌──┬──┬──┬──┐
│ │ │ │ │
├──┼──┼──┼──┤
│ │ │ │ │
└──┴──┴──┴──┘
- 格子小一些,信息密度高
- 文字最好 2-3 个字,不然放不下
- 适合功能很多(8-12个)的产品
- 「民族图鉴」用的就是 4 列
5列布局(超级APP)
┌─┬─┬─┬─┬─┐
│ │ │ │ │ │
├─┼─┼─┼─┼─┤
│ │ │ │ │ │
└─┴─┴─┴─┴─┘
- 格子很小,图标要简洁
- 文字最好 2 个字,或者干脆不要文字
- 适合功能超级多的超级 APP
- 支付宝、微信就是 5 列
6.4.2 瀑布流布局
不是所有宫格都是整齐的行列。瀑布流(Masonry)也是一种常见的宫格变体:
┌───┐ ┌───┐ ┌───┐
│ │ │ │ │ │
│ │ └───┘ │ │
└───┘ ┌───┐ │ │
│ │ └───┘
┌───┐ │ │ ┌───┐
│ │ └───┘ │ │
└───┘ └───┘
特点:
- 每个卡片高度不一样,根据内容自适应
- 错落有致,视觉上更活泼
- 适合图片类、内容类产品
- 小红书、Pinterest 就是瀑布流
瀑布流的实现比普通 Grid 复杂,因为需要计算每列的高度,把下一张图放到最短的那一列下面。
6.4.3 混合布局:大卡 + 小宫格
有时候,最重要的功能想让它更醒目,可以做"大卡 + 小宫格"的混合布局:
┌───────────┬─────┐
│ │ 1 │
│ 大卡片 ├─────┤
│ │ 2 │
├─────┬─────┼─────┤
│ 3 │ 4 │ 5 │
└─────┴─────┴─────┘
第一个功能是个大卡片,占 2×2 的位置,其他的是小格子。这样第一个功能特别醒目,点击率会高很多。
用 Grid 实现很简单:
Grid() {
// 第一个:跨2行2列
GridItem() {
BigCard()
}
.rowStart(0)
.columnStart(0)
.rowEnd(1)
.columnEnd(1)
// 后面的:普通格子
ForEach(this.otherEntries, (entry) => {
GridItem() {
SmallCard({ entry: entry })
}
}, e => e.id)
}
.columnsTemplate('1fr 1fr 1fr')
.rowsTemplate('1fr 1fr 1fr')
💡 布局是为内容服务的:
不要为了炫技而用复杂的布局。布局的目的是让用户更快找到想要的功能。什么布局最清晰、最好用,就用什么。大多数情况下,简单的 3-4 列宫格就够了。
6.5 「民族图鉴」8个快捷入口的设计思路
最后,我们来聊聊「民族图鉴」的 8 个快捷入口是怎么设计出来的。
6.5.1 功能选型:为什么是这 8 个?
我们选功能的标准有三个:
- 有代表性:能代表民族文化的不同方面
- 有趣味性:用户愿意点进去玩
- 可实现:开发难度适中,不会太复杂
基于这三个标准,我们选出了 8 个功能:
| 功能 | 为什么选它 | 定位 |
|---|---|---|
| 🧠 灵魂测试 | 趣味性强,容易传播,拉新效果好 | 引流功能 |
| 🏛️ 民族图鉴 | 核心功能,App 的基础 | 核心功能 |
| 🌍 民族分布 | 可视化,直观,有教育意义 | 特色功能 |
| 🎵 民族音乐 | 听觉体验,丰富内容形式 | 内容功能 |
| 🤖 AI助手 | 科技感,互动性强,有话题性 | 亮点功能 |
| 🖼️ 图片工具 | 实用性强,用户有需求 | 工具功能 |
| 📚 文化学堂 | 深度内容,提升 App 价值 | 学习功能 |
| 🎮 知识问答 | 游戏化,提升留存和活跃 | 留存功能 |
可以看到,这 8 个功能覆盖了不同的类型:有趣的、有用的、有深度的、有科技感的…… 搭配起来,用户总能找到自己感兴趣的。
6.5.2 排序逻辑:为什么是这个顺序?
排序是综合考虑了三个因素:
- 产品目标:灵魂测试是我们的特色和引流点,放第一个
- 核心功能:民族图鉴是 App 的基础,放第二个
- 类型搭配:不同类型的功能穿插排列,不要把同类型的放一起
你可以看到:
- 第1个:趣味型(灵魂测试)
- 第2个:核心型(民族图鉴)
- 第3个:特色型(民族分布)
- 第4个:内容型(民族音乐)
- 第5个:亮点型(AI助手)
- 第6个:工具型(图片工具)
- 第7个:学习型(文化学堂)
- 第8个:留存型(知识问答)
这样排列,用户从上到下扫一遍,能感受到 App 的丰富性——既有好玩的,又有用的,还有能学习的。
6.5.3 未来演进:入口会怎么变?
随着产品迭代,入口肯定会变。可能的演进方向:
1. 入口数量增加
- 从 8 个增加到 12 个
- 增加"更多"按钮,第二页放不常用的
2. 个性化排序
- 根据用户的使用习惯,自动调整入口顺序
- 常用的功能放前面,不常用的放后面
3. 可编辑入口
- 让用户自己选择哪些功能放在首页
- 用户可以自定义排序
但那都是以后的事了。现在我们只需要把这 8 个功能做好——把最核心的体验做到极致,比堆一堆功能有用多了。
💡 产品设计的哲学:
少即是多。一个 App 只要有一个功能让用户觉得"哇,这个好",就成功了。功能再多,没有一个突出的,用户转头就忘了。「民族图鉴」现阶段的目标,就是把"民族图鉴"这个核心功能做到极致,其他功能都是锦上添花。
📝 本章小结
核心知识点
本文深入讲解了 Grid 网格布局和功能入口的设计:
1. Grid 基础
- 核心概念:行、列、单元格
- columnsTemplate / rowsTemplate:定义行列布局
- fr 单位:比例分配空间,和 Flex 的 layoutWeight 类似
- rowsGap / columnsGap:行列间距
2. 入口卡片设计
- 结构:圆形图标 + 文字标签
- 图标 44vp 最小触控目标,整个卡片可点击
- 颜色搭配:功能语义、饱和度适中、明度接近
- 卡片化设计:白色圆角背景,有边框,有层次
3. 数据驱动
- 把入口数据和布局代码分离
- 用 ForEach 渲染 GridItem
- 好处:好维护、好扩展、好测试、好复用
4. Grid 进阶:不规则网格
- columnStart / columnEnd:跨列
- rowStart / rowEnd:跨行
- 可以做各种复杂的布局:Banner 布局、侧边栏布局、瀑布流
5. 响应式网格
- 根据屏幕宽度动态调整列数
- 断点设计:手机 3 列、平板 4 列、大屏 6 列
- 一套代码适配多种设备
6. Grid vs Row/Column 嵌套
- 整齐的少量网格:都可以,Row/Column 更直观
- 大量网格:Grid 更专业,性能更好
- 不规则网格:Grid 完胜,Row/Column 写起来太麻烦
最佳实践总结
✅ 功能入口用 Grid,不要用 Row/Column 嵌套
// ✅ Grid:简洁、可扩展
Grid() {
ForEach(entries, entry => {
GridItem() { EntryCard({ entry }) }
}, e => e.id)
}
.columnsTemplate('1fr 1fr 1fr')
✅ 图标 44vp,卡片 96vp,保证点击区域
// 图标 44vp(最小触控目标)
Text(icon).width(44).height(44).borderRadius(22)
// 整个卡片更高更大,好点
Column() { /* 图标 + 文字 */ }
.height(96)
.onClick(() => { ... })
✅ 颜色要有语义,饱和度明度统一
每个功能一个颜色,但风格统一
不要有的特别艳有的特别灰
不要超过 5-6 种主色,不然乱
✅ 数据和视图分离,用配置驱动
// 数据在配置文件里
export const QUICK_ENTRIES = [
{ id: 'xxx', icon: '...', label: '...', ... },
// ...
];
// 视图只管渲染
ForEach(QUICK_ENTRIES, entry => <GridItem>...</GridItem>)
✅ 响应式适配:手机3列、平板4列、大屏6列
// 根据屏幕宽度动态算列数
if (width > 840) columns = 6;
else if (width > 600) columns = 4;
else columns = 3;
✅ 二八定律:核心功能放首页,更多藏二级
首页放最常用的 5-6 个
更多功能放"全部"二级页面
不要把首页堆得满满当当
下一篇预告
首页的内容就全部讲完了——搜索框、冷知识、精选民族、快捷入口,一个完整的首页就搭好了。
接下来,我们进入第二个 Tab —— 百科页。
下一篇(第17篇)我们将讲解民族列表页——分类筛选与字母索引:
- 列表页的整体架构
- 分类筛选 Tab 的实现
- 字母索引(A-Z 快速定位)
- 列表头部吸顶效果
列表页是内容型 App 最核心的页面之一,也是交互最复杂的页面之一。
🔗 相关链接
- 项目源码: GitCode 仓库
- Grid 组件: 官方文档
- GridItem: 官方文档
- 响应式布局: 官方文档
💡 提示:功能入口网格看起来简单——不就是几个图标排排坐吗?但真要做好,学问不少。图标选什么?颜色怎么配?排序怎么定?哪些放首页、哪些藏起来?这些都不是技术问题,是产品问题。技术只是实现手段,真正决定体验的,是背后的产品思考。作为开发者,不要只想着"把图画出来就行了",多想想"为什么这么设计"、“有没有更好的方式”。带着产品思维写代码,你成长得会更快。
更多推荐




所有评论(0)