61. [HarmonyOS NEXT 实战案例五] 社交应用照片墙网格布局(上) 原创

全栈若城
发布于 2025-6-5 23:17
浏览
0收藏

[HarmonyOS NEXT 实战案例五] 社交应用照片墙网格布局(上)

项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star

效果演示

61. [HarmonyOS NEXT 实战案例五] 社交应用照片墙网格布局(上)-鸿蒙开发者社区

1. 概述

社交应用中的照片墙是一种常见的UI设计模式,它以网格形式展示用户的照片集合,让用户可以浏览、分享和互动。本教程将详细讲解如何使用HarmonyOS NEXT的GridRow和GridCol组件实现一个美观、灵活的社交应用照片墙网格布局。

在本教程中,我们将学习如何设计照片墙的数据结构、如何使用GridRow和GridCol组件创建网格布局、如何实现照片卡片,以及如何处理不同尺寸的照片。通过本教程,你将掌握使用GridRow和GridCol组件实现复杂网格布局的技巧。

2. 数据结构设计

首先,我们需要设计照片墙的数据结构。照片墙中的每张照片通常包含以下信息:

  • 照片资源:照片的资源路径
  • 照片描述:照片的描述文字
  • 点赞数:照片获得的点赞数量
  • 评论数:照片获得的评论数量
  • 用户信息:发布照片的用户信息
  • 发布时间:照片的发布时间
  • 照片尺寸:照片的宽高比例

根据这些需求,我们定义以下接口:

// 用户信息接口
interface User {
  id: string;       // 用户ID
  name: string;     // 用户名
  avatar: string;   // 用户头像
}

// 照片信息接口
interface Photo {
  id: string;           // 照片ID
  resource: ResourceStr; // 照片资源
  description: string;   // 照片描述
  likes: number;         // 点赞数
  comments: number;      // 评论数
  user: User;            // 发布用户
  publishTime: string;   // 发布时间
  aspectRatio: number;   // 宽高比例
  span: number;          // 在网格中占据的列数
}

3. 数据准备

接下来,我们准备一些模拟数据用于展示照片墙:

// 模拟用户数据
private users: User[] = [
  { id: '1', name: '摄影爱好者', avatar: $r('app.media.avatar1') },
  { id: '2', name: '旅行达人', avatar: $r('app.media.avatar2') },
  { id: '3', name: '美食家', avatar: $r('app.media.avatar3') },
  { id: '4', name: '设计师', avatar: $r('app.media.avatar4') },
  { id: '5', name: '运动健将', avatar: $r('app.media.avatar5') }
];

// 模拟照片数据
private photos: Photo[] = [
  {
    id: '1',
    resource: $r('app.media.photo1'),
    description: '美丽的海滩日落,难忘的夏日时光',
    likes: 256,
    comments: 42,
    user: this.users[0],
    publishTime: '2小时前',
    aspectRatio: 4/3,
    span: 2
  },
  {
    id: '2',
    resource: $r('app.media.photo2'),
    description: '城市天际线,繁华都市的夜景',
    likes: 189,
    comments: 23,
    user: this.users[1],
    publishTime: '3小时前',
    aspectRatio: 1/1,
    span: 1
  },
  {
    id: '3',
    resource: $r('app.media.photo3'),
    description: '美味的早餐,开启美好的一天',
    likes: 145,
    comments: 18,
    user: this.users[2],
    publishTime: '5小时前',
    aspectRatio: 3/4,
    span: 1
  },
  {
    id: '4',
    resource: $r('app.media.photo4'),
    description: '极简主义设计作品',
    likes: 321,
    comments: 56,
    user: this.users[3],
    publishTime: '昨天',
    aspectRatio: 16/9,
    span: 2
  },
  {
    id: '5',
    resource: $r('app.media.photo5'),
    description: '晨跑时拍摄的日出',
    likes: 210,
    comments: 34,
    user: this.users[4],
    publishTime: '昨天',
    aspectRatio: 1/1,
    span: 1
  },
  {
    id: '6',
    resource: $r('app.media.photo6'),
    description: '雨后的彩虹,大自然的奇迹',
    likes: 278,
    comments: 45,
    user: this.users[0],
    publishTime: '2天前',
    aspectRatio: 3/2,
    span: 2
  },
  {
    id: '7',
    resource: $r('app.media.photo7'),
    description: '古老的建筑,历史的痕迹',
    likes: 198,
    comments: 29,
    user: this.users[1],
    publishTime: '2天前',
    aspectRatio: 1/1,
    span: 1
  },
  {
    id: '8',
    resource: $r('app.media.photo8'),
    description: '精致的甜点,视觉与味觉的享受',
    likes: 167,
    comments: 21,
    user: this.users[2],
    publishTime: '3天前',
    aspectRatio: 4/5,
    span: 1
  }
];

在这个数据结构中,我们为每张照片设置了不同的宽高比例(aspectRatio)和在网格中占据的列数(span),以便实现不同大小的照片布局。

4. 布局实现

4.1 整体布局结构

首先,我们创建一个社交应用照片墙的整体布局结构:

@Component
export struct SocialPhotoWall {
  @State photos: Photo[] = []; // 照片数据
  @State columns: number = 3;  // 网格列数
  
  aboutToAppear() {
    // 初始化照片数据
    this.photos = this.getPhotoData();
  }
  
  build() {
    Column() {
      // 顶部标题栏
      this.TitleBar()
      
      // 照片墙网格
      this.PhotoGrid()
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
  
  // 获取照片数据方法
  private getPhotoData(): Photo[] {
    // 返回模拟的照片数据
    return [...]; // 这里是上面定义的photos数组
  }
  
  // 以下是各个组件的构建器
  @Builder
  private TitleBar() {
    // 实现标题栏
  }
  
  @Builder
  private PhotoGrid() {
    // 实现照片墙网格
  }
  
  @Builder
  private PhotoCard(photo: Photo) {
    // 实现照片卡片
  }
}

4.2 标题栏实现

接下来,我们实现标题栏:

@Builder
private TitleBar() {
  Row() {
    Text('照片墙')
      .fontSize(20)
      .fontWeight(FontWeight.Bold)
      .fontColor('#333333')
    
    Blank()
    
    Row() {
      Image($r('app.media.ic_search'))
        .width(24)
        .height(24)
        .margin({ right: 16 })
      
      Image($r('app.media.ic_add'))
        .width(24)
        .height(24)
    }
  }
  .width('100%')
  .height(56)
  .padding({ left: 16, right: 16 })
  .backgroundColor(Color.White)
}

4.3 照片墙网格实现

现在,我们使用GridRow和GridCol组件实现照片墙网格:

@Builder
private PhotoGrid() {
  Scroll() {
    GridRow({
      columns: this.columns,
      gutter: { x: 8, y: 8 }
    }) {
      ForEach(this.photos, (photo: Photo) => {
        GridCol({ span: photo.span }) {
          this.PhotoCard(photo)
        }
      })
    }
    .width('100%')
    .padding(8)
  }
  .scrollBar(BarState.Off)
  .scrollable(ScrollDirection.Vertical)
  .width('100%')
  .height('100%')
}

在这个实现中,我们使用GridRow组件创建一个网格布局,设置列数为this.columns(默认为3),并设置水平和垂直间距为8vp。然后,我们使用ForEach循环遍历照片数据,为每张照片创建一个GridCol,并设置其span属性为照片的span值,这样不同的照片可以占据不同数量的列。

4.4 照片卡片实现

最后,我们实现照片卡片:

@Builder
private PhotoCard(photo: Photo) {
  Column() {
    // 照片
    Stack() {
      Image(photo.resource)
        .width('100%')
        .aspectRatio(photo.aspectRatio)
        .borderRadius({ topLeft: 8, topRight: 8 })
      
      // 发布时间标签
      Text(photo.publishTime)
        .fontSize(12)
        .fontColor(Color.White)
        .backgroundColor('#80000000')
        .padding({ left: 6, right: 6, top: 2, bottom: 2 })
        .borderRadius(4)
        .position({ x: 8, y: 8 })
    }
    .width('100%')
    
    // 照片信息
    Column() {
      // 用户信息
      Row() {
        Image(photo.user.avatar)
          .width(24)
          .height(24)
          .borderRadius(12)
        
        Text(photo.user.name)
          .fontSize(14)
          .fontColor('#333333')
          .margin({ left: 8 })
      }
      .width('100%')
      .margin({ top: 8, bottom: 4 })
      
      // 照片描述
      if (photo.description) {
        Text(photo.description)
          .fontSize(14)
          .fontColor('#666666')
          .maxLines(2)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
          .margin({ top: 4, bottom: 8 })
          .width('100%')
      }
      
      // 互动信息
      Row() {
        Row() {
          Image($r('app.media.ic_like'))
            .width(16)
            .height(16)
          
          Text(photo.likes.toString())
            .fontSize(12)
            .fontColor('#999999')
            .margin({ left: 4 })
        }
        
        Row() {
          Image($r('app.media.ic_comment'))
            .width(16)
            .height(16)
          
          Text(photo.comments.toString())
            .fontSize(12)
            .fontColor('#999999')
            .margin({ left: 4 })
        }
        .margin({ left: 16 })
        
        Blank()
        
        Image($r('app.media.ic_more'))
          .width(16)
          .height(16)
      }
      .width('100%')
      .margin({ top: 8 })
    }
    .width('100%')
    .padding({ left: 8, right: 8, bottom: 8 })
  }
  .width('100%')
  .backgroundColor(Color.White)
  .borderRadius(8)
}

在照片卡片的实现中,我们使用Column组件作为容器,包含照片、用户信息、照片描述和互动信息等部分。对于照片部分,我们使用Image组件并设置aspectRatio属性为照片的宽高比例,以保持照片的原始比例。

5. GridRow和GridCol配置详解

在照片墙的实现中,我们使用了GridRow和GridCol组件来创建网格布局。下面详细讲解这两个组件的配置:

5.1 GridRow配置

GridRow({
  columns: this.columns,  // 设置列数为3
  gutter: { x: 8, y: 8 }  // 设置水平和垂直间距为8vp
})

GridRow组件的主要配置参数:

参数 类型 描述
columns number | GridRowColumnOptions 设置网格布局的列数
gutter Length | { x: Length, y: Length } 设置网格项之间的间距
breakpoints { value: Array<Length>, reference: BreakpointsReference } 设置断点值

在照片墙的实现中,我们设置了固定的列数(3列)和固定的间距(水平和垂直间距都为8vp)。

5.2 GridCol配置

GridCol({ span: photo.span })

GridCol组件的主要配置参数:

参数 类型 描述
span number | GridColSpanOptions 设置网格项占据的列数
offset number | GridColOffsetOptions 设置网格项的偏移列数
order number | GridColOrderOptions 设置网格项的排序

在照片墙的实现中,我们根据照片的span属性设置了不同的span值,使不同的照片可以占据不同数量的列,从而实现不同大小的照片布局。

6. 布局效果分析

通过上述实现,我们创建了一个灵活的社交应用照片墙网格布局。下面分析一下这个布局的效果:

6.1 不同大小的照片

通过设置不同的span值,我们实现了不同大小的照片布局:

  • span=1:占据1列,适合小尺寸的照片
  • span=2:占据2列,适合中等尺寸的照片
  • span=3:占据3列(整行),适合大尺寸的照片

这种布局方式可以根据照片的重要性或美观度,给予不同的展示空间,增强视觉层次感。

6.2 照片原始比例保持

通过设置Image组件的aspectRatio属性为照片的宽高比例,我们保持了照片的原始比例,避免了照片被拉伸或压缩导致的变形。

6.3 信息展示的完整性

每个照片卡片不仅展示了照片本身,还包含了用户信息、照片描述、发布时间和互动信息等,提供了完整的社交体验。

7. 完整代码

以下是社交应用照片墙网格布局的完整代码:

// 用户信息接口
interface User {
  id: string;       // 用户ID
  name: string;     // 用户名
  avatar: string;   // 用户头像
}

// 照片信息接口
interface Photo {
  id: string;           // 照片ID
  resource: ResourceStr; // 照片资源
  description: string;   // 照片描述
  likes: number;         // 点赞数
  comments: number;      // 评论数
  user: User;            // 发布用户
  publishTime: string;   // 发布时间
  aspectRatio: number;   // 宽高比例
  span: number;          // 在网格中占据的列数
}

@Component
export struct SocialPhotoWall {
  @State photos: Photo[] = []; // 照片数据
  @State columns: number = 3;  // 网格列数
  
  // 模拟用户数据
  private users: User[] = [
    { id: '1', name: '摄影爱好者', avatar: $r('app.media.avatar1') },
    { id: '2', name: '旅行达人', avatar: $r('app.media.avatar2') },
    { id: '3', name: '美食家', avatar: $r('app.media.avatar3') },
    { id: '4', name: '设计师', avatar: $r('app.media.avatar4') },
    { id: '5', name: '运动健将', avatar: $r('app.media.avatar5') }
  ];
  
  aboutToAppear() {
    // 初始化照片数据
    this.photos = this.getPhotoData();
  }
  
  build() {
    Column() {
      // 顶部标题栏
      this.TitleBar()
      
      // 照片墙网格
      this.PhotoGrid()
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
  
  // 获取照片数据方法
  private getPhotoData(): Photo[] {
    return [
      {
        id: '1',
        resource: $r('app.media.photo1'),
        description: '美丽的海滩日落,难忘的夏日时光',
        likes: 256,
        comments: 42,
        user: this.users[0],
        publishTime: '2小时前',
        aspectRatio: 4/3,
        span: 2
      },
      {
        id: '2',
        resource: $r('app.media.photo2'),
        description: '城市天际线,繁华都市的夜景',
        likes: 189,
        comments: 23,
        user: this.users[1],
        publishTime: '3小时前',
        aspectRatio: 1/1,
        span: 1
      },
      {
        id: '3',
        resource: $r('app.media.photo3'),
        description: '美味的早餐,开启美好的一天',
        likes: 145,
        comments: 18,
        user: this.users[2],
        publishTime: '5小时前',
        aspectRatio: 3/4,
        span: 1
      },
      {
        id: '4',
        resource: $r('app.media.photo4'),
        description: '极简主义设计作品',
        likes: 321,
        comments: 56,
        user: this.users[3],
        publishTime: '昨天',
        aspectRatio: 16/9,
        span: 2
      },
      {
        id: '5',
        resource: $r('app.media.photo5'),
        description: '晨跑时拍摄的日出',
        likes: 210,
        comments: 34,
        user: this.users[4],
        publishTime: '昨天',
        aspectRatio: 1/1,
        span: 1
      },
      {
        id: '6',
        resource: $r('app.media.photo6'),
        description: '雨后的彩虹,大自然的奇迹',
        likes: 278,
        comments: 45,
        user: this.users[0],
        publishTime: '2天前',
        aspectRatio: 3/2,
        span: 2
      },
      {
        id: '7',
        resource: $r('app.media.photo7'),
        description: '古老的建筑,历史的痕迹',
        likes: 198,
        comments: 29,
        user: this.users[1],
        publishTime: '2天前',
        aspectRatio: 1/1,
        span: 1
      },
      {
        id: '8',
        resource: $r('app.media.photo8'),
        description: '精致的甜点,视觉与味觉的享受',
        likes: 167,
        comments: 21,
        user: this.users[2],
        publishTime: '3天前',
        aspectRatio: 4/5,
        span: 1
      }
    ];
  }
  
  @Builder
  private TitleBar() {
    Row() {
      Text('照片墙')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
      
      Blank()
      
      Row() {
        Image($r('app.media.ic_search'))
          .width(24)
          .height(24)
          .margin({ right: 16 })
        
        Image($r('app.media.ic_add'))
          .width(24)
          .height(24)
      }
    }
    .width('100%')
    .height(56)
    .padding({ left: 16, right: 16 })
    .backgroundColor(Color.White)
  }
  
  @Builder
  private PhotoGrid() {
    Scroll() {
      GridRow({
        columns: this.columns,
        gutter: { x: 8, y: 8 }
      }) {
        ForEach(this.photos, (photo: Photo) => {
          GridCol({ span: photo.span }) {
            this.PhotoCard(photo)
          }
        })
      }
      .width('100%')
      .padding(8)
    }
    .scrollBar(BarState.Off)
    .scrollable(ScrollDirection.Vertical)
    .width('100%')
    .height('100%')
  }
  
  @Builder
  private PhotoCard(photo: Photo) {
    Column() {
      // 照片
      Stack() {
        Image(photo.resource)
          .width('100%')
          .aspectRatio(photo.aspectRatio)
          .borderRadius({ topLeft: 8, topRight: 8 })
        
        // 发布时间标签
        Text(photo.publishTime)
          .fontSize(12)
          .fontColor(Color.White)
          .backgroundColor('#80000000')
          .padding({ left: 6, right: 6, top: 2, bottom: 2 })
          .borderRadius(4)
          .position({ x: 8, y: 8 })
      }
      .width('100%')
      
      // 照片信息
      Column() {
        // 用户信息
        Row() {
          Image(photo.user.avatar)
            .width(24)
            .height(24)
            .borderRadius(12)
          
          Text(photo.user.name)
            .fontSize(14)
            .fontColor('#333333')
            .margin({ left: 8 })
        }
        .width('100%')
        .margin({ top: 8, bottom: 4 })
        
        // 照片描述
        if (photo.description) {
          Text(photo.description)
            .fontSize(14)
            .fontColor('#666666')
            .maxLines(2)
            .textOverflow({ overflow: TextOverflow.Ellipsis })
            .margin({ top: 4, bottom: 8 })
            .width('100%')
        }
        
        // 互动信息
        Row() {
          Row() {
            Image($r('app.media.ic_like'))
              .width(16)
              .height(16)
            
            Text(photo.likes.toString())
              .fontSize(12)
              .fontColor('#999999')
              .margin({ left: 4 })
          }
          
          Row() {
            Image($r('app.media.ic_comment'))
              .width(16)
              .height(16)
            
            Text(photo.comments.toString())
              .fontSize(12)
              .fontColor('#999999')
              .margin({ left: 4 })
          }
          .margin({ left: 16 })
          
          Blank()
          
          Image($r('app.media.ic_more'))
            .width(16)
            .height(16)
        }
        .width('100%')
        .margin({ top: 8 })
      }
      .width('100%')
      .padding({ left: 8, right: 8, bottom: 8 })
    }
    .width('100%')
    .backgroundColor(Color.White)
    .borderRadius(8)
  }
}

8. 与其他布局方式的比较

8.1 与Flex布局的比较

布局方式 优点 缺点 适用场景
GridRow/GridCol 1. 支持不同大小的网格项<br>2. 支持响应式布局<br>3. 配置简单,易于使用 1. 对于复杂的不规则布局支持有限 1. 照片墙<br>2. 商品列表<br>3. 卡片布局
Flex 1. 灵活性高<br>2. 支持多种对齐方式<br>3. 支持动态调整 1. 实现网格布局较复杂<br>2. 需要更多的嵌套 1. 导航栏<br>2. 工具栏<br>3. 简单的行列布局

8.2 与List布局的比较

布局方式 优点 缺点 适用场景
GridRow/GridCol 1. 支持二维网格布局<br>2. 支持不同大小的网格项<br>3. 支持响应式布局 1. 对于长列表的性能优化不如List 1. 照片墙<br>2. 商品网格<br>3. 卡片布局
List 1. 针对长列表优化<br>2. 支持懒加载<br>3. 支持列表项复用 1. 主要支持一维布局<br>2. 实现不同大小的列表项较复杂 1. 长列表<br>2. 聊天记录<br>3. 新闻列表

9. 总结

本教程详细讲解了如何使用HarmonyOS NEXT的GridRow和GridCol组件实现社交应用照片墙网格布局。通过合理设计数据结构、配置GridRow和GridCol组件,以及实现照片卡片,我们创建了一个灵活、美观的照片墙布局,能够展示不同大小的照片,并提供完整的社交信息展示。

GridRow和GridCol组件为实现网格布局提供了简单而强大的方式,特别适合照片墙、商品列表等场景。通过本教程,你应该已经掌握了使用这些组件创建复杂网格布局的基本技巧,可以根据实际需求进行灵活调整和扩展。

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
收藏
回复
举报
回复
    相关推荐