产品动态

嵌入式照片选择器

8 分钟阅读时间

嵌入式照片选择器:一种在应用中私下请求照片和视频的更顺畅方式

photopicker.png

做好准备,使用 Android 照片选择器的新方式来提升应用的用户体验吧!借助新的嵌入式照片选择器,用户可以直接在应用界面中选择照片和视频,体验顺畅且注重隐私保护。现在,您的应用可以获得与照片选择器相同的所有优势,包括访问云端内容,直接集成到应用体验中。

为何选择嵌入式?

我们知道,许多应用都希望在用户选择照片或视频时提供高度集成且顺畅的体验。嵌入式照片选择器正是为此而设计的,让用户无需离开应用即可快速访问最近的照片。他们还可以在自己偏好的云媒体提供商(例如 Google 相册)中浏览完整的媒体库,包括收藏夹、相册和搜索功能。这样,用户就不必在应用之间切换,也不必担心想要的照片是存储在本地还是云端。

无缝集成,增强隐私保护

借助嵌入式照片选择器,您的应用无需访问用户的照片或视频,直到用户实际选择某些内容为止。这意味着用户可以获得更好的隐私保护和更顺畅的体验。此外,嵌入式照片选择器可让用户访问其整个云端媒体库,而标准照片权限仅限于本地文件。

Google 信息中的嵌入式照片选择器

Google 信息展示了嵌入式照片选择器的强大功能。下面介绍了 Google 信息如何集成该选择器:

  • 直观的位置: 照片选择器位于相机按钮的正下方,让用户可以清楚地选择是拍摄新照片还是选择现有照片。
  • 动态预览: 用户点按照片后,系统会立即显示大尺寸预览,方便用户确认选择。如果用户取消选择照片,预览就会消失,让体验保持简洁明了。
  • 展开即可查看更多内容: 初始视图经过简化,方便用户访问最近的照片。不过,用户可以轻松展开照片选择器,浏览并选择媒体库中的所有照片和视频,包括 Google 相册中的云端内容。
  • 尊重用户选择: 嵌入式照片选择器仅授予对用户选择的特定照片或视频的访问权限,这意味着用户可以完全停止请求照片和视频权限。这样,Google 信息就不必处理用户仅授予对照片和视频的有限访问权限的情况。
gif1.gif
gif2.gif

实现

借助 Photo Picker Jetpack 库,您可以轻松集成嵌入式照片选择器。  

Jetpack Compose

首先,将 Jetpack Photo Picker 库作为依赖项添加。

implementation("androidx.photopicker:photopicker-compose:1.0.0-alpha01")

EmbeddedPhotoPicker 可组合函数提供了一种机制,可直接在 Compose 屏幕中添加嵌入式照片选择器界面。此可组合项会创建一个 SurfaceView,用于托管嵌入式照片选择器界面。它会管理与 EmbeddedPhotoPicker 服务的连接,处理用户互动,并将所选媒体 URI 传达给调用应用。  

  @Composable
fun EmbeddedPhotoPickerDemo() {
    // We keep track of the list of selected attachments
    var attachments by remember { mutableStateOf(emptyList<Uri>()) }

    val coroutineScope = rememberCoroutineScope()
    // We hide the bottom sheet by default but we show it when the user clicks on the button
    val scaffoldState = rememberBottomSheetScaffoldState(
        bottomSheetState = rememberStandardBottomSheetState(
            initialValue = SheetValue.Hidden,
            skipHiddenState = false
        )
    )

    // Customize the embedded photo picker
    val photoPickerInfo = EmbeddedPhotoPickerFeatureInfo
        .Builder()
        // Set limit the selection to 5 items
        .setMaxSelectionLimit(5)
        // Order the items selection (each item will have an index visible in the photo picker)
        .setOrderedSelection(true)
        // Set the accent color (red in this case, otherwise it follows the device's accent color)
        .setAccentColor(0xFF0000)
        .build()

    // The embedded photo picker state will be stored in this variable
    val photoPickerState = rememberEmbeddedPhotoPickerState(
        onSelectionComplete = {
            coroutineScope.launch {
                // Hide the bottom sheet once the user has clicked on the done button inside the picker
                scaffoldState.bottomSheetState.hide()
            }
        },
        onUriPermissionGranted = {
            // We update our list of attachments with the new Uris granted
            attachments += it
        },
        onUriPermissionRevoked = {
            // We update our list of attachments with the Uris revoked
            attachments -= it
        }
    )

       SideEffect {
        val isExpanded = scaffoldState.bottomSheetState.targetValue == SheetValue.Expanded

        // We show/hide the embedded photo picker to match the bottom sheet state
        photoPickerState.setCurrentExpanded(isExpanded)
    }

    BottomSheetScaffold(
        topBar = {
            TopAppBar(title = { Text("Embedded Photo Picker demo") })
        },
        scaffoldState = scaffoldState,
        sheetPeekHeight = if (scaffoldState.bottomSheetState.isVisible) 400.dp else 0.dp,
        sheetContent = {
            Column(Modifier.fillMaxWidth()) {
                // We render the embedded photo picker inside the bottom sheet
                EmbeddedPhotoPicker(
                    state = photoPickerState,
                    embeddedPhotoPickerFeatureInfo = photoPickerInfo
                )
            }
        }
    ) { innerPadding ->
        Column(Modifier.padding(innerPadding).fillMaxSize().padding(horizontal = 16.dp)) {
            Button(onClick = {
                coroutineScope.launch {
                    // We expand the bottom sheet, which will trigger the embedded picker to be shown
                    scaffoldState.bottomSheetState.partialExpand()
                }
            }) {
                Text("Open photo picker")
            }
            LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 64.dp)) {
                // We render the image using the Coil library
                itemsIndexed(attachments) { index, uri ->
                    AsyncImage(
                        model = uri,
                        contentDescription = "Image ${index + 1}",
                        contentScale = ContentScale.Crop,
                        modifier = Modifier.clickable {
                            coroutineScope.launch {
                                // When the user clicks on the media from the app's UI, we deselect it
                                // from the embedded photo picker by calling the method deselectUri
                                photoPickerState.deselectUri(uri)
                            }
                        }
                    )
                }
            }
        }
    }
}

视图

首先,将 Jetpack Photo Picker 库作为依赖项添加。

implementation("androidx.photopicker:photopicker:1.0.0-alpha01")

如需添加嵌入式照片选择器,您需要在布局文件中添加一个条目。  

  <view class="androidx.photopicker.EmbeddedPhotoPickerView"
    android:id="@+id/photopicker"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

并在 activity/fragment 中对其进行初始化。

  // We keep track of the list of selected attachments
private val _attachments = MutableStateFlow(emptyList<Uri>())
val attachments = _attachments.asStateFlow()

private lateinit var picker: EmbeddedPhotoPickerView
private var openSession: EmbeddedPhotoPickerSession? = null

val pickerListener = object EmbeddedPhotoPickerStateChangeListener {
    override fun onSessionOpened (newSession: EmbeddedPhotoPickerSession) {
        openSession = newSession
    }

    override fun onSessionError (throwable: Throwable) {}

    override fun onUriPermissionGranted(uris: List<Uri>) {
        _attachments += uris
    }

    override fun onUriPermissionRevoked (uris: List<Uri>) {
        _attachments -= uris
    }

    override fun onSelectionComplete() {
        // Hide the embedded photo picker as the user is done with the photo/video selection
    }
}

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.main_view)
    
    //
    // Add the embedded photo picker to a bottom sheet to allow the dragging to display the full photo library
    //

    picker = findViewById(R.id.photopicker)
    picker.addEmbeddedPhotoPickerStateChangeListener(pickerListener)
    picker.setEmbeddedPhotoPickerFeatureInfo(
        // Set a custom accent color
        EmbeddedPhotoPickerFeatureInfo.Builder().setAccentColor(0xFF0000).build()
    )
}

您可以调用 EmbeddedPhotoPickerSession 的不同方法来与嵌入式选择器互动。

  // Notify the embedded picker of a configuration change
openSession.notifyConfigurationChanged(newConfig)

// Update the embedded picker to expand following a user interaction
openSession.notifyPhotoPickerExpanded(/* expanded: */ true)

// Resize the embedded picker
openSession.notifyResized(/* width: */ 512, /* height: */ 256)

// Show/hide the embedded picker (after a form has been submitted)
openSession.notifyVisibilityChanged(/* visible: */ false)

// Remove unselected media from the embedded picker after they have been
// unselected from the host app's UI
openSession.requestRevokeUriPermission(removedUris)

请务必注意,嵌入式照片选择器体验适用于搭载 Android 14(API 级别 34)或更高版本且 SDK Extensions 为 15+ 的用户。详细了解照片选择器的设备可用性

为了增强用户隐私保护和安全性,系统会以防止任何绘制或叠加的方式呈现嵌入式照片选择器。这种有意为之的设计选择意味着,您的用户体验应将照片选择器的显示区域视为一个独立且专用的元素,就像您规划广告横幅一样。

如果您有任何反馈或建议,请向我们的问题跟踪器提交工单。

继续阅读