#57 cached_network_image 接入 - pubspec 新增 cached_network_image: ^3.3.1 - CachedNetworkImageProvider 替换 PhotoView 中的 NetworkImage - 磁盘+内存双缓存,同 URL 第二次加载无网络请求 #58 ImageViewerPage 完整重写 - Shimmer 加载占位(灰色渐变动画 + 进度百分比) - 加载失败重试按钮(_ErrorWidget) - 下拉关闭:>80pt 松手 pop,背景随拖动渐变透明 - 长按底部菜单:保存 / 分享 / 复制链接 - AppBar 右上角"⋮"快捷菜单 - 多图页面指示点(≤10张,活跃项宽度扩展为18pt) - Hero 动画(单图,heroTag: 'img_$url') - 点击切换 AppBar/工具栏显示/隐藏(沉浸式) - 全屏沉浸模式(SystemUiMode.immersiveSticky) #59 气泡接入 CachedNetworkImage - ImageMessageBubble: Image.network → CachedNetworkImage + Hero tag - ImageGridBubble._GridCell: Image.network → CachedNetworkImage - 灰色 placeholder + 200ms fadeIn Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5.0 KiB
5.0 KiB
图片查看与缓存 — 架构文档
对应 Gitea issues #32(初版)/ #57(缓存)/ #58(ImageViewerPage 升级)/ #59(气泡缓存接入) 参考实现:
im-client-im-devextended_photo_view.dart/photo_view_util.dart/FullScreenPicture.dart
1. 组件选型
核心组件
| 包 | 版本 | 作用 |
|---|---|---|
photo_view |
^0.15.0 |
全屏 pinch-to-zoom、PhotoViewGallery 多图横滑 |
cached_network_image |
^3.3.1 |
磁盘 + 内存双缓存(基于 flutter_cache_manager) |
image_gallery_saver_plus |
^3.0.5 |
保存到相册(iOS + Android) |
share_plus |
^10.0.0 |
系统分享 |
选型决策:cached_network_image vs extended_image
老项目使用 extended_image: ^10.0.0 + 自定义 DownloadMgr 做本地文件缓存。
本项目选择 cached_network_image 的原因:
- 本项目已有
photo_view,CachedNetworkImageProvider可直接作为imageProvider无缝替换NetworkImage extended_image将 pan/zoom/缓存耦合在一起,替换成本高于直接添加缓存层cached_network_image是 Flutter 生态最广泛使用的缓存方案,API 简单,与photo_view解耦
2. 功能清单
| 功能 | issue | 状态 |
|---|---|---|
| 单图全屏 + pinch-to-zoom(1x–5x) | #32 | ✅ |
| 多图横向滑动(PhotoViewGallery) | #32 | ✅ |
| 保存到相册 / 系统分享 | #32 | ✅ |
| 磁盘 + 内存缓存(CachedNetworkImageProvider) | #57 | ✅ |
| Shimmer 加载占位 → 渐入(fadeIn 200ms) | #58 | ✅ |
| 加载失败重试按钮 | #58 | ✅ |
| 下拉关闭(>80pt 松手 pop,背景渐变透明) | #58 | ✅ |
| 长按底部菜单(保存 / 分享 / 复制链接) | #58 | ✅ |
| 多图页面指示点(≤10 张) | #58 | ✅ |
| Hero 动画(单图,气泡→全屏) | #58 | ✅ |
| AppBar 点击切换显示/隐藏 | #58 | ✅ |
| ImageMessageBubble → CachedNetworkImage | #59 | ✅ |
| ImageGridBubble._GridCell → CachedNetworkImage | #59 | ✅ |
3. 数据流
3.1 缓存层
CachedNetworkImageProvider(url)
├─ 内存缓存命中 → 直接渲染
├─ 磁盘缓存命中 → 读取本地文件 → 渲染
└─ 未命中 → HTTP GET → 写入磁盘缓存 → 写入内存缓存 → 渲染
缓存路径(由 flutter_cache_manager 管理):
- iOS:
{Library}/Caches/libCachedImageData/ - Android:
{cacheDir}/libCachedImageData/
3.2 ImageViewerPage 打开流程
ImageMessageBubble.onTap
└─ ImageViewerPage.open(context, urls: [url], heroTag: 'img_$url')
└─ PageRouteBuilder(fade transition, opaque:false)
└─ ImageViewerPage
├─ Hero(tag: heroTag, child: PhotoView)
│ └─ CachedNetworkImageProvider(url)
│ ├─ loadingBuilder → _Shimmer(灰色+进度%)
│ └─ errorBuilder → _ErrorWidget(重试按钮)
├─ _PageDots(多图指示点)
└─ 底部工具栏(保存 / 分享)
3.3 下拉关闭逻辑
onVerticalDragUpdate: _dragOffset += delta.dy
→ Transform.translate(offset: Offset(0, _dragOffset))
→ backgroundOpacity = 1 - (_dragOffset.abs() / 80).clamp(0, 1) * 0.7
onVerticalDragEnd:
_dragOffset.abs() > 80pt → Navigator.pop()
else → _dragOffset = 0(弹回)
4. Hero 动画配置
// 气泡(ImageMessageBubble)— 发送端
Hero(
tag: 'img_$resolvedUrl', // URL 唯一性足够
child: CachedNetworkImage(...),
)
// 查看页(ImageViewerPage)— 接收端(单图时)
Hero(
tag: heroTag, // 传入同一 tag
child: PhotoView(...),
)
注意:多图 Gallery 时 Hero 仅对 initialIndex 图片生效;其余图片使用标准 fade transition。
5. ImageMessageBubble 变更对比
| 字段 | 旧版 | 新版 |
|---|---|---|
| imageProvider | Image.network |
CachedNetworkImage |
| loading 占位 | CircularProgressIndicator |
灰色块(shimmer颜色) |
| fadeIn | 无 | 200ms |
| 点击 | ImageViewerPage.open(urls) |
ImageViewerPage.open(urls, heroTag: 'img_$url') |
6. 文件索引
features/chat/view/
├── image_viewer_page.dart # 全屏查看页(全量重写,#57/#58)
└── widgets/
├── image_message_bubble.dart # 单图气泡(CachedNetworkImage,#59)
└── image_grid_bubble.dart # 宫格气泡(CachedNetworkImage,#59)
apps/im_app/pubspec.yaml # cached_network_image: ^3.3.1 新增(#57)
7. 已知限制
- 贴纸(typ=5)
StickerMessageBubble未接入CachedNetworkImage(贴纸尺寸固定 120pt,优先级低) - WebP 动图
cached_network_image默认支持,但 Flutter 的AnimatedImage在photo_view中需额外处理 - 缓存清理 未暴露 UI 入口;可调用
DefaultCacheManager().emptyCache()在设置页集成