接入 Android 照片选择器:单选、多选、持久授权与 HDR 转码
照片选择器(Photo Picker)把用户的媒体库按日期从新到旧展示出来。用户可以只把选中的照片或视频交给应用,而不必把整个媒体库的访问权限授予应用。如果设备上有符合条件的云媒体提供方,用户还可以选择远端保存的照片和视频。选择器会通过系统更新持续获得新功能,应用不必为每一次界面能力变化都修改代码。
本文据 Google Android Developers 的 Photo picker 完整文档翻译整理。原作者为 Google / Android Developers 文档贡献者;源页最后更新于 2026-10-01,本稿于 2026-10-09 核对。新增的代码修订和运行边界均单独标注,未在 Android 设备或模拟器上执行测试。

使用 Jetpack Activity 结果契约
原文要求引入 androidx.activity 1.7.0 或更高版本,以简化照片选择器接入。单选使用 PickVisualMedia,多选使用 PickMultipleVisualMedia。如果设备没有可用的照片选择器,库会自动回退到 ACTION_OPEN_DOCUMENT;后者支持 Android 4.4(API 19)及以上设备。可通过 isPhotoPickerAvailable() 检查选择器是否可用。
版本核验:“基础选择器支持 1.7.0+”不能解释为本文所有 API 都在 1.7.0 中存在。后文的 HDR 转码请求 API 属于 1.11.0 系列;截至本次核对,官方 Activity 发布页列出的稳定版为 1.13.0。采用新版本前仍需核对自己项目的编译 SDK、依赖和设备覆盖范围。
单选:处理 URI,也处理取消
在基于 Views 的 Activity 或 Fragment 中,可以注册单选启动器。回调既会在用户选中内容后调用,也会在用户关闭选择器后调用;后者返回 null。以下保留原文流程,但把“输出完整 URI”的日志改成仅记录选择状态,避免把媒体标识写入日志。
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
val pickMedia = registerForActivityResult(PickVisualMedia()) { uri ->
if (uri != null) {
// 在这里把 uri 交给应用的媒体处理逻辑。
Log.d("PhotoPicker", "Media selected")
} else {
Log.d("PhotoPicker", "No media selected")
}
}
根据希望用户选择的内容,以下 launch() 调用一次只选一个:
// 图片和视频
pickMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageAndVideo))
// 只选择图片
pickMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly))
// 只选择视频
pickMedia.launch(PickVisualMediaRequest(PickVisualMedia.VideoOnly))
// 限制到一个 MIME 类型,例如 GIF
val mimeType = "image/gif"
pickMedia.launch(
PickVisualMediaRequest(PickVisualMedia.SingleMimeType(mimeType))
)
Java 的对应写法如下。这里同样只展示注册与启动契约所需的片段,应用的 UI、生命周期宿主及媒体处理代码需要由项目补齐。
ActivityResultLauncher<PickVisualMediaRequest> pickMedia =
registerForActivityResult(new PickVisualMedia(), uri -> {
if (uri != null) {
Log.d("PhotoPicker", "Media selected");
} else {
Log.d("PhotoPicker", "No media selected");
}
});
// 以下启动方式选其一。
pickMedia.launch(new PickVisualMediaRequest.Builder()
.setMediaType(PickVisualMedia.ImageAndVideo.INSTANCE)
.build());
pickMedia.launch(new PickVisualMediaRequest.Builder()
.setMediaType(PickVisualMedia.ImageOnly.INSTANCE)
.build());
pickMedia.launch(new PickVisualMediaRequest.Builder()
.setMediaType(PickVisualMedia.VideoOnly.INSTANCE)
.build());
String mimeType = "image/gif";
pickMedia.launch(new PickVisualMediaRequest.Builder()
.setMediaType(new PickVisualMedia.SingleMimeType(mimeType))
.build());
使用 Compose 时,将注册方式换成 rememberLauncherForActivityResult;媒体类型和 PickVisualMediaRequest 的构造方式相同。启动动作应由用户操作等事件触发,下面并不是要求在组合执行时反复调用 launch()。
val pickMedia = rememberLauncherForActivityResult(PickVisualMedia()) { uri ->
if (uri != null) {
Log.d("PhotoPicker", "Media selected")
} else {
Log.d("PhotoPicker", "No media selected")
}
}
// 例如在按钮点击回调中:
pickMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageAndVideo))
原文说明,使用 PickVisualMedia 时,照片选择器以半屏模式打开。具体系统外观仍由设备的系统组件决定。
多选:数量上限需要在回退路径再次检查
多选契约允许声明希望用户选择的最大媒体数量。例如,最多选择 5 个文件:
import androidx.activity.result.contract.ActivityResultContracts.PickMultipleVisualMedia
val pickMultipleMedia =
registerForActivityResult(PickMultipleVisualMedia(5)) { uris ->
if (uris.isNotEmpty()) {
Log.d("PhotoPicker", "Number of items selected: ${uris.size}")
} else {
Log.d("PhotoPicker", "No media selected")
}
}
pickMultipleMedia.launch(
PickVisualMediaRequest(PickVisualMedia.ImageAndVideo)
)
Java 版本的回调返回列表;用户取消选择时应处理空列表:
ActivityResultLauncher<PickVisualMediaRequest> pickMultipleMedia =
registerForActivityResult(new PickMultipleVisualMedia(5), uris -> {
if (!uris.isEmpty()) {
Log.d("PhotoPicker", "Number of items selected: " + uris.size());
} else {
Log.d("PhotoPicker", "No media selected");
}
});
pickMultipleMedia.launch(new PickVisualMediaRequest.Builder()
.setMediaType(PickVisualMedia.ImageAndVideo.INSTANCE)
.build());
Compose 版本只需使用相应的记忆化启动器:
val pickMultipleMedia =
rememberLauncherForActivityResult(PickMultipleVisualMedia(5)) { uris ->
if (uris.isNotEmpty()) {
Log.d("PhotoPicker", "Number of items selected: ${uris.size}")
} else {
Log.d("PhotoPicker", "No media selected")
}
}
// 放在用户操作的事件处理代码中。
pickMultipleMedia.launch(
PickVisualMediaRequest(PickVisualMedia.ImageAndVideo)
)
多选也可以使用单选章节中介绍的图片、视频和 MIME 类型过滤方式。平台本身另有一个最大可选择数量,可通过 getPickImagesMaxLimit() 查询。
回退边界:当库回退到 ACTION_OPEN_DOCUMENT 时,系统会忽略指定的最大数量。因此 PickMultipleVisualMedia(5) 不能代替应用的业务校验。如果业务只允许 5 个文件,收到结果后仍应检查 uris.size,超过上限时让用户明确调整选择,而不是未经说明就提交多余文件。
哪些设备可以使用选择器
原文列出的系统选择器条件是 Android 11(API 30)或更高版本,并能通过 Google 系统更新获得模块化系统组件的更新。对支持 Google Play 服务的较旧设备,Android 4.4(API 19)至 Android 10(API 29),以及 Android 11 或 12 的 Android Go 设备,可以安装回移版本。
要让 Google Play 服务自动安装这个回移模块,可以在应用清单的 <application> 内加入以下条目。代码保留原文的 enabled="false" 和 exported="false",不要把它当成应公开导出的业务服务。
<!-- 由 Google Play 服务安装回移版本的照片选择器模块。 -->
<service
android:name="com.google.android.gms.metadata.ModuleDependencies"
android:enabled="false"
android:exported="false"
tools:ignore="MissingClass">
<intent-filter>
<action android:name="com.google.android.gms.metadata.MODULE_DEPENDENCIES" />
</intent-filter>
<meta-data
android:name="photopicker_activity:0:required"
android:value="" />
</service>
编辑补充:上面的 tools: 属性要求清单根节点声明 xmlns:tools="http://schemas.android.com/tools"。回移能力依赖设备和 Google Play 服务,不能据此承诺所有 API 19 设备都会获得同样的选择器;不满足条件时仍需正确处理文档选择回退。
后台长任务需要持久化访问授权
默认情况下,系统授予的媒体访问持续到设备重启或应用停止。上传大文件等后台长任务可能需要更长时间的读取权,此时对返回的 URI 调用 takePersistableUriPermission():
val flag = Intent.FLAG_GRANT_READ_URI_PERMISSION
context.contentResolver.takePersistableUriPermission(uri, flag)
int flag = Intent.FLAG_GRANT_READ_URI_PERMISSION;
context.getContentResolver().takePersistableUriPermission(uri, flag);
Java 修订:源页 Java 片段写为 context.contentResolver,这是 Kotlin 风格属性访问;本稿改为 Java 的 context.getContentResolver()。这是一处静态语法修订,未编译验证。
当前源文注明,一个应用同时最多可持有 5,000 项媒体授权;超过这一数量再授予新照片或视频时,系统会移除列表中最早的授权。只保存 URI 字符串并不等于保存了读取权。
编辑补充:持久授权仍可能失效,文件也可能被删除。后台任务应处理权限获取失败和之后读取失败,必要时让用户重新选择。不要把 URI 转换成假定存在的本地绝对路径,也不要因为已拿到 URI 就假定远端媒体已经在本机。
需要时,将 HDR 视频转为 SDR
Android 13(API 33)引入 HDR 视频拍摄能力,但旧应用可能无法正确处理这些格式,例如播放出现偏绿的人脸。照片选择器可以在把视频交给应用之前,将 HDR 转为标准动态范围 SDR,以改善兼容性。
转码默认不会启用。应用必须在发起选择请求时,通过 PickVisualMediaRequest.Builder.setMediaCapabilitiesForTranscoding() 声明自身能够处理的 HDR 类型。能力对象可以列出 HLG10、HDR10、HDR10+、Dolby Vision;没有列出的 HDR 类型视为应用不支持。传入 null 会完全禁用这项转码能力声明。
这项 API 要求 Android 13(API 33)及以上。原文要求 AndroidX Activity 1.11.0-alpha01 或其后包含此 API 的 alpha、beta、RC、稳定版本;当前 API 参考把 MediaCapabilities 标记为在 1.11.0 加入。不要因为文档展示了早期 alpha 版本就把项目固定在该 alpha。
下面的例子声明应用支持 HLG10,因此其他未声明支持的 HDR 格式可能需要转码。只有应用确实具备相应解码、处理和输出能力时才应声明支持。
import android.os.Build
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.MediaCapabilities
import androidx.annotation.RequiresApi
// pickMedia 使用前文注册的启动器。
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
fun launchPhotoPickerWithTranscodingSupport() {
val mediaCapabilities = MediaCapabilities.Builder()
.addSupportedHdrType(MediaCapabilities.TYPE_HLG10)
.build()
pickMedia.launch(
PickVisualMediaRequest.Builder()
.setMediaType(PickVisualMedia.VideoOnly)
.setMediaCapabilitiesForTranscoding(mediaCapabilities)
.build()
)
}
// 调用处必须判断运行时版本,注解本身不会替应用执行判断。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
launchPhotoPickerWithTranscodingSupport()
} else {
pickMedia.launch(PickVisualMediaRequest(PickVisualMedia.VideoOnly))
}
与源文的差异:源页写作 MediaCapabilities.HdrType.TYPE_HLG10,且没有导入实际的嵌套能力类;当前官方 API 参考把这些常量直接放在 PickVisualMedia.MediaCapabilities 上。本稿据该参考补齐导入,并改为 MediaCapabilities.TYPE_HLG10。另补充了调用处的运行时版本判断,并移除完整 URI 日志。这些是静态核对后的修订,不代表已经编译或在设备上测试。
是否实际转码取决于应用声明和用户所选视频。如果发生转码,应用收到的是转码后视频的 URI。
- 耗时和存储:转码需要处理时间,并产生占用存储空间的新文件,应把等待和失败纳入用户流程。
- 时长:当前文档给出的转码视频长度上限为 1 分钟。
- 缓存:转码缓存会在空闲维护期间被定期清理,不能把它当作永久保存的新文件。
- 设备:转码支持 Android 13(API 33)及以后版本;基础照片选择器的可用范围更广。
接入前后要核对的实际边界
一个完整的选择流程应同时处理“用户取消”“选择结果超过业务上限”“URI 授权失效”“云端内容读取失败”和“转码产生额外等待”。选择器缩小了媒体库授权范围,但不会替应用完成所有业务校验,也不会替应用长期保存媒体。
本稿对代码做了静态安全审查:源文的完整 URI 日志可能暴露媒体标识,已改为状态日志;Java 属性访问和 HDR 常量位置已据语言/API文档修订;清单服务保持不可导出。没有发现硬编码凭证或破坏性命令。未编译、未在设备/模拟器运行、未上传媒体,也没有把文档示例当作测试结论。没有发现其他问题,不等于没有漏洞。
来源与归属:Google / Android Developers、Android Open Source Project,Photo picker 原文。依照本次核对的 Android Content License,文档及代码示例除另有说明外按 Apache License 2.0 提供;其他站点内容适用其单独说明。Apache License 2.0 的完整文本随稿附带。本稿为未完纪中文翻译整理,新增技术示意图、代码修订及边界说明;中文翻译与原创示意图依单独取得的发布授权提供。Google、Android、Java 和 OpenJDK 的商标不因文档许可而获得额外使用许可。











暂无评论内容