169.[HarmonyOS NEXT 实战案例五:Grid] 动态网格布局基础篇

2025-06-30 22:46:37
104次阅读
0个评论

[HarmonyOS NEXT 实战案例五:Grid] 动态网格布局基础篇

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

效果演示

image.png

在移动应用开发中,网格布局是展示内容的常用方式,特别是对于图片、卡片等元素的展示。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. 页面结构设计

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

  1. 顶部导航栏:包含标题、搜索按钮和相机按钮
  2. 分类标签栏:用于筛选不同类别的内容
  3. 瀑布流网格:展示图片卡片
  4. 底部导航栏:包含首页、发现、发布、消息和我的等功能入口

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
    })
}

每个卡片包含以下部分:

  1. 图片区域:显示主图片,高度根据数据模型中的height属性动态设置
  2. 点赞按钮:位于图片右上角,可以切换点赞状态
  3. 内容区域:包含标题、描述、标签和作者信息等

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 })
}

内容区域的布局特点:

  1. 标题和描述:使用Text组件显示,设置最大行数和溢出处理
  2. 标签:使用ForEach循环渲染标签,最多显示3个
  3. 作者信息:左侧显示作者头像和名称
  4. 互动数据:右侧显示点赞数和评论数,使用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内容设计等方面进行了详细讲解,并实现了分类筛选、搜索、点赞等交互功能。

收藏00

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