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

    }

相关推荐
╰つ栺尖篴夢ゞ7 小时前
HarmonyOS NEXT之深入解析Grid网格布局打造精美的照片相册管理集
harmonyos next·网格布局·grid·相册相片管理
hey202005289 小时前
如何将 Maya 首选项重置为默认值
动画·maya
gis分享者11 小时前
学习threejs,结合anime.js打造炫酷文字粒子星空秀
动画·threejs·粒子·文字·anime·星空·炫酷
╰つ栺尖篴夢ゞ11 小时前
HarmonyOS NEXT之深入解析Grid网格布局列表的交互与状态管理
harmonyos next·列表·grid·griditem
沟通QQ8762239655 天前
含分布式电源配电网潮流计算及相关实践
动画
Highcharts.js7 天前
(最新)Highcharts Dashbords 仪表板 网格组件(Grid Component)使用文档
开发文档·仪表板·grid·highcharts·dashboards·包装器·网格组件
by__csdn8 天前
大前端:定义、演进与实践全景解析
前端·javascript·vue.js·react.js·typescript·ecmascript·动画
韩曙亮8 天前
【Web APIs】JavaScript 动画 ② ( 缓动动画 | 步长计算取整 )
前端·javascript·动画·web apis·缓动动画·匀速动画
Irene19919 天前
CSS Grid布局详解
css·grid