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

2025-06-30 22:47:21
105次阅读
0个评论

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

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

效果演示

image.png

在基础篇中,我们学习了如何使用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组件的高级配置、自定义布局策略、交互优化、动画效果等内容。通过这些技巧,你可以构建更加灵活、高效、美观的瀑布流界面。

收藏00

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