179.[HarmonyOS NEXT 实战案例八:Grid] 瀑布流网格布局进阶篇

2025-06-30 22:59:48
104次阅读
0个评论

[HarmonyOS NEXT 实战案例八:Grid] 瀑布流网格布局进阶篇

项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star

效果演示

image.png

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

登录 后评论。没有帐号? 注册 一个。