# 图片查看与缓存 — 架构文档 > 对应 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()` 在设置页集成