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 trybu stołowego i z niego 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 tego 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 naszym 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.
Przekształcenia te są wykonywane za pomocą macierzy przekształceń. Każda z tych transformacji ma własną macierz:
-
Nasz obiekt
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 z interfejsu w współrzędne bufora. W naszym przypadku chcemy, aby macierz działała w drugą stronę, przekształcając 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ę w postaci gradientu 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 ) } } } } }
Jak to działa:
- 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 parametremAnimatedVisibility, 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 promieniowy, 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 punktowy, który wyróżnia wykryte twarze. Pełny fragment kodu znajdziesz tutaj.
Ten efekt 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 gestami – 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 na 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ć interfejsu waitUntil API w Compose do oczekiwania na spełnienie określonych warunków.
Jose Alcérreca • Czas czytania: 3 minuty
-
Instrukcje
Niezależnie od tego, czy używasz Gemini w Android Studio, interfejsu wiersza poleceń Gemini, Antigravity czy agentów innych firm, takich jak Claude Code lub Codex, naszym celem jest zapewnienie możliwości tworzenia wysokiej jakości aplikacji na Androida w każdym miejscu.
Adarsh Fernando, Esteban de la Canal • Czas czytania: 4 minuty
-
Instrukcje
Zdając sobie sprawę, że szybkie zużycie baterii jest dla użytkowników Androida natychmiastowym skojarzeniem, Google podejmuje znaczące kroki, aby pomóc deweloperom w tworzeniu aplikacji bardziej energooszczędnych.
Alice Yuan • Czas czytania: 8 minut
Bądź na bieżąco
Otrzymuj co tydzień najnowsze informacje o tworzeniu aplikacji na Androida na swoją skrzynkę odbiorczą.