项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star
在移动应用开发中,网格布局是一种常见且实用的UI布局方式,特别适合展示图片、卡片等内容。当网格内容较多时,需要结合滚动功能,让用户能够流畅地浏览所有内容。本教程将详细讲解HarmonyOS NEXT中可滚动网格布局的实现方法,通过一个应用商店首页的案例,帮助开发者掌握Grid组件与Scroller的结合使用技巧。
可滚动网格布局是指使用Grid组件作为容器,并通过Scroller控制器实现内容滚动的布局方式。当网格内容超出屏幕显示范围时,用户可以通过滑动操作查看更多内容。这种布局方式特别适合展示大量同类型但又各自独立的内容,如应用列表、商品展示、图片库等。
在HarmonyOS NEXT中,Grid是网格容器组件,用于创建网格布局;而Scroller是滚动控制器,可以绑定到Grid等容器组件上,控制其滚动行为。两者结合使用,可以实现内容丰富、交互流畅的可滚动网格界面。
组件/控制器 | 作用 | 主要特性 |
---|---|---|
Grid | 网格容器组件 | 行列布局、间距控制、模板定义 |
GridItem | 网格子项组件 | 内容展示、事件处理、样式定义 |
Scroller | 滚动控制器 | 滚动控制、事件监听、滚动位置管理 |
本教程以一个应用商店首页为例,展示如何实现可滚动网格布局。该页面包含顶部搜索栏、应用分类标签、推荐应用网格列表和底部导航栏。
Column
├── 顶部搜索栏 (Row)
├── 应用分类标签 (Row + ForEach)
├── 推荐应用标题 (Row)
├── 推荐应用网格 (Grid + ForEach + GridItem)
└── 底部导航栏 (Row)
在实现可滚动网格布局之前,首先需要定义数据模型,用于存储和管理网格中显示的内容。在本案例中,我们定义了两个接口: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两个状态变量,用于存储实际数据。
首先,需要创建一个Scroller实例,用于控制Grid组件的滚动行为:
private scroller: Scroller = new Scroller()
在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事件监听,用于记录当前显示的第一个应用索引。
在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添加了点击事件处理。
为了复用星级评分的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参数,根据评分值显示对应数量的星星,并在右侧显示具体评分数值。
.columnsTemplate('1fr') // 单列布局
.rowsGap(16) // 行间距
.onScrollIndex((first: number) => {
console.log(`当前显示的第一个应用索引: ${first}`)
})
private scroller: Scroller = new Scroller()
// 在build方法中绑定到Grid
Grid(this.scroller) {
// 网格内容
}
方法/属性 | 说明 | 示例 |
---|---|---|
scrollTo | 滚动到指定位置 | scroller.scrollTo({xOffset: 0, yOffset: 100}) |
scrollEdge | 滚动到边缘 | scroller.scrollEdge(Edge.Top) |
scrollPage | 按页滚动 | scroller.scrollPage({next: true}) |
currentOffset | 获取当前滚动偏移量 | let offset = scroller.currentOffset() |
问题 | 解决方案 |
---|---|
网格项大小不一致 | 使用固定的宽高比,或者在GridItem中设置minHeight |
滚动不流畅 | 减少网格项的复杂度,优化图片加载 |
网格项内容溢出 | 使用maxLines和textOverflow控制文本显示 |
在下一篇教程中,我们将深入探讨可滚动网格布局的进阶技巧,包括多列布局、动态调整列数、网格项动画效果等内容,敬请期待!