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

在照片相册应用中,我们使用 @State 装饰器定义了几个关键的状态变量:
@State currentTab: number = 0; // 当前选中的标签页(0: 相册, 1: 最近项目)
@State albums: Album[] = []; // 相册数据
@State recentPhotos: Recentphoto[] = []; // 最近照片数据这些状态变量的变化会自动触发 UI 的更新,实现响应式的用户界面。
标签页切换是照片相册应用中的核心交互之一,我们通过以下方式实现:
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)(淡蓝色) | 选中标签背景变为淡蓝色 |
通过这种设计,用户可以清晰地识别当前所在的标签页,提升用户体验。
根据当前选中的标签页,我们使用条件渲染显示不同的内容:
if (this.currentTab === 0) {
// 相册视图
Column() {
// 相册内容...
}
} else {
// 最近项目视图
Column() {
// 最近项目内容...
}
}这种方式可以确保只渲染当前需要显示的内容,提高应用性能。
在照片相册应用中,我们为不同的内容区域设置了不同的列数:
// 相册视图 - 2列布局
Grid() {
// GridItem 内容...
}
.columnsTemplate('1fr 1fr') // 2列等宽布局
.columnsGap(16)
.rowsGap(16)
// 最近项目视图 - 3列布局
Grid() {
// GridItem 内容...
}
.columnsTemplate('1fr 1fr 1fr') // 3列等宽布局
.columnsGap(4)
.rowsGap(4)不同列数的设计考虑了以下因素:
在相册视图中,我们没有为 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() {
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}`)
})
})
}除了 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 })
}在最近项目视图中,我们为部分照片添加了位置信息显示:
// 位置信息覆盖层
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 })
}这种设计有以下特点:
rgba(0, 0, 0, 0.6) 创建半透明黑色背景,确保白色文字在各种照片上都清晰可见borderRadius 属性使覆盖层更加美观在照片相册应用中,我们为相册和照片添加了点击事件处理:
// 相册点击事件
GridItem() {
AlbumCard(album)
}
.onClick(() => {
console.log(`打开相册: ${album.name}`)
})
// 照片点击事件
GridItem() {
// 照片内容...
}
.onClick(() => {
console.log(`查看照片: ${photo.id}`)
})在实际应用中,点击事件可以用于以下功能:
在相册卡片中,我们使用了阴影效果增强视觉层次感:
.shadow({
radius: 8,
color: 'rgba(0, 0, 0, 0.08)',
offsetX: 0,
offsetY: 2
})阴影效果的设计考虑了以下因素:
rgba(0, 0, 0, 0.08),创造轻微的阴影效果,不会过于突兀offsetY: 2,使阴影向下偏移,符合自然光照的视觉习惯radius: 8,使阴影边缘适当模糊,更加自然底部工具栏的设计采用了以下技巧:
// 底部工具栏
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
})
// 其他图标按钮...
}底部工具栏的设计特点:
layoutWeight(1) 使各个按钮均匀分布在工具栏中为了适应不同屏幕尺寸,我们可以增强照片相册应用的响应式布局能力:
// 根据屏幕宽度动态调整列数和间距
@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)
}在本教程中,我们深入探讨了 HarmonyOS NEXT 中使用 Grid 组件实现照片相册应用的进阶技巧.