# 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
[](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