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

① 数据模型定义

  • 相册数据模型:
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%')
        }
    }
}
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐