一、基础布局实现
① 定义数据模型
-
首先需要定义图片数据:
interface ImageItem {
id: number; // 图片ID
title: string; // 图片标题
description: string; // 图片描述
image: 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; // 是否已收藏
location?: string; // 位置信息(可选)
camera?: string; // 相机信息(可选)
} -
使用 @State 装饰器定义图片数据数组,并初始化一些示例数据:
@State imageItems: ImageItem[] = [
{
id: 1,
title: '夕阳下的城市天际线',
description: '在高楼大厦间捕捉到的绝美夕阳,金色的光芒洒向整个城市',
image: r('app.media.big22'), width: 300, height: 400, author: { name: '摄影师小王', avatar: r('app.media.big22'),
isVerified: true
},
stats: {
likes: 1205,
comments: 89,
shares: 45,
views: 8930
},
tags: ['夕阳', '城市', '天际线', '摄影'],
category: '风景',
publishTime: '2024-01-10 18:30',
isLiked: false,
isCollected: false,
location: '上海外滩',
camera: 'Canon EOS R5'
},
// 其他图片数据...
] -
UI 状态管理:
@State selectedCategory: string = '全部' // 当前选中的分类
@State sortBy: string = '最新' // 当前排序方式
@State searchKeyword: string = '' // 搜索关键词
@State showImageDetail: boolean = false // 是否显示图片详情
@State selectedImage: ImageItem = {...} // 当前选中的图片
② 数据过滤与排序
-
实现一个 getFilteredImages 方法,用于根据分类、搜索关键词和排序方式过滤和排序图片数据:
getFilteredImages(): ImageItem[] {
let filtered = this.imageItems// 分类过滤 if (this.selectedCategory !== '全部') { filtered = filtered.filter(image => image.category === this.selectedCategory) } // 搜索过滤 if (this.searchKeyword.trim() !== '') { filtered = filtered.filter(image => image.title.includes(this.searchKeyword) || image.description.includes(this.searchKeyword) || image.tags.some(tag => tag.includes(this.searchKeyword)) ) } // 排序 switch (this.sortBy) { case '最热': filtered.sort((a, b) => b.stats.views - a.stats.views) break case '最多赞': filtered.sort((a, b) => b.stats.likes - a.stats.likes) break default: // 最新 filtered.sort((a, b) => new Date(b.publishTime).getTime() - new Date(a.publishTime).getTime()) } return filtered}
③ 瀑布流网格实现
-
HarmonyOS NEXT 提供了 WaterFlow 组件,专门用于实现瀑布流布局,现在可以使用它来展示图片卡片:
WaterFlow() {
ForEach(this.getFilteredImages(), (image: ImageItem) => {
FlowItem() {
// 图片卡片内容
}
})
}
.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 })
.backgroundColor('#F8F8F8') -
每个 FlowItem 包含一个图片卡片,结构如下:
FlowItem() {
Column() {
// 图片部分
Stack({ alignContent: Alignment.TopEnd }) {
Image(image.image)
.width('100%')
.aspectRatio(image.width / image.height) // 保持原始宽高比
.objectFit(ImageFit.Cover)
.borderRadius({ topLeft: 12, topRight: 12 })// 收藏按钮 Button() { Image(image.isCollected ? $r('app.media.big19') : $r('app.media.big20')) .width(16) .height(16) .fillColor(image.isCollected ? '#FFD700' : '#FFFFFF') } .width(32) .height(32) .borderRadius(16) .backgroundColor('rgba(0, 0, 0, 0.3)') .margin({ top: 8, right: 8 }) .onClick(() => { this.toggleCollect(image.id) }) } // 图片信息 Column() { // 标题 Text(image.title) .fontSize(14) .fontWeight(FontWeight.Bold) .fontColor('#333333') .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) .width('100%') .textAlign(TextAlign.Start) .margin({ bottom: 6 }) // 作者信息 Row() { Image(image.author.avatar) .width(24) .height(24) .borderRadius(12) Text(image.author.name) .fontSize(12) .fontColor('#666666') .margin({ left: 6 }) .layoutWeight(1) if (image.author.isVerified) { Image($r('app.media.big19')) .width(12) .height(12) .fillColor('#007AFF') } } .width('100%') .margin({ bottom: 8 }) // 互动数据 Row() { // 点赞按钮和数量 Button() { Row() { Image(image.isLiked ? $r('app.media.heart_filled') : $r('app.media.big19')) .width(14) .height(14) .fillColor(image.isLiked ? '#FF6B6B' : '#999999') .margin({ right: 2 }) Text(this.formatNumber(image.stats.likes)) .fontSize(10) .fontColor('#999999') } } .backgroundColor('transparent') .padding(0) .onClick(() => { this.toggleLike(image.id) }) // 评论数量 Row() { Image($r('app.media.big19')) .width(14) .height(14) .fillColor('#999999') .margin({ right: 2 }) Text(image.stats.comments.toString()) .fontSize(10) .fontColor('#999999') } .margin({ left: 12 }) Blank() // 发布时间 Text(this.getTimeAgo(image.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 }) .onClick(() => { this.selectedImage = image this.showImageDetail = true })}
④ WaterFlow 与 Grid 的区别
| 特性 | WaterFlow | Grid |
|---|---|---|
| 布局方式 | 瀑布流(等宽不等高) | 网格(等宽等高) |
| 子项组件 | FlowItem | GridItem |
| 列定义 | columnsTemplate | columnsTemplate |
| 自动适应内容高度 | 是 | 否 |
| 适用场景 | 图片展示、卡片流 | 规则网格布局 |
二、动态布局调整
① 响应式列数
-
根据屏幕宽度动态调整瀑布流的列数,实现更好的响应式布局:
@State columnsCount: number = 2 // 默认两列
onPageShow() {
// 获取屏幕宽度
const screenWidth = px2vp(getContext(this).width)// 根据屏幕宽度设置列数 if (screenWidth <= 320) { this.columnsCount = 1 } else if (screenWidth <= 600) { this.columnsCount = 2 } else if (screenWidth <= 840) { this.columnsCount = 3 } else { this.columnsCount = 4 }}
// 在WaterFlow组件中使用动态列数
WaterFlow() {
// ...
}
.columnsTemplate(this.getColumnsTemplate())
// ...// 生成列模板字符串
getColumnsTemplate(): string {
return Array(this.columnsCount).fill('1fr').join(' ')
} -
根据内容类型或重要性,动态调整卡片大小:
// 在FlowItem中根据图片类型设置不同的样式
FlowItem() {
Column() {
// ...
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.shadow({
radius: image.isHighlighted ? 10 : 6,
color: image.isHighlighted ? 'rgba(0, 0, 0, 0.15)' : 'rgba(0, 0, 0, 0.1)',
offsetX: 0,
offsetY: image.isHighlighted ? 4 : 2
})
// 高亮图片使用不同的边框
.border(image.isHighlighted ? {
width: 2,
color: '#007AFF',
style: BorderStyle.Solid
} : {
width: 0
})
}
② 卡片样式变体
-
为瀑布流卡片设计多种样式变体,增加视觉多样性:
// 定义卡片样式变体
enum CardStyle {
BASIC, // 基本样式
COMPACT, // 紧凑样式
FEATURED, // 特色样式
MINIMAL // 极简样式
}// 为每个图片分配样式变体
@State imageStyles: Map<number, CardStyle> = new Map()initImageStyles() {
this.imageItems.forEach(image => {
// 根据某些规则分配样式
if (image.stats.likes > 1000) {
this.imageStyles.set(image.id, CardStyle.FEATURED)
} else if (image.tags.includes('极简')) {
this.imageStyles.set(image.id, CardStyle.MINIMAL)
} else if (image.description.length < 20) {
this.imageStyles.set(image.id, CardStyle.COMPACT)
} else {
this.imageStyles.set(image.id, CardStyle.BASIC)
}
})
}// 在FlowItem中应用不同的样式
FlowItem() {
const style = this.imageStyles.get(image.id) || CardStyle.BASICColumn() { // 根据样式变体应用不同的布局和样式 switch (style) { case CardStyle.FEATURED: // 特色样式:大图、完整信息、特殊背景 // ... break case CardStyle.COMPACT: // 紧凑样式:小图、最少信息 // ... break case CardStyle.MINIMAL: // 极简样式:只有图片和标题 // ... break default: // 基本样式:标准布局 // ... break } }}
-
卡片加载动画:为瀑布流添加精美的动画效果,提升用户体验:
// 在FlowItem中添加加载动画
FlowItem() {
Column() {
// ...
}
.opacity(this.isItemLoaded(image.id) ? 1 : 0)
.animation({
duration: 300,
curve: Curve.EaseOut,
delay: this.getItemLoadDelay(image.id) // 错开延迟,实现瀑布效果
})
}// 控制项目加载状态
@State loadedItems: Set<number> = new Set()isItemLoaded(id: number): boolean {
return this.loadedItems.has(id)
}getItemLoadDelay(id: number): number {
// 根据项目在数组中的位置计算延迟
const index = this.imageItems.findIndex(item => item.id === id)
return index * 50 // 每项错开50ms
}// 在页面显示时触发加载动画
onPageShow() {
// 清空已加载项
this.loadedItems.clear()// 延迟添加项目,触发动画 setTimeout(() => { this.imageItems.forEach(item => { this.loadedItems.add(item.id) }) }, 100)}
-
交互反馈动画:
// 在FlowItem中添加点击反馈动画
.onClick(() => {
animateTo({
duration: 100,
curve: Curve.EaseIn,
iterations: 1,
playMode: PlayMode.Normal,
onFinish: () => {
this.selectedImage = image
this.showImageDetail = true
}
}, () => {
this.itemScales.set(image.id, 0.95) // 缩小效果
})animateTo({ duration: 100, curve: Curve.EaseOut, delay: 100, iterations: 1, playMode: PlayMode.Normal }, () => { this.itemScales.set(image.id, 1.0) // 恢复原始大小 })})
.scale({ x: this.itemScales.get(image.id) || 1.0, y: this.itemScales.get(image.id) || 1.0 })
三、拓展交互
① 长按
-
为图片卡片添加长按交互,显示快捷操作菜单:
// 在FlowItem中添加长按手势
.gesture(
LongPressGesture()
.onAction(() => {
this.showQuickActions(image.id)
})
) -
实现快捷操作菜单:
showQuickActions(imageId: number) {
const actions = [
{ icon: r('app.media.ic_like'), text: '点赞', action: () => this.toggleLike(imageId) }, { icon: r('app.media.ic_collect'), text: '收藏', action: () => this.toggleCollect(imageId) },
{ icon: r('app.media.ic_share'), text: '分享', action: () => {} }, { icon: r('app.media.ic_download'), text: '下载', action: () => {} }
]// 显示操作菜单}
② 拖拽排序
-
实现瀑布流卡片的拖拽排序功能:
// 添加拖拽状态
@State isDragging: boolean = false
@State draggedItemId: number = -1
@State dragPosition: { x: number, y: number } = { x: 0, y: 0 }// 在FlowItem中添加拖拽手势
.gesture(
PanGesture()
.onActionStart((event: GestureEvent) => {
if (this.editMode) { // 只在编辑模式下启用拖拽
this.isDragging = true
this.draggedItemId = image.id
this.dragPosition = { x: event.offsetX, y: event.offsetY }
}
})
.onActionUpdate((event: GestureEvent) => {
if (this.isDragging && this.draggedItemId === image.id) {
this.dragPosition = { x: event.offsetX, y: event.offsetY }
// 计算拖拽位置,判断是否需要交换位置
this.calculateDragSwap(event.offsetX, event.offsetY)
}
})
.onActionEnd(() => {
if (this.isDragging && this.draggedItemId === image.id) {
this.isDragging = false
this.draggedItemId = -1
// 完成拖拽排序
this.finalizeDragSort()
}
})
)
③ 下拉刷新与上拉加载
// 下拉刷新状态
@State isRefreshing: boolean = false
@State isLoadingMore: boolean = false
// 在主布局中添加下拉刷新
Refresh({ refreshing: $$this.isRefreshing }) {
Column() {
// 瀑布流内容
WaterFlow() {
// ...
}
// ...
// 底部加载更多
if (this.hasMoreData) {
Row() {
LoadingProgress()
.width(24)
.height(24)
.color('#999999')
Text('加载更多...')
.fontSize(14)
.fontColor('#999999')
.margin({ left: 8 })
}
.width('100%')
.height(60)
.justifyContent(FlexAlign.Center)
.visibility(this.isLoadingMore ? Visibility.Visible : Visibility.None)
}
}
.onRefreshing(() => {
// 模拟刷新数据
setTimeout(() => {
this.refreshData()
this.isRefreshing = false
}, 1500)
})
}
// 监听滚动到底部,加载更多
onReachEnd() {
if (!this.isLoadingMore && this.hasMoreData) {
this.isLoadingMore = true
// 模拟加载更多数据
setTimeout(() => {
this.loadMoreData()
this.isLoadingMore = false
}, 1500)
}
}
四、混合内容瀑布流
① 定义内容类型
// 内容类型枚举
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; // 商品折扣
}
② 内容类型构建器
// 图片内容构建器
@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) {
// 实现略
}
③ 混合内容瀑布流实现
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 })
五、瀑布流卡片交互动效
① 卡片悬停效果
// 卡片悬停状态
@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
})
② 卡片展开效果
-
实现卡片展开效果,点击卡片后在原位置展开显示更多内容:
// 卡片展开状态
@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
}
})
六、自定义瀑布流
① 自定义列高计算
-
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 = 0if (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 }}
② 自定义瀑布流布局
-
如下所示,使用 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 }) }}