学吧!Android 全新的嵌入式照片选择器

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) }  
                    )  
                }  
            }  
        }  
    }  
}

效果还需要花时间打磨,此处只展示 EmbeddedPhotoPickerBottomSheetScaffold 中的用法。

一点想法

在我看来,EmbeddedPhotoPicker 的核心价值在于将照片选择能力"嵌入"到应用界面中,而非作为独立 Activity 存在。

这一设计选择背后,我认为有跨平台的考量:iOS、HarmonyOS、Windows 等系统并没有 Android 这样的 Activity 机制,嵌入式组件能更好地适配不同平台的 UI 架构,同时保持用户操作的连贯性。

不过,该 API 目前仍处于 alpha 阶段(1.0.0-alpha01),需要 Android 14 和 SDK Extensions 15。建议在实现时做好兼容性检查和回退处理。

相关推荐
程序猿乐锅1 小时前
【MySQL | 第二篇】: 函数、约束、多表查询和事务
android·数据库·mysql
NiceCloud喜云1 小时前
Anthropic 发布 Project Glasswing:未公开模型 Mythos 已挖出 10000+ 漏洞,含 OpenBSD 27 年老 bug
android·java·数据库·c++·python·docker·bug
曼岛_1 小时前
[安卓逆向]逆向第一个安卓程序(二)
android·安卓逆向
sN2vuQ08W2 小时前
uni-app 实现视频聊天、屏幕分享,支持Android、HarmonyOS、iOS
android·uni-app·音视频
Mem0rin2 小时前
[LLM基础] Transformer 库的使用
android·深度学习·transformer
UXbot2 小时前
轻量级原型工具如何支持Web应用的完整设计到开发链路
android·前端·人工智能·ios·交互·ui设计
帅次2 小时前
Jetpack Compose 焦点与键盘:FocusRequester、imePadding 与 BringIntoView 实战
android·android studio·android jetpack·android runtime
曼岛_4 小时前
[安卓逆向]编写第一个安卓项目(一)
android·安卓逆向
rocpp16 小时前
Android 相册选择与拍照接入实践:MediaStore 分页、权限适配与 FileProvider
android