# media_pickers **Repository Path**: wei-echo/media_pickers ## Basic Information - **Project Name**: media_pickers - **Description**: 基于photo_manager ^3.9.0的 Flutter 相册/相机插件,支持Android 13+ 限制访问模式,内置中文/英文国际化。 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-05-25 - **Last Updated**: 2026-06-08 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Media Pickers [![pub package](https://img.shields.io/badge/pub-v1.0.1-blue)](https://pub.dev/packages/media_pickers) 基于 [`photo_manager ^3.9.0`](https://pub.dev/packages/photo_manager) 的 Flutter 相册/相机插件,支持 **Android 13+ 限制访问模式**,内置中文/英文国际化,仿微信风格交互。 ## 功能特性 | 功能 | 说明 | |------|------| | ✅ 拍照 & 录像 | 调用系统相机,返回 `MediaPickersAsset` | | ✅ 相册选择器 | 全屏 `MediaPickersPage`,支持多选/相册切换/预览 | | ✅ 微信风格连选 | 长按进入选择模式,拖动连选/连取消 | | ✅ 深色模式 | 支持 light/dark 主题,自动适配系统 | | ✅ 限制访问模式 | Android 13+ Banner 提示 + 系统选择器入口 | | ✅ 权限三态判定 | `authorized` / `limited` / `denied`,完全依赖 `grantResults` | | ✅ 合规弃用旧权限 | Android 13+ 使用 `READ_MEDIA_IMAGES/VIDEO`,`READ_EXTERNAL_STORAGE` 已废弃 | | ✅ 视频 duration 修复 | Android 通过 `MediaMetadataRetriever` 预加载,绕过 MediaStore 延迟 | | ✅ 系统缓存缩略图 | Android Q+ 走 `ContentResolver.loadThumbnail`,绕过 Glide,5-20ms 出图 | | ✅ 图片/视频过滤 | 支持按 GIF/LivePhoto/APNG/HEIF/WebP 类型及视频时长过滤 | | ✅ 类型标签显示 | 缩略图左下角显示 GIF/LIVE/APNG/HEIF/WEBP/视频时长标签 | | ✅ 相册排序 | All Photos > Camera > Videos > Screenshots > 字母序 | | ✅ 国际化 | 中英文内置,自动根据系统语言切换 | | ✅ 跨平台 | Android (minSdk 21+) / iOS 14+ | | ✅ 预览页面 | 支持图片/视频预览,视频播放控制,预览页删除已选资源 | | ✅ 按钮布局优化 | 预览按钮移至左下角,完成按钮在右侧,符合微信交互习惯 | | ✅ 已选缩略图删除 | 底部栏已选缩略图支持点击 × 按钮移除,实时同步选择状态 | | ✅ 预览页指定查看 | 预览页底部缩略图条支持点击切换查看指定图片/视频 | | ✅ 预览页移除选项 | 预览页右上角支持移除当前查看的已选项 | ## 安装 ```yaml dependencies: media_pickers: ^1.0.1 ``` > 插件会自动拉取 `photo_manager ^3.9.0` 和 `video_player ^2.9.2`。 国内镜像(可选): ```bash export PUB_HOSTED_URL=https://pub.flutter-io.cn export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn flutter pub get ``` ## 平台配置 ### Android **AndroidManifest.xml**(必须权限): ```xml ``` **file_paths.xml**(`android/app/src/main/res/xml/`): ```xml ``` **build.gradle**: ```gradle android { defaultConfig { minSdkVersion 21 targetSdkVersion 34 } } ``` ### iOS **Info.plist**: ```xml NSPhotoLibraryUsageDescription 需要访问相册以选择照片和视频 NSCameraUsageDescription 需要使用相机拍摄照片和视频 NSMicrophoneUsageDescription 录制视频时需要访问麦克风 ``` **Podfile**: ```ruby platform :ios, '14.0' ``` ## 快速开始 ### 方式一:使用内置选择器页面 ```dart final result = await Navigator.push( context, MaterialPageRoute( builder: (_) => MediaPickersPage( config: MediaPickersConfig( maxCount: 9, mediaType: MediaType.all, title: '选择照片', themeBrightness: Brightness.dark, // 可选,深色模式 ), ), ), ); if (result != null) { for (final asset in result.assets) { print('ID: ${asset.id}'); print('尺寸: ${asset.width}x${asset.height}'); print('时长: ${asset.duration}s'); final thumb = await asset.thumbnail; final file = await asset.file; } } ``` ### 方式二:直接调用相机 ```dart // 拍照 final photo = await MediaPickersPlugin.captureImage(context: context); // 录像 final video = await MediaPickersPlugin.captureVideo(context: context); ``` ### 方式三:快捷选择 ```dart // 选择图片 final images = await MediaPickersPlugin.pickImages( context: context, maxCount: 9, allowGIF: false, // 过滤动图 allowHEIF: true, allowWebP: true, themeBrightness: Brightness.light, ); // 选择视频(支持时长过滤) final videos = await MediaPickersPlugin.pickVideos( context: context, maxCount: 3, minVideoDuration: 3, // 至少 3 秒 maxVideoDuration: 60, // 最多 60 秒 themeBrightness: Brightness.dark, ); ``` ## API 参考 ### MediaPickersConfig ```dart MediaPickersConfig({ this.maxCount = 9, // 最大选择数量 this.minCount = 1, // 最小选择数量 this.mediaType = MediaType.all, // image / video / all this.allowAnimatedImages = true, // 允许选择 GIF this.allowLivePhotos = true, // 允许选择 Live Photo this.allowAPNG = true, // 允许选择 APNG this.allowHEIF = true, // 允许选择 HEIF/HEIC this.allowWebP = true, // 允许选择 WebP this.showGifIndicator = true, // 显示 GIF 标签 this.showLivePhotoIndicator = true, this.showWebPIndicator = true, this.showApngIndicator = true, this.showHeifIndicator = true, this.minVideoDuration = 0, // 视频最小时长(秒) this.maxVideoDuration = 0, // 视频最大时长(秒) this.enablePreview = true, // 显示预览按钮 this.themeColor, // 主题色 this.themeBrightness = Brightness.light, // 主题亮度 this.thumbnailSize = const ThumbnailSize(200, 200), this.gridCount = 4, this.confirmText = '完成', this.title = '选择照片', }) ``` **注意**: - `previewText` 已移除,改为内置国际化(中文"预览"/英文"Preview") - `showIndex` 已移除,逻辑简化为:`maxCount > 1 ? 显示序号 : 显示 ✓` ### MediaPickersAsset | 属性 | 类型 | 说明 | |------|------|------| | `id` | `String` | 资源唯一 ID | | `width` / `height` | `int` | 宽高(像素) | | `duration` | `int` | 视频时长(秒),图片为 0 | | `isVideo` / `isImage` | `bool` | 类型判断 | | `createDateTime` | `DateTime` | 创建时间 | | `mimeType` | `String?` | MIME 类型 | | `thumbnail` | `Future` | 缩略图(200x200) | | `getThumbnail(width, height)` | `Future` | 指定尺寸缩略图 | | `file` | `Future` | 原始文件 | | `asset` | `AssetEntity` | photo_manager 底层实体 | ### MediaPickersPlugin | 方法 | 说明 | |------|------| | `captureImage(context)` | 拍照,返回 `MediaPickersAsset?` | | `captureVideo(context, maxDurationSeconds)` | 录像,返回 `MediaPickersAsset?` | | `pickImages(context, ...)` | 选择图片,支持类型过滤 | | `pickVideos(context, ...)` | 选择视频,支持时长过滤 | #### pickImages 参数 ```dart Future> pickImages({ required BuildContext context, int maxCount = 9, int minCount = 1, bool allowAnimatedImages = true, bool allowLivePhotos = true, bool allowAPNG = true, bool allowHEIF = true, bool allowWebP = true, bool showGifIndicator = true, bool enablePreview = true, Brightness themeBrightness = Brightness.light, Color? themeColor, ThumbnailSize thumbnailSize = const ThumbnailSize(200, 200), int gridCount = 4, String confirmText = '完成', String title = '选择图片', }) ``` #### pickVideos 参数 ```dart Future> pickVideos({ required BuildContext context, int maxCount = 9, int minCount = 1, int minVideoDuration = 0, int maxVideoDuration = 0, bool enablePreview = true, Brightness themeBrightness = Brightness.light, Color? themeColor, ThumbnailSize thumbnailSize = const ThumbnailSize(200, 200), int gridCount = 4, String confirmText = '完成', String title = '选择视频', }) ``` **注意**: - `previewText` 已移除,改为内置国际化(中文"预览"/英文"Preview") - `showIndex` 已移除,逻辑简化为:`maxCount > 1 ? 显示序号 : 显示 ✓` ## 权限处理 **Android 权限判定**完全依赖 `grantResults`,不使用 `checkSelfPermission`(限制模式下会误判)。 三态: - `authorized`:`READ_MEDIA_IMAGES` 或 `READ_MEDIA_VIDEO` 已授权 - `limited`:`READ_MEDIA_VISUAL_USER_SELECTED` 已授权 - `denied`:均未授权 **限制访问模式**:顶部 Banner 提示,右上角"选取更多"按钮引导用户扩展可访问文件范围。 ## 缩略图架构 | 平台 | 路径 | 速度 | |------|------|------| | Android Q+ | `ContentResolver.loadThumbnail` → 系统缓存 | 5-20ms | | Android < Q | `ThumbnailUtils` 兜底 | 50-100ms | | iOS | `PHImageManager`(photo_manager 内置) | <50ms | 不走 Glide/MediaMetadataRetriever 视频帧解码,利用系统已有的缩略图缓存,大幅提速。 ## 项目结构 ``` media_pickers/ ├── lib/ │ ├── media_pickers.dart # Barrel export │ ├── media_pickers_plugin.dart # 入口类(拍照/录像/选择) │ ├── media_pickers_controller.dart # 控制器 + MediaPickersAsset + 批量操作 │ ├── media_pickers_config.dart # 配置类 │ ├── media_pickers_page.dart # 主页面 + 预览页 + 视频播放 │ ├── media_pickers_grid.dart # 微信风格长按连选网格 │ ├── media_pickers_thumbnail.dart # 缩略图 Widget + 类型标签 │ ├── media_pickers_localizations.dart # 国际化(中/英) │ └── permission_helper.dart # 权限工具 ├── android/.../MediaPickersPlugin.kt # Android 原生(缩略图/权限/相机) ├── ios/Classes/MediaPickersPlugin.swift # iOS 原生 └── example/ # 示例应用 ``` ## 微信风格连选 插件实现了仿微信的长按连选交互: 1. **长按**进入选择模式(350ms) 2. **拖动**连选/连取消(根据第一项状态决定) 3. **松手**退出选择模式 4. **点击**单项 toggle(和之前一样) **避免误触**: - 长按 350ms 才触发,正常滚动不会误触 - iOS 禁用弹簧效果(`ClampingScrollPhysics`) - 批量操作通过 `beginBatch()/endBatch()` 抑制通知,只重建一次 ## 界面布局与交互 ### 主页面布局 ``` ┌─────────────────────────────────┐ │ ← 返回 相册名称 ▼ 预览 │ ← AppBar ├─────────────────────────────────┤ │ │ │ 照片网格 (4列) │ │ │ │ │ ├────────────────┬────────────────┤ │ 📷 已选缩略图 │ 完成 (3) │ ← 底部栏 │ [×] [×] [×] │ │ └────────────────┴────────────────┘ ``` - **预览按钮**:AppBar 右侧,已选数量 > 0 时可用 - **底部栏**:左侧显示已选缩略图(支持横向滚动),右侧显示完成按钮 - **已选缩略图**:每个缩略图右上角有 × 按钮,点击可移除该项 ### 预览页面布局 ``` ┌─────────────────────────────────┐ │ ← 返回 2/9 移除 │ ← AppBar ├─────────────────────────────────┤ │ │ │ 大图预览区域 │ │ (支持缩放/视频播放) │ │ │ │ │ ├─────────────────────────────────┤ │ [×] [✓] [✓] [✓] [✓] 完成│ ← 底部缩略图条 └─────────────────────────────────┘ ``` - **顶部栏**:左侧返回按钮,中间显示当前页码/总数,右侧移除按钮 - **底部缩略图条**:显示所有已选资源,点击可切换查看指定图片/视频,当前查看项有高亮边框 - **移除按钮**:点击可从已选列表中移除当前查看的资源 ### 视频预览控制 视频预览时,点击画面显示/隐藏控制栏: - 播放/暂停按钮 - 进度条(支持拖拽跳转) - 当前时间 / 总时长显示 ## 深色模式 通过 `themeBrightness` 参数控制: ```dart MediaPickersConfig( themeBrightness: Brightness.dark, // 或 Brightness.light themeColor: Colors.blue, // 可选,自定义主题色 ) ``` 深色模式会自适应: - AppBar、底部栏、相册选择器 - 网格背景、缩略图选中遮罩 - 预览页面背景、控制按钮 - 错误占位符图标颜色 ## 示例应用 ```bash cd example && flutter run ``` 演示:拍照 / 录像 / 相册选择 / 过滤选项配置(GIF/LivePhoto/APNG/HEIF/WebP/视频时长)。 ## 常见问题 **Android 录像返回 null?** 使用精确文件名 + 多次重试机制解决。 **视频 duration=0?** Android 通过 `MediaMetadataRetriever` 预加载,已绕过 MediaStore 延迟。 **限制模式下看不到所有相册?** 正常行为,引导用户点击 Banner 中的"选取更多"。 **打开大相册速度慢?** 插件使用全量加载 + `createDateTime` 排序。10,000 张图约 2-3 秒,期间有 Loading 提示。 **缩略图不清晰?** Android Q+ 使用 `ContentResolver.loadThumbnail(Size(512,512))` 获取系统高质量缓存,再等比缩放显示。 **长按连选误触?** 长按 350ms 才触发,正常滚动不会误触。如仍有问题,可调整 `MediaPickersGrid` 中的长按时长。 **预览页删除后还在播旧视频?** 已修复:`_VideoPreview` 添加 `Key(asset.id)` + `didUpdateWidget` 重新初始化播放器。 --- ## 许可证 MIT