179.[HarmonyOS NEXT 实战案例八:Grid] 瀑布流网格布局进阶篇
2025-06-30 22:59:48
104次阅读
0个评论
[HarmonyOS NEXT 实战案例八:Grid] 瀑布流网格布局进阶篇
项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star
效果演示
1. 引言
在上一篇教程中,我们介绍了HarmonyOS NEXT中瀑布流网格布局的基础知识,包括数据模型设计、页面结构设计和WaterFlow组件的基本使用。本篇教程将深入探讨瀑布流网格布局的进阶技巧,包括高级交互设计、动态布局调整、自定义瀑布流样式等内容,帮助你打造更加精美和流畅的瀑布流界面。
2. 高级交互设计
2.1 图片详情对话框
在基础篇中,我们简单介绍了图片详情对话框的实现。现在,我们将深入探讨其完整实现,包括丰富的交互效果和精美的视觉设计。
@Builder
ImageDetailDialog() {
if (this.selectedImage) {
Column() {
// 顶部操作栏
Row() {
Button() {
Image($r('app.media.ic_back'))
.width(24)
.height(24)
.fillColor('#FFFFFF')
}
.width(36)
.height(36)
.borderRadius(18)
.backgroundColor('rgba(0, 0, 0, 0.3)')
.onClick(() => {
this.showImageDetail = false
})
Blank()
Button() {
Image($r('app.media.ic_more'))
.width(24)
.height(24)
.fillColor('#FFFFFF')
}
.width(36)
.height(36)
.borderRadius(18)
.backgroundColor('rgba(0, 0, 0, 0.3)')
}
.width('100%')
.padding({ left: 16, right: 16, top: 16 })
.position({ x: 0, y: 0 })
.zIndex(1)
// 图片
Image(this.selectedImage.image)
.width('100%')
.height('60%')
.objectFit(ImageFit.Contain)
.borderRadius({ topLeft: 16, topRight: 16 })
// 详情内容
Column() {
// 作者信息
Row() {
Image(this.selectedImage.author.avatar)
.width(40)
.height(40)
.borderRadius(20)
Column() {
Row() {
Text(this.selectedImage.author.name)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
if (this.selectedImage.author.isVerified) {
Image($r('app.media.ic_verified'))
.width(16)
.height(16)
.fillColor('#007AFF')
.margin({ left: 4 })
}
}
Text('摄影师')
.fontSize(12)
.fontColor('#999999')
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 12 })
.layoutWeight(1)
Button('关注')
.fontSize(14)
.fontColor('#FFFFFF')
.backgroundColor('#007AFF')
.borderRadius(16)
.padding({ left: 16, right: 16, top: 6, bottom: 6 })
}
.width('100%')
.margin({ bottom: 16 })
// 标题和描述
Text(this.selectedImage.title)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
.textAlign(TextAlign.Start)
.margin({ bottom: 8 })
Text(this.selectedImage.description)
.fontSize(14)
.fontColor('#666666')
.width('100%')
.textAlign(TextAlign.Start)
.margin({ bottom: 16 })
// 标签
Scroll() {
Row() {
ForEach(this.selectedImage.tags, (tag: string) => {
Text(`#${tag}`)
.fontSize(12)
.fontColor('#007AFF')
.backgroundColor('#E6F2FF')
.borderRadius(12)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.margin({ right: 8 })
})
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.margin({ bottom: 16 })
// 拍摄信息
if (this.selectedImage.location || this.selectedImage.camera) {
Column() {
if (this.selectedImage.location) {
Row() {
Image($r('app.media.ic_location'))
.width(16)
.height(16)
.fillColor('#999999')
.margin({ right: 8 })
Text(this.selectedImage.location)
.fontSize(12)
.fontColor('#999999')
}
.margin({ bottom: 8 })
}
if (this.selectedImage.camera) {
Row() {
Image($r('app.media.ic_camera'))
.width(16)
.height(16)
.fillColor('#999999')
.margin({ right: 8 })
Text(this.selectedImage.camera)
.fontSize(12)
.fontColor('#999999')
}
}
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.padding(12)
.backgroundColor('#F5F5F5')
.borderRadius(8)
.margin({ bottom: 16 })
}
// 互动按钮
Row() {
// 点赞按钮
Button() {
Column() {
Image(this.selectedImage.isLiked ? $r('app.media.ic_like_filled') : $r('app.media.ic_like'))
.width(24)
.height(24)
.fillColor(this.selectedImage.isLiked ? '#FF6B6B' : '#333333')
Text(this.formatNumber(this.selectedImage.stats.likes))
.fontSize(12)
.fontColor('#666666')
.margin({ top: 4 })
}
}
.backgroundColor('transparent')
.padding(0)
.layoutWeight(1)
.onClick(() => {
this.toggleLike(this.selectedImage.id)
})
// 评论按钮
Button() {
Column() {
Image($r('app.media.ic_comment'))
.width(24)
.height(24)
.fillColor('#333333')
Text(this.formatNumber(this.selectedImage.stats.comments))
.fontSize(12)
.fontColor('#666666')
.margin({ top: 4 })
}
}
.backgroundColor('transparent')
.padding(0)
.layoutWeight(1)
// 分享按钮
Button() {
Column() {
Image($r('app.media.ic_share'))
.width(24)
.height(24)
.fillColor('#333333')
Text(this.formatNumber(this.selectedImage.stats.shares))
.fontSize(12)
.fontColor('#666666')
.margin({ top: 4 })
}
}
.backgroundColor('transparent')
.padding(0)
.layoutWeight(1)
// 收藏按钮
Button() {
Column() {
Image(this.selectedImage.isCollected ? $r('app.media.ic_collect_filled') : $r('app.media.ic_collect'))
.width(24)
.height(24)
.fillColor(this.selectedImage.isCollected ? '#FFD700' : '#333333')
Text('收藏')
.fontSize(12)
.fontColor('#666666')
.margin({ top: 4 })
}
}
.backgroundColor('transparent')
.padding(0)
.layoutWeight(1)
.onClick(() => {
this.toggleCollect(this.selectedImage.id)
})
}
.width('100%')
.padding({ top: 16 })
.border({ width: { top: 1 }, color: { top: '#EEEEEE' } })
}
.padding(20)
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('95%')
.height('90%')
.backgroundColor('#FFFFFF')
.borderRadius(16)
}
}
2.2 手势交互
在瀑布流布局中,我们可以添加丰富的手势交互,提升用户体验:
2.2.1 长按交互
为图片卡片添加长按交互,显示快捷操作菜单:
// 在FlowItem中添加长按手势
.gesture(
LongPressGesture()
.onAction(() => {
this.showQuickActions(image.id)
})
)
实现快捷操作菜单:
showQuickActions(imageId: number) {
const actions = [
{ icon: $r('app.media.ic_like'), text: '点赞', action: () => this.toggleLike(imageId) },
{ icon: $r('app.media.ic_collect'), text: '收藏', action: () => this.toggleCollect(imageId) },
{ icon: $r('app.media.ic_share'), text: '分享', action: () => {} },
{ icon: $r('app.media.ic_download'), text: '下载', action: () => {} }
]
// 显示操作菜单
// 实现略
}
2.2.2 双指缩放
在图片详情对话框中,添加双指缩放手势,实现图片缩放功能:
// 在图片详情对话框中的图片组件上添加
.gesture(
PinchGesture()
.onActionStart((event: GestureEvent) => {
this.initialScale = this.imageScale
})
.onActionUpdate((event: GestureEvent) => {
this.imageScale = this.initialScale * event.scale
// 限制缩放范围
this.imageScale = Math.max(0.5, Math.min(3.0, this.imageScale))
})
)
3. 动态布局调整
3.1 响应式列数
根据屏幕宽度动态调整瀑布流的列数,实现更好的响应式布局:
@State columnsCount: number = 2 // 默认两列
onPageShow() {
// 获取屏幕宽度
const screenWidth = px2vp(getContext(this).width)
// 根据屏幕宽度设置列数
if (screenWidth <= 320) {
this.columnsCount = 1
} else if (screenWidth <= 600) {
this.columnsCount = 2
} else if (screenWidth <= 840) {
this.columnsCount = 3
} else {
this.columnsCount = 4
}
}
// 在WaterFlow组件中使用动态列数
WaterFlow() {
// ...
}
.columnsTemplate(this.getColumnsTemplate())
// ...
// 生成列模板字符串
getColumnsTemplate(): string {
return Array(this.columnsCount).fill('1fr').join(' ')
}
3.2 动态卡片大小
根据内容类型或重要性,动态调整卡片大小:
// 在FlowItem中根据图片类型设置不同的样式
FlowItem() {
Column() {
// ...
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.shadow({
radius: image.isHighlighted ? 10 : 6,
color: image.isHighlighted ? 'rgba(0, 0, 0, 0.15)' : 'rgba(0, 0, 0, 0.1)',
offsetX: 0,
offsetY: image.isHighlighted ? 4 : 2
})
// 高亮图片使用不同的边框
.border(image.isHighlighted ? {
width: 2,
color: '#007AFF',
style: BorderStyle.Solid
} : {
width: 0
})
}
4. 高级样式与视觉效果
4.1 卡片样式变体
为瀑布流卡片设计多种样式变体,增加视觉多样性:
// 定义卡片样式变体
enum CardStyle {
BASIC, // 基本样式
COMPACT, // 紧凑样式
FEATURED, // 特色样式
MINIMAL // 极简样式
}
// 为每个图片分配样式变体
@State imageStyles: Map<number, CardStyle> = new Map()
initImageStyles() {
this.imageItems.forEach(image => {
// 根据某些规则分配样式
if (image.stats.likes > 1000) {
this.imageStyles.set(image.id, CardStyle.FEATURED)
} else if (image.tags.includes('极简')) {
this.imageStyles.set(image.id, CardStyle.MINIMAL)
} else if (image.description.length < 20) {
this.imageStyles.set(image.id, CardStyle.COMPACT)
} else {
this.imageStyles.set(image.id, CardStyle.BASIC)
}
})
}
// 在FlowItem中应用不同的样式
FlowItem() {
const style = this.imageStyles.get(image.id) || CardStyle.BASIC
Column() {
// 根据样式变体应用不同的布局和样式
switch (style) {
case CardStyle.FEATURED:
// 特色样式:大图、完整信息、特殊背景
// ...
break
case CardStyle.COMPACT:
// 紧凑样式:小图、最少信息
// ...
break
case CardStyle.MINIMAL:
// 极简样式:只有图片和标题
// ...
break
default:
// 基本样式:标准布局
// ...
break
}
}
}
4.2 高级动画效果
为瀑布流添加精美的动画效果,提升用户体验:
4.2.1 卡片加载动画
// 在FlowItem中添加加载动画
FlowItem() {
Column() {
// ...
}
.opacity(this.isItemLoaded(image.id) ? 1 : 0)
.animation({
duration: 300,
curve: Curve.EaseOut,
delay: this.getItemLoadDelay(image.id) // 错开延迟,实现瀑布效果
})
}
// 控制项目加载状态
@State loadedItems: Set<number> = new Set()
isItemLoaded(id: number): boolean {
return this.loadedItems.has(id)
}
getItemLoadDelay(id: number): number {
// 根据项目在数组中的位置计算延迟
const index = this.imageItems.findIndex(item => item.id === id)
return index * 50 // 每项错开50ms
}
// 在页面显示时触发加载动画
onPageShow() {
// 清空已加载项
this.loadedItems.clear()
// 延迟添加项目,触发动画
setTimeout(() => {
this.imageItems.forEach(item => {
this.loadedItems.add(item.id)
})
}, 100)
}
4.2.2 交互反馈动画
// 在FlowItem中添加点击反馈动画
.onClick(() => {
animateTo({
duration: 100,
curve: Curve.EaseIn,
iterations: 1,
playMode: PlayMode.Normal,
onFinish: () => {
this.selectedImage = image
this.showImageDetail = true
}
}, () => {
this.itemScales.set(image.id, 0.95) // 缩小效果
})
animateTo({
duration: 100,
curve: Curve.EaseOut,
delay: 100,
iterations: 1,
playMode: PlayMode.Normal
}, () => {
this.itemScales.set(image.id, 1.0) // 恢复原始大小
})
})
.scale({ x: this.itemScales.get(image.id) || 1.0, y: this.itemScales.get(image.id) || 1.0 })
5. 高级交互功能
5.1 拖拽排序
实现瀑布流卡片的拖拽排序功能:
// 添加拖拽状态
@State isDragging: boolean = false
@State draggedItemId: number = -1
@State dragPosition: { x: number, y: number } = { x: 0, y: 0 }
// 在FlowItem中添加拖拽手势
.gesture(
PanGesture()
.onActionStart((event: GestureEvent) => {
if (this.editMode) { // 只在编辑模式下启用拖拽
this.isDragging = true
this.draggedItemId = image.id
this.dragPosition = { x: event.offsetX, y: event.offsetY }
}
})
.onActionUpdate((event: GestureEvent) => {
if (this.isDragging && this.draggedItemId === image.id) {
this.dragPosition = { x: event.offsetX, y: event.offsetY }
// 计算拖拽位置,判断是否需要交换位置
this.calculateDragSwap(event.offsetX, event.offsetY)
}
})
.onActionEnd(() => {
if (this.isDragging && this.draggedItemId === image.id) {
this.isDragging = false
this.draggedItemId = -1
// 完成拖拽排序
this.finalizeDragSort()
}
})
)
5.2 下拉刷新与上拉加载
实现瀑布流的下拉刷新和上拉加载更多功能:
// 下拉刷新状态
@State isRefreshing: boolean = false
@State isLoadingMore: boolean = false
// 在主布局中添加下拉刷新
Refresh({ refreshing: $$this.isRefreshing }) {
Column() {
// 瀑布流内容
WaterFlow() {
// ...
}
// ...
// 底部加载更多
if (this.hasMoreData) {
Row() {
LoadingProgress()
.width(24)
.height(24)
.color('#999999')
Text('加载更多...')
.fontSize(14)
.fontColor('#999999')
.margin({ left: 8 })
}
.width('100%')
.height(60)
.justifyContent(FlexAlign.Center)
.visibility(this.isLoadingMore ? Visibility.Visible : Visibility.None)
}
}
.onRefreshing(() => {
// 模拟刷新数据
setTimeout(() => {
this.refreshData()
this.isRefreshing = false
}, 1500)
})
}
// 监听滚动到底部,加载更多
onReachEnd() {
if (!this.isLoadingMore && this.hasMoreData) {
this.isLoadingMore = true
// 模拟加载更多数据
setTimeout(() => {
this.loadMoreData()
this.isLoadingMore = false
}, 1500)
}
}
6. 高级数据处理
6.1 虚拟化渲染
对于大量数据的瀑布流,可以实现虚拟化渲染,只渲染可见区域的内容,提升性能:
// 虚拟化渲染相关状态
@State visibleStartIndex: number = 0
@State visibleEndIndex: number = 20 // 初始可见数量
// 获取当前可见的图片数据
getVisibleImages(): ImageItem[] {
const filtered = this.getFilteredImages()
return filtered.slice(this.visibleStartIndex, this.visibleEndIndex)
}
// 监听滚动事件,更新可见范围
onScroll(scrollOffset: number, scrollState: ScrollState) {
// 根据滚动位置计算可见范围
const itemHeight = 300 // 估计的平均项高度
const screenHeight = px2vp(getContext(this).height)
const visibleItems = Math.ceil(screenHeight / itemHeight) + 5 // 多渲染几个,保证流畅
const newStartIndex = Math.max(0, Math.floor(scrollOffset / itemHeight) - 5)
const newEndIndex = newStartIndex + visibleItems
if (newStartIndex !== this.visibleStartIndex || newEndIndex !== this.visibleEndIndex) {
this.visibleStartIndex = newStartIndex
this.visibleEndIndex = newEndIndex
}
}
6.2 数据分组展示
根据不同的分类或日期,对瀑布流数据进行分组展示:
// 获取分组后的图片数据
getGroupedImages(): Map<string, ImageItem[]> {
const filtered = this.getFilteredImages()
const grouped = new Map<string, ImageItem[]>()
// 根据分类分组
filtered.forEach(image => {
if (!grouped.has(image.category)) {
grouped.set(image.category, [])
}
grouped.get(image.category)?.push(image)
})
return grouped
}
// 在布局中使用分组数据
build() {
Column() {
// 顶部搜索和筛选
// ...
// 分组瀑布流
ForEach(Array.from(this.getGroupedImages().keys()), (category: string) => {
Column() {
// 分组标题
Text(category)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.width('100%')
.padding({ left: 16, top: 16, bottom: 8 })
// 该分组的瀑布流
WaterFlow() {
ForEach(this.getGroupedImages().get(category) || [], (image: ImageItem) => {
FlowItem() {
// 图片卡片内容
// ...
}
})
}
.columnsTemplate('1fr 1fr')
// ...
}
})
}
}
7. 瀑布流布局的高级应用场景
7.1 社交媒体图片流
瀑布流布局非常适合社交媒体应用的图片展示,可以实现类似Pinterest、小红书等应用的图片流效果:
特点 | 实现方式 |
---|---|
不规则图片网格 | 使用WaterFlow组件,保持图片原始宽高比 |
卡片式布局 | 为每个FlowItem添加圆角、阴影和边距 |
互动功能 | 添加点赞、评论、收藏等互动按钮 |
无限滚动 | 实现上拉加载更多功能 |
沉浸式浏览 | 点击图片显示全屏详情 |
7.2 电商商品展示
瀑布流布局也适合电商应用的商品展示:
特点 | 实现方式 |
---|---|
商品卡片 | 展示商品图片、名称、价格和评分 |
多样化布局 | 根据商品类型或促销状态使用不同的卡片样式 |
筛选与排序 | 添加分类筛选和价格排序功能 |
快速操作 | 添加加入购物车、收藏等快捷按钮 |
标签展示 | 显示折扣、新品、热卖等标签 |
8. 总结
在下一篇教程中,我们将探讨瀑布流布局的高级应用,包括复杂业务场景实现、自定义瀑布流算法、高级动画效果等内容,帮助你掌握瀑布流布局的高级应用技巧。
00
- 0回答
- 4粉丝
- 0关注
相关话题
- 178.[HarmonyOS NEXT 实战案例八:Grid] 瀑布流网格布局基础篇
- 180.[HarmonyOS NEXT 实战案例八:Grid] 瀑布流网格布局高级篇
- 161. [HarmonyOS NEXT 实战案例二:Grid] 照片相册网格布局:进阶篇
- 167.[HarmonyOS NEXT 实战案例四:Grid] 可滚动网格布局进阶篇
- 173.[HarmonyOS NEXT 实战案例六:Grid] 响应式网格布局 - 进阶篇
- 170.[HarmonyOS NEXT 实战案例五:Grid] 动态网格布局进阶篇
- 185.[HarmonyOS NEXT 实战案例十:Grid] 仪表板网格布局进阶篇
- 176.[HarmonyOS NEXT 实战案例七:Grid] 嵌套网格布局进阶篇:高级布局与交互技巧
- [HarmonyOS NEXT 实战案例十八] 日历日程视图网格布局(进阶篇)
- 164.[HarmonyOS NEXT 实战案例三:Grid] 不规则网格布局进阶篇:新闻应用高级布局与交互
- [HarmonyOS NEXT 实战案例十五] 电商分类导航网格布局(进阶篇)
- 182.[HarmonyOS NEXT 实战案例九:Grid] 电商网格布局进阶篇:打造高级交互与视觉体验
- 158.[HarmonyOS NEXT 实战案例一:Grid] 基础网格布局进阶篇:电商商品列表的交互与状态管理
- 160.[HarmonyOS NEXT 实战案例二:Grid] 照片相册网格布局:基础篇
- 171.[HarmonyOS NEXT 实战案例五:Grid] 动态网格布局高级篇