一、基础布局实现

① 定义数据模型

  • 首先需要定义图片数据:
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 })
    }
}
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐