169.[HarmonyOS NEXT 实战案例五:Grid] 动态网格布局基础篇
[HarmonyOS NEXT 实战案例五:Grid] 动态网格布局基础篇
项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star
效果演示
在移动应用开发中,网格布局是展示内容的常用方式,特别是对于图片、卡片等元素的展示。HarmonyOS NEXT提供了强大的Grid组件,可以实现各种灵活的网格布局。本教程将详细讲解如何使用Grid组件实现动态网格布局,特别是瀑布流效果,这在社交媒体、图片分享等应用中非常常见。
1. 动态网格布局概述
动态网格布局是一种灵活的布局方式,可以根据内容自动调整元素的大小和位置。在本案例中,我们将实现一个类似于Pinterest、小红书等应用的瀑布流布局,展示不同高度的图片卡片。
1.1 瀑布流布局的特点
瀑布流布局具有以下特点:
特点 | 描述 |
---|---|
多列展示 | 内容以多列方式排列,通常为2-3列 |
高度不一 | 每个元素可以有不同的高度,根据内容自动调整 |
紧凑排列 | 元素之间紧密排列,充分利用屏幕空间 |
无限加载 | 支持滚动加载更多内容 |
1.2 应用场景
瀑布流布局适用于以下场景:
- 图片分享应用(如Pinterest、小红书)
- 商品展示页面
- 新闻卡片列表
- 社交媒体信息流
2. 数据模型设计
在实现瀑布流布局之前,我们需要先定义数据模型。在本案例中,我们定义了PhotoItems
接口来表示图片卡片的数据结构:
interface PhotoItems {
id: number,
imageUrl: Resource,
title: string,
author: string,
authorAvatar: Resource,
likes: number,
comments: number,
tags: string[],
height: number, // 用于模拟不同高度的图片
isLiked: boolean,
description: string
}
这个数据模型包含了图片卡片所需的所有信息,包括:
- 基本信息:ID、图片URL、标题、描述
- 作者信息:作者名称、头像
- 互动数据:点赞数、评论数、是否已点赞
- 分类信息:标签数组
- 布局信息:图片高度(用于瀑布流布局)
3. 页面结构设计
我们的瀑布流页面包含以下几个主要部分:
- 顶部导航栏:包含标题、搜索按钮和相机按钮
- 分类标签栏:用于筛选不同类别的内容
- 瀑布流网格:展示图片卡片
- 底部导航栏:包含首页、发现、发布、消息和我的等功能入口
3.1 组件状态管理
在WaterfallGrid
组件中,我们定义了以下状态变量:
@State photoItems: PhotoItems[] = [...] // 图片数据数组
@State selectedCategory: string = '全部' // 当前选中的分类
@State searchKeyword: string = '' // 搜索关键词
@State showSearch: boolean = false // 是否显示搜索框
这些状态变量用于控制页面的显示和交互:
photoItems
:存储所有图片卡片数据selectedCategory
:记录当前选中的分类标签searchKeyword
:存储用户输入的搜索关键词showSearch
:控制搜索框的显示和隐藏
4. 瀑布流网格实现
4.1 Grid组件基础配置
在HarmonyOS NEXT中,我们使用Grid组件来实现瀑布流布局。以下是Grid组件的基本配置:
Grid() {
ForEach(this.getFilteredPhotos(), (item: PhotoItems) => {
GridItem() {
// 卡片内容
}
})
}
.columnsTemplate('1fr 1fr') // 两列瀑布流
.rowsGap(16) // 行间距
.columnsGap(12) // 列间距
.width('100%')
.layoutWeight(1)
.padding({ left: 16, right: 16, bottom: 16 })
.backgroundColor('#F8F8F8')
.onScrollIndex((first: number) => {
console.log(`当前显示的第一个图片索引: ${first}`)
})
关键属性说明:
属性 | 说明 |
---|---|
columnsTemplate | 定义网格的列模板,'1fr 1fr'表示两列等宽 |
rowsGap | 行间距,单位为vp |
columnsGap | 列间距,单位为vp |
onScrollIndex | 滚动事件回调,返回当前显示的第一个元素索引 |
4.2 GridItem内容设计
每个GridItem包含一个完整的图片卡片,结构如下:
GridItem() {
Column() {
// 图片部分
Stack({ alignContent: Alignment.TopEnd }) {
Image(item.imageUrl)
.width('100%')
.height(item.height)
.objectFit(ImageFit.Cover)
.borderRadius({ topLeft: 12, topRight: 12 })
// 点赞按钮
Button() {
Image(item.isLiked ? $r('app.media.heart_filled') : $r('app.media.heart_outline'))
.width(20)
.height(20)
.fillColor(item.isLiked ? '#FF6B6B' : '#FFFFFF')
}
.width(36)
.height(36)
.borderRadius(18)
.backgroundColor('rgba(0, 0, 0, 0.3)')
.margin({ top: 8, right: 8 })
.onClick(() => {
this.toggleLike(item.id)
})
}
// 内容区域
Column() {
// 标题、描述、标签、作者信息等
}
.padding(12)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.shadow({
radius: 8,
color: 'rgba(0, 0, 0, 0.1)',
offsetX: 0,
offsetY: 2
})
}
每个卡片包含以下部分:
- 图片区域:显示主图片,高度根据数据模型中的height属性动态设置
- 点赞按钮:位于图片右上角,可以切换点赞状态
- 内容区域:包含标题、描述、标签和作者信息等
4.3 卡片内容区域详解
内容区域包含多个部分,布局如下:
Column() {
// 标题
Text(item.title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.width('100%')
.textAlign(TextAlign.Start)
// 描述
Text(item.description)
.fontSize(14)
.fontColor('#666666')
.maxLines(3)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.width('100%')
.textAlign(TextAlign.Start)
.margin({ top: 8 })
// 标签
if (item.tags.length > 0) {
Row() {
ForEach(item.tags.slice(0, 3), (tag: string, index) => {
Text(`#${tag}`)
.fontSize(12)
.fontColor('#007AFF')
.backgroundColor('rgba(0, 122, 255, 0.1)')
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.borderRadius(8)
.margin({ right: index < Math.min(item.tags.length, 3) - 1 ? 6 : 0 })
})
}
.width('100%')
.margin({ top: 12 })
}
// 作者信息和互动数据
Row() {
Row() {
Image(item.authorAvatar)
.width(24)
.height(24)
.borderRadius(12)
Text(item.author)
.fontSize(12)
.fontColor('#666666')
.margin({ left: 6 })
}
.layoutWeight(1)
Row() {
// 点赞数
Row() {
Image($r('app.media.big21'))
.width(14)
.height(14)
.fillColor('#FF6B6B')
Text(this.formatNumber(item.likes))
.fontSize(12)
.fontColor('#666666')
.margin({ left: 2 })
}
.margin({ right: 12 })
// 评论数
Row() {
Image($r('app.media.search_icon'))
.width(14)
.height(14)
.fillColor('#999999')
Text(this.formatNumber(item.comments))
.fontSize(12)
.fontColor('#666666')
.margin({ left: 2 })
}
}
}
.width('100%')
.margin({ top: 12 })
}
内容区域的布局特点:
- 标题和描述:使用Text组件显示,设置最大行数和溢出处理
- 标签:使用ForEach循环渲染标签,最多显示3个
- 作者信息:左侧显示作者头像和名称
- 互动数据:右侧显示点赞数和评论数,使用formatNumber方法格式化数字
5. 数据过滤与交互功能
5.1 分类筛选功能
我们实现了分类标签栏,用于筛选不同类别的内容:
Scroll() {
Row() {
ForEach(this.categories, (category: string, index) => {
Button(category)
.fontSize(14)
.fontColor(this.selectedCategory === category ? '#FFFFFF' : '#333333')
.backgroundColor(this.selectedCategory === category ? '#007AFF' : '#F0F0F0')
.borderRadius(16)
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
.margin({ right: index < this.categories.length - 1 ? 12 : 0 })
.onClick(() => {
this.selectedCategory = category
})
})
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.padding({ left: 16, right: 16, bottom: 16 })
.backgroundColor('#FFFFFF')
当用户点击分类标签时,会更新selectedCategory
状态,触发界面重新渲染,显示筛选后的内容。
5.2 搜索功能
搜索功能通过顶部导航栏中的搜索按钮触发,显示搜索输入框:
Row() {
Image($r('app.media.search_icon'))
.width(20)
.height(20)
.fillColor('#999999')
.margin({ left: 12 })
TextInput({ placeholder: '搜索图片、作者或标签' })
.fontSize(16)
.backgroundColor('transparent')
.border({ width: 0 })
.layoutWeight(1)
.margin({ left: 8, right: 12 })
.onChange((value: string) => {
this.searchKeyword = value
})
}
当用户输入搜索关键词时,会更新searchKeyword
状态,触发界面重新渲染,显示搜索结果。
5.3 数据过滤方法
我们实现了getFilteredPhotos
方法,用于根据分类和搜索关键词过滤数据:
getFilteredPhotos(): PhotoItems[] {
let filtered = this.photoItems
if (this.selectedCategory !== '全部') {
filtered = filtered.filter(item =>
item.tags.some(tag => tag.includes(this.selectedCategory))
)
}
if (this.searchKeyword.trim() !== '') {
filtered = filtered.filter(item =>
item.title.includes(this.searchKeyword) ||
item.author.includes(this.searchKeyword) ||
item.tags.some(tag => tag.includes(this.searchKeyword))
)
}
return filtered
}
这个方法首先根据选中的分类进行过滤,然后再根据搜索关键词进行过滤,最终返回符合条件的数据。
5.4 点赞功能
我们实现了toggleLike
方法,用于切换图片的点赞状态:
toggleLike(id: number) {
const item = this.photoItems.find(item => item.id === id)
if (item) {
item.isLiked = !item.isLiked
item.likes += item.isLiked ? 1 : -1
}
}
当用户点击点赞按钮时,会调用这个方法,更新对应图片的点赞状态和点赞数。
6. 辅助功能实现
6.1 数字格式化
为了美观地显示点赞数和评论数,我们实现了formatNumber
方法:
formatNumber(num: number): string {
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'k'
}
return num.toString()
}
这个方法将大于等于1000的数字转换为带k的形式,例如1234转换为1.2k。
总结
本教程详细讲解了如何使用HarmonyOS NEXT的Grid组件实现动态网格布局,特别是瀑布流效果。我们从数据模型设计、页面结构设计、Grid组件配置、GridItem内容设计等方面进行了详细讲解,并实现了分类筛选、搜索、点赞等交互功能。
- 0回答
- 4粉丝
- 0关注
- 171.[HarmonyOS NEXT 实战案例五:Grid] 动态网格布局高级篇
- 170.[HarmonyOS NEXT 实战案例五:Grid] 动态网格布局进阶篇
- 160.[HarmonyOS NEXT 实战案例二:Grid] 照片相册网格布局:基础篇
- 172.[HarmonyOS NEXT 实战案例六:Grid] 响应式网格布局 - 基础篇
- 166.[HarmonyOS NEXT 实战案例四:Grid] 可滚动网格布局基础篇
- 178.[HarmonyOS NEXT 实战案例八:Grid] 瀑布流网格布局基础篇
- 184.[HarmonyOS NEXT 实战案例十:Grid] 仪表板网格布局基础篇
- 163.[HarmonyOS NEXT 实战案例三:Grid] 不规则网格布局基础篇:打造新闻应用首页
- 162.[HarmonyOS NEXT 实战案例二:Grid] 照片相册网格布局:高级篇
- 168.[HarmonyOS NEXT 实战案例四:Grid] 可滚动网格布局高级篇
- 174.[HarmonyOS NEXT 实战案例六:Grid] 响应式网格布局 - 高级篇
- 180.[HarmonyOS NEXT 实战案例八:Grid] 瀑布流网格布局高级篇
- 161. [HarmonyOS NEXT 实战案例二:Grid] 照片相册网格布局:进阶篇
- 167.[HarmonyOS NEXT 实战案例四:Grid] 可滚动网格布局进阶篇
- 173.[HarmonyOS NEXT 实战案例六:Grid] 响应式网格布局 - 进阶篇