English | 中文
通过相册的抽象 API 对设备中的资源(图片、视频、音频)进行管理,不需要集成 UI。 在 Android、iOS、macOS and OpenHarmony上可用。
name | pub | github |
---|---|---|
wechat_assets_picker | ||
wechat_camera_picker |
查看 迁移指南 了解如何在破坏性改动之间迁移。
目录列表
你可以在 GitHub issues 上搜索到经常遇到的问题,比如构建错误,运行时异常等等。
有两种方式可以把依赖添加到你的项目中:
- (推荐) 运行
flutter pub add photo_manager
. - 或者直接添加到项目的
pubspec.yaml
中的dependencies
部分:
dependencies:
photo_manager: $latest_version
import 'package:photo_manager/photo_manager.dart';
最低的平台版本: Android API 16, iOS 9.0, macOS 10.15.
- Android:Android 配置准备.
- iOS:iOS 配置准备.
- macOS:与 iOS 几乎一致。
该插件使用 Kotlin 1.7.22
来构建。
如果你的项目使用了低于此版本的 Kotlin/Gradle/AGP,请升级到大于或等于指定版本。
更具体的做法:
- 更新你的 Gradle version (
gradle-wrapper.properties
) 到7.5.1
或者最新版本。 - 更新你的 Kotlin version (
ext.kotlin_version
) 到1.7.22
或者最新版本。 - 更新你的 AGP version (
com.android.tools.build:gradle
) 或者7.2.2
或者最新版本。
如果你没有设置 compileSdkVersion
或 targetSdkVersion
为 29
,你可以跳过本节。
Android 10 引入了 Scoped Storage,导致原始资源文件不能通过其文件路径直接访问。
如果你的 compileSdkVersion
或 targetSdkVersion
为 29,
为了能够成功获取到媒体资源,你可以考虑通过在 AndroidManifest.xml
中添加 android:requestLegacyExternalStorage="true"
,如下所示:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.fluttercandies.photo_manager_example">
<application
android:label="photo_manager_example"
android:icon="@mipmap/ic_launcher"
android:requestLegacyExternalStorage="true">
</application>
</manifest>
注意: 这样设置的应用无法上架 Google Play。
这不是必须的,插件缓存文件的时候还是可以正常工作的。
但是开发者需要主动清理缓存,最佳实践是启动应用时调用
PhotoManager.clearFileCache
清理缓存文件。
本插件使用 Glide 在 Android 平台上创建缩略图。
如果你发现 Glide 出现了一些警告日志,说明主项目需要实现 AppGlideModule
。
请查看 Generated API 的相关文档说明。
添加 NSPhotoLibraryUsageDescription
到你的项目的 ios/Runner/Info.plist
中:
<key>NSPhotoLibraryUsageDescription</key>
<string>In order to access your photo library</string>
在 iOS 11 或者更高版本中,如果你只需要请求相册的写入权限,
仅需要添加 NSPhotoLibraryAddUsageDescription
到你的项目的 ios/Runner/Info.plist
。
大部分的 API 只在获取到权限后才能正常使用。
final PermissionState ps = await PhotoManager.requestPermissionExtend();
if (ps.isAuth) {
// 已获取到权限
} else if (ps.hasAccess) {
// 已获取到权限(哪怕只是有限的访问权限)。
// iOS Android 目前都已经有了部分权限的概念。
} else {
// 权限受限制(iOS)或者被拒绝,使用 `==` 能够更准确的判断是受限还是拒绝。
// 你可以使用 `PhotoManager.openSetting()` 打开系统设置页面进行进一步的逻辑定制。
}
如果你确定你的应用已经授予了权限,你也可以忽略权限的检查:
PhotoManager.setIgnorePermissionCheck(true);
对于一些后台操作(应用未启动等)而言,忽略检查是比较合适的做法。
iOS14 引入了部分资源限制的权限 (PermissionState.limited
)。
PhotoManager.requestPermissionExtend()
会返回当前的权限状态 PermissionState
。
详情请参阅 PHAuthorizationStatus。
如果你想要重新选择在应用里能够读取到的资源,你可以使用 PhotoManager.presentLimited()
重新选择资源,
这个方法对于 iOS 14 以上的版本生效。
如果你想要禁止每次应用重新启动后访问媒体时自动弹出提示,
你可以将 Info.plist
的 Prevent limited photos access alert
设置为 YES
(或者像下面一样手动编写):
<key>PHPhotoLibraryPreventAutomaticLimitedAccessAlert</key>
<true/>
与 iOS 类似,Android 14 (API 34) 中也引入了这个概念。
它们在行为上略有不同(基于模拟器):
在 Android 中一旦授予某个资源的访问权限,就无法撤销,
即使再次使用 presentLimited
时不选中也不会撤销对它的访问权限。
相簿或者图集以抽象类 AssetPathEntity
的形式呈现,
在 Android 中它表示为具有相同 bucketId
的 MediaStore
记录的集合,
在 iOS/macOS 中则是 PHAssetCollection
的记录。
获取所有相册:
final List<AssetPathEntity> paths = await PhotoManager.getAssetPathList();
详情请参阅 getAssetPathList
。
参数名 | 说明 | 默认值 |
---|---|---|
hasAll | 如果你需要一个包含所有资源(AssetEntity) 的 PathEntity ,传入 true | true |
onlyAll | 如果你只需要一个包含所有资源的,传入true | false |
type | 资源文件的类型(视频、图片、音频) | RequestType.common |
filterOption | 用于筛选 AssetEntity,详情请参阅 过滤资源 | FilterOptionGroup() |
pathFilterOption | 只对 iOS 和 macOS生效,对应原生中的相册类型,详情请参阅 PMPathFilterOption。 | 默认为包含所有 |
自 2.7.0 版本开始提供,当前仅支持 iOS 和 macOS。
final List<PMDarwinAssetCollectionType> pathTypeList = []; // 配置为你需要的类型
final List<PMDarwinAssetCollectionSubtype> pathSubTypeList = []; // 配置为你需要的子类型
final darwinPathFilterOption = PMDarwinPathFilter(
type: pathTypeList,
subType: pathSubTypeList,
);
PMPathFilter pathFilter = PMPathFilter();
PMDarwinAssetCollectionType
的枚举值一一对应 PHAssetCollectionType | 苹果官网文档.
PMDarwinAssetCollectionSubtype
的枚举值一一对应 PHAssetCollectionSubType | 苹果官网文档.
资源(图片/视频/音频)以 AssetEntity
的方式呈现,
它抽象了原生中关于媒体对象的一系列属性和方法。
在 Android 中表示 MediaStore
记录的其中一些字段的集合,
在 iOS/macOS 则是 PHAsset
的记录。
你可以通过 分页方法 获取:
final List<AssetEntity> entities = await path.getAssetListPaged(page: 0, size: 80);
也可以通过 范围索引方法 获取:
final List<AssetEntity> entities = await path.getAssetListRange(start: 0, end: 80);
首先你需要获取资源的数量:
final int count = await PhotoManager.getAssetCount();
然后通过 分页方法 获取:
final List<AssetEntity> entities = await PhotoManager.getAssetListPaged(page: 0, pageCount: 80);
也通过 范围索引方法 获取:
final List<AssetEntity> entities = await PhotoManager.getAssetListRange(start: 0, end: 80);
注意: page
和start
都从 0 开始。
ID 在不同平台代表:
- Android 平台
MediaStore
的_id
字段; - iOS/macOS 平台
PHAsset
的localIdentifier
字段。
如果你想要实现持久化选择的相关功能,你需要存储资源的 ID,
并在之后的使用中通过 AssetEntity.fromId
` 来重新持有资源对象。
final AssetEntity? asset = await AssetEntity.fromId(id);
请留意资源可能访问受限,或随时被删除,所以结果可能为空。
你可以从原始数据或文件(如下载的图像、录制的视频等)创建 AssetEntity
。
创建的 AssetEntity
将储存到设备的图库中。
final Uint8List rawData = yourRawData;
// 将 `Uint8List` 保存为一张图片。
final AssetEntity? entity = await PhotoManager.editor.saveImage(
rawData,
title: 'write_your_own_title.jpg', // 可能影响 EXIF 信息的读取
);
// 通过路径来保存一张已存在的图片。
final AssetEntity? imageEntityWithPath = await PhotoManager.editor.saveImageWithPath(
path, // 使用源文件的绝对路径来保存,与复制类似。
title: 'same_as_above.jpg',
);
// 通过文件来保存视频。
final File videoFile = File('path/to/your/video.mp4');
final AssetEntity? videoEntity = await PhotoManager.editor.saveVideo(
videoFile, // 可以检查文件是否存在以获得更好的测试覆盖率。
title: 'write_your_own_title.mp4',
);
// [仅 iOS] 通过图片和视频来保存一张实况照片。
// 仅在图片和视频文件为同一张实况照片时才能生效。
final File imageFile = File('path/to/your/livephoto.heic');
final File videoFile = File('path/to/your/livevideo.mp4');
final AssetEntity? entity = await PhotoManager.editor.darwin.saveLivePhoto(
imageFile: imageFile,
videoFile: videoFile,
title: 'write_your_own_title.heic',
);
请留意资源可能访问受限,或随时被删除,所以结果可能为空。
iOS 为了节省磁盘空间,可能将资源仅保存在 iCloud 上。
从 iCloud 检索资源文件时,速度会取决于网络状况,可能非常缓慢,使用户感到焦虑。
你可以使用 PMProgressHandler
在加载文件时提示用户当前的进度。
推荐参考的实践是 wechat_asset_picker
中的
LocallyAvailableBuilder
,它会在下载文件时提供进度的展示。
从 v3.0.0 开始,插件不再提供任何 UI 组件。
AssetEntityImage
和 AssetEntityImageProvider
在
photo_manager_image_provider
插件中提供。
新的插件提供 AssetEntityImage
widget 和
AssetEntityImageProvider
来处理资源的展示:
import 'package:photo_manager_image_provider/photo_manager_image_provider.dart';
final Widget image = AssetEntityImage(
yourAssetEntity,
isOriginal: false,
thumbnailSize: const ThumbnailSize.square(200),
thumbnailFormat: ThumbnailFormat.jpeg,
);
final Widget imageFromProvider = Image(
image: AssetEntityImageProvider(
yourAssetEntity,
isOriginal: false,
thumbnailSize: const ThumbnailSize.square(200),
thumbnailFormat: ThumbnailFormat.jpeg,
),
);
该插件支持获取和过滤 iOS 上的实况照片。
只在过滤图片的时候支持过滤「实况照片」:
final List<AssetPathEntity> paths = await PhotoManager.getAssetPathList(
type: RequestType.image,
filterOption: FilterOptionGroup(onlyLivePhotos: true),
);
final AssetEntity entity = livePhotoEntity;
final String? mediaUrl = await entity.getMediaUrl();
final File? imageFile = await entity.file;
final File? videoFile = await entity.fileWithSubtype;
final File? originImageFile = await entity.originFile;
final File? originVideoFile = await entity.originFileWithSubtype;
在 Android 10 版本中获取带有位置信息和 EXIF 元数据的原始数据时,必须授予媒体位置权限。
若需要获取,请将 ACCESS_MEDIA_LOCATION
权限添加到清单中。
originFile
和 originBytes
的 getter 会返回资源的原始数据。
然而在 Flutter 中,某些情况的原始数据是无法使用的。以下是一些常见的情况:
- 在不同平台和版本中,HEIC 文件并未被完全支持。
我们建议你上传 JPEG 文件(HEIC 图片的
.file
), 以保持多个平台之间的一致行为。 查看 [flutter/flutter#20522][] 了解更多细节。 - 视频将仅以原始格式获取,而不是组合过的格式, 这可能会在播放视频时导致某些行为的差异。
该插件中有几个针对 AssetEntity
的 I/O 方法:
- 所有名称带有
file
的方法. AssetEntity.originBytes
.
在 iOS 上,文件的检索和缓存受到沙盒机制的限制。
能获取到 PHAsset
并不意味着该资源位于设备上。
一般来说,PHAsset
会有三种状态:
isLocallyAvailable
等于true
, 并且已经缓存:可以获取。isLocallyAvailable
等于true
, 但没有缓存:当你调用 I/O 方法时,资源需要缓存在沙盒中才可以获取。isLocallyAvailable
等于false
,通常意味着资源仅保存在 iCloud 上,或者某些视频尚未导出过。 在这种情况下,最好使用PMProgressHandler
提供响应式的用户界面。
插件会从原生平台广播资源变更的事件,但是在不同的平台和系统版本之间,事件携带的内容并不相同。 你可以参考 相关日志 了解各个版本和平台之间的事件日志。
要为这些事件注册回调,请使用 PhotoManager.addChangeCallback
添加回调,
并使用 PhotoManager.removeChangeCallback
移除回调,
与 addListener
和 removeListener
方法相似。
在添加/移除回调之后,你可以调用 [PhotoManager.startChangeNotify
方法启用通知,
以及 PhotoManager.stopChangeNotify
方法停止通知。
import 'package:flutter/services.dart';
void changeNotify(MethodCall call) {
// 你的自定义回调。
}
/// 注册你的回调方法。
PhotoManager.addChangeCallback(changeNotify);
/// 启用事件通知订阅。
PhotoManager.startChangeNotify();
/// 移除你的回调方法。
PhotoManager.removeChangeCallback(changeNotify);
/// 取消事件通知订阅。
PhotoManager.stopChangeNotify();
插件包含对资源过滤筛选的支持。
以下的方法包含 filterOption
参数,用于指定资源过滤的条件。
- PhotoManager
- getAssetPathList(可以通过
AssetPathEntity.filterOption
获取) - getAssetCount
- getAssetListRange
- getAssetListPaged
- getAssetPathList(可以通过
- AssetPathEntity
- 构造(不推荐直接使用)
- fromId
- obtainPathFromProperties(不推荐直接使用)
插件支持两种形式的资源筛选:
FilterOptionGroup
是 2.6.0 版本前唯一支持的筛选器实现。
final FilterOptionGroup filterOption = FilterOptionGroup(
imageOption: FilterOption(
sizeConstraint: SizeConstraint(
maxWidth: 10000,
maxHeight: 10000,
minWidth: 100,
minHeight: 100,
ignoreSize: false,
),
),
videoOption: FilterOption(
durationConstraint: DurationConstraint(
min: Duration(seconds: 1),
max: Duration(seconds: 30),
allowNullable: false,
),
),
createTimeCondition: DateTimeCondition(
min: DateTime(2020, 1, 1),
max: DateTime(2020, 12, 31),
),
orders: [
OrderOption(
type: OrderOptionType.createDate,
asc: false,
),
],
/// 其他选项
);
注意: CustomFilter
自 v2.6.0 引入。由于其存在时间较短,无法保证其稳定性。
如果在使用时遇到相关问题,请按照模板提交 issue。
CustomFilter
针对不同平台提供了更加灵活的筛选条件。
其使用方法更像平台本身的处理方法,即类 SQL 的筛选方式。
SQL 筛选的字段名称在不同平台上是不一致的,
在使用时请注意区分 CustomColumns.base
、CustomColumns.android
以及 CustomColumns.darwin
来获取正确的字段名称。
构造一个 CustomFilter
的例子:
CustomFilter createFilter() {
return CustomFilter.sql(
where: '${CustomColumns.base.width} > 100 AND ${CustomColumns.base.height} > 200',
orderBy: [OrderByItem.desc(CustomColumns.base.createDate)],
);
}
AdvancedCustomFilter
继承自 CustomFilter
,
可以通过 builder 方式创建一个筛选器。
CustomFilter createFilter() {
final group = WhereConditionGroup()
.and(
ColumnWhereCondition(
column: CustomColumns.base.width,
value: '100',
operator: '>',
),
)
.or(
ColumnWhereCondition(
column: CustomColumns.base.height,
value: '200',
operator: '>',
),
);
final filter = AdvancedCustomFilter()
.addWhereCondition(group)
.addOrderBy(column: CustomColumns.base.createDate, isAsc: false);
return filter;
}
CustomFilter
:自定义筛选器的基类。SqlCustomFilter
:类 SQL 的筛选器。AdvancedCustomFilter
:构建自定义选项的筛选器。OrderByItem
:实现排序的类,功能类似于 ORDER BY。WhereConditionItem
:条件筛选的抽象类,功能类似于 WHERE。WhereConditionGroup
:筛选条件组,将一组条件合并在同一个条件组。TextWhereCondition
:字符串筛选条件,在传递时不会检查是否有效。ColumnWhereCondition
:字段名筛选条件,在传递时会检查字段名是否有效。DateColumnWhereCondition
:日期筛选条件,由于 iOS/macOS 上的日期格式不同,该条件可以帮助处理这些差异。
CustomColumns
:不同平台的字段名。base
:适用于各个平台通用的字段名,但在 iOS 上 "id" 字段无效,甚至可能会导致错误。它只在 Android 上有效。android
:适用于 Android 的字段名。darwin
:适用于 iOS/macOS 的字段名。
由于 Android 10 限制了直接访问资源路径的能力,
因此图像缓存将在 I/O 处理过程中生成。
更具体地说,当调用 file
,originFile
或任何 I/O 操作时,
插件将保存一个文件到缓存文件夹以供进一步使用。
幸运的是,在 Android 11 及以上版本中,可以再次直接获取资源路径,
在 Android 10 中,你仍然可以使用 requestLegacyExternalStorage
访问存储中的文件而不缓存它们。
有关如何添加属性,请参见 Android 10 (Q, 29)。
该属性不是必需的。
iOS 没有直接提供 API 来访问相册资源的原始文件。
因此,当调用 file
、originFile
或相关的文件操作时,
将在当前应用程序的沙盒中生成对应的缓存文件。
如果在你的用例中占用磁盘空间很敏感, 那么你可以在使用完成后删除它(仅适用于 iOS)。
import 'dart:io';
Future<void> useEntity(AssetEntity entity) async {
File? file;
try {
file = await entity.file;
await handleFile(file!); // 处理获取的文件
} finally {
if (Platform.isIOS) {
file?.deleteSync(); // 处理完成后删除
}
}
}
你可以使用 PhotoManager.clearFileCache
方法来清除插件生成的所有缓存。
缓存的生成取决于不同平台、类型和分辨率等情况。
平台 | 缩略图 | 文件 / 原始文件 |
---|---|---|
Android | 生成 | 生成 (Android 10+) |
iOS | 不生成 | 生成 |
如果你的项目存在 Glide 的版本冲突问题,
那么你需要编辑 android/build.gradle
文件:
rootProject.allprojects {
subprojects {
project.configurations.all {
resolutionStrategy.eachDependency { details ->
if (details.requested.group == 'com.github.bumptech.glide'
&& details.requested.name.contains('glide')) {
details.useVersion '4.14.2'
}
}
}
}
}
如果你想了解如何同时使用 ProGuard 和 Glide,请参阅 ProGuard for Glide。
当你的应用在 API 34 (Android 14) 的设备上运行时,
就算你的 targetSdkVersion
和 compileSdkVersion
不是 34
,
你也需要在清单文件中添加以下权限配置:
<manifest>
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" /> <!-- 如果需要提供可选的资源的功能 -->
</manifest>
当你的应用在 API 33 (Android 13) 的设备上运行时,
就算你的 targetSdkVersion
和 compileSdkVersion
不是 33
,
你也需要在清单文件中添加以下权限配置:
<manifest>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <!-- 如果需要读取图片 -->
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <!-- 如果需要读取视频 -->
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> <!-- 如果需要读取音频 -->
</manifest>
默认情况下,无论设备上设置了什么语言,iOS 都只会以英语检索系统相册的名称。 要更改默认语言,请按照以下步骤操作:
-
选择你想要检索本地化的语言。
-
在不进行任何修改的情况下验证弹出屏幕。
-
重新构建你的 Flutter 项目。
现在系统相册的名称应该能够以对应的语言显示。
注意: 本地化相册名称不代表自定义。
警告: 此处的功能不能保证在所有平台和系统版本下完全可用, 因为它们涉及到数据修改。 它们可能在任何版本中随时被修改或删除。
某些 API 将对数据进行不可逆的修改和删除。 在使用这些功能时,请谨慎操作,并最好实现先行测试。
你可以使用 PhotoCachingManager.requestCacheAssets
或 PhotoCachingManager.requestCacheAssetsWithIds
方法
以特定的缩略图选项来加载部分资源的缩略图。
PhotoCachingManager().requestCacheAssets(assets: assets, option: option);
你也可以通过调用
PhotoCachingManager().cancelCacheRequest
方法随时停止预加载。
通常在 app 预览资源时,会使用缩略图进行展示。 但有时我们希望预加载资源以使其显示更快。
PhotoCachingManager
在 iOS 上使用的是 PHCachingImageManager,
在 Android 上使用 Glide 的文件缓存。
此方法将从你的图库中完全删除资源,请谨慎使用。
// 调用方法会返回被删除的资源,如果全部失败会返回空列表。
final List<String> result = await PhotoManager.editor.deleteWithIds(
<String>[entity.id],
);
删除后,你可以调用 refreshPathProperties
方法刷新相应的
AssetPathEntity
以便更新字段。
你可以使用 copyAssetToPath
方法将资源 “复制” 到目标 AssetPathEntity
中:
// 确保 anotherPathEntity 对于当前 app 而言可以访问。
final AssetPathEntity anotherPathEntity = anotherAccessiblePath;
final AssetEntity entity = yourEntity;
final AssetEntity? newEntity = await PhotoManager.editor.copyAssetToPath(
asset: entity,
pathEntity: anotherPathEntity,
); // 如果 anotherPathEntity 无法访问,结果会返回 null。
“复制” 在 Android 和 iOS 上有不同的含义:
- 对于 Android,它会插入源资源的副本:
- 在 SDK <= 28 上,该方法将复制大部分来源信息。
- 在 SDK >= 29 上,某些字段无法在插入期间修改,如 MediaColumns.RELATIVE_PATH.
- 对于 iOS,它会创建一个快捷方式,而不是创建一个新的资源。
- 某些相册是智能相册,它们的内容由系统自动管理,不能手动插入资源。
(对于 Android 30+,由于系统限制,此功能当前被屏蔽。)
// 确保 accessiblePath 对于当前 app 而言可以访问。
final AssetPathEntity pathEntity = accessiblePath;
final AssetEntity entity = yourEntity;
await PhotoManager.editor.android.moveAssetToAnother(
entity: entity,
target: pathEntity,
);
(对于 Android 30+,由于系统限制,此功能当前被屏蔽。)
await PhotoManager.editor.android.moveToTrash(list);
这个方法用于将资源移动到废纸篓,它仅支持安卓 API 30+,低于 30 的 API 会抛出异常。
这将删除所有本地不存在的相册条目。
安卓的 MediaStore
中的记录对应的文件可能会被其他的 app 或文件管理器删除。
这些异常行为通常是由文件管理器、辅助工具或 adb 工具造成的。
此操作很消耗资源,请不要重复调用。
await PhotoManager.editor.android.removeAllNoExistsAsset();
某些系统会在每个资源删除时分别弹出确认对话框,这是无法避免的。 请确认你需要调用该方法,并且你的客户接受反复弹窗确认。
PhotoManager.editor.darwin.createFolder(
name,
parent: parent, // 应为 null、根目录或者其他可访问的文件夹
);
PhotoManager.editor.darwin.createAlbum(
name,
parent: parent, // 应为 null、根目录或者其他可访问的文件夹
);
从特定相册中移除资源。 该移除不会从设备中删除,只会从相册中被移除。
// 确保你的路径能够访问。
final AssetPathEntity pathEntity = accessiblePath;
final AssetEntity entity = yourEntity;
final List<AssetEntity> entities = <AssetEntity>[yourEntity, anotherEntity];
// 移除相簿的单个图片
// 这将调用列表移除的方法作为实现。
await PhotoManager.editor.darwin.removeInAlbum(
yourEntity,
accessiblePath,
);
// 批量从相册中移除资源。
await PhotoManager.editor.darwin.removeAssetsInAlbum(
entities,
accessiblePath,
);
智能相册无法被删除。
PhotoManager.editor.darwin.deletePath();
目前支持大部分的功能,除了跟缓存相关。目前鸿蒙只支持图片和视频 2 种资源类型。
Feature | OpenHarmony |
---|---|
releaseCache | ❌ |
clearFileCache | ❌ |
requestCacheAssetsThumbnail | ❌ |
getSubPathEntities | ❌ |