HarmonyOS NEXT之深入解析Grid网格布局打造精美的照片相册管理集

一、照片相册的基础网格布局

① 数据模型定义

  • 相册数据模型:

    interface Album {
    id: number, // 相册唯一标识
    name: string, // 相册名称
    count: number, // 相册中的照片数量
    cover: Resource, // 相册封面图片
    date: string. // 相册创建或更新日期
    }

  • 照片数据模型:

    interface Recentphoto {
    id: number, // 照片唯一标识
    image: Resource, // 照片资源
    date: string, // 照片拍摄日期时间
    location?: string. // 照片拍摄地点
    }

② 页面布局

  • 标签切换,使用两个 Text 组件实现,通过 currentTab 状态变量控制当前选中的标签样式:

    // 标签切换
    Row() {
    Text('相册')
    .fontSize(16)
    .fontWeight(this.currentTab === 0 ? FontWeight.Bold : FontWeight.Normal)
    .fontColor(this.currentTab === 0 ? '#007AFF' : '#8E8E93')
    .padding({ left: 16, right: 16, top: 8, bottom: 8 })
    .borderRadius(16)
    .backgroundColor(this.currentTab === 0 ? 'rgba(0, 122, 255, 0.1)' : 'transparent')
    .onClick(() => {
    this.currentTab = 0
    })

    复制代码
      Text('最近项目')
          .fontSize(16)
          .fontWeight(this.currentTab === 1 ? FontWeight.Bold : FontWeight.Normal)
          .fontColor(this.currentTab === 1 ? '#007AFF' : '#8E8E93')
          .padding({ left: 16, right: 16, top: 8, bottom: 8 })
          .borderRadius(16)
          .backgroundColor(this.currentTab === 1 ? 'rgba(0, 122, 255, 0.1)' : 'transparent')
          .margin({ left: 12 })
          .onClick(() => {
              this.currentTab = 1
          })

    }
    .width('100%')
    .padding({ left: 20, right: 20, top: 8, bottom: 8 })
    .backgroundColor('#FFFFFF')

  • 相册视图(2 列布局),使用 Grid 组件实现 2 列布局,每个 GridItem 包含相册封面和相册信息:

    Column() {
    Text('我的相册')
    .fontSize(20)
    .fontWeight(FontWeight.Bold)
    .fontColor('#000000')
    .alignSelf(ItemAlign.Start)
    .margin({ bottom: 16 })

    复制代码
      Grid() {
          ForEach(this.albums, (album:Album) => {
              GridItem() {
                  Column() {
                      // 相册封面
                      Image(album.cover)
                          .width('100%')
                          .height(140)
                          .objectFit(ImageFit.Cover)
                          .borderRadius(12)
    
                      // 相册信息
                      Column() {
                          Text(album.name)
                              .fontSize(16)
                              .fontWeight(FontWeight.Medium)
                              .fontColor('#000000')
                              .maxLines(1)
                              .textOverflow({ overflow: TextOverflow.Ellipsis })
    
                          Row() {
                              Text(`${album.count}张`)
                                  .fontSize(14)
                                  .fontColor('#8E8E93')
    
                              Blank()
    
                              Text(album.date)
                                  .fontSize(12)
                                  .fontColor('#8E8E93')
                          }
                          .width('100%')
                          .margin({ top: 4 })
                      }
                      .alignItems(HorizontalAlign.Start)
                      .width('100%')
                      .margin({ top: 12 })
                  }
                  .width('100%')
                  .padding(16)
                  .backgroundColor('#FFFFFF')
                  .borderRadius(16)
                  .shadow({
                      radius: 8,
                      color: 'rgba(0, 0, 0, 0.08)',
                      offsetX: 0,
                      offsetY: 2
                  })
              }
              .onClick(() => {
                  console.log(`打开相册: ${album.name}`)
              })
          })
      }
      .columnsTemplate('1fr 1fr') // 2列布局
      .columnsGap(16)
      .rowsGap(16)
      .width('100%')
      .layoutWeight(1)

    }
    .width('100%')
    .layoutWeight(1)
    .padding({ left: 20, right: 20, top: 16, bottom: 20 })
    .backgroundColor('#F2F2F7')

  • 最近项目视图(3 列布局),使用 Grid 组件实现 3 列布局,每个 GridItem 包含照片和可选的位置信息覆盖层:

    Column() {
    Row() {
    Text('最近添加')
    .fontSize(20)
    .fontWeight(FontWeight.Bold)
    .fontColor('#000000')

    复制代码
          Blank()
    
          Text('选择')
              .fontSize(16)
              .fontColor('#007AFF')
      }
      .width('100%')
      .margin({ bottom: 16 })
    
      Grid() {
          ForEach(this.recentPhotos, (photo:Recentphoto) => {
              GridItem() {
                  Stack({ alignContent: Alignment.BottomStart }) {
                      Image(photo.image)
                          .width('100%')
                          .height(120)
                          .objectFit(ImageFit.Cover)
                          .borderRadius(8)
    
                      // 位置信息覆盖层
                      if (photo.location) {
                          Row() {
                              Image($r('app.media.location_icon'))
                                  .width(12)
                                  .height(12)
                                  .fillColor('#FFFFFF')
    
                              Text(photo.location)
                                  .fontSize(10)
                                  .fontColor('#FFFFFF')
                                  .margin({ left: 4 })
                          }
                          .padding({ left: 6, right: 6, top: 4, bottom: 4 })
                          .backgroundColor('rgba(0, 0, 0, 0.6)')
                          .borderRadius(8)
                          .margin({ left: 8, bottom: 8 })
                      }
                  }
                  .width('100%')
                  .height(120)
              }
              .onClick(() => {
                  console.log(`查看照片: ${photo.id}`)
              })
          })
      }
      .columnsTemplate('1fr 1fr 1fr') // 3列布局
      .columnsGap(4)
      .rowsGap(4)
      .width('100%')
      .layoutWeight(1)

    }
    .width('100%')
    .layoutWeight(1)
    .padding({ left: 20, right: 20, top: 16, bottom: 20 })
    .backgroundColor('#F2F2F7')

二、状态管理与交互

  • 使用 @State 装饰器定义了几个关键的状态变量:

    @State currentTab: number = 0; // 当前选中的标签页(0: 相册, 1: 最近项目)
    @State albums: Album[] = []; // 相册数据
    @State recentPhotos: Recentphoto[] = []; // 最近照片数据

  • 标签页切换是照片相册应用中的核心交互之一,可以通过以下方式实现:

    Text('相册')
    .fontSize(16)
    .fontWeight(this.currentTab === 0 ? FontWeight.Bold : FontWeight.Normal)
    .fontColor(this.currentTab === 0 ? '#007AFF' : '#8E8E93')
    .padding({ left: 16, right: 16, top: 8, bottom: 8 })
    .borderRadius(16)
    .backgroundColor(this.currentTab === 0 ? 'rgba(0, 122, 255, 0.1)' : 'transparent')
    .onClick(() => {
    this.currentTab = 0
    })

  • 根据当前选中的标签页,使用条件渲染显示不同的内容:

    if (this.currentTab === 0) {
    // 相册视图
    Column() {
    // 相册内容...
    }
    } else {
    // 最近项目视图
    Column() {
    // 最近项目内容...
    }
    }

三、Grid 组件进阶布局

① 不同列数的网格布局

  • 为不同的内容区域设置了不同的列数:

    // 相册视图 - 2列布局
    Grid() {
    // GridItem 内容...
    }
    .columnsTemplate('1fr 1fr') // 2列等宽布局
    .columnsGap(16)
    .rowsGap(16)

    // 最近项目视图 - 3列布局
    Grid() {
    // GridItem 内容...
    }
    .columnsTemplate('1fr 1fr 1fr') // 3列等宽布局
    .columnsGap(4)
    .rowsGap(4)

  • 不同列数的设计考虑以下因素:

    • 相册视图:每个相册包含的信息较多(封面、名称、照片数量、日期),需要更大的显示空间,因此采用 2 列布局;
    • 最近项目视图:照片本身是主要内容,信息较少,可以采用 3 列布局,在同样的空间内展示更多照片。

② 自适应高度的 GridItem

  • 不为 GridItem 设置固定高度,而是让其根据内容自适应:

    GridItem() {
    Column() {
    // 相册封面 - 固定高度
    Image(album.cover)
    .width('100%')
    .height(140)
    .objectFit(ImageFit.Cover)
    .borderRadius(12)

    复制代码
          // 相册信息 - 自适应高度
          Column() {
              Text(album.name)
                  .fontSize(16)
                  .fontWeight(FontWeight.Medium)
                  .fontColor('#000000')
                  .maxLines(1)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
    
              Row() {
                  Text(`${album.count}张`)
                      .fontSize(14)
                      .fontColor('#8E8E93')
    
                  Blank()
    
                  Text(album.date)
                      .fontSize(12)
                      .fontColor('#8E8E93')
              }
              .width('100%')
              .margin({ top: 4 })
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')
          .margin({ top: 12 })
      }
      .width('100%')
      .padding(16)

    }

  • 这样设计的优势在于:

    • 适应不同内容长度:相册名称可能有长有短,自适应高度可以确保所有内容都能完整显示;
    • 布局灵活性:不同 GridItem 可以有不同的高度,更符合实际内容的需求;
    • 维护简便:后续如果需要在 GridItem 中添加新的内容,不需要重新计算和调整高度。

③ 固定高度的 GridItem

  • 设置固定高度:

    GridItem() {
    Stack({ alignContent: Alignment.BottomStart }) {
    Image(photo.image)
    .width('100%')
    .height(120)
    .objectFit(ImageFit.Cover)
    .borderRadius(8)

    复制代码
          // 位置信息覆盖层
          if (photo.location) {
              // 位置信息内容...
          }
      }
      .width('100%')
      .height(120)

    }

  • 固定高度的设计适用于以下场景:

    • 内容统一:所有照片都使用相同的显示尺寸,视觉上更加整齐;
    • 性能优化:固定高度可以减少布局计算,提高渲染性能;
    • 网格美观:确保所有照片在网格中排列整齐,不会因为内容不同而导致高度不一。

四、组件复用与封装

① 可复用的 UI 组件

  • 提取相册卡片组件:

    @Builder
    function AlbumCard(album: Album) {
    Column() {
    // 相册封面
    Image(album.cover)
    .width('100%')
    .height(140)
    .objectFit(ImageFit.Cover)
    .borderRadius(12)

    复制代码
          // 相册信息
          Column() {
              Text(album.name)
                  .fontSize(16)
                  .fontWeight(FontWeight.Medium)
                  .fontColor('#000000')
                  .maxLines(1)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
    
              Row() {
                  Text(`${album.count}张`)
                      .fontSize(14)
                      .fontColor('#8E8E93')
    
                  Blank()
    
                  Text(album.date)
                      .fontSize(12)
                      .fontColor('#8E8E93')
              }
              .width('100%')
              .margin({ top: 4 })
          }
          .alignItems(HorizontalAlign.Start)
          .width('100%')
          .margin({ top: 12 })
      }
      .width('100%')
      .padding(16)
      .backgroundColor('#FFFFFF')
      .borderRadius(16)
      .shadow({
          radius: 8,
          color: 'rgba(0, 0, 0, 0.08)',
          offsetX: 0,
          offsetY: 2
      })

    }

  • 使用提取的组件:

    Grid() {
    ForEach(this.albums, (album:Album) => {
    GridItem() {
    AlbumCard(album)
    }
    .onClick(() => {
    console.log(打开相册: ${album.name})
    })
    })
    }

② 组件封装

  • 可以封装优化一下交互逻辑,使代码更加清晰:

    // 封装标签切换逻辑
    @Builder
    function TabItem(text: string, index: number, currentIndex: number, onTabClick: () => void) {
    Text(text)
    .fontSize(16)
    .fontWeight(currentIndex === index ? FontWeight.Bold : FontWeight.Normal)
    .fontColor(currentIndex === index ? '#007AFF' : '#8E8E93')
    .padding({ left: 16, right: 16, top: 8, bottom: 8 })
    .borderRadius(16)
    .backgroundColor(currentIndex === index ? 'rgba(0, 122, 255, 0.1)' : 'transparent')
    .onClick(onTabClick)
    }

    // 使用封装的标签组件
    Row() {
    TabItem('相册', 0, this.currentTab, () => { this.currentTab = 0 })
    TabItem('最近项目', 1, this.currentTab, () => { this.currentTab = 1 })
    .margin({ left: 12 })
    }

五、拓展功能实现

  • 照片位置信息显示:

    if (photo.location) {
    Row() {
    Image($r('app.media.location_icon'))
    .width(12)
    .height(12)
    .fillColor('#FFFFFF')

    复制代码
          Text(photo.location)
              .fontSize(10)
              .fontColor('#FFFFFF')
              .margin({ left: 4 })
      }
      .padding({ left: 6, right: 6, top: 4, bottom: 4 })
      .backgroundColor('rgba(0, 0, 0, 0.6)')
      .borderRadius(8)
      .margin({ left: 8, bottom: 8 })

    }

  • 为相册和照片添加了点击事件处理:

    // 相册点击事件
    GridItem() {
    AlbumCard(album)
    }
    .onClick(() => {
    console.log(打开相册: ${album.name})
    })

    // 照片点击事件
    GridItem() {
    // 照片内容...
    }
    .onClick(() => {
    console.log(查看照片: ${photo.id})
    })

  • 点击事件可以用于以下功能:

    • 打开相册详情:点击相册卡片,导航到相册详情页面,显示该相册中的所有照片;
    • 查看照片大图:点击照片,打开照片查看器,支持放大、缩小、滑动等操作;
    • 编辑照片信息:长按照片,弹出编辑菜单,支持修改照片信息、删除照片等操作。

六、Grid 组件高级应用

① Grid 组件高级定位应用

  • 网格项定位与跨行跨列:使用 Grid 组件的高级定位特性,实现更复杂的布局效果:

    // 使用 rowStart、rowEnd、columnStart、columnEnd 实现跨行跨列
    Grid() {
    // 标题行 - 跨越所有列
    GridItem() {
    Text('今日精选')
    .fontSize(20)
    .fontWeight(FontWeight.Bold)
    .fontColor('#000000')
    }
    .rowStart(0)
    .rowEnd(1)
    .columnStart(0)
    .columnEnd(3) // 跨越所有3列

    复制代码
      // 主图 - 跨越2行2列
      GridItem() {
          Image(this.featuredPhotos[0].image)
              .width('100%')
              .height('100%')
              .objectFit(ImageFit.Cover)
              .borderRadius(12)
      }
      .rowStart(1)
      .rowEnd(3) // 跨越2行
      .columnStart(0)
      .columnEnd(2) // 跨越2列
      
      // 右侧小图1
      GridItem() {
          Image(this.featuredPhotos[1].image)
              .width('100%')
              .height('100%')
              .objectFit(ImageFit.Cover)
              .borderRadius(12)
      }
      .rowStart(1)
      .rowEnd(2)
      .columnStart(2)
      .columnEnd(3)
      
      // 右侧小图2
      GridItem() {
          Image(this.featuredPhotos[2].image)
              .width('100%')
              .height('100%')
              .objectFit(ImageFit.Cover)
              .borderRadius(12)
      }
      .rowStart(2)
      .rowEnd(3)
      .columnStart(2)
      .columnEnd(3)

    }
    .columnsTemplate('1fr 1fr 1fr')
    .rowsTemplate('auto 1fr 1fr')
    .columnsGap(12)
    .rowsGap(12)
    .width('100%')
    .height(360)

  • 网格自动布局与手动布局结合,实现更灵活的照片展示,可以在同一个 Grid 中同时使用手动定位和自动布局,非常适合需要特殊处理某些网格项的场景:

    // 结合自动布局和手动布局
    Grid() {
    // 手动布局部分 - 精选照片
    GridItem() {
    // 精选照片内容...
    }
    .rowStart(0)
    .rowEnd(1)
    .columnStart(0)
    .columnEnd(3)

    复制代码
      // 自动布局部分 - 普通照片列表
      ForEach(this.normalPhotos, (photo: Recentphoto) => {
          GridItem() {
              // 普通照片内容...
          }
      })

    }
    .columnsTemplate('1fr 1fr 1fr')
    .rowsTemplate('auto 1fr 1fr 1fr') // 第一行为精选照片,后续行为普通照片

  • 嵌套 Grid 实现复杂布局,如分区展示、混合布局等:

    Grid() {
    // 相册分区
    GridItem() {
    Column() {
    Text('我的相册')
    .fontSize(20)
    .fontWeight(FontWeight.Bold)
    .fontColor('#000000')
    .alignSelf(ItemAlign.Start)
    .margin({ bottom: 16 })

    复制代码
              // 内层 Grid - 相册网格
              Grid() {
                  ForEach(this.albums, (album: Album) => {
                      GridItem() {
                          // 相册卡片内容...
                      }
                  })
              }
              .columnsTemplate('1fr 1fr')
              .columnsGap(16)
              .rowsGap(16)
              .width('100%')
          }
          .width('100%')
      }
      .rowStart(0)
      .rowEnd(1)
      .columnStart(0)
      .columnEnd(1)
      
      // 最近照片分区
      GridItem() {
          Column() {
              Text('最近照片')
                  .fontSize(20)
                  .fontWeight(FontWeight.Bold)
                  .fontColor('#000000')
                  .alignSelf(ItemAlign.Start)
                  .margin({ bottom: 16 })
              
              // 内层 Grid - 照片网格
              Grid() {
                  ForEach(this.recentPhotos.slice(0, 9), (photo: Recentphoto) => {
                      GridItem() {
                          // 照片内容...
                      }
                  })
              }
              .columnsTemplate('1fr 1fr 1fr')
              .columnsGap(4)
              .rowsGap(4)
              .width('100%')
          }
          .width('100%')
      }
      .rowStart(0)
      .rowEnd(1)
      .columnStart(1)
      .columnEnd(2)

    }
    .columnsTemplate('1fr 1fr')
    .columnsGap(24)
    .width('100%')

② 高级交互与动画效果

  • 网格项动画效果:

    // 网格项动画效果
    @State pressedItem: number = -1; // 记录当前按下的项

    GridItem() {
    Stack({ alignContent: Alignment.BottomStart }) {
    // 照片内容...
    }
    .width('100%')
    .height(120)
    .scale({ x: this.pressedItem === photo.id ? 0.95 : 1, y: this.pressedItem === photo.id ? 0.95 : 1 })
    .opacity(this.pressedItem === photo.id ? 0.8 : 1)
    .animation({
    duration: 100,
    curve: Curve.FastOutSlowIn
    })
    }
    .onTouch((event: TouchEvent) => {
    if (event.type === TouchType.Down) {
    this.pressedItem = photo.id;
    } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
    this.pressedItem = -1;
    }
    })
    .onClick(() => {
    console.log(查看照片: ${photo.id});
    })

  • 网格视图切换动画:

    @State currentTab: number = 0;
    @State previousTab: number = 0;
    @State animationValue: number = 0; // 0 表示相册视图,1 表示最近项目视图

    // 切换标签页的函数
    changeTab(index: number) {
    this.previousTab = this.currentTab;
    this.currentTab = index;

    复制代码
      // 创建动画效果
      animateTo({
          duration: 300,
          curve: Curve.EaseInOut,
          onFinish: () => {
              // 动画完成后更新状态
              this.animationValue = index;
          }
      }, () => {
          this.animationValue = index;
      });

    }

    // 在构建函数中使用动画值
    build() {
    Column() {
    // 标签切换部分...

    复制代码
          // 内容区域
          Stack() {
              // 相册视图
              Column() {
                  // 相册内容...
              }
              .width('100%')
              .layoutWeight(1)
              .opacity(1 - this.animationValue)
              .translate({ x: this.animationValue * -100 })
              
              // 最近项目视图
              Column() {
                  // 最近项目内容...
              }
              .width('100%')
              .layoutWeight(1)
              .opacity(this.animationValue)
              .translate({ x: (1 - this.animationValue) * 100 })
          }
          .width('100%')
          .layoutWeight(1)
      }

    }

  • 滚动加载与刷新:

    @State isRefreshing: boolean = false;
    @State isLoading: boolean = false;
    @State hasMorePhotos: boolean = true;

    Column() {
    // 最近项目视图
    Refresh({ refreshing: $$this.isRefreshing, offset: 120, friction: 100 }) {
    List({ space: 0 }) {
    // 照片网格
    ListItem() {
    Grid() {
    ForEach(this.recentPhotos, (photo: Recentphoto) => {
    GridItem() {
    // 照片内容...
    }
    })
    }
    .columnsTemplate('1fr 1fr 1fr')
    .columnsGap(4)
    .rowsGap(4)
    .width('100%')
    }

    复制代码
              // 加载更多
              if (this.hasMorePhotos) {
                  ListItem() {
                      Row() {
                          if (this.isLoading) {
                              LoadingProgress()
                                  .width(24)
                                  .height(24)
                                  .color('#007AFF')
                              
                              Text('加载中...')
                                  .fontSize(14)
                                  .fontColor('#8E8E93')
                                  .margin({ left: 8 })
                          } else {
                              Text('加载更多')
                                  .fontSize(14)
                                  .fontColor('#007AFF')
                          }
                      }
                      .width('100%')
                      .justifyContent(FlexAlign.Center)
                      .height(60)
                      .onClick(() => {
                          if (!this.isLoading) {
                              this.loadMorePhotos();
                          }
                      })
                  }
              }
          }
          .width('100%')
          .layoutWeight(1)
          .onReachEnd(() => {
              if (this.hasMorePhotos && !this.isLoading) {
                  this.loadMorePhotos();
              }
          })
      }
      .onRefreshing(() => {
          this.refreshPhotos();
      })

    }

    // 刷新照片数据
    async refreshPhotos() {
    this.isRefreshing = true;

    复制代码
      // 模拟网络请求
      await new Promise((resolve) => setTimeout(resolve, 1500));
      
      // 更新数据
      // ...
      
      this.isRefreshing = false;

    }

    // 加载更多照片
    async loadMorePhotos() {
    this.isLoading = true;

    复制代码
      // 模拟网络请求
      await new Promise((resolve) => setTimeout(resolve, 1500));
      
      // 添加更多照片
      // ...
      
      this.isLoading = false;
      
      // 判断是否还有更多照片
      // ...

    }

③ 复杂布局与交互场景

  • 照片分组与分类展示:

    interface PhotoGroup {
    title: string,
    date: string,
    photos: Recentphoto[]
    }

    @State photoGroups: PhotoGroup[] = [
    {
    title: '今天',
    date: '2023年5月15日',
    photos: [/* 照片数据 /]
    },
    {
    title: '昨天',
    date: '2023年5月14日',
    photos: [/
    照片数据 */]
    },
    // 更多分组...
    ];

    // 分组展示照片
    Column() {
    List({ space: 20 }) {
    ForEach(this.photoGroups, (group: PhotoGroup) => {
    ListItem() {
    Column() {
    // 分组标题
    Row() {
    Text(group.title)
    .fontSize(18)
    .fontWeight(FontWeight.Bold)
    .fontColor('#000000')

    复制代码
                          Text(group.date)
                              .fontSize(14)
                              .fontColor('#8E8E93')
                              .margin({ left: 8 })
                      }
                      .width('100%')
                      .margin({ bottom: 12 })
                      
                      // 照片网格
                      Grid() {
                          ForEach(group.photos, (photo: Recentphoto) => {
                              GridItem() {
                                  // 照片内容...
                              }
                          })
                      }
                      .columnsTemplate('1fr 1fr 1fr')
                      .columnsGap(4)
                      .rowsGap(4)
                      .width('100%')
                  }
                  .width('100%')
              }
          })
      }
      .width('100%')
      .layoutWeight(1)

    }

  • 实现照片的多选模式,可以支持批量删除、分享、移动等操作,提升用户效率:

    // 多选模式
    @State isSelectMode: boolean = false;
    @State selectedPhotos: number[] = []; // 存储已选中照片的 ID

    // 切换选择模式
    toggleSelectMode() {
    this.isSelectMode = !this.isSelectMode;
    if (!this.isSelectMode) {
    this.selectedPhotos = [];
    }
    }

    // 选择或取消选择照片
    toggleSelectPhoto(photoId: number) {
    const index = this.selectedPhotos.indexOf(photoId);
    if (index === -1) {
    this.selectedPhotos.push(photoId);
    } else {
    this.selectedPhotos.splice(index, 1);
    }
    }

    // 在 GridItem 中实现选择状态
    GridItem() {
    Stack({ alignContent: Alignment.TopEnd }) {
    // 照片内容...

    复制代码
          // 选择状态指示器
          if (this.isSelectMode) {
              Image(this.selectedPhotos.includes(photo.id) ? $r('app.media.selected_icon') : $r('app.media.unselected_icon'))
                  .width(24)
                  .height(24)
                  .margin({ top: 8, right: 8 })
          }
      }
      .width('100%')
      .height(120)

    }
    .onClick(() => {
    if (this.isSelectMode) {
    this.toggleSelectPhoto(photo.id);
    } else {
    console.log(查看照片: ${photo.id});
    }
    })

  • 拖拽排序功能,可以让用户自定义照片的排列顺序:

    @State isDragging: boolean = false;
    @State draggedPhotoId: number = -1;
    @State draggedPosition: { x: number, y: number } = { x: 0, y: 0 };
    @State originalPosition: { x: number, y: number } = { x: 0, y: 0 };
    @State photoPositions: Map<number, { row: number, col: number }> = new Map();

    // 在 GridItem 中实现拖拽功能
    GridItem() {
    Stack() {
    // 照片内容...
    }
    .width('100%')
    .height(120)
    .position({ x: this.draggedPhotoId === photo.id ? this.draggedPosition.x : 0, y: this.draggedPhotoId === photo.id ? this.draggedPosition.y : 0 })
    .zIndex(this.draggedPhotoId === photo.id ? 999 : 1)
    .opacity(this.draggedPhotoId === photo.id ? 0.8 : 1)
    .animation({
    duration: this.isDragging ? 0 : 300,
    curve: Curve.EaseOut
    })
    }
    .gesture(
    PanGesture({ fingers: 1, direction: PanDirection.All })
    .onActionStart((event: GestureEvent) => {
    if (this.isEditMode) {
    this.isDragging = true;
    this.draggedPhotoId = photo.id;
    this.originalPosition = { x: 0, y: 0 };
    this.draggedPosition = { x: 0, y: 0 };
    }
    })
    .onActionUpdate((event: GestureEvent) => {
    if (this.isDragging && this.draggedPhotoId === photo.id) {
    this.draggedPosition = {
    x: this.originalPosition.x + event.offsetX,
    y: this.originalPosition.y + event.offsetY
    };

    复制代码
                  // 计算当前位置对应的网格位置
                  // 实现照片位置交换逻辑
                  // ...
              }
          })
          .onActionEnd(() => {
              if (this.isDragging && this.draggedPhotoId === photo.id) {
                  this.isDragging = false;
                  this.draggedPosition = { x: 0, y: 0 };
                  this.draggedPhotoId = -1;
                  
                  // 更新照片顺序
                  // ...
              }
          })

    )

④ 瀑布流布局实现

复制代码
// 瀑布流布局
@State photoHeights: Map<number, number> = new Map(); // 存储每张照片的高度

// 计算每列的高度
getColumnHeight(columnIndex: number): number {
    let height = 0;
    for (const [photoId, photoInfo] of this.photoPositions.entries()) {
        if (photoInfo.col === columnIndex) {
            height += this.photoHeights.get(photoId) || 0;
        }
    }
    return height;
}

// 为新照片选择最短的列
getShortestColumn(): number {
    let shortestColumn = 0;
    let minHeight = this.getColumnHeight(0);
    
    for (let i = 1; i < 3; i++) { // 假设有3列
        const height = this.getColumnHeight(i);
        if (height < minHeight) {
            minHeight = height;
            shortestColumn = i;
        }
    }
    
    return shortestColumn;
}

// 在加载照片时计算位置
loadPhotos() {
    // 清空现有位置信息
    this.photoPositions.clear();
    
    // 为每张照片分配位置
    this.recentPhotos.forEach((photo, index) => {
        // 根据照片宽高比计算高度
        const aspectRatio = photo.width / photo.height;
        const width = px2vp(window.getWindowWidth() - 48) / 3; // 3列布局,减去边距和间距
        const height = width / aspectRatio;
        
        this.photoHeights.set(photo.id, height);
        
        // 选择最短的列
        const column = this.getShortestColumn();
        
        // 记录照片位置
        this.photoPositions.set(photo.id, {
            row: 0, // 行号在瀑布流中不重要
            col: column
        });
    });
}

// 在 Grid 中展示照片
Grid() {
    ForEach(this.recentPhotos, (photo: Recentphoto) => {
        GridItem() {
            Image(photo.image)
                .width('100%')
                .height(this.photoHeights.get(photo.id) || 120)
                .objectFit(ImageFit.Cover)
                .borderRadius(8)
        }
        .columnStart(this.photoPositions.get(photo.id)?.col || 0)
        .columnEnd((this.photoPositions.get(photo.id)?.col || 0) + 1)
    })
}
.columnsTemplate('1fr 1fr 1fr')
.columnsGap(4)
.rowsGap(4)
.width('100%')

⑤ 混合布局

复制代码
/ 混合布局策略
Column() {
    // 顶部轮播图
    Swiper() {
        ForEach(this.featuredPhotos, (photo: Recentphoto) => {
            Image(photo.image)
                .width('100%')
                .height(200)
                .objectFit(ImageFit.Cover)
                .borderRadius(16)
        })
    }
    .width('100%')
    .height(200)
    .margin({ bottom: 20 })
    .indicatorStyle({ selectedColor: '#007AFF' })
    
    // 相册快速访问 - 水平滚动
    Text('我的相册')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor('#000000')
        .alignSelf(ItemAlign.Start)
        .margin({ bottom: 12 })
    
    ScrollBar({ direction: ScrollBarDirection.Horizontal }) {
        Row() {
            ForEach(this.albums, (album: Album) => {
                Column() {
                    // 相册封面
                    Image(album.cover)
                        .width(120)
                        .height(120)
                        .objectFit(ImageFit.Cover)
                        .borderRadius(12)
                    
                    // 相册名称
                    Text(album.name)
                        .fontSize(14)
                        .fontColor('#000000')
                        .maxLines(1)
                        .textOverflow({ overflow: TextOverflow.Ellipsis })
                        .width(120)
                        .margin({ top: 8 })
                }
                .margin({ right: 16 })
            })
        }
        .width('100%')
        .padding({ left: 20, right: 20 })
    }
    .width('100%')
    .height(160)
    .margin({ bottom: 20 })
    
    // 最近照片 - 网格布局
    Text('最近照片')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor('#000000')
        .alignSelf(ItemAlign.Start)
        .margin({ bottom: 12 })
    
    Grid() {
        // 照片网格内容...
    }
    .columnsTemplate('1fr 1fr 1fr')
    .columnsGap(4)
    .rowsGap(4)
    .width('100%')
    .layoutWeight(1)
}

⑥ 动态布局

复制代码
// 动态布局适配
@State screenWidth: number = 0;
@State screenHeight: number = 0;
@State orientation: string = 'portrait'; // 'portrait' 或 'landscape'

aboutToAppear() {
    // 获取屏幕尺寸
    this.updateScreenSize();
    
    // 监听屏幕旋转
    window.on('resize', () => {
        this.updateScreenSize();
    });
}

updateScreenSize() {
    this.screenWidth = px2vp(window.getWindowWidth());
    this.screenHeight = px2vp(window.getWindowHeight());
    this.orientation = this.screenWidth > this.screenHeight ? 'landscape' : 'portrait';
}

build() {
    if (this.orientation === 'portrait') {
        // 竖屏布局
        Column() {
            // 竖屏内容...
        }
    } else {
        // 横屏布局
        Row() {
            // 左侧导航
            Column() {
                // 导航内容...
            }
            .width('25%')
            .height('100%')
            
            // 右侧内容
            Column() {
                // 相册/照片内容...
            }
            .width('75%')
            .height('100%')
        }
    }
}
相关推荐
╰つ栺尖篴夢ゞ7 小时前
HarmonyOS NEXT之深入解析Grid网格布局列表的交互与状态管理
harmonyos next·列表·grid·griditem
Highcharts.js7 天前
(最新)Highcharts Dashbords 仪表板 网格组件(Grid Component)使用文档
开发文档·仪表板·grid·highcharts·dashboards·包装器·网格组件
Irene19919 天前
CSS Grid布局详解
css·grid
昔人'1 个月前
grid: auto-fit 和 auto-fill区别
css·grid
╰つ栺尖篴夢ゞ1 个月前
HarmonyOS之深入解析如何实现语音朗读能力
华为·api·harmonyos next·语音朗读
摘星编程2 个月前
【案例实战】HarmonyOS SDK新体验:利用近场能力打造无缝的跨设备文件传输功能
华为·harmonyos·harmonyos next·nfc
李洋-蛟龙腾飞公司2 个月前
元服务上架自检
harmonyos next
Demoncode_y2 个月前
前端布局入门:flex、grid 及其他常用布局
前端·css·布局·flex·grid
it奔跑在路上3 个月前
DevEco Studio 编辑器的使用
华为·编辑器·harmonyos·harmonyos next