[HarmonyOS NEXT 实战案例五] 社交应用照片墙网格布局(下)
项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star
效果演示
1. 概述
在上一篇教程中,我们学习了如何使用GridRow和GridCol组件实现基本的社交应用照片墙网格布局。本篇教程将在此基础上,深入探讨如何优化布局、添加交互功能,以及实现更多高级特性,打造一个功能完善的社交应用照片墙。
2. 响应式布局实现
2.1 断点响应设置
为了适应不同屏幕尺寸的设备,我们可以使用GridRow组件的breakpoints属性设置断点响应:
@State columns: GridRowColumnOptions = { xs: 2, sm: 3, md: 4, lg: 6 };
// 在PhotoGrid方法中
GridRow({
columns: this.columns,
gutter: { x: 8, y: 8 },
breakpoints: { value: ['320vp', '600vp', '840vp'], reference: BreakpointsReference.WindowSize }
})
这里我们设置了三个断点值:320vp、600vp和840vp,并根据窗口大小自动调整列数:
- 小于320vp:2列
- 320vp-600vp:3列
- 600vp-840vp:4列
- 大于840vp:6列
2.2 不同断点下的布局效果
下表展示了不同断点下的照片墙网格布局效果:
断点 | 列数 | 适用设备 |
---|---|---|
<320vp | 2列 | 小屏手机 |
320vp-600vp | 3列 | 中屏手机 |
600vp-840vp | 4列 | 大屏手机/小屏平板 |
>840vp | 6列 | 平板/桌面设备 |
2.3 照片span值的动态调整
为了在不同断点下保持良好的布局效果,我们需要动态调整照片的span值:
private getPhotoSpan(photo: Photo, currentColumns: number): number {
// 根据当前列数和照片原始span值计算实际span值
const originalSpan = photo.span;
// 确保span值不超过当前列数
if (originalSpan > currentColumns) {
return currentColumns;
}
// 在列数较少时,减小大照片的span值
if (currentColumns <= 2 && originalSpan > 1) {
return Math.min(originalSpan, 2);
}
return originalSpan;
}
// 在PhotoGrid方法中
GridRow({
columns: this.columns,
gutter: { x: 8, y: 8 },
breakpoints: { value: ['320vp', '600vp', '840vp'], reference: BreakpointsReference.WindowSize }
}) {
ForEach(this.photos, (photo: Photo) => {
GridCol({
span: { xs: this.getPhotoSpan(photo, 2), sm: this.getPhotoSpan(photo, 3),
md: this.getPhotoSpan(photo, 4), lg: this.getPhotoSpan(photo, 6) }
}) {
this.PhotoCard(photo)
}
})
}
3. 照片卡片优化
3.1 添加阴影效果
为了提升照片卡片的视觉层次感,我们可以添加阴影效果:
Column() {
// 照片卡片内容
}
.width('100%')
.backgroundColor(Color.White)
.borderRadius(8)
.shadow({
radius: 8,
color: '#1A000000',
offsetX: 0,
offsetY: 2
})
3.2 添加照片加载状态
为了提升用户体验,我们可以添加照片加载状态:
@State loadingStates: Map<string, boolean> = new Map<string, boolean>();
aboutToAppear() {
// 初始化照片数据
this.photos = this.getPhotoData();
// 初始化加载状态
this.photos.forEach(photo => {
this.loadingStates.set(photo.id, true);
});
}
// 在PhotoCard方法中
Stack() {
// 加载占位图
if (this.loadingStates.get(photo.id)) {
Column() {
LoadingProgress()
.width(32)
.height(32)
.color('#2196F3')
}
.width('100%')
.height(100 * photo.aspectRatio)
.backgroundColor('#F0F0F0')
.borderRadius({ topLeft: 8, topRight: 8 })
}
Image(photo.resource)
.width('100%')
.aspectRatio(photo.aspectRatio)
.borderRadius({ topLeft: 8, topRight: 8 })
.onComplete(() => {
// 图片加载完成后更新状态
this.loadingStates.set(photo.id, false);
})
// 发布时间标签
Text(photo.publishTime)
.fontSize(12)
.fontColor(Color.White)
.backgroundColor('#80000000')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(4)
.position({ x: 8, y: 8 })
}
3.3 添加照片标签
为了提供更丰富的信息,我们可以为照片添加标签:
// 在Photo接口中添加标签字段
interface Photo {
// 其他字段...
tags?: string[]; // 照片标签
}
// 在PhotoCard方法中,照片下方添加标签
if (photo.tags && photo.tags.length > 0) {
Row() {
ForEach(photo.tags, (tag: string) => {
Text('#' + tag)
.fontSize(12)
.fontColor('#2196F3')
.backgroundColor('#E3F2FD')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(4)
.margin({ right: 4 })
})
}
.width('100%')
.margin({ top: 8 })
.flexWrap(FlexWrap.Wrap)
}
4. 交互功能实现
4.1 添加下拉刷新功能
为了提供更好的用户体验,我们可以添加下拉刷新功能:
@State refreshing: boolean = false;
build() {
Column() {
// 顶部标题栏
this.TitleBar()
// 照片墙网格
Refresh({ refreshing: this.refreshing }) {
this.PhotoGrid()
}
.onRefresh(() => {
this.refreshData();
})
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
private refreshData(): void {
this.refreshing = true;
// 模拟网络请求
setTimeout(() => {
// 随机调整照片顺序
this.photos = this.shuffleArray([...this.photos]);
this.refreshing = false;
}, 2000);
}
private shuffleArray<T>(array: T[]): T[] {
const newArray = [...array];
for (let i = newArray.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[newArray[i], newArray[j]] = [newArray[j], newArray[i]];
}
return newArray;
}
4.2 添加照片点击事件
为照片添加点击事件,实现查看大图的功能:
@State showFullscreenPhoto: boolean = false;
@State currentPhoto: Photo | null = null;
// 在PhotoCard方法中
Stack() {
// 照片内容
}
.width('100%')
.onClick(() => {
this.currentPhoto = photo;
this.showFullscreenPhoto = true;
})
// 在build方法末尾添加全屏查看组件
if (this.showFullscreenPhoto && this.currentPhoto) {
Stack() {
// 半透明背景
Column()
.width('100%')
.height('100%')
.backgroundColor('#80000000')
.onClick(() => {
this.showFullscreenPhoto = false;
})
// 照片
Column() {
Image(this.currentPhoto.resource)
.objectFit(ImageFit.Contain)
.width('100%')
.height('100%')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
// 关闭按钮
Button({ type: ButtonType.Circle }) {
Image($r('app.media.ic_close'))
.width(24)
.height(24)
.fillColor(Color.White)
}
.width(48)
.height(48)
.backgroundColor('#33000000')
.position({ x: '90%', y: '5%' })
.onClick(() => {
this.showFullscreenPhoto = false;
})
// 照片信息
Column() {
Row() {
Image(this.currentPhoto.user.avatar)
.width(32)
.height(32)
.borderRadius(16)
Column() {
Text(this.currentPhoto.user.name)
.fontSize(16)
.fontColor(Color.White)
Text(this.currentPhoto.publishTime)
.fontSize(12)
.fontColor('#E0E0E0')
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 8 })
}
.width('100%')
.padding(16)
if (this.currentPhoto.description) {
Text(this.currentPhoto.description)
.fontSize(14)
.fontColor(Color.White)
.width('100%')
.padding({ left: 16, right: 16, bottom: 16 })
}
}
.width('100%')
.position({ x: 0, y: '80%' })
.backgroundColor('#33000000')
}
.width('100%')
.height('100%')
.position({ x: 0, y: 0 })
.zIndex(999)
}
4.3 添加点赞和评论功能
为照片添加点赞和评论功能:
@State likedPhotos: Set<string> = new Set<string>();
// 在PhotoCard方法中的互动信息部分
Row() {
Row() {
Image(this.likedPhotos.has(photo.id) ? $r('app.media.ic_like_filled') : $r('app.media.ic_like'))
.width(16)
.height(16)
.fillColor(this.likedPhotos.has(photo.id) ? '#F44336' : '#999999')
.onClick((event: ClickEvent) => {
this.toggleLike(photo.id);
event.stopPropagation();
})
Text(this.getLikeCount(photo).toString())
.fontSize(12)
.fontColor('#999999')
.margin({ left: 4 })
}
Row() {
Image($r('app.media.ic_comment'))
.width(16)
.height(16)
.onClick((event: ClickEvent) => {
this.showCommentDialog(photo);
event.stopPropagation();
})
Text(photo.comments.toString())
.fontSize(12)
.fontColor('#999999')
.margin({ left: 4 })
}
.margin({ left: 16 })
Blank()
Image($r('app.media.ic_more'))
.width(16)
.height(16)
.onClick((event: ClickEvent) => {
this.showMoreOptions(photo);
event.stopPropagation();
})
}
private toggleLike(photoId: string): void {
if (this.likedPhotos.has(photoId)) {
this.likedPhotos.delete(photoId);
} else {
this.likedPhotos.add(photoId);
}
}
private getLikeCount(photo: Photo): number {
return photo.likes + (this.likedPhotos.has(photo.id) ? 1 : 0);
}
private showCommentDialog(photo: Photo): void {
// 实现评论对话框
AlertDialog.show({
title: '评论',
message: '该功能正在开发中...',
autoCancel: true,
alignment: DialogAlignment.Bottom,
offset: { dx: 0, dy: -20 },
primaryButton: {
value: '确定',
action: () => {
console.info('点击确定按钮');
}
}
});
}
private showMoreOptions(photo: Photo): void {
// 实现更多选项菜单
ActionSheet.show({
title: '更多选项',
sheets: [
{ title: '分享', action: () => { console.info('分享') } },
{ title: '收藏', action: () => { console.info('收藏') } },
{ title: '举报', action: () => { console.info('举报') } }
]
});
}
5. 高级特性实现
5.1 瀑布流布局
为了实现更美观的照片墙布局,我们可以使用瀑布流布局:
@State layoutType: string = 'grid'; // 'grid' 或 'waterfall'
// 在TitleBar方法中添加布局切换按钮
Row() {
Text('照片墙')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Blank()
Row() {
Button({ type: ButtonType.Circle }) {
Image(this.layoutType === 'grid' ? $r('app.media.ic_grid') : $r('app.media.ic_waterfall'))
.width(20)
.height(20)
.fillColor('#333333')
}
.width(36)
.height(36)
.backgroundColor('#F5F5F5')
.margin({ right: 16 })
.onClick(() => {
this.layoutType = this.layoutType === 'grid' ? 'waterfall' : 'grid';
})
Image($r('app.media.ic_search'))
.width(24)
.height(24)
.margin({ right: 16 })
Image($r('app.media.ic_add'))
.width(24)
.height(24)
}
}
// 修改PhotoGrid方法,支持瀑布流布局
@Builder
private PhotoGrid() {
if (this.layoutType === 'grid') {
// 网格布局
Scroll() {
GridRow({
columns: this.columns,
gutter: { x: 8, y: 8 },
breakpoints: { value: ['320vp', '600vp', '840vp'], reference: BreakpointsReference.WindowSize }
}) {
ForEach(this.photos, (photo: Photo) => {
GridCol({
span: { xs: this.getPhotoSpan(photo, 2), sm: this.getPhotoSpan(photo, 3),
md: this.getPhotoSpan(photo, 4), lg: this.getPhotoSpan(photo, 6) }
}) {
this.PhotoCard(photo)
}
})
}
.width('100%')
.padding(8)
}
.scrollBar(BarState.Off)
.scrollable(ScrollDirection.Vertical)
.width('100%')
.height('100%')
} else {
// 瀑布流布局
Scroll() {
Row() {
// 左列
Column() {
ForEach(this.getWaterfallPhotos(0), (photo: Photo) => {
this.PhotoCard(photo)
.margin({ bottom: 8 })
})
}
.width('50%')
.padding({ left: 8, right: 4 })
// 右列
Column() {
ForEach(this.getWaterfallPhotos(1), (photo: Photo) => {
this.PhotoCard(photo)
.margin({ bottom: 8 })
})
}
.width('50%')
.padding({ left: 4, right: 8 })
}
.width('100%')
.padding({ top: 8, bottom: 8 })
}
.scrollBar(BarState.Off)
.scrollable(ScrollDirection.Vertical)
.width('100%')
.height('100%')
}
}
private getWaterfallPhotos(columnIndex: number): Photo[] {
return this.photos.filter((_, index) => index % 2 === columnIndex);
}
5.2 添加照片过滤和分类
为了提供更好的照片浏览体验,我们可以添加照片过滤和分类功能:
// 照片分类接口
interface PhotoCategory {
id: string;
name: string;
}
@State categories: PhotoCategory[] = [
{ id: 'all', name: '全部' },
{ id: 'landscape', name: '风景' },
{ id: 'food', name: '美食' },
{ id: 'portrait', name: '人像' },
{ id: 'architecture', name: '建筑' }
];
@State currentCategory: string = 'all';
// 在Photo接口中添加分类字段
interface Photo {
// 其他字段...
category: string; // 照片分类
}
// 在TitleBar下方添加分类标签栏
@Builder
private CategoryTabs() {
Scroll() {
Row() {
ForEach(this.categories, (category: PhotoCategory) => {
Text(category.name)
.fontSize(14)
.fontColor(this.currentCategory === category.id ? '#2196F3' : '#666666')
.backgroundColor(this.currentCategory === category.id ? '#E3F2FD' : 'transparent')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(16)
.margin({ right: 8 })
.onClick(() => {
this.currentCategory = category.id;
})
})
}
.padding({ left: 16, right: 16 })
}
.scrollBar(BarState.Off)
.scrollable(ScrollDirection.Horizontal)
.width('100%')
.height(48)
.backgroundColor(Color.White)
}
// 在build方法中添加分类标签栏
build() {
Column() {
// 顶部标题栏
this.TitleBar()
// 分类标签栏
this.CategoryTabs()
// 照片墙网格
Refresh({ refreshing: this.refreshing }) {
this.PhotoGrid()
}
.onRefresh(() => {
this.refreshData();
})
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
// 修改PhotoGrid方法,根据当前分类过滤照片
private getFilteredPhotos(): Photo[] {
if (this.currentCategory === 'all') {
return this.photos;
} else {
return this.photos.filter(photo => photo.category === this.currentCategory);
}
}
// 在PhotoGrid方法中使用过滤后的照片
ForEach(this.getFilteredPhotos(), (photo: Photo) => {
// 照片卡片
})
5.3 添加照片编辑功能
为了提供更丰富的功能,我们可以添加照片编辑功能:
@State showPhotoEditor: boolean = false;
@State editingPhoto: Photo | null = null;
// 在PhotoCard方法中的更多选项菜单中添加编辑选项
private showMoreOptions(photo: Photo): void {
ActionSheet.show({
title: '更多选项',
sheets: [
{ title: '编辑', action: () => {
this.editingPhoto = photo;
this.showPhotoEditor = true;
} },
{ title: '分享', action: () => { console.info('分享') } },
{ title: '收藏', action: () => { console.info('收藏') } },
{ title: '举报', action: () => { console.info('举报') } }
]
});
}
// 在build方法末尾添加照片编辑器组件
if (this.showPhotoEditor && this.editingPhoto) {
Stack() {
Column() {
// 顶部工具栏
Row() {
Button('取消')
.backgroundColor('transparent')
.fontColor('#333333')
.onClick(() => {
this.showPhotoEditor = false;
})
Text('编辑照片')
.fontSize(16)
.fontWeight(FontWeight.Bold)
Button('保存')
.backgroundColor('transparent')
.fontColor('#2196F3')
.onClick(() => {
this.saveEditedPhoto();
this.showPhotoEditor = false;
})
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.justifyContent(FlexAlign.SpaceBetween)
.backgroundColor(Color.White)
// 照片预览
Image(this.editingPhoto.resource)
.objectFit(ImageFit.Contain)
.width('100%')
.height('60%')
.backgroundColor('#F5F5F5')
// 编辑工具
Column() {
// 滤镜选项
Text('滤镜')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.width('100%')
.padding({ left: 16 })
Scroll() {
Row() {
ForEach(['原图', '怀旧', '清新', '温暖', '冷色', '黑白'], (filter: string) => {
Column() {
Image(this.editingPhoto.resource)
.width(64)
.height(64)
.borderRadius(8)
Text(filter)
.fontSize(12)
.margin({ top: 4 })
}
.margin({ right: 12 })
})
}
.padding({ left: 16, right: 16 })
}
.scrollBar(BarState.Off)
.scrollable(ScrollDirection.Horizontal)
.height(100)
// 调整选项
Text('调整')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.width('100%')
.padding({ left: 16 })
.margin({ top: 16 })
Column() {
Row() {
Text('亮度')
.fontSize(14)
.width('20%')
Slider({ min: -100, max: 100, step: 1, value: 0 })
.width('80%')
.showTips(true)
}
.width('100%')
.padding({ left: 16, right: 16 })
.margin({ top: 8 })
Row() {
Text('对比度')
.fontSize(14)
.width('20%')
Slider({ min: -100, max: 100, step: 1, value: 0 })
.width('80%')
.showTips(true)
}
.width('100%')
.padding({ left: 16, right: 16 })
.margin({ top: 8 })
Row() {
Text('饱和度')
.fontSize(14)
.width('20%')
Slider({ min: -100, max: 100, step: 1, value: 0 })
.width('80%')
.showTips(true)
}
.width('100%')
.padding({ left: 16, right: 16 })
.margin({ top: 8 })
}
}
.width('100%')
.height('40%')
.backgroundColor(Color.White)
}
.width('100%')
.height('100%')
}
.width('100%')
.height('100%')
.position({ x: 0, y: 0 })
.zIndex(999)
.backgroundColor('#F5F5F5')
}
private saveEditedPhoto(): void {
// 保存编辑后的照片
console.info('保存编辑后的照片');
}
6. 完整代码
以下是优化后的社交应用照片墙网格布局的完整代码(部分代码省略):
// 用户信息接口
interface User {
id: string; // 用户ID
name: string; // 用户名
avatar: string; // 用户头像
}
// 照片信息接口
interface Photo {
id: string; // 照片ID
resource: ResourceStr; // 照片资源
description: string; // 照片描述
likes: number; // 点赞数
comments: number; // 评论数
user: User; // 发布用户
publishTime: string; // 发布时间
aspectRatio: number; // 宽高比例
span: number; // 在网格中占据的列数
tags?: string[]; // 照片标签
category: string; // 照片分类
}
// 照片分类接口
interface PhotoCategory {
id: string;
name: string;
}
@Component
export struct SocialPhotoWall {
@State photos: Photo[] = []; // 照片数据
@State columns: GridRowColumnOptions = { xs: 2, sm: 3, md: 4, lg: 6 }; // 网格列数
@State refreshing: boolean = false; // 刷新状态
@State loadingStates: Map<string, boolean> = new Map<string, boolean>(); // 照片加载状态
@State likedPhotos: Set<string> = new Set<string>(); // 已点赞的照片
@State showFullscreenPhoto: boolean = false; // 是否显示全屏照片
@State currentPhoto: Photo | null = null; // 当前查看的照片
@State layoutType: string = 'grid'; // 布局类型:'grid' 或 'waterfall'
@State categories: PhotoCategory[] = [ // 照片分类
{ id: 'all', name: '全部' },
{ id: 'landscape', name: '风景' },
{ id: 'food', name: '美食' },
{ id: 'portrait', name: '人像' },
{ id: 'architecture', name: '建筑' }
];
@State currentCategory: string = 'all'; // 当前分类
@State showPhotoEditor: boolean = false; // 是否显示照片编辑器
@State editingPhoto: Photo | null = null; // 当前编辑的照片
// 模拟用户数据
private users: User[] = [
{ id: '1', name: '摄影爱好者', avatar: $r('app.media.avatar1') },
{ id: '2', name: '旅行达人', avatar: $r('app.media.avatar2') },
{ id: '3', name: '美食家', avatar: $r('app.media.avatar3') },
{ id: '4', name: '设计师', avatar: $r('app.media.avatar4') },
{ id: '5', name: '运动健将', avatar: $r('app.media.avatar5') }
];
aboutToAppear() {
// 初始化照片数据
this.photos = this.getPhotoData();
// 初始化加载状态
this.photos.forEach(photo => {
this.loadingStates.set(photo.id, true);
});
}
build() {
Column() {
// 顶部标题栏
this.TitleBar()
// 分类标签栏
this.CategoryTabs()
// 照片墙网格
Refresh({ refreshing: this.refreshing }) {
this.PhotoGrid()
}
.onRefresh(() => {
this.refreshData();
})
// 全屏查看照片
if (this.showFullscreenPhoto && this.currentPhoto) {
this.FullscreenPhotoView()
}
// 照片编辑器
if (this.showPhotoEditor && this.editingPhoto) {
this.PhotoEditor()
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
// 获取照片数据方法
private getPhotoData(): Photo[] {
return [
{
id: '1',
resource: $r('app.media.photo1'),
description: '美丽的海滩日落,难忘的夏日时光',
likes: 256,
comments: 42,
user: this.users[0],
publishTime: '2小时前',
aspectRatio: 4/3,
span: 2,
tags: ['海滩', '日落', '夏日'],
category: 'landscape'
},
{
id: '2',
resource: $r('app.media.photo2'),
description: '城市天际线,繁华都市的夜景',
likes: 189,
comments: 23,
user: this.users[1],
publishTime: '3小时前',
aspectRatio: 1/1,
span: 1,
tags: ['城市', '夜景'],
category: 'architecture'
},
// 其他照片数据...
];
}
// 标题栏构建器
@Builder
private TitleBar() {
Row() {
Text('照片墙')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Blank()
Row() {
Button({ type: ButtonType.Circle }) {
Image(this.layoutType === 'grid' ? $r('app.media.ic_grid') : $r('app.media.ic_waterfall'))
.width(20)
.height(20)
.fillColor('#333333')
}
.width(36)
.height(36)
.backgroundColor('#F5F5F5')
.margin({ right: 16 })
.onClick(() => {
this.layoutType = this.layoutType === 'grid' ? 'waterfall' : 'grid';
})
Image($r('app.media.ic_search'))
.width(24)
.height(24)
.margin({ right: 16 })
Image($r('app.media.ic_add'))
.width(24)
.height(24)
}
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.backgroundColor(Color.White)
}
// 分类标签栏构建器
@Builder
private CategoryTabs() {
Scroll() {
Row() {
ForEach(this.categories, (category: PhotoCategory) => {
Text(category.name)
.fontSize(14)
.fontColor(this.currentCategory === category.id ? '#2196F3' : '#666666')
.backgroundColor(this.currentCategory === category.id ? '#E3F2FD' : 'transparent')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(16)
.margin({ right: 8 })
.onClick(() => {
this.currentCategory = category.id;
})
})
}
.padding({ left: 16, right: 16 })
}
.scrollBar(BarState.Off)
.scrollable(ScrollDirection.Horizontal)
.width('100%')
.height(48)
.backgroundColor(Color.White)
}
// 照片墙网格构建器
@Builder
private PhotoGrid() {
if (this.layoutType === 'grid') {
// 网格布局
Scroll() {
GridRow({
columns: this.columns,
gutter: { x: 8, y: 8 },
breakpoints: { value: ['320vp', '600vp', '840vp'], reference: BreakpointsReference.WindowSize }
}) {
ForEach(this.getFilteredPhotos(), (photo: Photo) => {
GridCol({
span: { xs: this.getPhotoSpan(photo, 2), sm: this.getPhotoSpan(photo, 3),
md: this.getPhotoSpan(photo, 4), lg: this.getPhotoSpan(photo, 6) }
}) {
this.PhotoCard(photo)
}
})
}
.width('100%')
.padding(8)
}
.scrollBar(BarState.Off)
.scrollable(ScrollDirection.Vertical)
.width('100%')
.height('100%')
} else {
// 瀑布流布局
Scroll() {
Row() {
// 左列
Column() {
ForEach(this.getWaterfallPhotos(0), (photo: Photo) => {
this.PhotoCard(photo)
.margin({ bottom: 8 })
})
}
.width('50%')
.padding({ left: 8, right: 4 })
// 右列
Column() {
ForEach(this.getWaterfallPhotos(1), (photo: Photo) => {
this.PhotoCard(photo)
.margin({ bottom: 8 })
})
}
.width('50%')
.padding({ left: 4, right: 8 })
}
.width('100%')
.padding({ top: 8, bottom: 8 })
}
.scrollBar(BarState.Off)
.scrollable(ScrollDirection.Vertical)
.width('100%')
.height('100%')
}
}
// 照片卡片构建器
@Builder
private PhotoCard(photo: Photo) {
Column() {
// 照片
Stack() {
// 加载占位图
if (this.loadingStates.get(photo.id)) {
Column() {
LoadingProgress()
.width(32)
.height(32)
.color('#2196F3')
}
.width('100%')
.height(100 * photo.aspectRatio)
.backgroundColor('#F0F0F0')
.borderRadius({ topLeft: 8, topRight: 8 })
}
Image(photo.resource)
.width('100%')
.aspectRatio(photo.aspectRatio)
.borderRadius({ topLeft: 8, topRight: 8 })
.onComplete(() => {
// 图片加载完成后更新状态
this.loadingStates.set(photo.id, false);
})
// 发布时间标签
Text(photo.publishTime)
.fontSize(12)
.fontColor(Color.White)
.backgroundColor('#80000000')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(4)
.position({ x: 8, y: 8 })
}
.width('100%')
.onClick(() => {
this.currentPhoto = photo;
this.showFullscreenPhoto = true;
})
// 照片信息
Column() {
// 用户信息
Row() {
Image(photo.user.avatar)
.width(24)
.height(24)
.borderRadius(12)
Text(photo.user.name)
.fontSize(14)
.fontColor('#333333')
.margin({ left: 8 })
}
.width('100%')
.margin({ top: 8, bottom: 4 })
// 照片描述
if (photo.description) {
Text(photo.description)
.fontSize(14)
.fontColor('#666666')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 4, bottom: 8 })
.width('100%')
}
// 照片标签
if (photo.tags && photo.tags.length > 0) {
Row() {
ForEach(photo.tags, (tag: string) => {
Text('#' + tag)
.fontSize(12)
.fontColor('#2196F3')
.backgroundColor('#E3F2FD')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(4)
.margin({ right: 4 })
})
}
.width('100%')
.margin({ top: 8 })
.flexWrap(FlexWrap.Wrap)
}
// 互动信息
Row() {
Row() {
Image(this.likedPhotos.has(photo.id) ? $r('app.media.ic_like_filled') : $r('app.media.ic_like'))
.width(16)
.height(16)
.fillColor(this.likedPhotos.has(photo.id) ? '#F44336' : '#999999')
.onClick((event: ClickEvent) => {
this.toggleLike(photo.id);
event.stopPropagation();
})
Text(this.getLikeCount(photo).toString())
.fontSize(12)
.fontColor('#999999')
.margin({ left: 4 })
}
Row() {
Image($r('app.media.ic_comment'))
.width(16)
.height(16)
.onClick((event: ClickEvent) => {
this.showCommentDialog(photo);
event.stopPropagation();
})
Text(photo.comments.toString())
.fontSize(12)
.fontColor('#999999')
.margin({ left: 4 })
}
.margin({ left: 16 })
Blank()
Image($r('app.media.ic_more'))
.width(16)
.height(16)
.onClick((event: ClickEvent) => {
this.showMoreOptions(photo);
event.stopPropagation();
})
}
.width('100%')
.margin({ top: 8 })
}
.width('100%')
.padding({ left: 8, right: 8, bottom: 8 })
}
.width('100%')
.backgroundColor(Color.White)
.borderRadius(8)
.shadow({
radius: 8,
color: '#1A000000',
offsetX: 0,
offsetY: 2
})
}
// 全屏照片查看构建器
@Builder
private FullscreenPhotoView() {
Stack() {
// 半透明背景
Column()
.width('100%')
.height('100%')
.backgroundColor('#80000000')
.onClick(() => {
this.showFullscreenPhoto = false;
})
// 照片
Column() {
Image(this.currentPhoto.resource)
.objectFit(ImageFit.Contain)
.width('100%')
.height('100%')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
// 关闭按钮
Button({ type: ButtonType.Circle }) {
Image($r('app.media.ic_close'))
.width(24)
.height(24)
.fillColor(Color.White)
}
.width(48)
.height(48)
.backgroundColor('#33000000')
.position({ x: '90%', y: '5%' })
.onClick(() => {
this.showFullscreenPhoto = false;
})
// 照片信息
Column() {
Row() {
Image(this.currentPhoto.user.avatar)
.width(32)
.height(32)
.borderRadius(16)
Column() {
Text(this.currentPhoto.user.name)
.fontSize(16)
.fontColor(Color.White)
Text(this.currentPhoto.publishTime)
.fontSize(12)
.fontColor('#E0E0E0')
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 8 })
}
.width('100%')
.padding(16)
if (this.currentPhoto.description) {
Text(this.currentPhoto.description)
.fontSize(14)
.fontColor(Color.White)
.width('100%')
.padding({ left: 16, right: 16, bottom: 16 })
}
}
.width('100%')
.position({ x: 0, y: '80%' })
.backgroundColor('#33000000')
}
.width('100%')
.height('100%')
.position({ x: 0, y: 0 })
.zIndex(999)
}
// 照片编辑器构建器
@Builder
private PhotoEditor() {
Stack() {
Column() {
// 顶部工具栏
Row() {
Button('取消')
.backgroundColor('transparent')
.fontColor('#333333')
.onClick(() => {
this.showPhotoEditor = false;
})
Text('编辑照片')
.fontSize(16)
.fontWeight(FontWeight.Bold)
Button('保存')
.backgroundColor('transparent')
.fontColor('#2196F3')
.onClick(() => {
this.saveEditedPhoto();
this.showPhotoEditor = false;
})
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.justifyContent(FlexAlign.SpaceBetween)
.backgroundColor(Color.White)
// 照片预览
Image(this.editingPhoto.resource)
.objectFit(ImageFit.Contain)
.width('100%')
.height('60%')
.backgroundColor('#F5F5F5')
// 编辑工具
Column() {
// 滤镜选项
Text('滤镜')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.width('100%')
.padding({ left: 16 })
Scroll() {
Row() {
ForEach(['原图', '怀旧', '清新', '温暖', '冷色', '黑白'], (filter: string) => {
Column() {
Image(this.editingPhoto.resource)
.width(64)
.height(64)
.borderRadius(8)
Text(filter)
.fontSize(12)
.margin({ top: 4 })
}
.margin({ right: 12 })
})
}
.padding({ left: 16, right: 16 })
}
.scrollBar(BarState.Off)
.scrollable(ScrollDirection.Horizontal)
.height(100)
// 调整选项
Text('调整')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.width('100%')
.padding({ left: 16 })
.margin({ top: 16 })
Column() {
Row() {
Text('亮度')
.fontSize(14)
.width('20%')
Slider({ min: -100, max: 100, step: 1, value: 0 })
.width('80%')
.showTips(true)
}
.width('100%')
.padding({ left: 16, right: 16 })
.margin({ top: 8 })
Row() {
Text('对比度')
.fontSize(14)
.width('20%')
Slider({ min: -100, max: 100, step: 1, value: 0 })
.width('80%')
.showTips(true)
}
.width('100%')
.padding({ left: 16, right: 16 })
.margin({ top: 8 })
Row() {
Text('饱和度')
.fontSize(14)
.width('20%')
Slider({ min: -100, max: 100, step: 1, value: 0 })
.width('80%')
.showTips(true)
}
.width('100%')
.padding({ left: 16, right: 16 })
.margin({ top: 8 })
}
}
.width('100%')
.height('40%')
.backgroundColor(Color.White)
}
.width('100%')
.height('100%')
}
.width('100%')
.height('100%')
.position({ x: 0, y: 0 })
.zIndex(999)
.backgroundColor('#F5F5F5')
}
// 辅助方法
private getPhotoSpan(photo: Photo, currentColumns: number): number {
// 根据当前列数和照片原始span值计算实际span值
const originalSpan = photo.span;
// 确保span值不超过当前列数
if (originalSpan > currentColumns) {
return currentColumns;
}
// 在列数较少时,减小大照片的span值
if (currentColumns <= 2 && originalSpan > 1) {
return Math.min(originalSpan, 2);
}
return originalSpan;
}
private getFilteredPhotos(): Photo[] {
if (this.currentCategory === 'all') {
return this.photos;
} else {
return this.photos.filter(photo => photo.category === this.currentCategory);
}
}
private getWaterfallPhotos(columnIndex: number): Photo[] {
return this.getFilteredPhotos().filter((_, index) => index % 2 === columnIndex);
}
private refreshData(): void {
this.refreshing = true;
// 模拟网络请求
setTimeout(() => {
// 随机调整照片顺序
this.photos = this.shuffleArray([...this.photos]);
this.refreshing = false;
}, 2000);
}
private shuffleArray<T>(array: T[]): T[] {
const newArray = [...array];
for (let i = newArray.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[newArray[i], newArray[j]] = [newArray[j], newArray[i]];
}
return newArray;
}
private toggleLike(photoId: string): void {
if (this.likedPhotos.has(photoId)) {
this.likedPhotos.delete(photoId);
} else {
this.likedPhotos.add(photoId);
}
}
private getLikeCount(photo: Photo): number {
return photo.likes + (this.likedPhotos.has(photo.id) ? 1 : 0);
}
private showCommentDialog(photo: Photo): void {
// 实现评论对话框
AlertDialog.show({
title: '评论',
message: '该功能正在开发中...',
autoCancel: true,
alignment: DialogAlignment.Bottom,
offset: { dx: 0, dy: -20 },
primaryButton: {
value: '确定',
action: () => {
console.info('点击确定按钮');
}
}
});
}
private showMoreOptions(photo: Photo): void {
// 实现更多选项菜单
ActionSheet.show({
title: '更多选项',
sheets: [
{ title: '编辑', action: () => {
this.editingPhoto = photo;
this.showPhotoEditor = true;
} },
{ title: '分享', action: () => { console.info('分享') } },
{ title: '收藏', action: () => { console.info('收藏') } },
{ title: '举报', action: () => { console.info('举报') } }
]
});
}
private saveEditedPhoto(): void {
// 保存编辑后的照片
console.info('保存编辑后的照片');
}
}
7. 总结
本教程详细讲解了如何优化社交应用照片墙网格布局,添加交互功能,以及实现更多高级特性。通过使用GridRow和GridCol组件的高级特性,我们实现了响应式布局,使照片墙能够适应不同屏幕尺寸的设备。同时,我们还添加了照片卡片优化、交互功能、瀑布流布局、照片过滤和分类、照片编辑等功能,打造了一个功能完善的社交应用照片墙。
通过本教程,你应该已经掌握了如何使用HarmonyOS NEXT的GridRow和GridCol组件实现复杂的网格布局,以及如何添加各种交互功能和高级特性,提升用户体验。这些技能可以应用到各种需要网格布局的场景中,如电商商品展示、新闻列表、音乐专辑等。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。