¡Hola! Te damos la bienvenida de nuevo a nuestra serie sobre CameraX y Jetpack Compose. En las publicaciones anteriores, hemos explicado los conceptos básicos para configurar una vista previa de la cámara y hemos añadido la función de enfoque al tocar.
🧱 Parte 1: crear una vista previa básica de la cámara con el nuevo artefacto camera-compose. Hemos tratado la gestión de permisos y la integración básica.
👆 Parte 2: usar el sistema de gestos, los gráficos y las corrutinas de Compose para implementar un toque para enfocar visual.
🔦 Parte 3 (esta publicación): descubre cómo superponer elementos de la interfaz de usuario de Compose en la vista previa de la cámara para ofrecer una experiencia de usuario más completa.
📂 Parte 4: usar APIs adaptativas y el framework de animación de Compose para animar de forma fluida el cambio al modo Mesa y viceversa en teléfonos plegables.
En esta entrada, vamos a centrarnos en algo más atractivo visualmente: implementar un efecto de foco sobre la vista previa de la cámara usando la detección de caras como base para el efecto. ¿Por qué?, te preguntarás. No lo sé. Pero tiene un aspecto genial 🙂. Y, lo que es más importante, demuestra cómo podemos traducir fácilmente las coordenadas de los sensores a coordenadas de la interfaz de usuario, lo que nos permite usarlas en Compose.
Habilitar la detección de caras
Primero, vamos a modificar CameraPreviewViewModel para habilitar la detección de caras. Usaremos la API Camera2Interop, que nos permite interactuar con la API Camera2 subyacente desde CameraX. Esto nos da la oportunidad de usar funciones de la cámara que no expone CameraX directamente. Tenemos que hacer los siguientes cambios:
- Crea un StateFlow que contenga los límites de la cara como una lista de
Rects. - Define la opción de solicitud de captura
STATISTICS_FACE_DETECT_MODEen FULL para habilitar la detección de caras. - Define un
CaptureCallbackpara obtener la información de la cara 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 objetos Rect que representan los cuadros delimitadores de las caras detectadas en las coordenadas del sensor.
Traducir las coordenadas del sensor a las coordenadas de la interfaz de usuario
Los recuadros delimitadores de las caras detectadas que hemos almacenado en la sección anterior usan coordenadas del sistema de coordenadas del sensor. Para dibujar los cuadros delimitadores en nuestra interfaz de usuario, debemos transformar estas coordenadas para que sean correctas en el sistema de coordenadas de Compose. Tenemos que hacer lo siguiente:
- Transformar las coordenadas del sensor en coordenadas del búfer de vista previa
- Transforma las coordenadas del búfer de vista previa en coordenadas de la interfaz de Compose.
Estas transformaciones se realizan mediante matrices de transformación. Cada una de las transformaciones tiene su propia matriz:
- Nuestro
SurfaceRequestconserva una instancia deTransformationInfo, que contiene una matrizsensorToBufferTranform. - Nuestro
CameraXViewfindertiene unCoordinateTransformerasociado. Puede que recuerdes que ya usamos este transformador en la entrada de blog anterior para transformar las coordenadas de tocar para enfocar.
Podemos crear un método auxiliar que haga 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 la lista de caras detectadas y, por cada cara, ejecutamos la transformación.
- El
CoordinateTransformer.transformMatrixque obtenemos de nuestroCameraXViewfindertransforma las coordenadas de la interfaz de usuario en coordenadas de búfer de forma predeterminada. En nuestro caso, queremos que la matriz funcione al revés, es decir, que transforme las coordenadas del búfer en coordenadas de la interfaz de usuario. Por lo tanto, usamos el métodoinvert()para invertir la matriz. - Primero, transformamos la cara de las coordenadas del sensor a las coordenadas del búfer mediante
sensorToBufferTransformMatrixy, a continuación, transformamos esas coordenadas del búfer a las coordenadas de la interfaz de usuario mediantebufferToUiTransformMatrix.
Implementar el efecto de foco
Ahora, vamos a actualizar el elemento componible CameraPreviewContent para dibujar el efecto de foco. Usaremos un elemento componible Canvas para dibujar una máscara de degradado sobre la vista previa y hacer que las caras detectadas 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 ) } } } } }
Funciona así:
- Recopilamos la lista de caras del modelo de vista.
- Para asegurarnos de que no recomponemos toda la pantalla cada vez que cambia la lista de caras detectadas, usamos
derivedStateOfpara registrar si se detecta alguna cara. Después, se puede usar conAnimatedVisibilitypara animar la superposición de color de entrada y salida. - El
surfaceRequestcontiene la información que necesitamos para transformar las coordenadas del sensor en coordenadas del búfer en elSurfaceRequest.TransformationInfo. Usamos la funciónproduceStatepara configurar un listener 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 cubra toda la pantalla. - Diferimos la lectura de la variable
sensorFaceRectshasta que estemos dentro del bloque de dibujoCanvas. Después, transformamos las coordenadas en coordenadas de la interfaz de usuario. - Iteramos sobre las caras detectadas y, por cada cara, dibujamos un degradado radial que hará que el interior del rectángulo de la cara sea transparente.
- Usamos
BlendMode.DstOutpara asegurarnos de que recortamos el gradiente del rectángulo rosa y creamos el efecto de foco.
Nota: Cuando cambies la cámara a DEFAULT_FRONT_CAMERA, verás que el foco está reflejado. Se trata de un problema conocido que se está monitorizando en la herramienta de seguimiento de incidencias de Google.
Resultado
Con este código, tenemos un efecto de foco totalmente funcional que destaca las caras detectadas. Puedes consultar el fragmento de código completo aquí.
Este efecto es solo el principio. Con la potencia de Compose, puedes crear una gran variedad de experiencias de cámara visualmente impresionantes. Al poder transformar las coordenadas de los sensores y los búferes en coordenadas de la interfaz de Compose y viceversa, podemos utilizar todas las funciones de la interfaz de Compose e integrarlas sin problemas con el sistema de cámara subyacente. Con animaciones, gráficos avanzados de la interfaz de usuario, una gestión sencilla del estado de la interfaz de usuario y un control total por gestos, el límite es tu imaginación.
En la última publicación de la serie, veremos cómo usar las APIs adaptativas y el framework de animación de Compose para cambiar de forma fluida entre diferentes interfaces de cámara en dispositivos plegables. ¡No te las 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 el artículo y enviarnos sus comentarios. Posibilitado por el arduo trabajo de Yasith Vidanaarachch.
Seguir leyendo
-
Instrucciones
En este artículo se explica cómo usar la API de prueba waitUntil en Compose para esperar a que se cumplan determinadas condiciones.
Jose Alcérreca • Tiempo de lectura: 3 min
-
Instrucciones
Google es consciente de que el consumo excesivo de batería es una de las principales preocupaciones de los usuarios de Android, por lo que ha tomado medidas importantes para ayudar a los desarrolladores a crear aplicaciones más eficientes.
Alice Yuan • Lectura de 8 minutos
-
Instrucciones
Queríamos mostrarte ejemplos de funciones basadas en IA que usan modelos en el dispositivo y en la nube, así como inspirarte para que crees experiencias atractivas para tus usuarios.
Thomas Ezan, Ivy Knight • Tiempo de lectura: 2 min
Mantente al día
Recibe cada semana en tu bandeja de entrada las últimas novedades sobre el desarrollo para Android.