¡Hola! Te damos la bienvenida nuevamente a nuestra serie sobre CameraX y Jetpack Compose. En las publicaciones anteriores, explicamos los conceptos básicos para configurar una vista previa de la cámara y agregamos la función de enfoque con un toque.
🧱 Parte 1: Cómo compilar una vista previa básica de la cámara con el nuevo artefacto camera-compose. Explicamos el control de permisos y la integración básica.
👆 Parte 2: Cómo usar el sistema de gestos, los gráficos y las corrutinas de Compose para implementar un enfoque visual con un toque.
🔦 Parte 3 (esta publicación): Cómo superponer elementos de la IU de Compose sobre la vista previa de la cámara para brindar una experiencia del usuario más enriquecida.
📂 Parte 4: Cómo usar las APIs adaptativas y el framework de animación de Compose para animar de forma fluida el cambio al modo de mesa y desde este en teléfonos plegables.
En esta publicación, analizaremos algo un poco más atractivo visualmente: la implementación de un efecto de foco sobre la vista previa de la cámara, usando la detección de rostros como base para el efecto. ¿Por qué?, te preguntarás. No lo sé. Pero se ve genial 🙂. Y, lo que es más importante, demuestra cómo podemos traducir fácilmente las coordenadas del sensor en coordenadas de la IU, lo que nos permite usarlas en Compose.
Habilita la detección de rostros
Primero, modifiquemos CameraPreviewViewModel para habilitar la detección de rostros. Usaremos la Camera2Interop API, que nos permite interactuar con la API de Camera2 subyacente desde CameraX. Esto nos da la oportunidad de usar funciones de la cámara que CameraX no expone directamente. Debemos realizar los siguientes cambios:
-
Crea un StateFlow que contenga los límites de los rostros como una lista de
Rects. -
Configura la opción de solicitud de captura
STATISTICS_FACE_DETECT_MODEen FULL, lo que habilita la detección de rostros. -
Configura un
CaptureCallbackpara obtener la información del rostro del resultado de la captura.
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 { ... }
Con estos cambios, nuestro modelo de vista ahora emite una lista de Rect objetos que representan los cuadros delimitadores de los rostros detectados en las coordenadas del sensor.
Traduce las coordenadas del sensor a las coordenadas de la IU
Los cuadros delimitadores de los rostros detectados que almacenamos en la sección anterior usan coordenadas en el sistema de coordenadas del sensor. Para dibujar los cuadros delimitadores en nuestra IU, debemos transformar estas coordenadas para que sean correctas en el sistema de coordenadas de Compose. Debemos hacer lo siguiente:
- Transformar las coordenadas del sensor en coordenadas del búfer de vista previa
- Transformar las coordenadas del búfer de vista previa en coordenadas de la IU de Compose
Estas transformaciones se realizan con matrices de transformación. Cada una de las transformaciones tiene su propia matriz:
-
Nuestro
SurfaceRequestcontiene una instancia deTransformationInfo, que contiene una matrizsensorToBufferTranform. -
Nuestro
CameraXViewfindertiene unCoordinateTransformerasociado. Quizás recuerdes que ya usamos este transformador en la entrada de blog anterior para transformar las coordenadas de enfoque con un toque.
Podemos crear un método auxiliar que pueda realizar la transformación por nosotros:
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 }
- Iteramos por la lista de rostros detectados y, para cada rostro, ejecutamos la transformación.
-
El
CoordinateTransformer.transformMatrixque obtenemos de nuestroCameraXViewfindertransforma las coordenadas de la IU en coordenadas del búfer de forma predeterminada. En nuestro caso, queremos que la matriz funcione al revés, transformando las coordenadas del búfer en coordenadas de la IU. Por lo tanto, usamos el métodoinvert()para invertir la matriz. -
Primero, transformamos el rostro de las coordenadas del sensor a las coordenadas del búfer con
sensorToBufferTransformMatrixy, luego, transformamos esas coordenadas del búfer en coordenadas de la IU conbufferToUiTransformMatrix.
Implementa el efecto de foco
Ahora, actualicemos el elemento componible CameraPreviewContent para dibujar el efecto de foco. Usaremos un elemento componible Canvas para dibujar una máscara de gradiente sobre la vista previa, lo que hará que los rostros detectados sean visibles:
@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 ) } } } } }
Aquí te mostramos cómo funciona:
- Recopilamos la lista de rostros del modelo de vista.
-
Para asegurarnos de no recomponer toda la pantalla cada vez que cambia la lista de rostros detectados, usamos
derivedStateOfpara hacer un seguimiento de si se detecta algún rostro. Luego, se puede usar conAnimatedVisibilitypara animar la superposición de color dentro y fuera. -
surfaceRequestcontiene la información que necesitamos para transformar las coordenadas del sensor en coordenadas del búfer enSurfaceRequest.TransformationInfo. Usamos la funciónproduceStatepara configurar un objeto de escucha en la solicitud de superficie y borrarlo cuando el elemento componible abandona el árbol de composición. -
Usamos un
Canvaspara dibujar un rectángulo rosa translúcido que cubre toda la pantalla. -
Diferimos la lectura de la variable
sensorFaceRectshasta que estemos dentro del bloque de dibujoCanvas. Luego, transformamos las coordenadas en coordenadas de la IU. - Iteramos sobre los rostros detectados y, para cada rostro, dibujamos un gradiente radial que hará que el interior del rectángulo del rostro sea transparente.
-
Usamos
BlendMode.DstOutpara asegurarnos de que estamos recortando el gradiente del rectángulo rosa, lo que crea el efecto de foco.
Nota: Cuando cambies la cámara a DEFAULT_FRONT_CAMERA notarás que el foco se refleja. Este es un problema conocido, que se registra en el Issue Tracker de Google.
Resultado
Con este código, tenemos un efecto de foco completamente funcional que destaca los rostros detectados. Puedes encontrar el fragmento de código completo aquí.
Este efecto es solo el comienzo. Con el poder de Compose, puedes crear una gran cantidad de experiencias de cámara visualmente impresionantes. Poder transformar las coordenadas del sensor y del búfer en coordenadas de la IU de Compose y viceversa significa que podemos utilizar todas las funciones de la IU de Compose y las integramos sin problemas con el sistema de cámara subyacente. Con animaciones, gráficos avanzados de la IU, administración simple del estado de la IU y control total de gestos, tu imaginación es el límite.
En la última publicación de la serie, analizaremos cómo usar las APIs adaptativas y el framework de animación de Compose para realizar una transición sin problemas entre diferentes IUs de cámara en dispositivos plegables. ¡No te lo pierdas!
Los fragmentos de código de este blog tienen la siguiente licencia:
// Copyright 2024 Google LLC. SPDX-License-Identifier: Apache-2.0
Muchas gracias a Nick Butcher, Alex Vanyo, Trevor McGuire, Don Turner y Lauren Ward por revisar y enviar comentarios. Esto fue posible gracias al arduo trabajo de Yasith Vidanaarachch.
Seguir leyendo
-
Cómo hacer
En este artículo, aprenderás a usar la API de prueba waitUntil en Compose para esperar a que se cumplan ciertas condiciones.
Jose Alcérreca • Lectura de 3 min
-
Cómo hacer
Ya sea que uses Gemini en Android Studio, Gemini CLI, Antigravity o agentes de terceros como Claude Code o Codex, nuestro objetivo es garantizar que el desarrollo de Android de alta calidad sea posible en todas partes.
Adarsh Fernando, Esteban de la Canal • Lectura de 4 min
-
Cómo hacer
En reconocimiento de que el agotamiento excesivo de la batería es una prioridad para los usuarios de Android, Google ha tomado medidas significativas para ayudar a los desarrolladores a compilar apps más eficientes en cuanto a la energía.
Alice Yuan • Lectura de 8 min
Mantente al día
Recibe la información más reciente sobre el desarrollo de Android en tu bandeja de entrada todas las semanas.