Files
customer-im-client-dev/Doc/image_viewer_architecture.md
pp-bot 0995a4bf79 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>
2026-03-24 22:03:08 +09:00

144 lines
5.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 图片查看与缓存 — 架构文档
> 对应 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()` 在设置页集成