Ciao! Ti diamo il bentornato alla nostra serie su CameraX e Jetpack Compose. Nei post precedenti abbiamo trattato le nozioni di base per la configurazione di un'anteprima della videocamera e abbiamo aggiunto la funzionalità di messa a fuoco con tocco.
🧱 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 Compose, della grafica e delle coroutine per implementare un tocco per mettere a fuoco visivo.
🔦 Parte 3 (questo post): esplorare come sovrapporre gli elementi dell'interfaccia utente di Compose all'anteprima della fotocamera per un'esperienza utente più ricca.
📂 Parte 4: utilizzo di API adattive e del framework di animazione Compose per animare in modo fluido il passaggio alla modalità da tavolo e viceversa sugli smartphone pieghevoli.
In questo post, ci concentreremo su qualcosa di più coinvolgente dal punto di vista visivo: l'implementazione di un effetto spotlight sopra l'anteprima della videocamera, utilizzando il rilevamento del volto come base per l'effetto. Perché, ti chiederai. Non saprei. Ma è sicuramente molto bello 🙂. 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 riconoscimento facciale
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 abbiamo l'opportunità di utilizzare funzionalità della fotocamera che non sono esposte direttamente da CameraX. Dobbiamo apportare le seguenti modifiche:
- Crea un StateFlow che contenga i limiti del volto come elenco di
Rect. - Imposta l'opzione di richiesta di acquisizione
STATISTICS_FACE_DETECT_MODEsu FULL, che attiva il rilevamento dei volti. - Imposta un
CaptureCallbackper ottenere le informazioni sul volto 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 genera un elenco di oggetti Rect che rappresentano i rettangoli di selezione dei volti rilevati nelle coordinate del sensore.
Tradurre le coordinate del sensore in coordinate dell'interfaccia utente
I riquadri di selezione dei volti rilevati che abbiamo memorizzato nell'ultima sezione utilizzano le coordinate nel sistema di coordinate del sensore. Per disegnare i riquadri di selezione 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
- Trasforma le coordinate del buffer di anteprima in coordinate della UI Compose
Queste trasformazioni vengono eseguite utilizzando matrici di trasformazione. Ogni trasformazione ha la propria 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 e per ogni volto eseguiamo la trasformazione.
- Il
CoordinateTransformer.transformMatrixche otteniamo dal nostroCameraXViewfindertrasforma le coordinate dall'UI alle coordinate del buffer per impostazione predefinita. Nel nostro caso, vogliamo che la matrice funzioni al contrario, trasformando le coordinate del buffer in coordinate dell'interfaccia utente. Pertanto, utilizziamo il metodoinvert()per invertire la matrice. - Innanzitutto, trasformiamo il viso dalle coordinate del sensore alle coordinate del buffer utilizzando
sensorToBufferTransformMatrix, quindi trasformiamo le coordinate del buffer in coordinate della UI utilizzandobufferToUiTransformMatrix.
Implementare l'effetto riflettore
Ora aggiorniamo il composable CameraPreviewContent per disegnare l'effetto spotlight. Utilizzeremo un elemento componibile Canvas per disegnare una maschera della 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:
- Raccogliamo l'elenco dei volti dal modello di visualizzazione.
- Per assicurarci di non ricomporre l'intero schermo ogni volta che cambia l'elenco dei volti rilevati, utilizziamo
derivedStateOfper tenere traccia del rilevamento di eventuali volti. Questo valore può essere utilizzato conAnimatedVisibilityper animare la visualizzazione e la scomparsa della sovrapposizione colorata. 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 lo cancelliamo quando il componibile esce dall'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. Poi trasformiamo le coordinate in coordinate dell'interfaccia utente. - Iteriamo sui volti rilevati e, per ciascuno, disegniamo una sfumatura radiale che renderà trasparente l'interno del rettangolo del volto.
- Utilizziamo
BlendMode.DstOutper assicurarci di ritagliare il gradiente dal rettangolo rosa, creando l'effetto riflettore.
Nota: quando passi alla videocamera DEFAULT_FRONT_CAMERA, noterai che il riflettore è specchiato. Si tratta di un problema noto, monitorato nel Google Issue Tracker.
Risultato
Con questo codice, abbiamo un effetto spotlight 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 visive straordinarie con la videocamera. La possibilità di trasformare le coordinate del sensore e del buffer in coordinate dell'interfaccia utente Compose e viceversa ci consente di utilizzare tutte le funzionalità dell'interfaccia utente Compose e integrarle perfettamente con il sistema di 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.
Nel post finale della serie, vedremo come utilizzare le API adattive e il framework di animazione Compose per passare senza problemi da un'interfaccia utente della fotocamera all'altra sui dispositivi pieghevoli. Continuate a seguirmi!
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 il feedback. Reso possibile grazie al duro lavoro di Yasith Vidanaarachch.
Continua a leggere
-
Istruzioni
In 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
-
Istruzioni
Consapevole del fatto che il consumo eccessivo della batteria è una delle principali preoccupazioni degli utenti Android, Google ha adottato misure significative per aiutare gli sviluppatori a creare app più efficienti dal punto di vista energetico.
Alice Yuan • Lettura di 8 minuti
-
Istruzioni
Volevamo fornirti esempi di funzionalità basate sull'AI che utilizzano modelli sul dispositivo e sul cloud e ispirarti a creare esperienze piacevoli per i tuoi utenti.
Thomas Ezan, Ivy Knight • Lettura di 2 minuti
Segui gli aggiornamenti
Ricevi ogni settimana gli ultimi approfondimenti sullo sviluppo per Android direttamente nella tua casella di posta.