HarmonyOS Next之深入解析使用Grid实现瀑布流网格布局
本文介绍了如何在HarmonyOS NEXT中实现图片瀑布流布局。首先定义了包含图片ID、标题、描述、尺寸、作者信息和统计数据的ImageItem接口。使用@State装饰器管理图片数据和UI状态,包括分类选择、排序方式和搜索关键词。通过getFilteredImages方法实现数据过滤和排序功能。最后利用WaterFlow组件构建两列瀑布流布局,每个FlowItem包含图片卡片,支持保持原始宽高
·
一、基础布局实现
① 定义数据模型
- 首先需要定义图片数据:
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.BASIC
Column() {
// 根据样式变体应用不同的布局和样式
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 = 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 }
}
② 自定义瀑布流布局
- 如下所示,使用 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 })
}
}
更多推荐



所有评论(0)