178.[HarmonyOS NEXT 实战案例八:Grid] 瀑布流网格布局基础篇
[HarmonyOS NEXT 实战案例八:Grid] 瀑布流网格布局基础篇
项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star
效果演示
1. 引言
瀑布流布局是一种常见的网格布局方式,特点是内容块按照等宽不等高的方式排列,就像瀑布一样自上而下流动,非常适合展示图片、卡片等内容。在HarmonyOS NEXT中,我们可以使用WaterFlow组件轻松实现瀑布流布局效果。本教程将详细讲解如何使用WaterFlow组件实现一个图片社交应用的瀑布流布局。
2. 数据模型设计
2.1 ImageItem接口
首先,我们需要定义图片数据的接口。根据代码中的使用情况,我们可以推断出ImageItem接口的结构如下:
interface ImageItem {
id: number; // 图片ID
title: string; // 图片标题
description: string; // 图片描述
image: Resource; // 图片资源
width: number; // 图片宽度
height: number; // 图片高度
author: { // 作者信息
name: string; // 作者名称
avatar: Resource; // 作者头像
isVerified: boolean; // 是否认证
};
stats: { // 统计信息
likes: number; // 点赞数
comments: number; // 评论数
shares: number; // 分享数
views: number; // 浏览数
};
tags: string[]; // 标签列表
category: string; // 分类
publishTime: string; // 发布时间
isLiked: boolean; // 是否已点赞
isCollected: boolean; // 是否已收藏
location?: string; // 位置信息(可选)
camera?: string; // 相机信息(可选)
}
2.2 示例数据初始化
在组件中,我们使用@State装饰器定义了图片数据数组,并初始化了一些示例数据:
@State imageItems: ImageItem[] = [
{
id: 1,
title: '夕阳下的城市天际线',
description: '在高楼大厦间捕捉到的绝美夕阳,金色的光芒洒向整个城市',
image: $r('app.media.big22'),
width: 300,
height: 400,
author: {
name: '摄影师小王',
avatar: $r('app.media.big22'),
isVerified: true
},
stats: {
likes: 1205,
comments: 89,
shares: 45,
views: 8930
},
tags: ['夕阳', '城市', '天际线', '摄影'],
category: '风景',
publishTime: '2024-01-10 18:30',
isLiked: false,
isCollected: false,
location: '上海外滩',
camera: 'Canon EOS R5'
},
// 其他图片数据...
]
2.3 状态管理
除了图片数据,我们还需要管理一些UI状态:
@State selectedCategory: string = '全部' // 当前选中的分类
@State sortBy: string = '最新' // 当前排序方式
@State searchKeyword: string = '' // 搜索关键词
@State showImageDetail: boolean = false // 是否显示图片详情
@State selectedImage: ImageItem = {...} // 当前选中的图片
3. 辅助功能实现
3.1 数据过滤与排序
我们实现了一个getFilteredImages
方法,用于根据分类、搜索关键词和排序方式过滤和排序图片数据:
getFilteredImages(): ImageItem[] {
let filtered = this.imageItems
// 分类过滤
if (this.selectedCategory !== '全部') {
filtered = filtered.filter(image => image.category === this.selectedCategory)
}
// 搜索过滤
if (this.searchKeyword.trim() !== '') {
filtered = filtered.filter(image =>
image.title.includes(this.searchKeyword) ||
image.description.includes(this.searchKeyword) ||
image.tags.some(tag => tag.includes(this.searchKeyword))
)
}
// 排序
switch (this.sortBy) {
case '最热':
filtered.sort((a, b) => b.stats.views - a.stats.views)
break
case '最多赞':
filtered.sort((a, b) => b.stats.likes - a.stats.likes)
break
default: // 最新
filtered.sort((a, b) => new Date(b.publishTime).getTime() - new Date(a.publishTime).getTime())
}
return filtered
}
3.2 交互功能
我们实现了点赞和收藏功能:
// 切换点赞状态
toggleLike(imageId: number) {
const image = this.imageItems.find(img => img.id === imageId)
if (image) {
image.isLiked = !image.isLiked
image.stats.likes += image.isLiked ? 1 : -1
}
}
// 切换收藏状态
toggleCollect(imageId: number) {
const image = this.imageItems.find(img => img.id === imageId)
if (image) {
image.isCollected = !image.isCollected
}
}
3.3 格式化工具
为了更好地展示数据,我们实现了数字格式化和时间差计算方法:
// 格式化数字
formatNumber(num: number): string {
if (num >= 10000) {
return `${(num / 10000).toFixed(1)}万`
} else if (num >= 1000) {
return `${(num / 1000).toFixed(1)}k`
}
return num.toString()
}
// 计算时间差
getTimeAgo(publishTime: string): string {
const now = new Date()
const publish = new Date(publishTime)
const diff = now.getTime() - publish.getTime()
const hours = Math.floor(diff / (1000 * 60 * 60))
if (hours < 1) {
return '刚刚'
} else if (hours < 24) {
return `${hours}小时前`
} else {
const days = Math.floor(hours / 24)
return `${days}天前`
}
}
4. 页面结构设计
4.1 整体布局
我们的瀑布流页面包含以下几个主要部分:
- 顶部搜索和筛选区域
- 瀑布流网格区域
- 图片详情对话框(点击图片时显示)
整体结构如下:
build() {
Stack() {
Column() {
// 顶部搜索和筛选
Column() {
// 搜索栏
// ...
// 分类和排序
// ...
}
// 瀑布流网格
WaterFlow() {
// ...
}
}
// 图片详情对话框
if (this.showImageDetail) {
// ...
}
}
}
4.2 顶部搜索和筛选区域
顶部区域包含搜索栏和分类/排序选项:
// 搜索栏
Row() {
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
})
}
.width('100%')
.height(44)
.backgroundColor('#F5F5F5')
.borderRadius(22)
.layoutWeight(1)
Button() {
Image($r('app.media.camera_icon'))
.width(24)
.height(24)
.fillColor('#333333')
}
.width(44)
.height(44)
.borderRadius(22)
.backgroundColor('#F0F0F0')
.margin({ left: 12 })
}
// 分类和排序
Row() {
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 ? 8 : 0 })
.onClick(() => {
this.selectedCategory = category
})
})
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.layoutWeight(1)
Button(this.sortBy)
.fontSize(12)
.fontColor('#666666')
.backgroundColor('#F0F0F0')
.borderRadius(12)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.margin({ left: 12 })
}
5. 瀑布流网格实现
5.1 WaterFlow组件
HarmonyOS NEXT提供了WaterFlow组件,专门用于实现瀑布流布局。我们使用它来展示图片卡片:
WaterFlow() {
ForEach(this.getFilteredImages(), (image: ImageItem) => {
FlowItem() {
// 图片卡片内容
}
})
}
.columnsTemplate('1fr 1fr') // 两列布局
.itemConstraintSize({
minWidth: 0,
maxWidth: '100%',
minHeight: 0,
maxHeight: '100%'
})
.columnsGap(8) // 列间距
.rowsGap(8) // 行间距
.width('100%')
.layoutWeight(1)
.padding({ left: 16, right: 16, bottom: 16 })
.backgroundColor('#F8F8F8')
5.2 FlowItem内容设计
每个FlowItem包含一个图片卡片,结构如下:
FlowItem() {
Column() {
// 图片部分
Stack({ alignContent: Alignment.TopEnd }) {
Image(image.image)
.width('100%')
.aspectRatio(image.width / image.height) // 保持原始宽高比
.objectFit(ImageFit.Cover)
.borderRadius({ topLeft: 12, topRight: 12 })
// 收藏按钮
Button() {
Image(image.isCollected ? $r('app.media.big19') : $r('app.media.big20'))
.width(16)
.height(16)
.fillColor(image.isCollected ? '#FFD700' : '#FFFFFF')
}
.width(32)
.height(32)
.borderRadius(16)
.backgroundColor('rgba(0, 0, 0, 0.3)')
.margin({ top: 8, right: 8 })
.onClick(() => {
this.toggleCollect(image.id)
})
}
// 图片信息
Column() {
// 标题
Text(image.title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.width('100%')
.textAlign(TextAlign.Start)
.margin({ bottom: 6 })
// 作者信息
Row() {
Image(image.author.avatar)
.width(24)
.height(24)
.borderRadius(12)
Text(image.author.name)
.fontSize(12)
.fontColor('#666666')
.margin({ left: 6 })
.layoutWeight(1)
if (image.author.isVerified) {
Image($r('app.media.big19'))
.width(12)
.height(12)
.fillColor('#007AFF')
}
}
.width('100%')
.margin({ bottom: 8 })
// 互动数据
Row() {
// 点赞按钮和数量
Button() {
Row() {
Image(image.isLiked ? $r('app.media.heart_filled') : $r('app.media.big19'))
.width(14)
.height(14)
.fillColor(image.isLiked ? '#FF6B6B' : '#999999')
.margin({ right: 2 })
Text(this.formatNumber(image.stats.likes))
.fontSize(10)
.fontColor('#999999')
}
}
.backgroundColor('transparent')
.padding(0)
.onClick(() => {
this.toggleLike(image.id)
})
// 评论数量
Row() {
Image($r('app.media.big19'))
.width(14)
.height(14)
.fillColor('#999999')
.margin({ right: 2 })
Text(image.stats.comments.toString())
.fontSize(10)
.fontColor('#999999')
}
.margin({ left: 12 })
Blank()
// 发布时间
Text(this.getTimeAgo(image.publishTime))
.fontSize(10)
.fontColor('#999999')
}
.width('100%')
}
.padding(12)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.shadow({
radius: 6,
color: 'rgba(0, 0, 0, 0.1)',
offsetX: 0,
offsetY: 2
})
.onClick(() => {
this.selectedImage = image
this.showImageDetail = true
})
}
5.3 WaterFlow与Grid的区别
WaterFlow组件是专门为瀑布流布局设计的,与Grid组件相比有以下区别:
特性 | WaterFlow | Grid |
---|---|---|
布局方式 | 瀑布流(等宽不等高) | 网格(等宽等高) |
子项组件 | FlowItem | GridItem |
列定义 | columnsTemplate | columnsTemplate |
自动适应内容高度 | 是 | 否 |
适用场景 | 图片展示、卡片流 | 规则网格布局 |
6. 图片详情对话框
当用户点击图片卡片时,我们会显示一个详情对话框,展示图片的完整信息:
@Builder
ImageDetailDialog() {
if (this.selectedImage) {
Column() {
// 图片
Image(this.selectedImage.image)
.width('100%')
.height('60%')
.objectFit(ImageFit.Contain)
.borderRadius({ topLeft: 16, topRight: 16 })
// 详情内容
Column() {
// 作者信息
// ...
// 标题和描述
// ...
// 标签
// ...
// 拍摄信息
// ...
// 互动按钮
// ...
}
.padding(20)
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('95%')
.height('90%')
.backgroundColor('#FFFFFF')
.borderRadius(16)
}
}
在主布局中,我们使用条件渲染来显示或隐藏详情对话框:
// 图片详情对话框
if (this.showImageDetail) {
Column() {
this.ImageDetailDialog()
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0, 0, 0, 0.8)')
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.showImageDetail = false
})
}
7. 瀑布流布局的优势与技巧
7.1 瀑布流布局的优势
优势 | 描述 |
---|---|
空间利用率高 | 根据内容高度自动排列,减少空白区域 |
视觉吸引力强 | 不规则的排列方式更具视觉冲击力 |
适合图片展示 | 可以保持图片原始比例,展示效果更好 |
无限滚动友好 | 适合实现无限滚动加载的交互模式 |
7.2 实现技巧
-
保持原始宽高比:使用
aspectRatio
属性保持图片的原始宽高比,避免图片变形。Image(image.image) .width('100%') .aspectRatio(image.width / image.height)
-
合理设置间距:使用
columnsGap
和rowsGap
设置合适的间距,提升视觉效果。WaterFlow() .columnsGap(8) .rowsGap(8)
-
添加阴影效果:为卡片添加轻微的阴影效果,增强层次感。
.shadow({ radius: 6, color: 'rgba(0, 0, 0, 0.1)', offsetX: 0, offsetY: 2 })
-
文本溢出处理:对标题等文本使用
maxLines
和textOverflow
属性,确保布局整齐。Text(image.title) .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis })
8. 总结
在下一篇教程中,我们将深入探讨瀑布流布局的进阶技巧,包括性能优化、动画效果、交互体验等方面的内容。
- 0回答
- 4粉丝
- 0关注
- 180.[HarmonyOS NEXT 实战案例八:Grid] 瀑布流网格布局高级篇
- 179.[HarmonyOS NEXT 实战案例八:Grid] 瀑布流网格布局进阶篇
- 160.[HarmonyOS NEXT 实战案例二:Grid] 照片相册网格布局:基础篇
- 172.[HarmonyOS NEXT 实战案例六:Grid] 响应式网格布局 - 基础篇
- 166.[HarmonyOS NEXT 实战案例四:Grid] 可滚动网格布局基础篇
- 169.[HarmonyOS NEXT 实战案例五:Grid] 动态网格布局基础篇
- 184.[HarmonyOS NEXT 实战案例十:Grid] 仪表板网格布局基础篇
- 163.[HarmonyOS NEXT 实战案例三:Grid] 不规则网格布局基础篇:打造新闻应用首页
- 171.[HarmonyOS NEXT 实战案例五:Grid] 动态网格布局高级篇
- 162.[HarmonyOS NEXT 实战案例二:Grid] 照片相册网格布局:高级篇
- 168.[HarmonyOS NEXT 实战案例四:Grid] 可滚动网格布局高级篇
- 174.[HarmonyOS NEXT 实战案例六:Grid] 响应式网格布局 - 高级篇
- 161. [HarmonyOS NEXT 实战案例二:Grid] 照片相册网格布局:进阶篇
- 167.[HarmonyOS NEXT 实战案例四:Grid] 可滚动网格布局进阶篇
- 173.[HarmonyOS NEXT 实战案例六:Grid] 响应式网格布局 - 进阶篇