HarmonyOS Next之深入解析Grid网格布局打造精美的照片相册管理集
本文介绍了照片相册应用的基础网格布局实现方案。首先定义了相册和照片的数据模型,包含ID、名称、数量、封面、日期等字段。然后详细展示了页面布局的实现代码:1)使用Text组件实现标签切换功能,通过currentTab状态变量控制样式;2)采用Grid组件实现2列相册视图布局,包含封面图片和相册信息;3)使用3列网格布局展示最近项目,每个项目包含照片和可选的位置信息覆盖层。布局代码展示了ArkUI组件
·
一、照片相册的基础网格布局
① 数据模型定义
- 相册数据模型:
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%')
}
}
}
更多推荐



所有评论(0)