大家好!欢迎回到我们的 CameraX 和 Jetpack Compose 系列博文。在之前的博文中,我们介绍了设置相机预览的基础知识,并添加了点按对焦功能。
🧱 第 1 部分: 使用新的 camera-compose 工件构建基本的相机预览。我们介绍了权限处理和基本集成。
👆 第 2 部分: 使用 Compose 手势系统、图形和协程实现视觉点按对焦。
🔦 第 3 部分(本博文): 探索如何在相机预览上叠加 Compose 界面元素,以获得更丰富的用户体验。
📂 第 4 部分:使用自适应 API 和 Compose 动画框架,在可折叠手机上顺畅地实现桌面模式的动画效果。
在本博文中,我们将深入探讨一些更具视觉吸引力的内容,即在相机预览上实现聚光灯效果,并以人脸检测作为效果的基础。您可能会问,为什么要这样做?我也不确定。但它看起来确实很酷 🙂。更重要的是,它演示了如何轻松地将传感器坐标转换为界面坐标,以便在 Compose 中使用它们!
启用人脸检测
首先,我们来修改 CameraPreviewViewModel 以启用人脸检测。我们将使用 Camera2Interop API,该 API 允许我们从 CameraX 与底层 Camera2 API 进行交互。这样,我们就有机会使用 CameraX 未直接公开的相机功能。我们需要进行以下更改:
-
创建一个 StateFlow,其中包含人脸边界作为
Rects 的列表。 -
将
STATISTICS_FACE_DETECT_MODE捕获请求选项设置为 FULL,以启用人脸检测。 -
设置
CaptureCallback以从捕获结果中获取人脸信息。
class CameraPreviewViewModel : ViewModel() { ... private val _sensorFaceRects = MutableStateFlow(listOf<Rect>()) val sensorFaceRects: StateFlow<List<Rect>> = _sensorFaceRects.asStateFlow() private val cameraPreviewUseCase = Preview.Builder() .apply { Camera2Interop.Extender(this) .setCaptureRequestOption( CaptureRequest.STATISTICS_FACE_DETECT_MODE, CaptureRequest.STATISTICS_FACE_DETECT_MODE_FULL ) .setSessionCaptureCallback(object : CameraCaptureSession.CaptureCallback() { override fun onCaptureCompleted( session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult ) { super.onCaptureCompleted(session, request, result) result.get(CaptureResult.STATISTICS_FACES) ?.map { face -> face.bounds.toComposeRect() } ?.toList() ?.let { faces -> _sensorFaceRects.update { faces } } } }) } .build().apply { ... }
完成这些更改后,我们的视图模型现在会发出一个 Rect 对象列表,这些对象表示传感器坐标中检测到的人脸的边界框。
将传感器坐标转换为界面坐标
我们在上一部分中存储的检测到的人脸的边界框使用传感器坐标系 中的坐标。如需在界面中绘制边界框,我们需要转换这些坐标,使其在 Compose 坐标系中正确无误。我们需要:
- 将传感器坐标 转换为预览缓冲区坐标
- 将预览缓冲区坐标 转换为 Compose 界面坐标
这些转换是使用转换矩阵完成的。每个转换都有自己的矩阵:
-
我们的
SurfaceRequest会保留一个TransformationInfo实例,其中包含sensorToBufferTranform矩阵。 -
我们的
CameraXViewfinder具有关联的CoordinateTransformer。您可能还记得,我们在上一篇博文中已经使用此转换器来转换点按对焦坐标。
我们可以创建一个辅助方法来为我们执行转换:
private fun List<Rect>.transformToUiCoords( transformationInfo: SurfaceRequest.TransformationInfo?, uiToBufferCoordinateTransformer: MutableCoordinateTransformer ): List<Rect> = this.map { sensorRect -> val bufferToUiTransformMatrix = Matrix().apply { setFrom(uiToBufferCoordinateTransformer.transformMatrix) invert() } val sensorToBufferTransformMatrix = Matrix().apply { transformationInfo?.let { setFrom(it.sensorToBufferTransform) } } val bufferRect = sensorToBufferTransformMatrix.map(sensorRect) val uiRect = bufferToUiTransformMatrix.map(bufferRect) uiRect }
- 我们遍历检测到的人脸列表,并为每个人脸执行转换。
-
我们从
CameraXViewfinder获取的CoordinateTransformer.transformMatrix默认会将坐标从界面转换为缓冲区坐标。在本例中,我们希望矩阵以相反的方式工作,即将缓冲区坐标转换为界面坐标。因此,我们使用invert()方法反转矩阵。 -
我们首先使用
sensorToBufferTransformMatrix将人脸从传感器坐标转换为缓冲区坐标,然后使用bufferToUiTransformMatrix将这些缓冲区坐标转换为界面坐标。
实现聚光灯效果
现在,我们来更新 CameraPreviewContent 可组合项以绘制聚光灯效果。我们将使用 Canvas 可组合函数在预览上绘制渐变蒙版,使检测到的人脸可见:
@Composable fun CameraPreviewContent( viewModel: CameraPreviewViewModel, modifier: Modifier = Modifier, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current ) { val surfaceRequest by viewModel.surfaceRequest.collectAsStateWithLifecycle() val sensorFaceRects by viewModel.sensorFaceRects.collectAsStateWithLifecycle() val transformationInfo by produceState<SurfaceRequest.TransformationInfo?>(null, surfaceRequest) { try { surfaceRequest?.setTransformationInfoListener(Runnable::run) { transformationInfo -> value = transformationInfo } awaitCancellation() } finally { surfaceRequest?.clearTransformationInfoListener() } } val shouldSpotlightFaces by remember { derivedStateOf { sensorFaceRects.isNotEmpty() && transformationInfo != null} } val spotlightColor = Color(0xDDE60991) .. surfaceRequest?.let { request -> val coordinateTransformer = remember { MutableCoordinateTransformer() } CameraXViewfinder( surfaceRequest = request, coordinateTransformer = coordinateTransformer, modifier = .. ) AnimatedVisibility(shouldSpotlightFaces, enter = fadeIn(), exit = fadeOut()) { Canvas(Modifier.fillMaxSize()) { val uiFaceRects = sensorFaceRects.transformToUiCoords( transformationInfo = transformationInfo, uiToBufferCoordinateTransformer = coordinateTransformer ) // Fill the whole space with the color drawRect(spotlightColor) // Then extract each face and make it transparent uiFaceRects.forEach { faceRect -> drawRect( Brush.radialGradient( 0.4f to Color.Black, 1f to Color.Transparent, center = faceRect.center, radius = faceRect.minDimension * 2f, ), blendMode = BlendMode.DstOut ) } } } } }
其运作方式如下:
- 我们从视图模型中收集人脸列表。
-
为了确保在检测到的人脸列表发生更改时不会重新组合整个屏幕,我们使用
derivedStateOf来跟踪是否检测到任何人脸。然后,我们可以将此方法与AnimatedVisibility结合使用,以实现彩色叠加层的动画效果。 -
surfaceRequest包含在SurfaceRequest.TransformationInfo中将传感器坐标转换为缓冲区坐标所需的信息。我们使用produceState函数在 Surface 请求中设置监听器,并在可组合项离开组合树时清除此监听器。 -
我们使用
Canvas绘制一个覆盖整个屏幕的半透明粉色矩形。 -
我们会延迟读取
sensorFaceRects变量,直到进入Canvas绘制块。然后,我们将坐标转换为界面坐标。 - 我们遍历检测到的人脸,并为每个人脸绘制一个径向渐变,使人脸矩形的内部透明。
-
我们使用
BlendMode.DstOut来确保从粉色矩形中剪切渐变,从而创建聚光灯效果。
注意:当您将相机更改为 DEFAULT_FRONT_CAMERA 时,您会注意到聚光灯是镜像的!这是一个已知问题,已在 Google 问题跟踪器中进行跟踪。
结果
有了这段代码,我们就拥有了一个功能齐全的聚光灯效果,可以突出显示检测到的人脸。您可以在此处找到完整的代码段。
这种效果只是一个开始,借助 Compose 的强大功能,您可以创建无数令人惊艳的相机体验。能够将传感器和缓冲区坐标转换为 Compose 界面坐标并返回,意味着我们可以利用所有 Compose 界面功能,并将其与底层相机系统无缝集成。借助动画、高级界面图形、简单的界面状态管理和完整的手势控制,您的想象力就是极限!
在本系列的最后一篇博文中,我们将深入探讨如何使用自适应 API 和 Compose 动画框架,在可折叠设备上无缝切换不同的相机界面。敬请期待!
本博客中的代码段具有以下许可:
// Copyright 2024 Google LLC. SPDX-License-Identifier: Apache-2.0
非常感谢 Nick Butcher、Alex Vanyo、Trevor McGuire、Don Turner 和 Lauren Ward 的审核和反馈。感谢 Yasith Vidanaarachch 的辛勤工作。
继续阅读
-
操作指南
在本文中,您将了解如何在 Compose 中使用 waitUntil 测试 API 来等待满足特定条件。
Jose Alcérreca • 阅读时间:3 分钟
-
操作指南
无论您是在 Android Studio 中使用 Gemini、Gemini CLI、Antigravity 还是 Claude Code 或 Codex 等第三方智能体,我们的使命都是确保在任何地方都能进行高质量的 Android 开发。
Adarsh Fernando, Esteban de la Canal • 阅读时间:4 分钟
-
操作指南
考虑到 Android 用户最关心的是电池过度耗电问题,Google 一直在采取重大措施,帮助开发者构建能效更高的应用。
Alice Yuan • 阅读时间:8 分钟
随时了解最新动态
每周都会将最新的 Android 开发见解发送到您的收件箱 每周。