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

简介: 在上一篇教程中,我们学习了如何使用GridRow和GridCol组件实现基本的社交应用照片墙网格布局。本篇教程将在此基础上,深入探讨如何优化布局、添加交互功能,以及实现更多高级特性,打造一个功能完善的社交应用照片墙。

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

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

效果演示

img_01838240.png

1. 概述

在上一篇教程中,我们学习了如何使用GridRow和GridCol组件实现基本的社交应用照片墙网格布局。本篇教程将在此基础上,深入探讨如何优化布局、添加交互功能,以及实现更多高级特性,打造一个功能完善的社交应用照片墙。

2. 响应式布局实现

2.1 断点响应设置

为了适应不同屏幕尺寸的设备,我们可以使用GridRow组件的breakpoints属性设置断点响应:

@State columns: GridRowColumnOptions = {
    xs: 2, sm: 3, md: 4, lg: 6 };

// 在PhotoGrid方法中
GridRow({
   
  columns: this.columns,
  gutter: {
    x: 8, y: 8 },
  breakpoints: {
    value: ['320vp', '600vp', '840vp'], reference: BreakpointsReference.WindowSize }
})

这里我们设置了三个断点值:320vp、600vp和840vp,并根据窗口大小自动调整列数:

  • 小于320vp:2列
  • 320vp-600vp:3列
  • 600vp-840vp:4列
  • 大于840vp:6列

2.2 不同断点下的布局效果

下表展示了不同断点下的照片墙网格布局效果:

断点 列数 适用设备
<320vp 2列 小屏手机
320vp-600vp 3列 中屏手机
600vp-840vp 4列 大屏手机/小屏平板
>840vp 6列 平板/桌面设备

2.3 照片span值的动态调整

为了在不同断点下保持良好的布局效果,我们需要动态调整照片的span值:

private getPhotoSpan(photo: Photo, currentColumns: number): number {
   
  // 根据当前列数和照片原始span值计算实际span值
  const originalSpan = photo.span;

  // 确保span值不超过当前列数
  if (originalSpan > currentColumns) {
   
    return currentColumns;
  }

  // 在列数较少时,减小大照片的span值
  if (currentColumns <= 2 && originalSpan > 1) {
   
    return Math.min(originalSpan, 2);
  }

  return originalSpan;
}

// 在PhotoGrid方法中
GridRow({
   
  columns: this.columns,
  gutter: {
    x: 8, y: 8 },
  breakpoints: {
    value: ['320vp', '600vp', '840vp'], reference: BreakpointsReference.WindowSize }
}) {
   
  ForEach(this.photos, (photo: Photo) => {
   
    GridCol({
    
      span: {
    xs: this.getPhotoSpan(photo, 2), sm: this.getPhotoSpan(photo, 3), 
             md: this.getPhotoSpan(photo, 4), lg: this.getPhotoSpan(photo, 6) } 
    }) {
   
      this.PhotoCard(photo)
    }
  })
}

3. 照片卡片优化

3.1 添加阴影效果

为了提升照片卡片的视觉层次感,我们可以添加阴影效果:

Column() {
   
  // 照片卡片内容
}
.width('100%')
.backgroundColor(Color.White)
.borderRadius(8)
.shadow({
   
  radius: 8,
  color: '#1A000000',
  offsetX: 0,
  offsetY: 2
})

3.2 添加照片加载状态

为了提升用户体验,我们可以添加照片加载状态:

@State loadingStates: Map<string, boolean> = new Map<string, boolean>();

aboutToAppear() {
   
  // 初始化照片数据
  this.photos = this.getPhotoData();

  // 初始化加载状态
  this.photos.forEach(photo => {
   
    this.loadingStates.set(photo.id, true);
  });
}

// 在PhotoCard方法中
Stack() {
   
  // 加载占位图
  if (this.loadingStates.get(photo.id)) {
   
    Column() {
   
      LoadingProgress()
        .width(32)
        .height(32)
        .color('#2196F3')
    }
    .width('100%')
    .height(100 * photo.aspectRatio)
    .backgroundColor('#F0F0F0')
    .borderRadius({
    topLeft: 8, topRight: 8 })
  }

  Image(photo.resource)
    .width('100%')
    .aspectRatio(photo.aspectRatio)
    .borderRadius({
    topLeft: 8, topRight: 8 })
    .onComplete(() => {
   
      // 图片加载完成后更新状态
      this.loadingStates.set(photo.id, false);
    })

  // 发布时间标签
  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 })
}

3.3 添加照片标签

为了提供更丰富的信息,我们可以为照片添加标签:

// 在Photo接口中添加标签字段
interface Photo {
   
  // 其他字段...
  tags?: string[];  // 照片标签
}

// 在PhotoCard方法中,照片下方添加标签
if (photo.tags && photo.tags.length > 0) {
   
  Row() {
   
    ForEach(photo.tags, (tag: string) => {
   
      Text('#' + tag)
        .fontSize(12)
        .fontColor('#2196F3')
        .backgroundColor('#E3F2FD')
        .padding({
    left: 6, right: 6, top: 2, bottom: 2 })
        .borderRadius(4)
        .margin({
    right: 4 })
    })
  }
  .width('100%')
  .margin({
    top: 8 })
  .flexWrap(FlexWrap.Wrap)
}

4. 交互功能实现

4.1 添加下拉刷新功能

为了提供更好的用户体验,我们可以添加下拉刷新功能:

@State refreshing: boolean = false;

build() {
   
  Column() {
   
    // 顶部标题栏
    this.TitleBar()

    // 照片墙网格
    Refresh({
    refreshing: this.refreshing }) {
   
      this.PhotoGrid()
    }
    .onRefresh(() => {
   
      this.refreshData();
    })
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#F5F5F5')
}

private refreshData(): void {
   
  this.refreshing = true;
  // 模拟网络请求
  setTimeout(() => {
   
    // 随机调整照片顺序
    this.photos = this.shuffleArray([...this.photos]);
    this.refreshing = false;
  }, 2000);
}

private shuffleArray<T>(array: T[]): T[] {
   
  const newArray = [...array];
  for (let i = newArray.length - 1; i > 0; i--) {
   
    const j = Math.floor(Math.random() * (i + 1));
    [newArray[i], newArray[j]] = [newArray[j], newArray[i]];
  }
  return newArray;
}

4.2 添加照片点击事件

为照片添加点击事件,实现查看大图的功能:

@State showFullscreenPhoto: boolean = false;
@State currentPhoto: Photo | null = null;

// 在PhotoCard方法中
Stack() {
   
  // 照片内容
}
.width('100%')
.onClick(() => {
   
  this.currentPhoto = photo;
  this.showFullscreenPhoto = true;
})

// 在build方法末尾添加全屏查看组件
if (this.showFullscreenPhoto && this.currentPhoto) {
   
  Stack() {
   
    // 半透明背景
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor('#80000000')
      .onClick(() => {
   
        this.showFullscreenPhoto = false;
      })

    // 照片
    Column() {
   
      Image(this.currentPhoto.resource)
        .objectFit(ImageFit.Contain)
        .width('100%')
        .height('100%')
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)

    // 关闭按钮
    Button({
    type: ButtonType.Circle }) {
   
      Image($r('app.media.ic_close'))
        .width(24)
        .height(24)
        .fillColor(Color.White)
    }
    .width(48)
    .height(48)
    .backgroundColor('#33000000')
    .position({
    x: '90%', y: '5%' })
    .onClick(() => {
   
      this.showFullscreenPhoto = false;
    })

    // 照片信息
    Column() {
   
      Row() {
   
        Image(this.currentPhoto.user.avatar)
          .width(32)
          .height(32)
          .borderRadius(16)

        Column() {
   
          Text(this.currentPhoto.user.name)
            .fontSize(16)
            .fontColor(Color.White)

          Text(this.currentPhoto.publishTime)
            .fontSize(12)
            .fontColor('#E0E0E0')
        }
        .alignItems(HorizontalAlign.Start)
        .margin({
    left: 8 })
      }
      .width('100%')
      .padding(16)

      if (this.currentPhoto.description) {
   
        Text(this.currentPhoto.description)
          .fontSize(14)
          .fontColor(Color.White)
          .width('100%')
          .padding({
    left: 16, right: 16, bottom: 16 })
      }
    }
    .width('100%')
    .position({
    x: 0, y: '80%' })
    .backgroundColor('#33000000')
  }
  .width('100%')
  .height('100%')
  .position({
    x: 0, y: 0 })
  .zIndex(999)
}

4.3 添加点赞和评论功能

为照片添加点赞和评论功能:

@State likedPhotos: Set<string> = new Set<string>();

// 在PhotoCard方法中的互动信息部分
Row() {
   
  Row() {
   
    Image(this.likedPhotos.has(photo.id) ? $r('app.media.ic_like_filled') : $r('app.media.ic_like'))
      .width(16)
      .height(16)
      .fillColor(this.likedPhotos.has(photo.id) ? '#F44336' : '#999999')
      .onClick((event: ClickEvent) => {
   
        this.toggleLike(photo.id);
        event.stopPropagation();
      })

    Text(this.getLikeCount(photo).toString())
      .fontSize(12)
      .fontColor('#999999')
      .margin({
    left: 4 })
  }

  Row() {
   
    Image($r('app.media.ic_comment'))
      .width(16)
      .height(16)
      .onClick((event: ClickEvent) => {
   
        this.showCommentDialog(photo);
        event.stopPropagation();
      })

    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)
    .onClick((event: ClickEvent) => {
   
      this.showMoreOptions(photo);
      event.stopPropagation();
    })
}

private toggleLike(photoId: string): void {
   
  if (this.likedPhotos.has(photoId)) {
   
    this.likedPhotos.delete(photoId);
  } else {
   
    this.likedPhotos.add(photoId);
  }
}

private getLikeCount(photo: Photo): number {
   
  return photo.likes + (this.likedPhotos.has(photo.id) ? 1 : 0);
}

private showCommentDialog(photo: Photo): void {
   
  // 实现评论对话框
  AlertDialog.show({
   
    title: '评论',
    message: '该功能正在开发中...',
    autoCancel: true,
    alignment: DialogAlignment.Bottom,
    offset: {
    dx: 0, dy: -20 },
    primaryButton: {
   
      value: '确定',
      action: () => {
   
        console.info('点击确定按钮');
      }
    }
  });
}

private showMoreOptions(photo: Photo): void {
   
  // 实现更多选项菜单
  ActionSheet.show({
   
    title: '更多选项',
    sheets: [
      {
    title: '分享', action: () => {
    console.info('分享') } },
      {
    title: '收藏', action: () => {
    console.info('收藏') } },
      {
    title: '举报', action: () => {
    console.info('举报') } }
    ]
  });
}

5. 高级特性实现

5.1 瀑布流布局

为了实现更美观的照片墙布局,我们可以使用瀑布流布局:

@State layoutType: string = 'grid'; // 'grid' 或 'waterfall'

// 在TitleBar方法中添加布局切换按钮
Row() {
   
  Text('照片墙')
    .fontSize(20)
    .fontWeight(FontWeight.Bold)
    .fontColor('#333333')

  Blank()

  Row() {
   
    Button({
    type: ButtonType.Circle }) {
   
      Image(this.layoutType === 'grid' ? $r('app.media.ic_grid') : $r('app.media.ic_waterfall'))
        .width(20)
        .height(20)
        .fillColor('#333333')
    }
    .width(36)
    .height(36)
    .backgroundColor('#F5F5F5')
    .margin({
    right: 16 })
    .onClick(() => {
   
      this.layoutType = this.layoutType === 'grid' ? 'waterfall' : 'grid';
    })

    Image($r('app.media.ic_search'))
      .width(24)
      .height(24)
      .margin({
    right: 16 })

    Image($r('app.media.ic_add'))
      .width(24)
      .height(24)
  }
}

// 修改PhotoGrid方法,支持瀑布流布局
@Builder
private PhotoGrid() {
   
  if (this.layoutType === 'grid') {
   
    // 网格布局
    Scroll() {
   
      GridRow({
   
        columns: this.columns,
        gutter: {
    x: 8, y: 8 },
        breakpoints: {
    value: ['320vp', '600vp', '840vp'], reference: BreakpointsReference.WindowSize }
      }) {
   
        ForEach(this.photos, (photo: Photo) => {
   
          GridCol({
    
            span: {
    xs: this.getPhotoSpan(photo, 2), sm: this.getPhotoSpan(photo, 3), 
                   md: this.getPhotoSpan(photo, 4), lg: this.getPhotoSpan(photo, 6) } 
          }) {
   
            this.PhotoCard(photo)
          }
        })
      }
      .width('100%')
      .padding(8)
    }
    .scrollBar(BarState.Off)
    .scrollable(ScrollDirection.Vertical)
    .width('100%')
    .height('100%')
  } else {
   
    // 瀑布流布局
    Scroll() {
   
      Row() {
   
        // 左列
        Column() {
   
          ForEach(this.getWaterfallPhotos(0), (photo: Photo) => {
   
            this.PhotoCard(photo)
              .margin({
    bottom: 8 })
          })
        }
        .width('50%')
        .padding({
    left: 8, right: 4 })

        // 右列
        Column() {
   
          ForEach(this.getWaterfallPhotos(1), (photo: Photo) => {
   
            this.PhotoCard(photo)
              .margin({
    bottom: 8 })
          })
        }
        .width('50%')
        .padding({
    left: 4, right: 8 })
      }
      .width('100%')
      .padding({
    top: 8, bottom: 8 })
    }
    .scrollBar(BarState.Off)
    .scrollable(ScrollDirection.Vertical)
    .width('100%')
    .height('100%')
  }
}

private getWaterfallPhotos(columnIndex: number): Photo[] {
   
  return this.photos.filter((_, index) => index % 2 === columnIndex);
}

5.2 添加照片过滤和分类

为了提供更好的照片浏览体验,我们可以添加照片过滤和分类功能:

// 照片分类接口
interface PhotoCategory {
   
  id: string;
  name: string;
}

@State categories: PhotoCategory[] = [
  {
    id: 'all', name: '全部' },
  {
    id: 'landscape', name: '风景' },
  {
    id: 'food', name: '美食' },
  {
    id: 'portrait', name: '人像' },
  {
    id: 'architecture', name: '建筑' }
];

@State currentCategory: string = 'all';

// 在Photo接口中添加分类字段
interface Photo {
   
  // 其他字段...
  category: string;  // 照片分类
}

// 在TitleBar下方添加分类标签栏
@Builder
private CategoryTabs() {
   
  Scroll() {
   
    Row() {
   
      ForEach(this.categories, (category: PhotoCategory) => {
   
        Text(category.name)
          .fontSize(14)
          .fontColor(this.currentCategory === category.id ? '#2196F3' : '#666666')
          .backgroundColor(this.currentCategory === category.id ? '#E3F2FD' : 'transparent')
          .padding({
    left: 12, right: 12, top: 6, bottom: 6 })
          .borderRadius(16)
          .margin({
    right: 8 })
          .onClick(() => {
   
            this.currentCategory = category.id;
          })
      })
    }
    .padding({
    left: 16, right: 16 })
  }
  .scrollBar(BarState.Off)
  .scrollable(ScrollDirection.Horizontal)
  .width('100%')
  .height(48)
  .backgroundColor(Color.White)
}

// 在build方法中添加分类标签栏
build() {
   
  Column() {
   
    // 顶部标题栏
    this.TitleBar()

    // 分类标签栏
    this.CategoryTabs()

    // 照片墙网格
    Refresh({
    refreshing: this.refreshing }) {
   
      this.PhotoGrid()
    }
    .onRefresh(() => {
   
      this.refreshData();
    })
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#F5F5F5')
}

// 修改PhotoGrid方法,根据当前分类过滤照片
private getFilteredPhotos(): Photo[] {
   
  if (this.currentCategory === 'all') {
   
    return this.photos;
  } else {
   
    return this.photos.filter(photo => photo.category === this.currentCategory);
  }
}

// 在PhotoGrid方法中使用过滤后的照片
ForEach(this.getFilteredPhotos(), (photo: Photo) => {
   
  // 照片卡片
})

5.3 添加照片编辑功能

为了提供更丰富的功能,我们可以添加照片编辑功能:

@State showPhotoEditor: boolean = false;
@State editingPhoto: Photo | null = null;

// 在PhotoCard方法中的更多选项菜单中添加编辑选项
private showMoreOptions(photo: Photo): void {
   
  ActionSheet.show({
   
    title: '更多选项',
    sheets: [
      {
    title: '编辑', action: () => {
    
        this.editingPhoto = photo;
        this.showPhotoEditor = true;
      } },
      {
    title: '分享', action: () => {
    console.info('分享') } },
      {
    title: '收藏', action: () => {
    console.info('收藏') } },
      {
    title: '举报', action: () => {
    console.info('举报') } }
    ]
  });
}

// 在build方法末尾添加照片编辑器组件
if (this.showPhotoEditor && this.editingPhoto) {
   
  Stack() {
   
    Column() {
   
      // 顶部工具栏
      Row() {
   
        Button('取消')
          .backgroundColor('transparent')
          .fontColor('#333333')
          .onClick(() => {
   
            this.showPhotoEditor = false;
          })

        Text('编辑照片')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)

        Button('保存')
          .backgroundColor('transparent')
          .fontColor('#2196F3')
          .onClick(() => {
   
            this.saveEditedPhoto();
            this.showPhotoEditor = false;
          })
      }
      .width('100%')
      .height(56)
      .padding({
    left: 16, right: 16 })
      .justifyContent(FlexAlign.SpaceBetween)
      .backgroundColor(Color.White)

      // 照片预览
      Image(this.editingPhoto.resource)
        .objectFit(ImageFit.Contain)
        .width('100%')
        .height('60%')
        .backgroundColor('#F5F5F5')

      // 编辑工具
      Column() {
   
        // 滤镜选项
        Text('滤镜')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .width('100%')
          .padding({
    left: 16 })

        Scroll() {
   
          Row() {
   
            ForEach(['原图', '怀旧', '清新', '温暖', '冷色', '黑白'], (filter: string) => {
   
              Column() {
   
                Image(this.editingPhoto.resource)
                  .width(64)
                  .height(64)
                  .borderRadius(8)

                Text(filter)
                  .fontSize(12)
                  .margin({
    top: 4 })
              }
              .margin({
    right: 12 })
            })
          }
          .padding({
    left: 16, right: 16 })
        }
        .scrollBar(BarState.Off)
        .scrollable(ScrollDirection.Horizontal)
        .height(100)

        // 调整选项
        Text('调整')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .width('100%')
          .padding({
    left: 16 })
          .margin({
    top: 16 })

        Column() {
   
          Row() {
   
            Text('亮度')
              .fontSize(14)
              .width('20%')

            Slider({
    min: -100, max: 100, step: 1, value: 0 })
              .width('80%')
              .showTips(true)
          }
          .width('100%')
          .padding({
    left: 16, right: 16 })
          .margin({
    top: 8 })

          Row() {
   
            Text('对比度')
              .fontSize(14)
              .width('20%')

            Slider({
    min: -100, max: 100, step: 1, value: 0 })
              .width('80%')
              .showTips(true)
          }
          .width('100%')
          .padding({
    left: 16, right: 16 })
          .margin({
    top: 8 })

          Row() {
   
            Text('饱和度')
              .fontSize(14)
              .width('20%')

            Slider({
    min: -100, max: 100, step: 1, value: 0 })
              .width('80%')
              .showTips(true)
          }
          .width('100%')
          .padding({
    left: 16, right: 16 })
          .margin({
    top: 8 })
        }
      }
      .width('100%')
      .height('40%')
      .backgroundColor(Color.White)
    }
    .width('100%')
    .height('100%')
  }
  .width('100%')
  .height('100%')
  .position({
    x: 0, y: 0 })
  .zIndex(999)
  .backgroundColor('#F5F5F5')
}

private saveEditedPhoto(): void {
   
  // 保存编辑后的照片
  console.info('保存编辑后的照片');
}

6. 完整代码

以下是优化后的社交应用照片墙网格布局的完整代码(部分代码省略):

// 用户信息接口
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;          // 在网格中占据的列数
  tags?: string[];       // 照片标签
  category: string;      // 照片分类
}

// 照片分类接口
interface PhotoCategory {
   
  id: string;
  name: string;
}

@Component
export struct SocialPhotoWall {
   
  @State photos: Photo[] = []; // 照片数据
  @State columns: GridRowColumnOptions = {
    xs: 2, sm: 3, md: 4, lg: 6 }; // 网格列数
  @State refreshing: boolean = false; // 刷新状态
  @State loadingStates: Map<string, boolean> = new Map<string, boolean>(); // 照片加载状态
  @State likedPhotos: Set<string> = new Set<string>(); // 已点赞的照片
  @State showFullscreenPhoto: boolean = false; // 是否显示全屏照片
  @State currentPhoto: Photo | null = null; // 当前查看的照片
  @State layoutType: string = 'grid'; // 布局类型:'grid' 或 'waterfall'
  @State categories: PhotoCategory[] = [ // 照片分类
    {
    id: 'all', name: '全部' },
    {
    id: 'landscape', name: '风景' },
    {
    id: 'food', name: '美食' },
    {
    id: 'portrait', name: '人像' },
    {
    id: 'architecture', name: '建筑' }
  ];
  @State currentCategory: string = 'all'; // 当前分类
  @State showPhotoEditor: boolean = false; // 是否显示照片编辑器
  @State editingPhoto: Photo | null = null; // 当前编辑的照片

  // 模拟用户数据
  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();

    // 初始化加载状态
    this.photos.forEach(photo => {
   
      this.loadingStates.set(photo.id, true);
    });
  }

  build() {
   
    Column() {
   
      // 顶部标题栏
      this.TitleBar()

      // 分类标签栏
      this.CategoryTabs()

      // 照片墙网格
      Refresh({
    refreshing: this.refreshing }) {
   
        this.PhotoGrid()
      }
      .onRefresh(() => {
   
        this.refreshData();
      })

      // 全屏查看照片
      if (this.showFullscreenPhoto && this.currentPhoto) {
   
        this.FullscreenPhotoView()
      }

      // 照片编辑器
      if (this.showPhotoEditor && this.editingPhoto) {
   
        this.PhotoEditor()
      }
    }
    .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,
        tags: ['海滩', '日落', '夏日'],
        category: 'landscape'
      },
      {
   
        id: '2',
        resource: $r('app.media.photo2'),
        description: '城市天际线,繁华都市的夜景',
        likes: 189,
        comments: 23,
        user: this.users[1],
        publishTime: '3小时前',
        aspectRatio: 1/1,
        span: 1,
        tags: ['城市', '夜景'],
        category: 'architecture'
      },
      // 其他照片数据...
    ];
  }

  // 标题栏构建器
  @Builder
  private TitleBar() {
   
    Row() {
   
      Text('照片墙')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')

      Blank()

      Row() {
   
        Button({
    type: ButtonType.Circle }) {
   
          Image(this.layoutType === 'grid' ? $r('app.media.ic_grid') : $r('app.media.ic_waterfall'))
            .width(20)
            .height(20)
            .fillColor('#333333')
        }
        .width(36)
        .height(36)
        .backgroundColor('#F5F5F5')
        .margin({
    right: 16 })
        .onClick(() => {
   
          this.layoutType = this.layoutType === 'grid' ? 'waterfall' : 'grid';
        })

        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 CategoryTabs() {
   
    Scroll() {
   
      Row() {
   
        ForEach(this.categories, (category: PhotoCategory) => {
   
          Text(category.name)
            .fontSize(14)
            .fontColor(this.currentCategory === category.id ? '#2196F3' : '#666666')
            .backgroundColor(this.currentCategory === category.id ? '#E3F2FD' : 'transparent')
            .padding({
    left: 12, right: 12, top: 6, bottom: 6 })
            .borderRadius(16)
            .margin({
    right: 8 })
            .onClick(() => {
   
              this.currentCategory = category.id;
            })
        })
      }
      .padding({
    left: 16, right: 16 })
    }
    .scrollBar(BarState.Off)
    .scrollable(ScrollDirection.Horizontal)
    .width('100%')
    .height(48)
    .backgroundColor(Color.White)
  }

  // 照片墙网格构建器
  @Builder
  private PhotoGrid() {
   
    if (this.layoutType === 'grid') {
   
      // 网格布局
      Scroll() {
   
        GridRow({
   
          columns: this.columns,
          gutter: {
    x: 8, y: 8 },
          breakpoints: {
    value: ['320vp', '600vp', '840vp'], reference: BreakpointsReference.WindowSize }
        }) {
   
          ForEach(this.getFilteredPhotos(), (photo: Photo) => {
   
            GridCol({
    
              span: {
    xs: this.getPhotoSpan(photo, 2), sm: this.getPhotoSpan(photo, 3), 
                     md: this.getPhotoSpan(photo, 4), lg: this.getPhotoSpan(photo, 6) } 
            }) {
   
              this.PhotoCard(photo)
            }
          })
        }
        .width('100%')
        .padding(8)
      }
      .scrollBar(BarState.Off)
      .scrollable(ScrollDirection.Vertical)
      .width('100%')
      .height('100%')
    } else {
   
      // 瀑布流布局
      Scroll() {
   
        Row() {
   
          // 左列
          Column() {
   
            ForEach(this.getWaterfallPhotos(0), (photo: Photo) => {
   
              this.PhotoCard(photo)
                .margin({
    bottom: 8 })
            })
          }
          .width('50%')
          .padding({
    left: 8, right: 4 })

          // 右列
          Column() {
   
            ForEach(this.getWaterfallPhotos(1), (photo: Photo) => {
   
              this.PhotoCard(photo)
                .margin({
    bottom: 8 })
            })
          }
          .width('50%')
          .padding({
    left: 4, right: 8 })
        }
        .width('100%')
        .padding({
    top: 8, bottom: 8 })
      }
      .scrollBar(BarState.Off)
      .scrollable(ScrollDirection.Vertical)
      .width('100%')
      .height('100%')
    }
  }

  // 照片卡片构建器
  @Builder
  private PhotoCard(photo: Photo) {
   
    Column() {
   
      // 照片
      Stack() {
   
        // 加载占位图
        if (this.loadingStates.get(photo.id)) {
   
          Column() {
   
            LoadingProgress()
              .width(32)
              .height(32)
              .color('#2196F3')
          }
          .width('100%')
          .height(100 * photo.aspectRatio)
          .backgroundColor('#F0F0F0')
          .borderRadius({
    topLeft: 8, topRight: 8 })
        }

        Image(photo.resource)
          .width('100%')
          .aspectRatio(photo.aspectRatio)
          .borderRadius({
    topLeft: 8, topRight: 8 })
          .onComplete(() => {
   
            // 图片加载完成后更新状态
            this.loadingStates.set(photo.id, false);
          })

        // 发布时间标签
        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%')
      .onClick(() => {
   
        this.currentPhoto = photo;
        this.showFullscreenPhoto = true;
      })

      // 照片信息
      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%')
        }

        // 照片标签
        if (photo.tags && photo.tags.length > 0) {
   
          Row() {
   
            ForEach(photo.tags, (tag: string) => {
   
              Text('#' + tag)
                .fontSize(12)
                .fontColor('#2196F3')
                .backgroundColor('#E3F2FD')
                .padding({
    left: 6, right: 6, top: 2, bottom: 2 })
                .borderRadius(4)
                .margin({
    right: 4 })
            })
          }
          .width('100%')
          .margin({
    top: 8 })
          .flexWrap(FlexWrap.Wrap)
        }

        // 互动信息
        Row() {
   
          Row() {
   
            Image(this.likedPhotos.has(photo.id) ? $r('app.media.ic_like_filled') : $r('app.media.ic_like'))
              .width(16)
              .height(16)
              .fillColor(this.likedPhotos.has(photo.id) ? '#F44336' : '#999999')
              .onClick((event: ClickEvent) => {
   
                this.toggleLike(photo.id);
                event.stopPropagation();
              })

            Text(this.getLikeCount(photo).toString())
              .fontSize(12)
              .fontColor('#999999')
              .margin({
    left: 4 })
          }

          Row() {
   
            Image($r('app.media.ic_comment'))
              .width(16)
              .height(16)
              .onClick((event: ClickEvent) => {
   
                this.showCommentDialog(photo);
                event.stopPropagation();
              })

            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)
            .onClick((event: ClickEvent) => {
   
              this.showMoreOptions(photo);
              event.stopPropagation();
            })
        }
        .width('100%')
        .margin({
    top: 8 })
      }
      .width('100%')
      .padding({
    left: 8, right: 8, bottom: 8 })
    }
    .width('100%')
    .backgroundColor(Color.White)
    .borderRadius(8)
    .shadow({
   
      radius: 8,
      color: '#1A000000',
      offsetX: 0,
      offsetY: 2
    })
  }

  // 全屏照片查看构建器
  @Builder
  private FullscreenPhotoView() {
   
    Stack() {
   
      // 半透明背景
      Column()
        .width('100%')
        .height('100%')
        .backgroundColor('#80000000')
        .onClick(() => {
   
          this.showFullscreenPhoto = false;
        })

      // 照片
      Column() {
   
        Image(this.currentPhoto.resource)
          .objectFit(ImageFit.Contain)
          .width('100%')
          .height('100%')
      }
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.Center)

      // 关闭按钮
      Button({
    type: ButtonType.Circle }) {
   
        Image($r('app.media.ic_close'))
          .width(24)
          .height(24)
          .fillColor(Color.White)
      }
      .width(48)
      .height(48)
      .backgroundColor('#33000000')
      .position({
    x: '90%', y: '5%' })
      .onClick(() => {
   
        this.showFullscreenPhoto = false;
      })

      // 照片信息
      Column() {
   
        Row() {
   
          Image(this.currentPhoto.user.avatar)
            .width(32)
            .height(32)
            .borderRadius(16)

          Column() {
   
            Text(this.currentPhoto.user.name)
              .fontSize(16)
              .fontColor(Color.White)

            Text(this.currentPhoto.publishTime)
              .fontSize(12)
              .fontColor('#E0E0E0')
          }
          .alignItems(HorizontalAlign.Start)
          .margin({
    left: 8 })
        }
        .width('100%')
        .padding(16)

        if (this.currentPhoto.description) {
   
          Text(this.currentPhoto.description)
            .fontSize(14)
            .fontColor(Color.White)
            .width('100%')
            .padding({
    left: 16, right: 16, bottom: 16 })
        }
      }
      .width('100%')
      .position({
    x: 0, y: '80%' })
      .backgroundColor('#33000000')
    }
    .width('100%')
    .height('100%')
    .position({
    x: 0, y: 0 })
    .zIndex(999)
  }

  // 照片编辑器构建器
  @Builder
  private PhotoEditor() {
   
    Stack() {
   
      Column() {
   
        // 顶部工具栏
        Row() {
   
          Button('取消')
            .backgroundColor('transparent')
            .fontColor('#333333')
            .onClick(() => {
   
              this.showPhotoEditor = false;
            })

          Text('编辑照片')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)

          Button('保存')
            .backgroundColor('transparent')
            .fontColor('#2196F3')
            .onClick(() => {
   
              this.saveEditedPhoto();
              this.showPhotoEditor = false;
            })
        }
        .width('100%')
        .height(56)
        .padding({
    left: 16, right: 16 })
        .justifyContent(FlexAlign.SpaceBetween)
        .backgroundColor(Color.White)

        // 照片预览
        Image(this.editingPhoto.resource)
          .objectFit(ImageFit.Contain)
          .width('100%')
          .height('60%')
          .backgroundColor('#F5F5F5')

        // 编辑工具
        Column() {
   
          // 滤镜选项
          Text('滤镜')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .width('100%')
            .padding({
    left: 16 })

          Scroll() {
   
            Row() {
   
              ForEach(['原图', '怀旧', '清新', '温暖', '冷色', '黑白'], (filter: string) => {
   
                Column() {
   
                  Image(this.editingPhoto.resource)
                    .width(64)
                    .height(64)
                    .borderRadius(8)

                  Text(filter)
                    .fontSize(12)
                    .margin({
    top: 4 })
                }
                .margin({
    right: 12 })
              })
            }
            .padding({
    left: 16, right: 16 })
          }
          .scrollBar(BarState.Off)
          .scrollable(ScrollDirection.Horizontal)
          .height(100)

          // 调整选项
          Text('调整')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .width('100%')
            .padding({
    left: 16 })
            .margin({
    top: 16 })

          Column() {
   
            Row() {
   
              Text('亮度')
                .fontSize(14)
                .width('20%')

              Slider({
    min: -100, max: 100, step: 1, value: 0 })
                .width('80%')
                .showTips(true)
            }
            .width('100%')
            .padding({
    left: 16, right: 16 })
            .margin({
    top: 8 })

            Row() {
   
              Text('对比度')
                .fontSize(14)
                .width('20%')

              Slider({
    min: -100, max: 100, step: 1, value: 0 })
                .width('80%')
                .showTips(true)
            }
            .width('100%')
            .padding({
    left: 16, right: 16 })
            .margin({
    top: 8 })

            Row() {
   
              Text('饱和度')
                .fontSize(14)
                .width('20%')

              Slider({
    min: -100, max: 100, step: 1, value: 0 })
                .width('80%')
                .showTips(true)
            }
            .width('100%')
            .padding({
    left: 16, right: 16 })
            .margin({
    top: 8 })
          }
        }
        .width('100%')
        .height('40%')
        .backgroundColor(Color.White)
      }
      .width('100%')
      .height('100%')
    }
    .width('100%')
    .height('100%')
    .position({
    x: 0, y: 0 })
    .zIndex(999)
    .backgroundColor('#F5F5F5')
  }

  // 辅助方法
  private getPhotoSpan(photo: Photo, currentColumns: number): number {
   
    // 根据当前列数和照片原始span值计算实际span值
    const originalSpan = photo.span;

    // 确保span值不超过当前列数
    if (originalSpan > currentColumns) {
   
      return currentColumns;
    }

    // 在列数较少时,减小大照片的span值
    if (currentColumns <= 2 && originalSpan > 1) {
   
      return Math.min(originalSpan, 2);
    }

    return originalSpan;
  }

  private getFilteredPhotos(): Photo[] {
   
    if (this.currentCategory === 'all') {
   
      return this.photos;
    } else {
   
      return this.photos.filter(photo => photo.category === this.currentCategory);
    }
  }

  private getWaterfallPhotos(columnIndex: number): Photo[] {
   
    return this.getFilteredPhotos().filter((_, index) => index % 2 === columnIndex);
  }

  private refreshData(): void {
   
    this.refreshing = true;
    // 模拟网络请求
    setTimeout(() => {
   
      // 随机调整照片顺序
      this.photos = this.shuffleArray([...this.photos]);
      this.refreshing = false;
    }, 2000);
  }

  private shuffleArray<T>(array: T[]): T[] {
   
    const newArray = [...array];
    for (let i = newArray.length - 1; i > 0; i--) {
   
      const j = Math.floor(Math.random() * (i + 1));
      [newArray[i], newArray[j]] = [newArray[j], newArray[i]];
    }
    return newArray;
  }

  private toggleLike(photoId: string): void {
   
    if (this.likedPhotos.has(photoId)) {
   
      this.likedPhotos.delete(photoId);
    } else {
   
      this.likedPhotos.add(photoId);
    }
  }

  private getLikeCount(photo: Photo): number {
   
    return photo.likes + (this.likedPhotos.has(photo.id) ? 1 : 0);
  }

  private showCommentDialog(photo: Photo): void {
   
    // 实现评论对话框
    AlertDialog.show({
   
      title: '评论',
      message: '该功能正在开发中...',
      autoCancel: true,
      alignment: DialogAlignment.Bottom,
      offset: {
    dx: 0, dy: -20 },
      primaryButton: {
   
        value: '确定',
        action: () => {
   
          console.info('点击确定按钮');
        }
      }
    });
  }

  private showMoreOptions(photo: Photo): void {
   
    // 实现更多选项菜单
    ActionSheet.show({
   
      title: '更多选项',
      sheets: [
        {
    title: '编辑', action: () => {
    
          this.editingPhoto = photo;
          this.showPhotoEditor = true;
        } },
        {
    title: '分享', action: () => {
    console.info('分享') } },
        {
    title: '收藏', action: () => {
    console.info('收藏') } },
        {
    title: '举报', action: () => {
    console.info('举报') } }
      ]
    });
  }

  private saveEditedPhoto(): void {
   
    // 保存编辑后的照片
    console.info('保存编辑后的照片');
  }
}

7. 总结

本教程详细讲解了如何优化社交应用照片墙网格布局,添加交互功能,以及实现更多高级特性。通过使用GridRow和GridCol组件的高级特性,我们实现了响应式布局,使照片墙能够适应不同屏幕尺寸的设备。同时,我们还添加了照片卡片优化、交互功能、瀑布流布局、照片过滤和分类、照片编辑等功能,打造了一个功能完善的社交应用照片墙。

通过本教程,你应该已经掌握了如何使用HarmonyOS NEXT的GridRow和GridCol组件实现复杂的网格布局,以及如何添加各种交互功能和高级特性,提升用户体验。这些技能可以应用到各种需要网格布局的场景中,如电商商品展示、新闻列表、音乐专辑等。

相关文章
|
24天前
|
开发者 UED
HarmonyOS Next快速入门:通用属性
本教程以《HarmonyOS Next快速入门》为基础,涵盖应用开发核心技能。通过代码实例讲解尺寸、位置、布局约束、Flex布局、边框、背景及图像效果等属性设置方法。如`.width()`调整宽度,`.align()`设定对齐方式,`.border()`配置边框样式,以及模糊、阴影等视觉效果的实现。结合实际案例,帮助开发者掌握HarmonyOS组件属性的灵活运用,提升开发效率与用户体验。适合初学者及进阶开发者学习。
64 0
|
24天前
|
开发者
HarmonyOS Next快速入门:通用事件
本教程聚焦HarmonyOS应用开发,涵盖事件处理的核心内容。包括事件分发、触屏事件、键鼠事件、焦点事件及拖拽事件等。通过代码实例讲解点击事件、触控事件(Down/Move/Up)、获焦与失焦事件的处理逻辑,以及气泡弹窗的应用。适合开发者快速掌握HarmonyOS Next中通用事件的使用方法,提升应用交互体验。
63 0
|
24天前
|
开发者 容器
HarmonyOS Next快速入门:Button组件
本教程摘自《HarmonyOS Next快速入门》,聚焦HarmonyOS应用开发中的Button组件。Button支持胶囊、圆形和普通三种类型,可通过子组件实现复杂功能,如嵌入图片或文字。支持自定义样式(边框弧度、文本样式、背景色等)及点击事件处理。示例代码展示了不同类型按钮的创建与交互逻辑,助开发者快速上手。适合HarmonyOS初学者及对UI组件感兴趣的开发者学习。
68 0
|
24天前
|
开发者
HarmonyOS Next快速入门:Image组件
本教程摘自《HarmonyOS Next快速入门》,专注于HarmonyOS应用开发中的Image组件使用。Image组件支持多种图片格式(如png、jpg、svg等),可渲染本地资源、网络图片、媒体库文件及PixelMap像素图。通过设置`objectFit`属性,实现不同缩放类型;利用`fillColor`属性调整矢量图颜色。示例代码涵盖本地、网络及资源图片的加载与样式设置,同时需在`module.json5`中声明网络权限以加载外部资源。适合开发者快速掌握HarmonyOS图像展示功能。
80 0
|
29天前
|
开发者
鸿蒙开发:资讯项目实战之项目初始化搭建
目前来说,我们的资讯项目只是往前迈了很小的一步,仅仅实现了项目创建,步虽小,但概念性的知识很多,这也是这个项目的初衷,让大家不仅仅可以掌握日常的技术开发,也能让大家理解实际的项目开发知识。
鸿蒙开发:资讯项目实战之项目初始化搭建
|
23天前
|
缓存 JavaScript IDE
鸿蒙开发:基于最新API,如何实现组件化运行
手动只是让大家了解切换的原理,在实际开发中,可不推荐手动,下篇文章,我们将通过脚本或者插件,快速实现组件化模块之间的切换,实现独立运行,敬请期待!
鸿蒙开发:基于最新API,如何实现组件化运行
|
1月前
|
SQL 弹性计算 数据库
鸿蒙5开发宝藏案例分享---优化应用时延问题
鸿蒙性能优化指南来了!从UI渲染到数据库操作,6大实战案例助你提升应用流畅度。布局层级优化、数据加载并发、数据库查询提速、相机资源延迟释放、手势识别灵敏调整及转场动画精调,全面覆盖性能痛点。附赠性能自检清单,帮助开发者高效定位问题,让应用运行如飞!来自华为官方文档的精华内容,建议收藏并反复研读,共同探讨更多优化技巧。
|
1月前
|
缓存
鸿蒙5开发宝藏案例分享---Swiper组件性能优化实战
本文分享了鸿蒙系统中Swiper组件的性能优化技巧,包括:1) 使用`LazyForEach`替代`ForEach`实现懒加载,显著降低内存占用;2) 通过`cachedCount`精准控制缓存数量,平衡流畅度与内存消耗;3) 利用`onAnimationStart`在抛滑时提前加载资源,提升构建效率;4) 添加`@Reusable`装饰器复用组件实例,减少创建开销。实际应用后,图库页帧率从45fps提升至58fps,效果显著。适合处理复杂列表或轮播场景,欢迎交流经验!
|
1月前
|
缓存 JavaScript 前端开发
鸿蒙5开发宝藏案例分享---Web开发优化案例分享
本文深入解读鸿蒙官方文档中的 `ArkWeb` 性能优化技巧,从预启动进程到预渲染,涵盖预下载、预连接、预取POST等八大优化策略。通过代码示例详解如何提升Web页面加载速度,助你打造流畅的HarmonyOS应用体验。内容实用,按需选用,让H5页面快到飞起!
|
1月前
|
数据库
鸿蒙5开发宝藏案例分享---跨线程性能优化指南
本文深入探讨鸿蒙系统跨线程序列化性能优化,借助DevEco Profiler工具定位序列化瓶颈。通过Sendable接口改造、数据瘦身等方法,将5万本书对象的序列化耗时从260ms+降至&lt;8ms,甚至&lt;1ms。总结避坑经验,建议常态化使用Profiler检测,避免传递大对象,提升多线程开发效率。