166.[HarmonyOS NEXT 实战案例四:Grid] 可滚动网格布局基础篇

2025-06-30 22:44:43
104次阅读
0个评论

[HarmonyOS NEXT 实战案例四:Grid] 可滚动网格布局基础篇

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

效果演示

image.png

1. 引言

在移动应用开发中,网格布局是一种常见且实用的UI布局方式,特别适合展示图片、卡片等内容。当网格内容较多时,需要结合滚动功能,让用户能够流畅地浏览所有内容。本教程将详细讲解HarmonyOS NEXT中可滚动网格布局的实现方法,通过一个应用商店首页的案例,帮助开发者掌握Grid组件与Scroller的结合使用技巧。

2. 可滚动网格布局概述

2.1 什么是可滚动网格布局?

可滚动网格布局是指使用Grid组件作为容器,并通过Scroller控制器实现内容滚动的布局方式。当网格内容超出屏幕显示范围时,用户可以通过滑动操作查看更多内容。这种布局方式特别适合展示大量同类型但又各自独立的内容,如应用列表、商品展示、图片库等。

2.2 Grid与Scroller的关系

在HarmonyOS NEXT中,Grid是网格容器组件,用于创建网格布局;而Scroller是滚动控制器,可以绑定到Grid等容器组件上,控制其滚动行为。两者结合使用,可以实现内容丰富、交互流畅的可滚动网格界面。

组件/控制器 作用 主要特性
Grid 网格容器组件 行列布局、间距控制、模板定义
GridItem 网格子项组件 内容展示、事件处理、样式定义
Scroller 滚动控制器 滚动控制、事件监听、滚动位置管理

3. 案例分析:应用商店首页

本教程以一个应用商店首页为例,展示如何实现可滚动网格布局。该页面包含顶部搜索栏、应用分类标签、推荐应用网格列表和底部导航栏。

3.1 页面结构概览

Column
├── 顶部搜索栏 (Row)
├── 应用分类标签 (Row + ForEach)
├── 推荐应用标题 (Row)
├── 推荐应用网格 (Grid + ForEach + GridItem)
└── 底部导航栏 (Row)

3.2 数据模型定义

在实现可滚动网格布局之前,首先需要定义数据模型,用于存储和管理网格中显示的内容。在本案例中,我们定义了两个接口:Category(应用分类)和FeaturedApp(推荐应用)。

interface Category {
    id: number,
    name: string,
    icon: Resource,
    color: string
}

interface FeaturedApp {
    id: number,
    name: string,
    developer: string,
    icon: Resource,
    rating: number,
    downloads: string,
    size: string,
    category: string,
    isFree: boolean,
    screenshots: Resource[]
}

这两个接口定义了应用分类和推荐应用的数据结构,包含了展示所需的各种属性。在组件中,我们使用@State装饰器定义了categories和featuredApps两个状态变量,用于存储实际数据。

4. 实现可滚动网格布局

4.1 创建滚动控制器

首先,需要创建一个Scroller实例,用于控制Grid组件的滚动行为:

private scroller: Scroller = new Scroller()

4.2 构建网格容器

在build方法中,我们使用Grid组件创建网格容器,并将scroller绑定到Grid上:

Grid(this.scroller) {
    // 网格内容
}
.columnsTemplate('1fr') // 单列布局
.rowsGap(16)
.width('100%')
.layoutWeight(1)
.padding({ left: 16, right: 16, bottom: 16 })
.backgroundColor('#F8F8F8')
.onScrollIndex((first: number) => {
    console.log(`当前显示的第一个应用索引: ${first}`)
})

这里我们设置了Grid的列模板为'1fr'(单列布局),行间距为16,宽度为100%,并使用layoutWeight(1)使Grid占据剩余空间。同时,我们还设置了内边距和背景色,并添加了onScrollIndex事件监听,用于记录当前显示的第一个应用索引。

4.3 创建网格项

在Grid容器中,我们使用ForEach循环遍历featuredApps数组,为每个应用创建一个GridItem:

ForEach(this.featuredApps, (app:FeaturedApp) => {
    GridItem() {
        Column() {
            // 应用图标和基本信息
            Row() {
                // 应用图标
                Image(app.icon)
                    .width(60)
                    .height(60)
                    .borderRadius(12)
                    .shadow({
                        radius: 4,
                        color: 'rgba(0, 0, 0, 0.2)',
                        offsetX: 0,
                        offsetY: 2
                    })

                // 应用信息
                Column() {
                    // 应用名称
                    Text(app.name)
                        .fontSize(16)
                        .fontWeight(FontWeight.Bold)
                        .fontColor('#333333')
                        .maxLines(1)
                        .textOverflow({ overflow: TextOverflow.Ellipsis })

                    // 开发者
                    Text(app.developer)
                        .fontSize(12)
                        .fontColor('#666666')
                        .margin({ top: 2 })

                    // 星级评分
                    this.StarRating(app.rating)

                    // 下载量和大小
                    Row() {
                        Text(app.downloads)
                            .fontSize(10)
                            .fontColor('#999999')

                        Text('•')
                            .fontSize(10)
                            .fontColor('#999999')
                            .margin({ left: 4, right: 4 })

                        Text(app.size)
                            .fontSize(10)
                            .fontColor('#999999')
                    }
                    .margin({ top: 4 })
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)
                .margin({ left: 12 })

                // 获取/购买按钮
                Button(app.isFree ? '获取' : '购买')
                    .fontSize(14)
                    .fontColor('#FFFFFF')
                    .backgroundColor('#007AFF')
                    .borderRadius(16)
                    .width(60)
                    .height(32)
            }
            .width('100%')
            .alignItems(VerticalAlign.Top)

            // 应用截图
            Row() {
                ForEach(app.screenshots.slice(0, 3), (screenshot:Resource, index) => {
                    Image(screenshot)
                        .width(80)
                        .height(140)
                        .objectFit(ImageFit.Cover)
                        .borderRadius(8)
                        .margin({ right: index < 2 ? 8 : 0 })
                })
            }
            .width('100%')
            .margin({ top: 16 })

            // 分类标签
            Row() {
                Text(app.category)
                    .fontSize(10)
                    .fontColor('#007AFF')
                    .backgroundColor('rgba(0, 122, 255, 0.1)')
                    .padding({ left: 8, right: 8, top: 4, bottom: 4 })
                    .borderRadius(8)

                if (!app.isFree) {
                    Text('付费')
                        .fontSize(10)
                        .fontColor('#FF9500')
                        .backgroundColor('rgba(255, 149, 0, 0.1)')
                        .padding({ left: 8, right: 8, top: 4, bottom: 4 })
                        .borderRadius(8)
                        .margin({ left: 8 })
                }

                Blank()
            }
            .width('100%')
            .margin({ top: 12 })
        }
        .width('100%')
        .padding(16)
        .backgroundColor('#FFFFFF')
        .borderRadius(16)
        .shadow({
            radius: 8,
            color: 'rgba(0, 0, 0, 0.1)',
            offsetX: 0,
            offsetY: 2
        })
    }
    .onClick(() => {
        console.log(`点击应用: ${app.name}`)
    })
})

每个GridItem包含一个Column,用于垂直排列应用信息。在Column中,我们依次展示应用的图标和基本信息、应用截图和分类标签。同时,我们还为GridItem添加了点击事件处理。

4.4 自定义构建器:星级评分

为了复用星级评分的UI逻辑,我们创建了一个自定义构建器StarRating:

@Builder
StarRating(rating: number) {
    Row() {
        ForEach([1,2,3,4,5], (star:number) => {
            Image(star <= rating ? $r('app.media.heart_filled') : $r('app.media.heart_outline'))
                .width(12)
                .height(12)
                .fillColor(star <= rating ? '#FFD700' : '#E0E0E0')
        })

        Text(rating.toString())
            .fontSize(12)
            .fontColor('#666666')
            .margin({ left: 4 })
    }
}

这个构建器接收一个rating参数,根据评分值显示对应数量的星星,并在右侧显示具体评分数值。

5. Grid组件关键属性解析

5.1 列模板与行间距

.columnsTemplate('1fr') // 单列布局
.rowsGap(16) // 行间距
  • columnsTemplate:定义网格的列模板,'1fr'表示单列布局,每列占据可用空间的一份。
  • rowsGap:定义行与行之间的间距,单位为vp。

5.2 滚动事件监听

.onScrollIndex((first: number) => {
    console.log(`当前显示的第一个应用索引: ${first}`)
})
  • onScrollIndex:当网格滚动时触发,参数first表示当前显示的第一个网格项的索引。

6. 滚动控制器(Scroller)的使用

6.1 创建与绑定

private scroller: Scroller = new Scroller()

// 在build方法中绑定到Grid
Grid(this.scroller) {
    // 网格内容
}

6.2 常用方法与属性

方法/属性 说明 示例
scrollTo 滚动到指定位置 scroller.scrollTo({xOffset: 0, yOffset: 100})
scrollEdge 滚动到边缘 scroller.scrollEdge(Edge.Top)
scrollPage 按页滚动 scroller.scrollPage({next: true})
currentOffset 获取当前滚动偏移量 let offset = scroller.currentOffset()

7. 布局技巧与最佳实践

7.1 网格布局设计原则

  1. 内容优先:根据内容特性选择合适的网格布局方式。
  2. 一致性:保持网格项的视觉一致性,包括大小、间距、样式等。
  3. 响应式:考虑不同屏幕尺寸下的显示效果,适当调整列数和大小。
  4. 交互反馈:为网格项添加适当的点击反馈,提升用户体验。

7.2 性能优化技巧

  1. 懒加载:对于大量数据,可以结合onScrollIndex实现懒加载。
  2. 合理分页:避免一次加载过多数据,可以实现分页加载。
  3. 图片优化:使用合适大小的图片,避免加载过大的图片资源。

7.3 常见问题解决方案

问题 解决方案
网格项大小不一致 使用固定的宽高比,或者在GridItem中设置minHeight
滚动不流畅 减少网格项的复杂度,优化图片加载
网格项内容溢出 使用maxLines和textOverflow控制文本显示

8. 总结

在下一篇教程中,我们将深入探讨可滚动网格布局的进阶技巧,包括多列布局、动态调整列数、网格项动画效果等内容,敬请期待!

收藏00

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