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

5.0 KiB
Raw Permalink Blame History

图片查看与缓存 — 架构文档

对应 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_viewCachedNetworkImageProvider 可直接作为 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 动画配置

// 气泡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 的 AnimatedImagephoto_view 中需额外处理
  • 缓存清理 未暴露 UI 入口;可调用 DefaultCacheManager().emptyCache() 在设置页集成