feat(image): 图片查看全量升级 — 缓存/Hero/下拉关闭/长按菜单(#57~#59)

#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>
This commit is contained in:
pp-bot
2026-03-24 22:03:08 +09:00
parent 21b7201590
commit 0995a4bf79
5 changed files with 605 additions and 138 deletions

View File

@@ -0,0 +1,143 @@
# 图片查看与缓存 — 架构文档
> 对应 Gitea issues #32初版/ #57缓存/ #58ImageViewerPage 升级)/ #59气泡缓存接入
> 参考实现:`im-client-im-dev` `extended_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-zoom1x5x| #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')
└─ PageRouteBuilderfade 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 动画配置
```dart
// 气泡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()` 在设置页集成