在 Jetpack Compose 中解锁 CameraX 的强大功能

在 Jetpack Compose 中解锁 CameraX 的强大功能

作者 / Google 开发者关系工程师 Jolanda Verhoef

本文是 "相机与媒体 Spotlight Week" 系列的内容之一。此系列中,我们会提供文章、视频、示例代码等资源,以帮助您提升应用中的媒体体验。

我们了解到您喜欢 CameraX 和 Jetpack Compose 库提供的强大功能,但希望使用更符合语言习惯的 Compose API 来构建相机界面。今年,我们的工程团队开发了两个新的 Compose 工件,即低层级 viewfinder-compose 和高层级 camera-compose。两者现在均已推出 alpha 版本 🚀🚀🚀。

在本系列文章中,我们不仅将为您介绍如何将 camera-compose API 集成到应用中,还将向您展示与 Compose 集成所带来的一些令人愉悦的界面体验。所有令人赞叹的 Compose 功能 (例如自适应 API 和动画支持) 均已与相机预览无缝集成。

完成所有这些操作后,我们的最终应用将如下所示:

此外,应用可以顺畅地在桌面模式之间切换:

到本文 (该系列第一篇文章) 的末尾,您将构建一个功能齐全的相机取景器,并将在后续系列文章中对其进行扩展。欢迎您跟随我们一起编写代码,在实践中更好地学习。

添加库依赖项

假设您已经在应用中设置了 Compose。如果您想继续,只需在 Android Studio 中新建一个应用即可。我们通常使用最新的 Canary 版本,因为这个版本会提供最新的 Compose 模板。

向您的 libs.versions.toml 中添加以下内容:

[versions]
..
camerax = "1.5.0-alpha03"
accompanist = "0.36.0" # or whatever matches with your Compose version

[libraries]
..
# Contains the basic camera functionality such as SurfaceRequest
androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "camerax" }
# Contains the CameraXViewfinder composable
androidx-camera-compose = { module = "androidx.camera:camera-compose", version.ref = "camerax" }
# Allows us to bind the camera preview to our UI lifecycle
androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camerax" }
# The specific camera implementation that renders the preview
androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camerax" }
# The helper library to grant the camera permission
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }

然后,将这些添加到您的模块 build.gradle.kts 依赖项块中:

dependencies {  
	..  
	implementation(libs.androidx.camera.core)
	implementation(libs.androidx.camera.compose)
	implementation(libs.androidx.camera.lifecycle)
	implementation(libs.androidx.camera.camera2)
	implementation(libs.accompanist.permissions)
}

为了授予相机权限,我们添加了所有依赖项,然后实际显示相机预览。接下来,让我们看看如何授予正确的权限。

授予相机权限

通过使用 Accompanist 权限库,我们可以轻松地授予正确的相机权限。首先,我们需要设置 AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-feature android:name="android.hardware.camera" android:required="true" />
    <uses-permission android:name="android.permission.CAMERA" />

    ..

</manifest>

现在,我们只需按照库的说明授予正确的权限:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            MyApplicationTheme {
                CameraPreviewScreen()
            }
        }
    }
}

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CameraPreviewScreen(modifier: Modifier = Modifier) {
    val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
    if (cameraPermissionState.status.isGranted) {
        CameraPreviewContent(modifier)
    } else {
        Column(
            modifier = modifier.fillMaxSize().wrapContentSize().widthIn(max = 480.dp),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            val textToShow = if (cameraPermissionState.status.shouldShowRationale) {
                // If the user has denied the permission but the rationale can be shown,
                // then gently explain why the app requires this permission
                "Whoops! Looks like we need your camera to work our magic!" +
                    "Don't worry, we just wanna see your pretty face (and maybe some cats).  " +
                    "Grant us permission and let's get this party started!"
            } else {
                // If it's the first time the user lands on this feature, or the user
                // doesn't want to be asked again for this permission, explain that the
                // permission is required
                "Hi there! We need your camera to work our magic! ✨\n" +
                    "Grant us permission and let's get this party started! \uD83C\uDF89"
            }
            Text(textToShow, textAlign = TextAlign.Center)
            Spacer(Modifier.height(16.dp))
            Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
                Text("Unleash the Camera!")
            }
        }
    }
}

@Composable
private fun CameraPreviewContent(modifier: Modifier = Modifier) {
    // TODO: Implement
}

这样,我们就得到了一个良好的界面,用户可以在显示相机预览之前授予相机权限:

创建 ViewModel

将业务逻辑与界面分开是一种很好的实践。为此,我们可以为屏幕创建视图模型来实现这一点。这个视图模型设置了 CameraX Preview 用例。请注意,CameraX 中的用例代表了可以使用该库实现的各种工作流程的配置,即预览、捕获、录制和分析。视图模型还将界面绑定到相机提供程序:

class CameraPreviewViewModel : ViewModel() {
    // Used to set up a link between the Camera and your UI.
    private val _surfaceRequest = MutableStateFlow<SurfaceRequest?>(null)
    val surfaceRequest: StateFlow<SurfaceRequest?> = _surfaceRequest

    private val cameraPreviewUseCase = Preview.Builder().build().apply {
        setSurfaceProvider { newSurfaceRequest ->
            _surfaceRequest.update { newSurfaceRequest }
        }
    }

    suspend fun bindToCamera(appContext: Context, lifecycleOwner: LifecycleOwner) {
        val processCameraProvider = ProcessCameraProvider.awaitInstance(appContext)
        processCameraProvider.bindToLifecycle(
            lifecycleOwner, DEFAULT_FRONT_CAMERA, cameraPreviewUseCase
        )

        // Cancellation signals we're done with the camera
        try { awaitCancellation() } finally { processCameraProvider.unbindAll() }
    }
}

此处会执行大量操作。代码定义了一个 CameraPreviewViewModel 类,负责管理相机预览。此类使用 CameraX Preview 构建器来配置预览与界面的绑定方式。bindToCamera 函数用于初始化相机,并将其绑定到指定的 LifecycleOwner,以确保相机至少在生命周期处于 "启动" 状态时运行,并启动预览流。

相机作为相机库的内部组件,需要渲染到界面提供的 surface。因此,库需要有一种方法来请求 surface。这正是 SurfaceRequest 的用途。因此,每当相机表示需要 surface 时,就会触发 surfaceRequest。然后将该请求转发给界面,以便将 surface 传递给请求对象。

最后,我们需要等待界面与相机完成绑定,并确保释放相机资源以避免资源泄漏。

实现相机预览界面

现在我们有了一个视图模型,可以实现 CameraPreviewContent 可组合项。该项从视图模型中读取 surface 请求,在可组合项位于组合树中时绑定到相机,并从库中调用 CameraXViewfinder

@Composable
fun CameraPreviewContent(
    viewModel: CameraPreviewViewModel,
    modifier: Modifier = Modifier,
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current
) {
    val surfaceRequest by viewModel.surfaceRequest.collectAsStateWithLifecycle()
    val context = LocalContext.current
    LaunchedEffect(lifecycleOwner) { 
        viewModel.bindToCamera(context.applicationContext, lifecycleOwner) 
    }

    surfaceRequest?.let { request ->
        CameraXViewfinder(
            surfaceRequest = request,
            modifier = modifier
        )
    }
}

如上部分所述,surfaceRequest 允许相机库在需要渲染时请求一个 surface。在这段代码中,我们收集这些 surfaceRequest 实例,并将它们转发给属于 camera-compose 组件的 CameraXViewfinder。

结果

就这样,我们构建了一个功能齐全的全屏取景器。了解完整代码片段,请访问 相关文档

本文中的代码段包含以下许可证:

// Copyright 2024 Google LLC. SPDX-License-Identifier: Apache-2.0

欢迎您持续关注我们,及时了解更多开发技术和产品更新等资讯动态!

版权声明

禁止一切形式的转载-禁止商用-禁止衍生 申请授权

脉脉不得语
脉脉不得语
Zhengzhou Website
Android Developer | https://androiddevtools.cn and https://androidweekly.io Funder | GDG Zhengzhou Funder & Ex Organizer | http://Toast.show(∞) Podcast Host

你已经成功订阅到 Android 开发技术周报
太棒了!接下来,完成检验以获得全部访问权限 Android 开发技术周报
欢迎回来!你已经成功登录了。
Unable to sign you in. Please try again.
成功!您的帐户已完全激活,您现在可以访问所有内容。
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.
🍗