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:
143
Doc/image_viewer_architecture.md
Normal file
143
Doc/image_viewer_architecture.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# 图片查看与缓存 — 架构文档
|
||||
|
||||
> 对应 Gitea issues #32(初版)/ #57(缓存)/ #58(ImageViewerPage 升级)/ #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-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 动画配置
|
||||
|
||||
```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()` 在设置页集成
|
||||
Reference in New Issue
Block a user