
2022 年,Android 团队推出了照片选择器,让用户能更隐私地分享媒体文件。用户可以通过可浏览的界面按日期从新到旧查看媒体库,并且只授予应用访问所选文件的权限,而非整个媒体库。
不过,最初的照片选择器是一个独立的系统 Activity,会打断用户当前的操作,将其带离应用。
为此,AndroidX 推出了全新的嵌入式照片选择器(EmbeddedPhotoPicker),允许用户直接在应用内浏览和选择媒体,体验更连贯,同时保持了 Android 一贯的高安全标准。
回顾旧版照片选择器
在深入了解嵌入式版本之前,先回顾一下传统的实现方式。以下是使用 Jetpack Compose 的简化示例:
kotlin
@Composable
fun DetachedPhotoPicker(modifier: Modifier = Modifier) {
var attachments by remember { mutableStateOf<List<Uri>>(emptyList()) }
val photoPickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickMultipleVisualMedia(5),
onResult = { uris -> attachments = uris }
)
Column(modifier.fillMaxSize().padding(horizontal = 16.dp)) {
Button(onClick = {
photoPickerLauncher.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)
}) {
Text("Open photo picker")
}
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 64.dp),
modifier = Modifier.fillMaxWidth()
) {
itemsIndexed(attachments) { index, uri ->
AsyncImage(
model = uri,
contentDescription = "Image ${index + 1}",
contentScale = ContentScale.Crop,
modifier = Modifier.border(1.dp, Color.White)
)
}
}
}
}

在这个流程中,用户点击按钮后,会跳转到系统界面选择媒体,然后再返回应用。
虽然功能上没问题,但整个过程存在明显的"上下文切换",这正是嵌入式照片选择器想要解决的问题。
嵌入式照片选择器
那么,为什么我们需要一个新的嵌入式照片选择器?
因为它专为追求流畅、不间断用户体验的应用而设计,直白点,那就是它不会打断你当前的界面和操作的流程,不会产生割裂感。
用户无需跳出应用,就能在应用界面中直接浏览和选择媒体。它的体验更像是应用的一部分,而不是一个突兀的外部界面:
- 更自然的用户体验:用户可以即时访问最近的照片和完整的云库(如 Google Photos),支持收藏、相册和搜索功能,无需关心照片存储在本地还是云端。
- 实时交互和连续选择:用户可以连续选择或取消选择媒体,无需关闭选择器。在聊天应用中,用户可以在输入框中即时看到所选照片的预览,应用 UI 中的选择状态会与照片选择器实时同步。
- 无摩擦的隐私保护:应用无需申请广泛的存储权限,只获取用户所选特定文件的访问权。同时,嵌入式选择器完整支持云媒体访问,无需额外的安全开销。
开始适配
首先在 build.gradle 中添加依赖:
kotlin
dependencies {
implementation("androidx.photopicker:photopicker-compose:1.0.0-alpha01")
}
不过,有一点需要提前说明,嵌入式照片选择器有特定的设备要求:
- 运行 Android 14(API 级别 34) 或更高版本
- 拥有 SDK Extensions 版本 15 或更高版本
因此,建议在运行时检查其可用性。
如果设备不满足条件,应用应优雅地回退到经典照片选择器。
使用 SdkExtensions API 进行检查:
kotlin
fun isEmbeddedPhotoPickerAvailable(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
SdkExtensions.getExtensionVersion(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) >= 15
}
在 Compose 中实现
验证兼容性后,实现非常简单。最基础的用法只需要一个状态对象和 EmbeddedPhotoPicker 组合项:
kotlin
@RequiresExtension(extension = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, version = 15)
@OptIn(ExperimentalPhotoPickerComposeApi::class)
@Composable
fun BasicEmbeddedPicker() {
val photoPickerState = rememberEmbeddedPhotoPickerState()
Column {
EmbeddedPhotoPicker(
state = photoPickerState
)
}
}

没错,它就这么 duang 的一下,显示出来了。就像你应用的一部分!
个性化选择器
嵌入式版本的一大优势是可定制性。
通过 EmbeddedPhotoPickerFeatureInfo 构建器,可以根据应用需求调整体验:
kotlin
@RequiresExtension(extension = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, version = 15)
@OptIn(ExperimentalPhotoPickerComposeApi::class)
@Composable
fun CustomizedEmbeddedPicker() {
val photoPickerState = rememberEmbeddedPhotoPickerState()
val primaryColor = MaterialTheme.colorScheme.primary
val photoPickerInfo = EmbeddedPhotoPickerFeatureInfo.Builder()
.setMaxSelectionLimit(3) // 限制最多选择 3 项
.setOrderedSelection(false) // 禁用选择顺序
.setAccentColor(primaryColor.value.toLong()) // 使用主题主色调
.build()
EmbeddedPhotoPicker(
state = photoPickerState,
embeddedPhotoPickerFeatureInfo = photoPickerInfo
)
}

如果选择多了,还能有个提示!
按 MIME 类型过滤
通过 MIME 类型过滤器可以控制用户看到的媒体类型:
kotlin
val photoPickerInfo = EmbeddedPhotoPickerFeatureInfo.Builder()
.setMimeTypes(listOf("image/webp")) // 只显示 webP 图片
.build()
实时处理选择
通过状态可以定义实时同步的回调:
kotlin
@RequiresExtension(extension = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, version = 15)
@OptIn(ExperimentalPhotoPickerComposeApi::class)
@Composable
fun RealTimeEmbeddedPicker() {
val photoPickerState = rememberEmbeddedPhotoPickerState(
onSelectionComplete = {
// 用户点击了选择器内的"完成"按钮
"Complete".shortToast()
},
onUriPermissionGranted = { uris ->
// 立即将新选择添加到应用的本地状态
"Select".shortToast()
},
onUriPermissionRevoked = { uris ->
// 从 UI 中移除取消选择的项目
"unSelect".shortToast()
}
)
EmbeddedPhotoPicker(
state = photoPickerState
)
}

控制显示状态
EmbeddedPhotoPickerState 提供了控制选择器显示状态的属性------"预览"状态(只显示最近照片)或"展开"状态(显示完整库、相册和搜索):
kotlin
val photoPickerState = rememberEmbeddedPhotoPickerState()
photoPickerState.setCurrentExpanded(true) // 默认显示完整库视图
嵌入式照片选择器可以放在界面中的任何位置。
但在 BottomSheet 中使用效果最佳,这与 Google 其他应用的体验保持一致。
对于更宽的屏幕,可以利用辅助页面布局将选择器与主要内容并排显示,充分利用平板电脑或折叠屏的额外空间:
kotlin
NavigableSupportingPaneScaffold(
navigator = navigator,
mainPane = {
// 主要内容(例如消息列表)
},
supportingPane = {
// 选择器直接显示在侧面板中
EmbeddedPhotoPicker(state = photoPickerState)
},
)
如果选择器显示在固定容器中,建议默认设置 setCurrentExpanded(true),确保用户立即看到内容。
在底部工作表中使用
以下是使用 BottomSheetScaffold 的完整示例,演示如何将工作表的展开状态与选择器同步,并管理所选媒体:
kotlin
@RequiresExtension(extension = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, version = 15)
@OptIn(ExperimentalPhotoPickerComposeApi::class, ExperimentalMaterial3Api::class)
@Composable
fun EmbeddedPhotoPickerDemo() {
var attachments by remember { mutableStateOf(emptyList<Uri>()) }
val scope = rememberCoroutineScope()
val scaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = rememberStandardBottomSheetState(
initialValue = SheetValue.Hidden,
skipHiddenState = false
)
)
val photoPickerState = rememberEmbeddedPhotoPickerState(
onSelectionComplete = { scope.launch { scaffoldState.bottomSheetState.hide() } },
onUriPermissionGranted = { attachments += it },
onUriPermissionRevoked = { attachments -= it }
)
// 将选择器视图与底部工作表状态同步
SideEffect {
val isExpanded = scaffoldState.bottomSheetState.targetValue == SheetValue.Expanded
photoPickerState.setCurrentExpanded(isExpanded)
}
BottomSheetScaffold(
scaffoldState = scaffoldState,
sheetPeekHeight = if (scaffoldState.bottomSheetState.isVisible) 400.dp else 0.dp,
sheetContent = {
EmbeddedPhotoPicker(
state = photoPickerState,
embeddedPhotoPickerFeatureInfo = EmbeddedPhotoPickerFeatureInfo.Builder()
.setMaxSelectionLimit(5)
.setOrderedSelection(true)
.setMimeTypes(listOf("image/*"))
.build()
)
}
) { padding ->
Column(Modifier.padding(padding).fillMaxSize().padding(16.dp)) {
Button(onClick = { scope.launch { scaffoldState.bottomSheetState.show() } }) {
Text("Open Photo Picker")
}
LazyVerticalGrid(columns = GridCells.Adaptive(64.dp)) {
items(attachments) { uri ->
AsyncImage(
model = uri,
contentDescription = null,
modifier = Modifier.clickable { photoPickerState.deselectUri(uri) }
)
}
}
}
}
}

效果还需要花时间打磨,此处只展示 EmbeddedPhotoPicker 在 BottomSheetScaffold 中的用法。
一点想法
在我看来,EmbeddedPhotoPicker 的核心价值在于将照片选择能力"嵌入"到应用界面中,而非作为独立 Activity 存在。
这一设计选择背后,我认为有跨平台的考量:iOS、HarmonyOS、Windows 等系统并没有 Android 这样的 Activity 机制,嵌入式组件能更好地适配不同平台的 UI 架构,同时保持用户操作的连贯性。
不过,该 API 目前仍处于 alpha 阶段(1.0.0-alpha01),需要 Android 14 和 SDK Extensions 15。建议在实现时做好兼容性检查和回退处理。