Ciao! È un piacere rivederti nella nostra serie su CameraX e Jetpack Compose. Nei post precedenti abbiamo trattato i concetti di base per la configurazione di un'anteprima della videocamera e abbiamo aggiunto la funzionalità di tocco per mettere a fuoco.
🧱 Parte 1: creazione di un'anteprima di base della videocamera utilizzando il nuovo artefatto camera-compose. Abbiamo trattato la gestione delle autorizzazioni e l'integrazione di base.
👆 Parte 2: utilizzo del sistema di gesti, della grafica e delle coroutine di Compose per implementare un tocco per mettere a fuoco visivo.
🔦 Parte 3 (questo post): esplorazione di come sovrapporre gli elementi dell'interfaccia utente di Compose all'anteprima della videocamera per un'esperienza utente più ricca.
📂 Parte 4: utilizzo delle API adattive e del framework di animazione di Compose per animare senza problemi la modalità da tavolo sui telefoni pieghevoli.
In questo post, ci occuperemo di qualcosa di un po' più coinvolgente dal punto di vista visivo: l'implementazione di un effetto di messa a fuoco sull'anteprima della videocamera, utilizzando il rilevamento dei volti come base per l'effetto. Perché, ti chiederai? Non ne sono sicuro. Ma è sicuramente fantastico 🙂. E, cosa ancora più importante, dimostra come possiamo tradurre facilmente le coordinate del sensore in coordinate dell'interfaccia utente, consentendoci di utilizzarle in Compose.
Attivare il rilevamento dei volti
Innanzitutto, modifichiamo CameraPreviewViewModel per attivare il rilevamento dei volti. Utilizzeremo l'API Camera2Interop, che ci consente di interagire con l'API Camera2 sottostante da CameraX. In questo modo possiamo utilizzare le funzionalità della videocamera che non sono esposte direttamente da CameraX. Dobbiamo apportare le seguenti modifiche:
- Crea un StateFlow che contenga i limiti dei volti come elenco di
Rect. - Imposta l'opzione della richiesta di acquisizione
STATISTICS_FACE_DETECT_MODEsu FULL, che attiva il rilevamento dei volti. - Imposta un
CaptureCallbackper ottenere le informazioni sui volti dal risultato dell'acquisizione.
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 queste modifiche, il nostro modello di visualizzazione ora emette un elenco di Rect oggetti che rappresentano i riquadri di delimitazione dei volti rilevati nelle coordinate del sensore.
Tradurre le coordinate del sensore in coordinate dell'interfaccia utente
I riquadri di delimitazione dei volti rilevati che abbiamo memorizzato nella sezione precedente utilizzano le coordinate nel sistema di coordinate del sensore. Per disegnare i riquadri di delimitazione nella nostra UI, dobbiamo trasformare queste coordinate in modo che siano corrette nel sistema di coordinate di Compose. Dobbiamo:
- Trasformare le coordinate del sensore in coordinate del buffer di anteprima.
- Trasformare le coordinate del buffer di anteprima in coordinate dell'interfaccia utente di Compose.
Queste trasformazioni vengono eseguite utilizzando le matrici di trasformazione. Ogni trasformazione ha la sua matrice:
- Il nostro
SurfaceRequestcontiene un'istanzaTransformationInfo, che contiene una matricesensorToBufferTranform. - Il nostro
CameraXViewfinderha unCoordinateTransformerassociato. Ricorderai che abbiamo già utilizzato questo trasformatore nel post del blog precedente per trasformare le coordinate del tocco per mettere a fuoco.
Possiamo creare un metodo helper che esegua la trasformazione per noi:
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 }
- Iteriamo l'elenco dei volti rilevati ed eseguiamo la trasformazione per ogni volto.
- Il
CoordinateTransformer.transformMatrixche otteniamo dal nostroCameraXViewfindertrasforma le coordinate dall'interfaccia utente alle coordinate del buffer per impostazione predefinita. Nel nostro caso, vogliamo che la matrice funzioni in modo opposto, trasformando le coordinate del buffer in coordinate dell'interfaccia utente. Pertanto, utilizziamo il metodoinvert()per invertire la matrice. - Innanzitutto, trasformiamo il volto dalle coordinate del sensore alle coordinate del buffer utilizzando
sensorToBufferTransformMatrix, quindi trasformiamo queste coordinate del buffer in coordinate dell'interfaccia utente utilizzandobufferToUiTransformMatrix.
Implementare l'effetto di messa a fuoco
Ora aggiorniamo il composable CameraPreviewContent per disegnare l'effetto di messa a fuoco. Utilizzeremo un Canvas composable per disegnare una maschera di sfumatura sull'anteprima, rendendo visibili i volti rilevati:
@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 ) } } } } }
Ecco come funziona:
- Raccogliamo l'elenco dei volti dal modello di visualizzazione.
- Per assicurarci di non ricomporre l'intero schermo ogni volta che l'elenco dei volti rilevati cambia, utilizziamo
derivedStateOfper tenere traccia se vengono rilevati volti. Questo può essere utilizzato conAnimatedVisibilityper animare la sovrapposizione colorata in entrata e in uscita. surfaceRequestcontiene le informazioni necessarie per trasformare le coordinate del sensore in coordinate del buffer inSurfaceRequest.TransformationInfo. Utilizziamo la funzioneproduceStateper configurare un listener nella richiesta di superficie e cancellare questo listener quando il composable lascia l'albero di composizione.- Utilizziamo un
Canvasper disegnare un rettangolo rosa traslucido che copre l'intero schermo. - Posticipiamo la lettura della variabile
sensorFaceRectsfino a quando non ci troviamo all'interno del blocco di disegnoCanvas. Quindi trasformiamo le coordinate in coordinate dell'interfaccia utente. - Iteriamo i volti rilevati e, per ogni volto, disegniamo una sfumatura radiale che renderà trasparente l'interno del rettangolo del volto.
- Utilizziamo
BlendMode.DstOutper assicurarci di ritagliare la sfumatura dal rettangolo rosa, creando l'effetto di messa a fuoco.
Nota: quando cambi la videocamera in DEFAULT_FRONT_CAMERA noterai che la messa a fuoco è specchiata. Si tratta di un problema noto, monitorato nel Google Issue Tracker.
Risultato
Con questo codice, abbiamo un effetto di messa a fuoco completamente funzionale che mette in evidenza i volti rilevati. Puoi trovare lo snippet di codice completo qui.
Questo effetto è solo l'inizio: utilizzando la potenza di Compose, puoi creare una miriade di esperienze con la videocamera visivamente straordinarie. La possibilità di trasformare le coordinate del sensore e del buffer in coordinate dell'interfaccia utente di Compose e viceversa ci consente di utilizzare tutte le funzionalità dell'interfaccia utente di Compose e di integrarle senza problemi con il sistema della videocamera sottostante. Con animazioni, grafica avanzata dell'interfaccia utente, gestione semplice dello stato dell'interfaccia utente e controllo completo dei gesti, l'unico limite è la tua immaginazione.
Nell'ultimo post della serie, vedremo come utilizzare le API adattive e il framework di animazione di Compose per passare senza problemi da un'interfaccia utente della videocamera all'altra sui dispositivi pieghevoli. Torna presto a trovarci.
Gli snippet di codice in questo blog hanno la seguente licenza:
// Copyright 2024 Google LLC. SPDX-License-Identifier: Apache-2.0
Un ringraziamento speciale a Nick Butcher, Alex Vanyo, Trevor McGuire, Don Turner e Lauren Ward per la revisione e i feedback. Reso possibile grazie al duro lavoro di Yasith Vidanaarachch.
-
Guide praticheIn questo articolo imparerai a utilizzare l'API di test waitUntil in Compose per attendere che vengano soddisfatte determinate condizioni.
Jose Alcérreca • Lettura di 3 minuti -
Guide praticheSebbene le prestazioni delle app siano spesso associate a un'interfaccia utente fluida e a tempi di avvio rapidi, la memoria è la base silenziosa su cui si basano queste metriche visibili. Non è un segreto che stiamo assistendo a un cambiamento in cui la memoria del dispositivo è più importante che mai.
Alice Yuan, Ajesh Pai, Fung Lam • Lettura di 10 minuti -
Guide praticheOggi siamo felici di annunciare una nuova credenziale email verificata emessa da Google, che gli sviluppatori possono ora recuperare direttamente dall'API Digital Credential del Gestore delle credenziali di Android.
Niharika Arora, Jean-Pierre Pralle • Lettura di 3 minuti
Ricevi ogni settimana gli ultimi approfondimenti sullo sviluppo di Android direttamente nella tua casella di posta.