Guides pratiques
Créer un effet de mise en avant avec CameraX et Jetpack Compose
Temps de lecture : 8 min
Bonjour ! Bienvenue dans notre série sur CameraX et Jetpack Compose. Dans les articles précédents, nous avons abordé les principes de base de la configuration d'un aperçu de la caméra et ajouté la fonctionnalité de mise au point par pression.
🧱 Partie 1 : Créer un aperçu de caméra de base à l'aide du nouvel artefact camera-compose. Nous avons abordé la gestion des autorisations et l'intégration de base.
👆 Partie 2 : Utiliser le système de gestes, les graphiques et les coroutines Compose pour implémenter une fonctionnalité de mise au point visuelle par appui.
🔦 Partie 3 (cet article) : découvrez comment superposer des éléments d'interface utilisateur Compose sur l'aperçu de votre caméra pour une expérience utilisateur plus riche.
📂 Partie 4 : Utiliser les API adaptatives et le framework d'animation Compose pour passer en douceur du mode sur table au mode plié sur les téléphones pliables.
Dans cet article, nous allons nous intéresser à quelque chose d'un peu plus visuel : l'implémentation d'un effet de mise en avant sur l'aperçu de notre caméra, en utilisant la détection des visages comme base de l'effet. Pourquoi ? Je ne sais pas. Mais il a l'air cool 🙂. Et, plus important encore, il montre comment nous pouvons facilement traduire les coordonnées du capteur en coordonnées d'UI, ce qui nous permet de les utiliser dans Compose !
Activer la détection de visages
Commençons par modifier CameraPreviewViewModel pour activer la détection des visages. Nous allons utiliser l'API Camera2Interop, qui nous permet d'interagir avec l'API Camera2 sous-jacente à partir de CameraX. Cela nous permet d'utiliser des fonctionnalités de l'appareil photo qui ne sont pas directement exposées par CameraX. Nous devons apporter les modifications suivantes :
- Créez un StateFlow contenant les limites du visage sous forme de liste de
Rect. - Définissez l'option de demande de capture
STATISTICS_FACE_DETECT_MODEsur FULL (COMPLET), ce qui active la détection des visages. - Définissez un
CaptureCallbackpour obtenir les informations sur le visage à partir du résultat de la capture.
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 { ... }
Avec ces modifications, notre modèle de vue émet désormais une liste d'objets Rect représentant les cadres de délimitation des visages détectés dans les coordonnées du capteur.
Traduire les coordonnées du capteur en coordonnées de l'UI
Les cadres de sélection des visages détectés que nous avons stockés dans la section précédente utilisent des coordonnées dans le système de coordonnées du capteur. Pour dessiner les cadres de sélection dans notre UI, nous devons transformer ces coordonnées afin qu'elles soient correctes dans le système de coordonnées Compose. Nous devons :
- Transformer les coordonnées du capteur en coordonnées du tampon d'aperçu
- Transformer les coordonnées du tampon d'aperçu en coordonnées de l'UI Compose
Ces transformations sont effectuées à l'aide de matrices de transformation. Chacune des transformations possède sa propre matrice :
- Notre
SurfaceRequestcontient une instanceTransformationInfo, qui contient une matricesensorToBufferTranform. - Notre
CameraXViewfinderest associé à unCoordinateTransformer. Vous vous souvenez peut-être que nous avons déjà utilisé ce transformateur dans l'article de blog précédent pour transformer les coordonnées de l'action "Appuyer pour faire la mise au point".
Nous pouvons créer une méthode d'assistance qui peut effectuer la transformation pour nous :
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 }
- Nous parcourons la liste des visages détectés et, pour chacun d'eux, nous exécutons la transformation.
- Le
CoordinateTransformer.transformMatrixque nous obtenons à partir de notreCameraXViewfindertransforme par défaut les coordonnées de l'UI en coordonnées du tampon. Dans notre cas, nous voulons que la matrice fonctionne dans l'autre sens, en transformant les coordonnées du tampon en coordonnées de l'UI. Par conséquent, nous utilisons la méthodeinvert()pour inverser la matrice. - Nous transformons d'abord le visage des coordonnées du capteur en coordonnées du tampon à l'aide de
sensorToBufferTransformMatrix, puis nous transformons ces coordonnées du tampon en coordonnées de l'UI à l'aide debufferToUiTransformMatrix.
Implémenter l'effet de surbrillance
Maintenant, mettons à jour le composable CameraPreviewContent pour dessiner l'effet de mise en avant. Nous allons utiliser un composable Canvas pour dessiner un masque de dégradé sur l'aperçu, ce qui rendra les visages détectés 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 ) } } } } }
Voici comment cela fonctionne :
- Nous collectons la liste des visages à partir du modèle de vue.
- Pour nous assurer de ne pas recomposer l'intégralité de l'écran chaque fois que la liste des visages détectés change, nous utilisons
derivedStateOfpour savoir si des visages sont détectés. Il peut ensuite être utilisé avecAnimatedVisibilitypour animer la superposition colorée. - Le
surfaceRequestcontient les informations dont nous avons besoin pour transformer les coordonnées du capteur en coordonnées du tampon dans leSurfaceRequest.TransformationInfo. Nous utilisons la fonctionproduceStatepour configurer un écouteur dans la requête de surface et effacer cet écouteur lorsque le composable quitte l'arborescence de composition. - Nous utilisons
Canvaspour dessiner un rectangle rose translucide qui recouvre l'intégralité de l'écran. - Nous différons la lecture de la variable
sensorFaceRectsjusqu'à ce que nous soyons à l'intérieur du bloc de dessinCanvas. Nous transformons ensuite les coordonnées en coordonnées d'interface utilisateur. - Nous parcourons les visages détectés et, pour chacun d'eux, nous dessinons un dégradé radial qui rendra l'intérieur du rectangle du visage transparent.
- Nous utilisons
BlendMode.DstOutpour nous assurer de découper le dégradé du rectangle rose, créant ainsi l'effet de projecteur.
Remarque : Lorsque vous passez à la caméra DEFAULT_FRONT_CAMERA, vous remarquerez que le projecteur est inversé. Il s'agit d'un problème connu, qui est suivi dans l' outil de suivi des problèmes Google.
Résultat
Avec ce code, nous avons un effet de projecteur entièrement fonctionnel qui met en évidence les visages détectés. Pour consulter l'extrait de code complet, cliquez ici.
Cet effet n'est qu'un début. En utilisant la puissance de Compose, vous pouvez créer une myriade d'expériences de caméra visuellement époustouflantes. La possibilité de transformer les coordonnées du capteur et du tampon en coordonnées de l'UI Compose et inversement nous permet d'utiliser toutes les fonctionnalités de l'UI Compose et de les intégrer de manière transparente au système de caméras sous-jacent. Avec les animations, les graphismes avancés de l'UI, la gestion simple de l'état de l'UI et le contrôle total des gestes, votre imagination est la seule limite !
Dans le dernier article de la série, nous verrons comment utiliser les API adaptatives et le framework d'animation Compose pour passer facilement d'une UI de caméra à une autre sur les appareils pliables. Restez attentifs !
Les extraits de code de ce blog sont soumis à la licence suivante :
// Copyright 2024 Google LLC. SPDX-License-Identifier: Apache-2.0
Merci beaucoup à Nick Butcher, Alex Vanyo, Trevor McGuire, Don Turner et Lauren Ward pour leurs commentaires et leur avis. Grâce au travail acharné de Yasith Vidanaarachch.
Lire la suite
-
Guides pratiques
Dans cet article, vous allez apprendre à utiliser l'API de test waitUntil dans Compose pour attendre que certaines conditions soient remplies.
Jose Alcérreca • Temps de lecture : 3 min
-
Guides pratiques
Google a pris des mesures importantes pour aider les développeurs à créer des applications plus économes en énergie, car il sait que la décharge excessive de la batterie est une préoccupation majeure pour les utilisateurs d'Android.
Alice Yuan • Temps de lecture : 8 min
-
Guides pratiques
Nous voulions vous fournir des exemples de fonctionnalités optimisées par l'IA à l'aide de modèles sur l'appareil et dans le cloud, et vous inciter à créer des expériences agréables pour vos utilisateurs.
Thomas Ezan, Ivy Knight • Temps de lecture : 2 min
Restez informé
Recevez chaque semaine les dernières informations sur le développement Android directement dans votre boîte de réception.