
点开一张照片,进入详情页,这是相册App里最基础也最重要的交互。
今天来聊聊照片详情页的实现思路和具体代码。
设计思考
照片详情页的核心是让用户专注于照片本身。
所以我把背景设成纯黑色,让照片内容更突出。
顶部保留必要的操作按钮,底部可以弹出照片信息。
页面基础结构
先看整体框架:
dart
class PhotoDetailScreen extends StatelessWidget {
final PhotoModel photo;
const PhotoDetailScreen({super.key, required this.photo});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.black,
iconTheme: const IconThemeData(color: Colors.white),
页面接收一个
PhotoModel对象,包含照片的所有信息。背景和
AppBar都设成黑色,营造沉浸式的观看体验。
iconTheme设成白色,确保返回按钮在黑色背景上能看清。
收藏功能实现
收藏按钮放在右上角,需要实时显示当前状态:
dart
actions: [
Consumer<AlbumProvider>(
builder: (context, provider, _) {
final currentPhoto = provider.photos.firstWhere((p) => p.id == photo.id);
return IconButton(
icon: Icon(
currentPhoto.isFavorite ? Icons.favorite : Icons.favorite_border,
color: currentPhoto.isFavorite ? Colors.red : Colors.white,
),
onPressed: () => provider.toggleFavorite(photo.id),
);
},
),
用
Consumer监听AlbumProvider,这样收藏状态变化时图标会自动更新。从
provider.photos里重新查找当前照片,确保拿到最新的isFavorite状态。收藏后图标变成实心红色爱心,未收藏是空心白色,区分很明显。
信息按钮
点击信息按钮可以查看照片的详细信息:
dart
IconButton(
icon: const Icon(Icons.info_outline, color: Colors.white),
onPressed: () => _showPhotoInfo(context),
),
用
info_outline图标,轮廓风格和整体设计统一。点击后调用
_showPhotoInfo方法弹出底部面板。
更多操作菜单
删除这种危险操作放在菜单里,避免误触:
dart
PopupMenuButton<String>(
iconColor: Colors.white,
onSelected: (value) {
if (value == 'delete') {
_showDeleteDialog(context);
}
},
itemBuilder: (context) => [
const PopupMenuItem(value: 'delete', child: Text('删除照片')),
],
),
],
),
PopupMenuButton点击后展开菜单,目前只有删除一个选项。后续可以在这里加分享、编辑等功能。
删除不是直接执行,而是先弹确认对话框。
照片展示区域
页面主体就是照片本身:
dart
body: Center(
child: Container(
color: _getColorForPhoto(photo.url),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.photo, color: Colors.white.withOpacity(0.5), size: 100.sp),
SizedBox(height: 16.h),
Text(
photo.description,
style: TextStyle(color: Colors.white, fontSize: 18.sp),
),
],
),
),
),
),
);
}
这里用颜色块加图标来模拟照片显示,实际项目中换成
Image组件加载真实图片。照片描述显示在下方,字体稍大一些方便阅读。
整体居中显示,上下左右留有空间。
颜色生成方法
每张照片的背景色根据URL哈希值动态生成:
dart
Color _getColorForPhoto(String url) {
final colors = [
Colors.pink[300]!,
Colors.purple[300]!,
Colors.blue[300]!,
Colors.teal[300]!,
Colors.green[300]!,
Colors.orange[300]!,
];
return colors[url.hashCode % colors.length];
}
准备了6种柔和的颜色,用哈希取模的方式选择。
同一张照片每次打开颜色都一样,不会随机变化。
颜色选的都是300色阶,不会太亮也不会太暗。
照片信息面板
点击信息按钮后从底部弹出详情面板:
dart
void _showPhotoInfo(BuildContext context) {
final familyProvider = context.read<FamilyProvider>();
final taggedMembers = familyProvider.getMembersByIds(photo.taggedMembers);
showModalBottomSheet(
context: context,
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)),
),
先从
FamilyProvider获取照片中标记的家人信息。
showModalBottomSheet从底部弹出,用户可以下滑关闭。顶部圆角让面板看起来更柔和。
dart
builder: (context) => Container(
padding: EdgeInsets.all(24.w),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'照片信息',
style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold),
),
SizedBox(height: 16.h),
mainAxisSize: MainAxisSize.min让面板高度自适应内容,不会占满整个屏幕。标题加粗显示,和下面的信息项区分开。
信息项展示
展示照片的各种属性:
dart
_buildInfoRow('描述', photo.description),
_buildInfoRow('拍摄时间', DateFormat('yyyy-MM-dd HH:mm').format(photo.createdAt)),
if (photo.location != null) _buildInfoRow('地点', photo.location!),
if (photo.tags.isNotEmpty) _buildInfoRow('标签', photo.tags.join(', ')),
if (taggedMembers.isNotEmpty)
_buildInfoRow('相关家人', taggedMembers.map((m) => m.name).join(', ')),
SizedBox(height: 16.h),
],
),
),
);
}
描述和拍摄时间是必显示的,其他信息有才显示。
时间用
DateFormat格式化成易读的格式。标签和家人名字用逗号连接成字符串。
信息行组件
把信息行的构建抽成单独方法:
dart
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 8.h),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 80.w,
child: Text(
label,
style: TextStyle(color: Colors.grey, fontSize: 14.sp),
),
),
标签固定宽度80,这样所有行的值都能对齐。
标签用灰色,和右边的值形成对比。
crossAxisAlignment.start让多行文本顶部对齐。
dart
Expanded(
child: Text(
value,
style: TextStyle(fontSize: 14.sp),
),
),
],
),
);
}
值用
Expanded占据剩余空间,文本过长会自动换行。上下间距8,信息项之间不会太挤。
删除确认对话框
删除操作需要二次确认:
dart
void _showDeleteDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('删除照片'),
content: const Text('确定要删除这张照片吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
用
AlertDialog弹出确认框,标题和内容说明清楚要做什么。取消按钮只是关闭对话框,不做其他操作。
dart
TextButton(
onPressed: () {
context.read<AlbumProvider>().deletePhoto(photo.id);
Navigator.pop(context);
Navigator.pop(context);
},
child: const Text('删除', style: TextStyle(color: Colors.red)),
),
],
),
);
}
}
确认删除后调用
provider的deletePhoto方法。删除按钮用红色文字,提醒用户这是危险操作。
连续两次
Navigator.pop,先关对话框再退出详情页。
数据模型配合
照片详情页依赖PhotoModel的数据结构:
dart
class PhotoModel {
final String id;
final String albumId;
final String url;
final String description;
final DateTime createdAt;
final String? location;
final List<String> tags;
final List<String> taggedMembers;
final bool isFavorite;
id用于唯一标识照片,删除和收藏操作都需要它。
taggedMembers存的是家人ID列表,需要通过FamilyProvider转换成名字。
isFavorite控制收藏状态,由AlbumProvider统一管理。
Provider方法
收藏和删除功能在AlbumProvider里实现:
dart
void toggleFavorite(String photoId) {
final index = _photos.indexWhere((p) => p.id == photoId);
if (index != -1) {
_photos[index] = _photos[index].copyWith(
isFavorite: !_photos[index].isFavorite,
);
notifyListeners();
}
}
找到对应照片,用
copyWith创建新对象并切换收藏状态。
notifyListeners通知所有监听者更新UI。
dart
void deletePhoto(String photoId) {
_photos.removeWhere((p) => p.id == photoId);
notifyListeners();
}
删除就是从列表里移除对应ID的照片。
实际项目中还需要同步删除服务器数据和本地缓存。
小结
照片详情页的实现不复杂,核心是沉浸式的黑色背景和清晰的操作入口。
收藏用Consumer实时监听状态,信息用底部面板展示,删除有二次确认。
这些细节加起来,用户体验就会好很多。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net