161. [HarmonyOS NEXT 实战案例二:Grid] 照片相册网格布局:进阶篇

2025-06-30 22:42:13
102次阅读
0个评论

[HarmonyOS NEXT 实战案例二:Grid] 照片相册网格布局:进阶篇

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

效果演示

image.png

1. 状态管理与交互设计

1.1 状态变量设计

在照片相册应用中,我们使用 @State 装饰器定义了几个关键的状态变量:

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

这些状态变量的变化会自动触发 UI 的更新,实现响应式的用户界面。

1.2 标签页切换交互

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

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

标签页切换的交互设计包含以下几个方面:

交互元素 默认状态 选中状态 交互效果
文本字重 Normal Bold 选中标签文本加粗
文本颜色 #8E8E93(灰色) #007AFF(蓝色) 选中标签文本变为蓝色
背景颜色 transparent(透明) rgba(0, 122, 255, 0.1)(淡蓝色) 选中标签背景变为淡蓝色

通过这种设计,用户可以清晰地识别当前所在的标签页,提升用户体验。

1.3 条件渲染内容区域

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

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

这种方式可以确保只渲染当前需要显示的内容,提高应用性能。

2. Grid 组件高级布局技巧

2.1 不同列数的网格布局

在照片相册应用中,我们为不同的内容区域设置了不同的列数:

// 相册视图 - 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 列布局,在同样的空间内展示更多照片

2.2 自适应高度的 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)
}

这种设计的优势在于:

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

2.3 固定高度的 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)
}

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

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

3. 组件复用与封装

3.1 提取可复用的 UI 组件

在照片相册应用中,我们可以将一些重复使用的 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}`)
        })
    })
}

3.2 封装交互逻辑

除了 UI 组件,我们还可以封装交互逻辑,使代码更加清晰:

// 封装标签切换逻辑
@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 })
}

4. 高级交互功能实现

4.1 照片位置信息显示

在最近项目视图中,我们为部分照片添加了位置信息显示:

// 位置信息覆盖层
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 })
}

这种设计有以下特点:

  1. 条件渲染:只有当照片有位置信息时才显示覆盖层
  2. 半透明背景:使用 rgba(0, 0, 0, 0.6) 创建半透明黑色背景,确保白色文字在各种照片上都清晰可见
  3. 位置图标:添加位置图标,增强视觉识别性
  4. 圆角处理:使用 borderRadius 属性使覆盖层更加美观

4.2 点击事件处理

在照片相册应用中,我们为相册和照片添加了点击事件处理:

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

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

在实际应用中,点击事件可以用于以下功能:

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

5. 布局优化与视觉设计

5.1 阴影效果增强

在相册卡片中,我们使用了阴影效果增强视觉层次感:

.shadow({
    radius: 8,
    color: 'rgba(0, 0, 0, 0.08)',
    offsetX: 0,
    offsetY: 2
})

阴影效果的设计考虑了以下因素:

  1. 轻微阴影:使用低透明度的阴影色 rgba(0, 0, 0, 0.08),创造轻微的阴影效果,不会过于突兀
  2. 向下偏移:设置 offsetY: 2,使阴影向下偏移,符合自然光照的视觉习惯
  3. 适当模糊:设置 radius: 8,使阴影边缘适当模糊,更加自然

5.2 底部工具栏设计

底部工具栏的设计采用了以下技巧:

// 底部工具栏
Row() {
    // 普通图标按钮
    Column() {
        Image($r('app.media.photos_icon'))
            .width(28)
            .height(28)
            .fillColor('#007AFF')

        Text('照片')
            .fontSize(10)
            .fontColor('#007AFF')
            .margin({ top: 2 })
    }
    .layoutWeight(1)

    // 中间的主要操作按钮
    Button() {
        Image($r('app.media.camera_icon'))
            .width(32)
            .height(32)
            .fillColor('#FFFFFF')
    }
    .width(60)
    .height(60)
    .borderRadius(30)
    .backgroundColor('#007AFF')
    .shadow({
        radius: 12,
        color: 'rgba(0, 122, 255, 0.3)',
        offsetX: 0,
        offsetY: 4
    })

    // 其他图标按钮...
}

底部工具栏的设计特点:

  1. 突出主要操作:拍照按钮使用了更大的尺寸、圆形设计和阴影效果,使其成为视觉焦点
  2. 当前页面标识:当前页面对应的图标和文字使用蓝色,其他使用灰色,帮助用户识别当前位置
  3. 均匀分布:使用 layoutWeight(1) 使各个按钮均匀分布在工具栏中

5.3 响应式布局增强

为了适应不同屏幕尺寸,我们可以增强照片相册应用的响应式布局能力:

// 根据屏幕宽度动态调整列数和间距
@State screenWidth: number = 0;

aboutToAppear() {
    // 获取屏幕宽度
    this.screenWidth = px2vp(window.getWindowWidth());
}

build() {
    // 根据屏幕宽度计算列数和间距
    let albumColumns = '1fr 1fr';
    let photoColumns = '1fr 1fr 1fr';
    let albumGap = 16;
    let photoGap = 4;
    
    if (this.screenWidth >= 600) {
        albumColumns = '1fr 1fr 1fr';
        photoColumns = '1fr 1fr 1fr 1fr';
        albumGap = 20;
        photoGap = 8;
    }
    
    if (this.screenWidth >= 840) {
        albumColumns = '1fr 1fr 1fr 1fr';
        photoColumns = '1fr 1fr 1fr 1fr 1fr';
        albumGap = 24;
        photoGap = 12;
    }
    
    // 使用计算得到的值设置 Grid 布局
    // ...
    Grid() {
        // 相册内容...
    }
    .columnsTemplate(albumColumns)
    .columnsGap(albumGap)
    .rowsGap(albumGap)
}

6. 总结

在本教程中,我们深入探讨了 HarmonyOS NEXT 中使用 Grid 组件实现照片相册应用的进阶技巧.

收藏00

登录 后评论。没有帐号? 注册 一个。