178.[HarmonyOS NEXT 实战案例八:Grid] 瀑布流网格布局基础篇

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

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

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

效果演示

image.png

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 整体布局

我们的瀑布流页面包含以下几个主要部分:

  1. 顶部搜索和筛选区域
  2. 瀑布流网格区域
  3. 图片详情对话框(点击图片时显示)

整体结构如下:

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 实现技巧

  1. 保持原始宽高比:使用aspectRatio属性保持图片的原始宽高比,避免图片变形。

    Image(image.image)
        .width('100%')
        .aspectRatio(image.width / image.height)
    
  2. 合理设置间距:使用columnsGaprowsGap设置合适的间距,提升视觉效果。

    WaterFlow()
        .columnsGap(8)
        .rowsGap(8)
    
  3. 添加阴影效果:为卡片添加轻微的阴影效果,增强层次感。

    .shadow({
        radius: 6,
        color: 'rgba(0, 0, 0, 0.1)',
        offsetX: 0,
        offsetY: 2
    })
    
  4. 文本溢出处理:对标题等文本使用maxLinestextOverflow属性,确保布局整齐。

    Text(image.title)
        .maxLines(2)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
    

8. 总结

在下一篇教程中,我们将深入探讨瀑布流布局的进阶技巧,包括性能优化、动画效果、交互体验等方面的内容。

收藏00

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