170. [HarmonyOS NEXT 实战案例五:Grid] 动态网格布局进阶篇_grid布局 3列 动态行
[HarmonyOS NEXT 实战案例五:Grid] 动态网格布局进阶篇
文章目录
- [HarmonyOS NEXT 实战案例五:Grid] 动态网格布局进阶篇
-
- 效果演示
- 1. Grid组件高级配置
-
- 1.1 列模板与行模板
-
- 列模板(columnsTemplate)
- 行模板(rowsTemplate)
- 1.2 网格项位置控制
- 1.3 网格滚动控制
- 1.4 滚动事件处理
- 2. 高级布局策略
-
- 2.1 动态调整列数
- 2.2 混合布局策略
- 2.3 分组瀑布流
- 3. 高级交互功能
-
- 3.1 下拉刷新
- 3.2 加载更多
- 3.3 图片懒加载
- 4. 动画与过渡效果
-
- 4.1 网格项动画
- 4.2 滚动过渡效果
- 5. 自定义网格项组件
- 总结
项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star
效果演示
在基础篇中,我们学习了如何使用HarmonyOS NEXT的Grid组件实现基本的瀑布流布局。本篇教程将深入探讨动态网格布局的进阶技巧,包括Grid组件的高级配置、自定义布局策略、交互优化等内容,帮助你构建更加灵活、高效的瀑布流界面。
1. Grid组件高级配置
1.1 列模板与行模板
HarmonyOS NEXT的Grid组件提供了灵活的列模板和行模板配置,可以实现更复杂的布局效果。
列模板(columnsTemplate)
在基础篇中,我们使用了简单的两列等宽布局:
.columnsTemplate(\'1fr 1fr\')
实际上,columnsTemplate支持更复杂的配置:
我们可以根据实际需求调整列模板,例如实现三列瀑布流:
.columnsTemplate(\'1fr 1fr 1fr\')
或者实现左右两列不等宽的布局:
.columnsTemplate(\'1.5fr 1fr\')
行模板(rowsTemplate)
除了列模板,Grid还支持行模板配置:
.rowsTemplate(\'1fr 2fr 1fr\')
这在瀑布流布局中较少使用,因为瀑布流通常是根据内容高度自动调整的。但在某些特殊场景下,可以使用行模板实现特定的布局效果。
1.2 网格项位置控制
Grid组件允许精确控制GridItem的位置,通过以下属性:
例如,我们可以让某个特定的GridItem跨越两列:
GridItem() { // 内容}.columnStart(1).columnEnd(3)
这在实现特殊布局时非常有用,例如在瀑布流中插入一个横幅广告。
1.3 网格滚动控制
Grid组件提供了丰富的滚动控制属性:
Grid() { // GridItems}.scrollBar(BarState.Auto) // 自动显示滚动条.scrollBarColor(Color.Gray) // 滚动条颜色.scrollBarWidth(10) // 滚动条宽度.edgeEffect(EdgeEffect.Spring) // 滚动到边缘时的效果
滚动效果(edgeEffect)支持以下选项:
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 = 2aboutToAppear() { // 获取设备宽度 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 = falsebuild() { 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 = truebuild() { 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 图片懒加载
为了提高性能,我们可以实现图片的懒加载,只有当图片进入可视区域时才加载:
@Componentstruct 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 = 0build() { 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 = 0build() { 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. 自定义网格项组件
为了提高代码的可维护性和复用性,我们可以将网格项封装为独立的组件:
@Componentstruct 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组件的高级配置、自定义布局策略、交互优化、动画效果等内容。通过这些技巧,你可以构建更加灵活、高效、美观的瀑布流界面。