180.[HarmonyOS NEXT 实战案例八:Grid] 瀑布流网格布局高级篇
[HarmonyOS NEXT 实战案例八:Grid] 瀑布流网格布局高级篇
项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star
效果演示
1. 引言
在前两篇教程中,我们介绍了HarmonyOS NEXT中瀑布流网格布局的基础知识和进阶技巧。本篇教程将深入探讨瀑布流网格布局的高级应用,包括复杂业务场景实现、自定义瀑布流算法、高级动画效果和性能优化等内容,帮助你掌握瀑布流布局的高级应用技巧,打造出专业级的瀑布流界面。
2. 复杂业务场景实现
2.1 混合内容瀑布流
在实际应用中,瀑布流不仅可以展示图片,还可以展示多种类型的内容,如文章、视频、商品等。下面我们将实现一个混合内容的瀑布流:
2.1.1 定义内容类型
// 内容类型枚举
enum ContentType {
IMAGE, // 图片
VIDEO, // 视频
ARTICLE, // 文章
PRODUCT // 商品
}
// 混合内容接口
interface MixedContent {
id: number;
type: ContentType; // 内容类型
title: string; // 标题
description: string; // 描述
coverImage: Resource; // 封面图片
width: number; // 宽度
height: number; // 高度
author: { // 作者信息
name: string;
avatar: Resource;
isVerified: boolean;
};
stats: { // 统计信息
likes: number;
comments: number;
shares: number;
views: number;
};
tags: string[]; // 标签
category: string; // 分类
publishTime: string; // 发布时间
isLiked: boolean; // 是否已点赞
isCollected: boolean; // 是否已收藏
// 不同类型的特定属性
duration?: number; // 视频时长(秒)
articleLength?: number; // 文章字数
price?: number; // 商品价格
discount?: number; // 商品折扣
}
2.1.2 内容类型构建器
为不同类型的内容创建专用的构建器:
// 图片内容构建器
@Builder
ImageContentItem(content: MixedContent) {
Column() {
Stack({ alignContent: Alignment.BottomStart }) {
Image(content.coverImage)
.width('100%')
.aspectRatio(content.width / content.height)
.objectFit(ImageFit.Cover)
.borderRadius({ topLeft: 12, topRight: 12 })
// 作者信息悬浮在图片底部
Row() {
Image(content.author.avatar)
.width(24)
.height(24)
.borderRadius(12)
.border({ width: 2, color: '#FFFFFF' })
Text(content.author.name)
.fontSize(12)
.fontColor('#FFFFFF')
.margin({ left: 6 })
if (content.author.isVerified) {
Image($r('app.media.ic_verified'))
.width(12)
.height(12)
.fillColor('#007AFF')
.margin({ left: 4 })
}
}
.padding(8)
.width('100%')
.linearGradient({
angle: 180,
colors: [['rgba(0,0,0,0)', 0.0], ['rgba(0,0,0,0.7)', 1.0]]
})
}
// 图片信息
Column() {
Text(content.title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.width('100%')
.textAlign(TextAlign.Start)
.margin({ bottom: 6 })
// 互动数据
Row() {
// 点赞数
Row() {
Image(content.isLiked ? $r('app.media.ic_like_filled') : $r('app.media.ic_like'))
.width(14)
.height(14)
.fillColor(content.isLiked ? '#FF6B6B' : '#999999')
.margin({ right: 2 })
Text(this.formatNumber(content.stats.likes))
.fontSize(10)
.fontColor('#999999')
}
// 评论数
Row() {
Image($r('app.media.ic_comment'))
.width(14)
.height(14)
.fillColor('#999999')
.margin({ right: 2 })
Text(content.stats.comments.toString())
.fontSize(10)
.fontColor('#999999')
}
.margin({ left: 12 })
Blank()
// 发布时间
Text(this.getTimeAgo(content.publishTime))
.fontSize(10)
.fontColor('#999999')
}
.width('100%')
}
.padding(12)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.shadow({
radius: 6,
color: 'rgba(0, 0, 0, 0.1)',
offsetX: 0,
offsetY: 2
})
}
// 视频内容构建器
@Builder
VideoContentItem(content: MixedContent) {
Column() {
Stack({ alignContent: Alignment.Center }) {
Image(content.coverImage)
.width('100%')
.aspectRatio(16 / 9) // 视频通常使用16:9比例
.objectFit(ImageFit.Cover)
.borderRadius({ topLeft: 12, topRight: 12 })
// 播放按钮
Button() {
Image($r('app.media.ic_play'))
.width(24)
.height(24)
.fillColor('#FFFFFF')
}
.width(48)
.height(48)
.borderRadius(24)
.backgroundColor('rgba(0, 0, 0, 0.5)')
// 视频时长
Text(this.formatDuration(content.duration || 0))
.fontSize(12)
.fontColor('#FFFFFF')
.backgroundColor('rgba(0, 0, 0, 0.5)')
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.position({ x: '85%', y: '85%' })
}
// 视频信息
Column() {
Text(content.title)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.width('100%')
.textAlign(TextAlign.Start)
.margin({ bottom: 6 })
// 作者信息
Row() {
Image(content.author.avatar)
.width(20)
.height(20)
.borderRadius(10)
Text(content.author.name)
.fontSize(12)
.fontColor('#666666')
.margin({ left: 6 })
.layoutWeight(1)
// 观看数
Row() {
Image($r('app.media.ic_view'))
.width(14)
.height(14)
.fillColor('#999999')
.margin({ right: 2 })
Text(this.formatNumber(content.stats.views))
.fontSize(10)
.fontColor('#999999')
}
}
.width('100%')
.margin({ bottom: 6 })
// 互动数据
Row() {
// 点赞数
Row() {
Image(content.isLiked ? $r('app.media.ic_like_filled') : $r('app.media.ic_like'))
.width(14)
.height(14)
.fillColor(content.isLiked ? '#FF6B6B' : '#999999')
.margin({ right: 2 })
Text(this.formatNumber(content.stats.likes))
.fontSize(10)
.fontColor('#999999')
}
// 评论数
Row() {
Image($r('app.media.ic_comment'))
.width(14)
.height(14)
.fillColor('#999999')
.margin({ right: 2 })
Text(content.stats.comments.toString())
.fontSize(10)
.fontColor('#999999')
}
.margin({ left: 12 })
Blank()
// 发布时间
Text(this.getTimeAgo(content.publishTime))
.fontSize(10)
.fontColor('#999999')
}
.width('100%')
}
.padding(12)
.alignItems(HorizontalAlign.Start)
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.shadow({
radius: 6,
color: 'rgba(0, 0, 0, 0.1)',
offsetX: 0,
offsetY: 2
})
}
// 文章内容构建器
@Builder
ArticleContentItem(content: MixedContent) {
// 实现略
}
// 商品内容构建器
@Builder
ProductContentItem(content: MixedContent) {
// 实现略
}
2.1.3 混合内容瀑布流实现
WaterFlow() {
ForEach(this.getFilteredContents(), (content: MixedContent) => {
FlowItem() {
// 根据内容类型使用不同的构建器
if (content.type === ContentType.IMAGE) {
this.ImageContentItem(content)
} else if (content.type === ContentType.VIDEO) {
this.VideoContentItem(content)
} else if (content.type === ContentType.ARTICLE) {
this.ArticleContentItem(content)
} else if (content.type === ContentType.PRODUCT) {
this.ProductContentItem(content)
}
}
})
}
.columnsTemplate('1fr 1fr')
.itemConstraintSize({
minWidth: 0,
maxWidth: '100%',
minHeight: 0,
maxHeight: '100%'
})
.columnsGap(8)
.rowsGap(8)
.width('100%')
.layoutWeight(1)
.padding({ left: 16, right: 16, bottom: 16 })
2.2 瀑布流卡片交互动效
为瀑布流卡片添加丰富的交互动效,提升用户体验:
2.2.1 卡片悬停效果
// 卡片悬停状态
@State hoveredItemId: number = -1
// 在FlowItem中添加悬停效果
FlowItem() {
// 内容构建器
// ...
}
.onHover((isHover: boolean) => {
if (isHover) {
this.hoveredItemId = content.id
} else if (this.hoveredItemId === content.id) {
this.hoveredItemId = -1
}
})
.scale({
x: this.hoveredItemId === content.id ? 1.03 : 1.0,
y: this.hoveredItemId === content.id ? 1.03 : 1.0
})
.shadow({
radius: this.hoveredItemId === content.id ? 10 : 6,
color: this.hoveredItemId === content.id ? 'rgba(0, 0, 0, 0.15)' : 'rgba(0, 0, 0, 0.1)',
offsetX: 0,
offsetY: this.hoveredItemId === content.id ? 4 : 2
})
.animation({
duration: 200,
curve: Curve.EaseOut
})
2.2.2 卡片展开效果
实现卡片展开效果,点击卡片后在原位置展开显示更多内容:
// 卡片展开状态
@State expandedItemId: number = -1
// 在FlowItem中添加展开效果
FlowItem() {
Column() {
// 基本内容
// ...
// 展开内容
if (this.expandedItemId === content.id) {
Column() {
// 更多内容
Text(content.description)
.fontSize(14)
.fontColor('#666666')
.width('100%')
.textAlign(TextAlign.Start)
.margin({ top: 12, bottom: 12 })
// 标签
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(content.tags, (tag: string) => {
Text(`#${tag}`)
.fontSize(12)
.fontColor('#007AFF')
.backgroundColor('#E6F2FF')
.borderRadius(12)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.margin({ right: 8, bottom: 8 })
})
}
.width('100%')
.margin({ bottom: 12 })
// 互动按钮
Row() {
// 点赞按钮
Button() {
Row() {
Image(content.isLiked ? $r('app.media.ic_like_filled') : $r('app.media.ic_like'))
.width(16)
.height(16)
.fillColor(content.isLiked ? '#FF6B6B' : '#333333')
Text('点赞')
.fontSize(12)
.fontColor(content.isLiked ? '#FF6B6B' : '#333333')
.margin({ left: 4 })
}
}
.backgroundColor('transparent')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.border({ width: 1, color: '#EEEEEE' })
.borderRadius(16)
.layoutWeight(1)
.onClick(() => {
this.toggleLike(content.id)
})
// 评论按钮
Button() {
Row() {
Image($r('app.media.ic_comment'))
.width(16)
.height(16)
.fillColor('#333333')
Text('评论')
.fontSize(12)
.fontColor('#333333')
.margin({ left: 4 })
}
}
.backgroundColor('transparent')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.border({ width: 1, color: '#EEEEEE' })
.borderRadius(16)
.layoutWeight(1)
.margin({ left: 8 })
// 分享按钮
Button() {
Row() {
Image($r('app.media.ic_share'))
.width(16)
.height(16)
.fillColor('#333333')
Text('分享')
.fontSize(12)
.fontColor('#333333')
.margin({ left: 4 })
}
}
.backgroundColor('transparent')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.border({ width: 1, color: '#EEEEEE' })
.borderRadius(16)
.layoutWeight(1)
.margin({ left: 8 })
}
.width('100%')
}
.width('100%')
.padding({ top: 0, bottom: 12, left: 12, right: 12 })
.animation({
duration: 300,
curve: Curve.EaseOut
})
}
}
// ...
}
.onClick(() => {
if (this.expandedItemId === content.id) {
this.expandedItemId = -1
} else {
this.expandedItemId = content.id
}
})
3. 自定义瀑布流算法
3.1 自定义列高计算
HarmonyOS NEXT的WaterFlow组件已经内置了瀑布流布局算法,但在某些特殊场景下,我们可能需要自定义列高计算逻辑,以实现更精确的布局控制:
// 列高度记录
@State columnHeights: number[] = []
// 初始化列高度
initColumnHeights(columnsCount: number) {
this.columnHeights = new Array(columnsCount).fill(0)
}
// 获取最短列的索引
getShortestColumnIndex(): number {
return this.columnHeights.indexOf(Math.min(...this.columnHeights))
}
// 更新列高度
updateColumnHeight(columnIndex: number, itemHeight: number) {
this.columnHeights[columnIndex] += itemHeight
}
// 计算项目位置
calculateItemPosition(item: MixedContent): { column: number, height: number } {
// 根据内容类型和尺寸估算高度
let estimatedHeight = 0
if (item.type === ContentType.IMAGE) {
// 图片高度 = 宽度 / 宽高比 + 信息区域高度
const columnWidth = px2vp(getContext(this).width) / this.columnsCount - 8 // 减去间距
const imageHeight = columnWidth / (item.width / item.height)
estimatedHeight = imageHeight + 100 // 100是信息区域的估计高度
} else if (item.type === ContentType.VIDEO) {
// 视频固定使用16:9比例
const columnWidth = px2vp(getContext(this).width) / this.columnsCount - 8
const videoHeight = columnWidth / (16 / 9)
estimatedHeight = videoHeight + 120
} else if (item.type === ContentType.ARTICLE) {
estimatedHeight = 200 // 文章卡片的估计高度
} else if (item.type === ContentType.PRODUCT) {
estimatedHeight = 250 // 商品卡片的估计高度
}
// 获取最短列
const shortestColumn = this.getShortestColumnIndex()
// 更新列高度
this.updateColumnHeight(shortestColumn, estimatedHeight)
return { column: shortestColumn, height: estimatedHeight }
}
3.2 自定义瀑布流布局
在某些复杂场景下,我们可能需要完全自定义瀑布流布局,而不使用WaterFlow组件。下面是一个使用Grid组件实现自定义瀑布流布局的示例:
// 自定义瀑布流布局
build() {
Column() {
// 顶部搜索和筛选
// ...
// 自定义瀑布流
Grid() {
ForEach(this.getFilteredContents(), (content: MixedContent) => {
// 计算位置
const position = this.calculateItemPosition(content)
GridItem() {
// 根据内容类型使用不同的构建器
if (content.type === ContentType.IMAGE) {
this.ImageContentItem(content)
} else if (content.type === ContentType.VIDEO) {
this.VideoContentItem(content)
} else if (content.type === ContentType.ARTICLE) {
this.ArticleContentItem(content)
} else if (content.type === ContentType.PRODUCT) {
this.ProductContentItem(content)
}
}
.columnStart(position.column)
.columnEnd(position.column + 1)
.height(position.height)
})
}
.columnsTemplate(this.getColumnsTemplate())
.columnsGap(8)
.rowsGap(8)
.width('100%')
.layoutWeight(1)
.padding({ left: 16, right: 16, bottom: 16 })
}
}
4. 高级动画与过渡效果
4.1 滚动动画
为瀑布流添加滚动动画,使内容随着滚动产生视差效果:
// 滚动偏移量
@State scrollOffset: number = 0
// 在主布局中监听滚动
Column() {
// 顶部搜索和筛选
// ...
// 瀑布流
Scroll() {
WaterFlow() {
ForEach(this.getFilteredContents(), (content: MixedContent, index) => {
FlowItem() {
// 内容构建器
// ...
}
.opacity(this.calculateOpacity(index))
.translate({
x: 0,
y: this.calculateTranslateY(index)
})
.animation({
duration: 300,
curve: Curve.EaseOut
})
})
}
// ...
}
.onScroll((offset: number) => {
this.scrollOffset = offset
})
}
// 计算透明度
calculateOpacity(index: number): number {
const itemPosition = index * 200 // 估计每个项目的位置
const screenHeight = px2vp(getContext(this).height)
// 项目进入屏幕时逐渐显示
if (itemPosition > this.scrollOffset + screenHeight) {
return 0
} else if (itemPosition > this.scrollOffset + screenHeight - 200) {
return (this.scrollOffset + screenHeight - itemPosition) / 200
} else {
return 1
}
}
// 计算Y轴偏移
calculateTranslateY(index: number): number {
const itemPosition = index * 200
const screenHeight = px2vp(getContext(this).height)
// 项目进入屏幕时从下方滑入
if (itemPosition > this.scrollOffset + screenHeight - 200) {
return 50 - (this.scrollOffset + screenHeight - itemPosition) / 4
} else {
return 0
}
}
4.2 项目过渡动画
为瀑布流项目添加过渡动画,使项目在添加、删除或更新时有平滑的过渡效果:
// 项目动画状态
@State itemAnimationStates: Map<number, string> = new Map()
// 添加新项目
addNewItem(item: MixedContent) {
// 设置新项目的初始状态
this.itemAnimationStates.set(item.id, 'entering')
// 添加到数据源
this.contentItems.push(item)
// 延迟更新状态,触发动画
setTimeout(() => {
this.itemAnimationStates.set(item.id, 'entered')
}, 50)
}
// 删除项目
removeItem(itemId: number) {
// 设置删除状态,触发动画
this.itemAnimationStates.set(itemId, 'exiting')
// 延迟删除,等待动画完成
setTimeout(() => {
const index = this.contentItems.findIndex(item => item.id === itemId)
if (index !== -1) {
this.contentItems.splice(index, 1)
}
this.itemAnimationStates.delete(itemId)
}, 300)
}
// 在FlowItem中应用动画状态
FlowItem() {
// 内容构建器
// ...
}
.opacity(this.getItemOpacity(content.id))
.scale({
x: this.getItemScale(content.id),
y: this.getItemScale(content.id)
})
.animation({
duration: 300,
curve: Curve.EaseOut
})
// 获取项目透明度
getItemOpacity(itemId: number): number {
const state = this.itemAnimationStates.get(itemId) || 'entered'
switch (state) {
case 'entering':
return 0
case 'exiting':
return 0
default:
return 1
}
}
// 获取项目缩放
getItemScale(itemId: number): number {
const state = this.itemAnimationStates.get(itemId) || 'entered'
switch (state) {
case 'entering':
return 0.8
case 'exiting':
return 0.8
default:
return 1.0
}
}
5. 高级数据处理与性能优化
5.1 数据分页加载
对于大量数据的瀑布流,实现分页加载是提升性能的关键:
// 分页加载相关状态
@State currentPage: number = 1
@State pageSize: number = 20
@State totalPages: number = 0
@State isLoading: boolean = false
@State hasMoreData: boolean = true
// 加载数据
async loadData(page: number = 1, append: boolean = false) {
if (this.isLoading) return
this.isLoading = true
try {
// 模拟API请求
const response = await this.fetchData(page, this.pageSize)
if (!append) {
// 首次加载或刷新,替换数据
this.contentItems = response.items
} else {
// 加载更多,追加数据
this.contentItems = [...this.contentItems, ...response.items]
}
this.currentPage = page
this.totalPages = response.totalPages
this.hasMoreData = page < response.totalPages
} catch (error) {
console.error('Failed to load data:', error)
} finally {
this.isLoading = false
}
}
// 模拟数据请求
async fetchData(page: number, pageSize: number): Promise<{ items: MixedContent[], totalPages: number }> {
// 实际应用中,这里应该是真实的API请求
return new Promise((resolve) => {
setTimeout(() => {
// 模拟数据
const items: MixedContent[] = []
// 生成模拟数据...
resolve({
items,
totalPages: 10
})
}, 1000)
})
}
// 加载更多数据
loadMoreData() {
if (!this.isLoading && this.hasMoreData) {
this.loadData(this.currentPage + 1, true)
}
}
5.2 图片懒加载
实现图片懒加载,只加载可见区域的图片,减少内存占用和提升性能:
// 图片加载状态
@State loadedImages: Set<number> = new Set()
@State visibleItems: Set<number> = new Set()
// 更新可见项目
updateVisibleItems(startIndex: number, endIndex: number) {
const newVisibleItems = new Set<number>()
for (let i = startIndex; i <= endIndex; i++) {
if (i >= 0 && i < this.contentItems.length) {
newVisibleItems.add(this.contentItems[i].id)
}
}
this.visibleItems = newVisibleItems
// 预加载可见项目的图片
this.preloadImages()
}
// 预加载图片
preloadImages() {
this.visibleItems.forEach(itemId => {
if (!this.loadedImages.has(itemId)) {
const item = this.contentItems.find(item => item.id === itemId)
if (item) {
// 创建Image对象预加载图片
const img = new Image()
img.src = item.coverImage
img.onload = () => {
this.loadedImages.add(itemId)
}
}
}
})
}
// 在FlowItem中使用懒加载
FlowItem() {
Column() {
Stack({ alignContent: Alignment.Center }) {
if (this.loadedImages.has(content.id)) {
// 显示已加载的图片
Image(content.coverImage)
.width('100%')
.aspectRatio(content.width / content.height)
.objectFit(ImageFit.Cover)
} else {
// 显示占位图
Image($r('app.media.placeholder'))
.width('100%')
.aspectRatio(content.width / content.height)
.objectFit(ImageFit.Cover)
// 加载指示器
LoadingProgress()
.width(36)
.height(36)
.color('#FFFFFF')
}
}
// ...
}
// ...
}
6. 瀑布流布局的高级应用场景
6.1 社交媒体探索页
瀑布流布局非常适合社交媒体的探索页,可以展示多种类型的内容,如图片、视频、文章等:
功能 | 实现方式 |
---|---|
混合内容展示 | 使用不同的内容构建器展示不同类型的内容 |
个性化推荐 | 根据用户兴趣和行为推荐内容 |
无限滚动 | 实现分页加载和上拉加载更多功能 |
内容交互 | 添加点赞、评论、收藏、分享等交互功能 |
内容详情 | 点击内容显示详情页或展开卡片 |
6.2 电商商品展示
瀑布流布局也适合电商应用的商品展示,可以实现类似淘宝、京东等应用的商品瀑布流:
功能 | 实现方式 |
---|---|
商品卡片 | 展示商品图片、名称、价格、评分等信息 |
商品筛选 | 添加分类、价格区间、销量等筛选条件 |
商品排序 | 支持按价格、销量、评分等方式排序 |
商品标签 | 显示折扣、包邮、新品等标签 |
快速购买 | 添加加入购物车、立即购买等快捷按钮 |
6.3 图片库应用
瀑布流布局非常适合图片库应用,可以实现类似相册的功能:
功能 | 实现方式 |
---|---|
图片分组 | 按日期、位置、相册等方式分组展示图片 |
多选操作 | 支持多选图片进行批量操作 |
图片编辑 | 点击图片进入编辑模式 |
图片详情 | 显示图片的拍摄信息、位置等详情 |
图片分享 | 支持分享图片到社交媒体 |
7. 总结
本教程深入探讨了HarmonyOS NEXT中瀑布流网格布局的高级应用,包括复杂业务场景实现、自定义瀑布流算法、高级动画与过渡效果、高级数据处理与性能优化等内容。通过这些高级技巧,你可以打造出专业级的瀑布流界面,满足各种复杂业务场景的需求。
- 0回答
- 4粉丝
- 0关注
- 178.[HarmonyOS NEXT 实战案例八:Grid] 瀑布流网格布局基础篇
- 179.[HarmonyOS NEXT 实战案例八:Grid] 瀑布流网格布局进阶篇
- 171.[HarmonyOS NEXT 实战案例五:Grid] 动态网格布局高级篇
- 162.[HarmonyOS NEXT 实战案例二:Grid] 照片相册网格布局:高级篇
- 168.[HarmonyOS NEXT 实战案例四:Grid] 可滚动网格布局高级篇
- 174.[HarmonyOS NEXT 实战案例六:Grid] 响应式网格布局 - 高级篇
- 165.[HarmonyOS NEXT 实战案例三:Grid] 不规则网格布局高级篇:复杂布局与高级技巧
- 176.[HarmonyOS NEXT 实战案例七:Grid] 嵌套网格布局进阶篇:高级布局与交互技巧
- 177.[HarmonyOS NEXT 实战案例七:Grid] 嵌套网格布局高级篇:复杂业务场景与高级定制
- 183.[HarmonyOS NEXT 实战案例九:Grid] 电商网格布局高级篇:复杂场景与性能优化
- 164.[HarmonyOS NEXT 实战案例三:Grid] 不规则网格布局进阶篇:新闻应用高级布局与交互
- 160.[HarmonyOS NEXT 实战案例二:Grid] 照片相册网格布局:基础篇
- 172.[HarmonyOS NEXT 实战案例六:Grid] 响应式网格布局 - 基础篇
- 166.[HarmonyOS NEXT 实战案例四:Grid] 可滚动网格布局基础篇
- 169.[HarmonyOS NEXT 实战案例五:Grid] 动态网格布局基础篇