您好!欢迎回来继续学习我们的 CameraX 和 Jetpack Compose 系列教程。在之前的博文中,我们介绍了设置相机预览的基本知识,并添加了点按对焦功能。
🧱 第 1 部分:使用新的 camera-compose 制品构建基本的相机预览。我们介绍了权限处理和基本集成。
👆 第 2 部分:使用 Compose 手势系统、图形和协程来实现点按即聚焦的视觉效果。
🔦 第 3 部分(本文):探索如何将 Compose 界面元素叠加在相机预览之上,以获得更丰富的用户体验。
📂 第 4 部分:使用自适应 API 和 Compose 动画框架在可折叠手机上顺畅地切换到桌面模式和从桌面模式切换。
在这篇博文中,我们将深入探讨一些更具视觉吸引力的内容,即在相机预览上实现聚光效果,并使用人脸检测作为效果的基础。您可能会问,为什么?我不确定。但它看起来确实很酷炫 🙂。更重要的是,它展示了如何轻松将传感器坐标转换为界面坐标,从而让我们可以在 Compose 中使用它们!
启用人脸检测功能
首先,我们修改 CameraPreviewViewModel 以启用人脸检测。我们将使用 Camera2Interop API,以便从 CameraX 与底层 Camera2 API 进行交互。这样一来,我们就有机会使用 CameraX 未直接公开的相机功能。我们需要进行以下更改:
- 创建一个 StateFlow,其中包含面部边界(以
Rect列表的形式)。 - 将
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 用户非常关注电池电量消耗过快的问题,Google 一直在采取重大措施来帮助开发者构建更省电的应用。
Alice Yuan • 阅读用时:8 分钟
-
操作方法
我们希望通过提供使用设备端模型和云端模型的 AI 赋能功能示例,激发您为用户打造出色体验的灵感。
Thomas Ezan, Ivy Knight • 阅读用时:2 分钟
随时了解最新动态
每周通过电子邮件接收最新的 Android 开发洞见。