Cześć! Witamy ponownie w naszej serii o CameraX i Jetpack Compose. W poprzednich postach omówiliśmy podstawy konfigurowania podglądu z kamery i dodawania funkcji ustawiania ostrości przez dotknięcie.
🧱 Część 1: tworzenie podstawowego podglądu z kamery za pomocą nowego artefaktu camera-compose. Omówiliśmy obsługę uprawnień i podstawową integrację.
👆Część 2: implementowanie wizualnego dotknięcia w celu ustawienia ostrości za pomocą systemu gestów, grafiki i korutyn Compose.
🔦 Część 3 (ten post): dowiedz się, jak nakładać elementy interfejsu Compose na podgląd z kamery, aby zapewnić użytkownikom lepsze wrażenia.
📂 Część 4: używanie adaptacyjnych interfejsów API i platformy animacji Compose do płynnego przechodzenia do i z trybu stołowego na telefonach składanych.
W tym poście zajmiemy się czymś bardziej atrakcyjnym wizualnie – wdrożeniem efektu reflektora na podglądzie z kamery z wykorzystaniem wykrywania twarzy jako podstawy efektu. Dlaczego? Nie wiem. Ale wygląda świetnie 🙂. A co ważniejsze, pokazuje, jak łatwo możemy przekształcić współrzędne czujnika na współrzędne interfejsu, co pozwala nam używać ich w Compose.
Włączanie wykrywania twarzy
Najpierw zmodyfikujmy klasę CameraPreviewViewModel, aby włączyć wykrywanie twarzy. Użyjemy interfejsu Camera2Interop API, który umożliwia interakcję z interfejsem Camera2 API z poziomu CameraX. Dzięki temu możemy korzystać z funkcji aparatu, które nie są bezpośrednio udostępniane przez CameraX. Musimy wprowadzić te zmiany:
- Utwórz StateFlow, który zawiera granice twarzy jako listę obiektów
Rect. - Ustaw opcję żądania przechwytywania
STATISTICS_FACE_DETECT_MODEna FULL, co włącza wykrywanie twarzy. - Ustaw
CaptureCallback, aby uzyskać informacje o twarzy z wyniku przechwytywania.
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 { ... }
Po wprowadzeniu tych zmian nasz model widoku emituje listę obiektów Rect reprezentujących ramki ograniczające wykryte twarze we współrzędnych czujnika.
Przekształcanie współrzędnych czujnika na współrzędne interfejsu
Pola ograniczające wykrytych twarzy, które zostały zapisane w ostatniej sekcji, używają współrzędnych w układzie współrzędnych czujnika. Aby narysować ramki w interfejsie, musimy przekształcić te współrzędne, aby były prawidłowe w systemie współrzędnych Compose. Musimy:
- Przekształć współrzędne czujnika we współrzędne bufora podglądu.
- Przekształć współrzędne bufora podglądu na współrzędne interfejsu Compose.
Te przekształcenia są wykonywane za pomocą macierzy przekształceń. Każda z tych transformacji ma własną macierz:
- Nasz
SurfaceRequestprzechowuje instancjęTransformationInfo, która zawiera macierzsensorToBufferTranform. - Nasz
CameraXViewfinderma powiązanyCoordinateTransformer. Pamiętasz, że w poprzednim poście na blogu użyliśmy już tego transformatora do przekształcenia współrzędnych dotknięcia w celu ustawienia ostrości.
Możemy utworzyć metodę pomocniczą, która wykona przekształcenie:
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 }
- Przechodzimy przez listę wykrytych twarzy i w przypadku każdej z nich wykonujemy przekształcenie.
CoordinateTransformer.transformMatrix, które otrzymujemy zCameraXViewfinder, domyślnie przekształca współrzędne interfejsu w współrzędne bufora. W naszym przypadku chcemy, aby macierz działała w drugą stronę, czyli przekształcała współrzędne bufora na współrzędne interfejsu. Dlatego do odwrócenia macierzy używamy metodyinvert().- Najpierw przekształcamy twarz ze współrzędnych czujnika na współrzędne bufora za pomocą funkcji
sensorToBufferTransformMatrix, a następnie przekształcamy te współrzędne bufora na współrzędne interfejsu za pomocą funkcjibufferToUiTransformMatrix.
Wdrażanie efektu wyróżnienia
Teraz zaktualizujmy funkcję kompozycyjną CameraPreviewContent, aby narysować efekt wyróżnienia. Użyjemy komponentu Canvas, aby narysować maskę gradientową na podglądzie, dzięki czemu wykryte twarze będą widoczne:
@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 ) } } } } }
Działa to w następujący sposób:
- Listę twarzy pobieramy z modelu widoku.
- Aby nie odświeżać całego ekranu za każdym razem, gdy zmieni się lista wykrytych twarzy, używamy zmiennej
derivedStateOf, która śledzi, czy w ogóle wykryto jakieś twarze. Możesz go użyć z funkcjąAnimatedVisibility, aby animować kolorową nakładkę. surfaceRequestzawiera informacje potrzebne do przekształcenia współrzędnych czujnika na współrzędne bufora wSurfaceRequest.TransformationInfo. FunkcjiproduceStateużywamy do skonfigurowania odbiornika w żądaniu powierzchni i wyczyszczenia go, gdy komponent kompozycyjny opuści drzewo kompozycji.- Używamy
Canvas, aby narysować półprzezroczysty różowy prostokąt, który pokrywa cały ekran. - Odczytanie zmiennej
sensorFaceRectsodkładamy do momentu, gdy znajdziemy się w bloku rysowaniaCanvas. Następnie przekształcamy współrzędne na współrzędne interfejsu. - Iterujemy po wykrytych twarzach i dla każdej z nich rysujemy gradient promienisty, który sprawi, że wnętrze prostokąta twarzy będzie przezroczyste.
- Używamy
BlendMode.DstOut, aby wyciąć gradient z różowego prostokąta i utworzyć efekt reflektora.
Uwaga: gdy zmienisz kamerę na DEFAULT_FRONT_CAMERA, zauważysz, że reflektor jest odwrócony. Jest to znany problem, który jest śledzony w narzędziu Google Issue Tracker.
Wynik
Dzięki temu kodowi mamy w pełni funkcjonalny efekt wyróżnienia, który podkreśla wykryte twarze. Pełny fragment kodu znajdziesz tutaj.
To dopiero początek – dzięki Compose możesz tworzyć niezliczone, oszałamiające wizualnie funkcje aparatu. Możliwość przekształcania współrzędnych czujnika i bufora na współrzędne interfejsu Compose i z powrotem oznacza, że możemy korzystać ze wszystkich funkcji interfejsu Compose i bezproblemowo integrować je z systemem aparatu. Animacje, zaawansowana grafika interfejsu, proste zarządzanie stanem interfejsu i pełna kontrola gestów – ogranicza Cię tylko wyobraźnia.
W ostatnim poście z tej serii omówimy, jak używać adaptacyjnych interfejsów API i platformy animacji Compose, aby płynnie przełączać się między różnymi interfejsami aparatu na urządzeniach składanych. Więcej informacji już wkrótce.
Fragmenty kodu w tym blogu są objęte tą licencją:
// Copyright 2024 Google LLC. SPDX-License-Identifier: Apache-2.0
Dziękujemy Nickowi Butcherowi, Alexowi Vanyo, Trevorowi McGuire, Donowi Turnerowi i Lauren Ward za sprawdzenie i przekazanie opinii. Dzięki ciężkiej pracy Yasitha Vidanaarachchiego.
Czytaj dalej
-
Instrukcje
Z tego artykułu dowiesz się, jak używać w Compose interfejsu waitUntil API do testowania, aby poczekać na spełnienie określonych warunków.
Jose Alcérreca • Czas czytania: 3 minuty
-
Instrukcje
Wydajność aplikacji jest często utożsamiana z płynnym interfejsem i krótkim czasem uruchamiania, ale pamięć jest cichym fundamentem, na którym opierają się te widoczne wskaźniki. Nie jest tajemnicą, że pamięć urządzenia jest obecnie ważniejsza niż kiedykolwiek wcześniej.
Alice Yuan, Ajesh Pai, Fung Lam • Czas czytania: 10 min
-
Instrukcje
Z przyjemnością informujemy, że wprowadzamy nowy certyfikat zweryfikowanego adresu e-mail wydawany przez Google, który deweloperzy mogą teraz pobierać bezpośrednio z interfejsu Digital Credential API Menedżera danych logowania na Androidzie.
Niharika Arora, Jean-Pierre Pralle • Czas czytania: 3 minuty
Bądź na bieżąco
Otrzymuj co tydzień najnowsze informacje o tworzeniu aplikacji na Androida na swoją skrzynkę odbiorczą.