暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

170.[HarmonyOS NEXT 实战案例五:Grid] 动态网格布局进阶篇

原创 若城 2025-06-29
138

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

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

效果演示

在基础篇中,我们学习了如何使用HarmonyOS NEXT的Grid组件实现基本的瀑布流布局。本篇教程将深入探讨动态网格布局的进阶技巧,包括Grid组件的高级配置、自定义布局策略、交互优化等内容,帮助你构建更加灵活、高效的瀑布流界面。

1. Grid组件高级配置

1.1 列模板与行模板

HarmonyOS NEXT的Grid组件提供了灵活的列模板和行模板配置,可以实现更复杂的布局效果。

列模板(columnsTemplate)

在基础篇中,我们使用了简单的两列等宽布局:

.columnsTemplate('1fr 1fr')

实际上,columnsTemplate支持更复杂的配置:

配置方式 示例 说明
等宽列 ‘1fr 1fr 1fr’ 三列等宽布局
固定宽度列 ‘100px 1fr 100px’ 左右固定宽度,中间自适应
比例列 ‘2fr 1fr’ 左列占2份,右列占1份
混合配置 ‘100px 2fr 1fr’ 左侧固定宽度,中右按比例分配

我们可以根据实际需求调整列模板,例如实现三列瀑布流:

.columnsTemplate('1fr 1fr 1fr')

或者实现左右两列不等宽的布局:

.columnsTemplate('1.5fr 1fr')

行模板(rowsTemplate)

除了列模板,Grid还支持行模板配置:

.rowsTemplate('1fr 2fr 1fr')

这在瀑布流布局中较少使用,因为瀑布流通常是根据内容高度自动调整的。但在某些特殊场景下,可以使用行模板实现特定的布局效果。

1.2 网格项位置控制

Grid组件允许精确控制GridItem的位置,通过以下属性:

属性 说明
rowStart 指定网格项起始行号
rowEnd 指定网格项结束行号
columnStart 指定网格项起始列号
columnEnd 指定网格项结束列号

例如,我们可以让某个特定的GridItem跨越两列:

GridItem() { // 内容 } .columnStart(1) .columnEnd(3)

这在实现特殊布局时非常有用,例如在瀑布流中插入一个横幅广告。

1.3 网格滚动控制

Grid组件提供了丰富的滚动控制属性:

Grid() { // GridItems } .scrollBar(BarState.Auto) // 自动显示滚动条 .scrollBarColor(Color.Gray) // 滚动条颜色 .scrollBarWidth(10) // 滚动条宽度 .edgeEffect(EdgeEffect.Spring) // 滚动到边缘时的效果

滚动效果(edgeEffect)支持以下选项:

选项 说明
EdgeEffect.Spring 弹性效果,滚动到边缘时会有回弹
EdgeEffect.None 无效果
EdgeEffect.Fade 淡出效果

1.4 滚动事件处理

在基础篇中,我们使用了onScrollIndex事件来监听滚动位置:

.onScrollIndex((first: number) => { console.log(`当前显示的第一个图片索引: ${first}`) })

Grid组件还提供了更多滚动事件:

.onScroll((xOffset: number, yOffset: number) => { // 处理滚动事件,xOffset和yOffset是当前滚动位置 }) .onScrollStop(() => { // 滚动停止时触发 }) .onReachStart(() => { // 滚动到顶部时触发 }) .onReachEnd(() => { // 滚动到底部时触发,可用于实现加载更多 })

这些事件可以用于实现各种高级功能,例如:

  • 滚动到底部时加载更多数据
  • 滚动时显示/隐藏顶部导航栏
  • 滚动停止时加载图片,提高性能

2. 高级布局策略

2.1 动态调整列数

在不同尺寸的设备上,我们可能需要显示不同数量的列。可以通过监听设备宽度动态调整列模板:

@State columnsCount: number = 2 aboutToAppear() { // 获取设备宽度 const deviceWidth = px2vp(window.getWindowWidth()) // 根据宽度设置列数 if (deviceWidth < 600) { this.columnsCount = 2 // 窄屏设备显示2列 } else if (deviceWidth < 840) { this.columnsCount = 3 // 中等宽度设备显示3列 } else { this.columnsCount = 4 // 宽屏设备显示4列 } } build() { Grid() { // GridItems } .columnsTemplate(this.getColumnsTemplate()) } getColumnsTemplate(): string { return '1fr '.repeat(this.columnsCount).trim() }

这样,我们的瀑布流布局就能够自适应不同尺寸的设备。

2.2 混合布局策略

在某些场景下,我们可能需要在瀑布流中插入特殊的布局元素,例如广告横幅、分组标题等。可以通过条件渲染和位置控制实现:

Grid() { // 特殊横幅(跨越所有列) GridItem() { Banner() } .columnSpan(this.columnsCount) // 跨越所有列 // 普通图片卡片 ForEach(this.getFilteredPhotos(), (item: PhotoItems) => { GridItem() { PhotoCard(item) } }) }

2.3 分组瀑布流

我们可以实现分组的瀑布流布局,每个分组有自己的标题:

Grid() { ForEach(this.getPhotoGroups(), (group) => { // 分组标题(跨越所有列) GridItem() { Text(group.title) .fontSize(18) .fontWeight(FontWeight.Bold) .width('100%') .padding(16) } .columnSpan(this.columnsCount) // 分组内的图片卡片 ForEach(group.items, (item: PhotoItems) => { GridItem() { PhotoCard(item) } }) }) }

3. 高级交互功能

3.1 下拉刷新

我们可以结合Refresh组件实现下拉刷新功能:

@State refreshing: boolean = false build() { Refresh({ refreshing: $$this.refreshing }) { Grid() { // GridItems } // Grid配置 } .onRefresh(() => { this.refreshData() }) } async refreshData() { this.refreshing = true // 模拟网络请求 await new Promise(resolve => setTimeout(resolve, 2000)) // 更新数据 this.photoItems = this.getRandomPhotos() this.refreshing = false }

3.2 加载更多

结合onReachEnd事件,我们可以实现滚动到底部加载更多数据:

@State loading: boolean = false @State hasMore: boolean = true build() { Column() { Grid() { // GridItems // 加载更多指示器 if (this.loading || this.hasMore) { GridItem() { if (this.loading) { LoadingProgress() .width(24) .height(24) } else { Text('上拉加载更多') .fontSize(14) .fontColor('#999999') } } .columnSpan(this.columnsCount) .height(50) .justifyContent(FlexAlign.Center) } } .onReachEnd(() => { if (!this.loading && this.hasMore) { this.loadMore() } }) } } async loadMore() { if (this.loading || !this.hasMore) return this.loading = true // 模拟网络请求 await new Promise(resolve => setTimeout(resolve, 2000)) // 加载更多数据 const newItems = this.getMorePhotos() if (newItems.length > 0) { this.photoItems = [...this.photoItems, ...newItems] } else { this.hasMore = false } this.loading = false }

3.3 图片懒加载

为了提高性能,我们可以实现图片的懒加载,只有当图片进入可视区域时才加载:

@Component struct LazyImage { @Prop src: Resource @Prop width: string | number @Prop height: string | number @State loaded: boolean = false @State visible: boolean = false aboutToAppear() { // 使用IntersectionObserver检测可见性 // 这里简化处理,实际应使用更复杂的逻辑 setTimeout(() => { this.visible = true }, 100) } build() { Stack() { if (this.visible) { Image(this.src) .width(this.width) .height(this.height) .objectFit(ImageFit.Cover) .opacity(this.loaded ? 1 : 0) .onComplete(() => { this.loaded = true }) } if (!this.loaded) { Column() { LoadingProgress() .width(24) .height(24) } .width('100%') .height('100%') .backgroundColor('#F0F0F0') .justifyContent(FlexAlign.Center) } } .width(this.width) .height(this.height) } }

然后在GridItem中使用LazyImage替代普通Image:

GridItem() { Column() { Stack({ alignContent: Alignment.TopEnd }) { LazyImage({ src: item.imageUrl, width: '100%', height: item.height }) .borderRadius({ topLeft: 12, topRight: 12 }) // 点赞按钮 } // 内容区域 } }

4. 动画与过渡效果

4.1 网格项动画

我们可以为GridItem添加动画效果,使界面更加生动:

@State animationIndex: number = 0 build() { Grid() { ForEach(this.getFilteredPhotos(), (item: PhotoItems, index) => { GridItem() { PhotoCard(item) } .opacity(this.animationIndex > index ? 1 : 0) .translate({ y: this.animationIndex > index ? 0 : 20 }) .animation({ delay: 50 * index, duration: 300, curve: Curve.EaseOut }) }) } } aboutToAppear() { // 触发动画 setTimeout(() => { this.animationIndex = this.photoItems.length }, 100) }

这样,网格项会依次淡入并从下方滑入,创造出瀑布流的动态效果。

4.2 滚动过渡效果

我们可以根据滚动位置添加过渡效果,例如顶部导航栏的透明度变化:

@State scrollY: number = 0 build() { Column() { // 顶部导航栏 Row() { // 导航栏内容 } .width('100%') .height(56) .padding({ left: 16, right: 16 }) .backgroundColor(Color.lerp(new Color('#FFFFFF00'), new Color('#FFFFFFFF'), Math.min(this.scrollY / 100, 1))) .shadow({ radius: 8, color: `rgba(0, 0, 0, ${Math.min(this.scrollY / 100, 0.1)})`, offsetY: 2 }) // 网格内容 Grid() { // GridItems } .onScroll((_, yOffset) => { this.scrollY = yOffset }) } }

这样,当用户向下滚动时,顶部导航栏会从透明逐渐变为不透明,并添加阴影效果。

5. 自定义网格项组件

为了提高代码的可维护性和复用性,我们可以将网格项封装为独立的组件:

@Component struct PhotoCard { @ObjectLink item: PhotoItems @Consume('toggleLike') toggleLike: (id: number) => void @Consume('formatNumber') formatNumber: (num: number) => string build() { Column() { // 图片部分 Stack({ alignContent: Alignment.TopEnd }) { Image(this.item.imageUrl) .width('100%') .height(this.item.height) .objectFit(ImageFit.Cover) .borderRadius({ topLeft: 12, topRight: 12 }) // 点赞按钮 Button() { Image(this.item.isLiked ? $r('app.media.heart_filled') : $r('app.media.heart_outline')) .width(20) .height(20) .fillColor(this.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(this.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 }) } }

然后在主组件中使用:

@Provide('toggleLike') toggleLike = this.toggleLike.bind(this) @Provide('formatNumber') formatNumber = this.formatNumber.bind(this) build() { Grid() { ForEach(this.getFilteredPhotos(), (item: PhotoItems) => { GridItem() { PhotoCard({ item: item }) } }) } }

这样可以使代码结构更加清晰,便于维护和扩展。

总结

本教程深入探讨了HarmonyOS NEXT中动态网格布局的进阶技巧,包括Grid组件的高级配置、自定义布局策略、交互优化、动画效果等内容。通过这些技巧,你可以构建更加灵活、高效、美观的瀑布流界面。

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论