HarmonyOS Next之深入解析使用Grid实现瀑布流网格布局

一、基础布局实现

① 定义数据模型

  • 首先需要定义图片数据:

    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 })
      }

    }

相关推荐
twe77582585 天前
用3D动画揭示技术路线的多样性
科技·3d·制造·动画
LqKKsNUdXlA9 天前
多通道卷积神经网络 变压器 故障诊断 MATLAB (附赠变压器振动信号数据集) 关键词
动画
CappuccinoRose10 天前
CSS 语法学习文档(十九)
前端·css·属性·flex·grid·学习资源·格式化上下文
CappuccinoRose11 天前
CSS 语法学习文档(十七)
前端·css·学习·布局·houdini·瀑布流布局·csspaintingapi
twe775825814 天前
参数调控与3D动画的互动魅力
科技·3d·制造·动画
twe775825817 天前
用3D动画解密3D IC封装中的微观世界
科技·3d·制造·动画
李洋-蛟龙腾飞公司20 天前
开发智能体调试与预览---真机测试
harmonyos next
_风华ts22 天前
创建并使用AimOffset
ue5·动画·虚幻·虚幻引擎·aimoffset
hudawei99622 天前
flutter和Android动画的对比
android·flutter·动画
hudawei99623 天前
TweenAnimationBuilder和AnimatedBuilder两种动画的比较
flutter·ui·动画·tweenanimation·animatedbuilder