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: używanie systemu gestów, grafiki i współprogramów Compose do implementowania wizualnego dotykowego ustawiania ostrości.
🔦 Część 3 (ten post): omawianie sposobu nakładania elementów interfejsu Compose na podgląd z kamery w celu zapewnienia użytkownikom lepszych wrażeń.
📂 Część 4: używanie adaptacyjnych interfejsów API i platformy animacji Compose do płynnego przechodzenia do/z trybu Na stole na telefonach składanych.
W tym poście zajmiemy się czymś bardziej atrakcyjnym wizualnie – implementacją efektu reflektora na podglądzie z kamery, używając do tego wykrywania twarzy. 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 CameraPreviewViewModel, aby włączyć wykrywanie twarzy. Użyjemy interfejsu Camera2Interop API, który umożliwia interakcję z podstawowym 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ę
Rects. - Ustaw opcję żądania przechwytywania
STATISTICS_FACE_DETECT_MODEna FULL, co włącza wykrywanie twarzy. - Ustaw
CaptureCallback, aby pobierać 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
Ramki ograniczające wykryte twarze, które zapisaliśmy w poprzedniej sekcji, używają współrzędnych w układzie współrzędnych czujnika. Aby narysować ramki ograniczające w interfejsie, musimy przekształcić te współrzędne tak, aby były prawidłowe w układzie współrzędnych Compose. Musimy:
- przekształcić współrzędne czujnika na współrzędne bufora podglądu,
- przekształcić współrzędne bufora podglądu na współrzędne interfejsu Compose.
Te przekształcenia są wykonywane za pomocą macierzy przekształceń. Każde przekształcenie ma własną macierz:
- Nasz
SurfaceRequestzawiera 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łcania współrzędnych dotykowego ustawiania 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 }
- Iterujemy po liście wykrytych twarzy i dla każdej z nich wykonujemy przekształcenie.
- Domyślnie
CoordinateTransformer.transformMatrix, który pobieramy zCameraXViewfinder, przekształca współrzędne z interfejsu na współrzędne bufora. W naszym przypadku chcemy, aby macierz działała w odwrotnym kierunku, przekształcając współrzędne bufora na współrzędne interfejsu. Dlatego używamy metodyinvert()do odwrócenia macierzy. - Najpierw przekształcamy twarz ze współrzędnych czujnika na współrzędne bufora za pomocą
sensorToBufferTransformMatrix, a następnie przekształcamy te współrzędne bufora na współrzędne interfejsu za pomocąbufferToUiTransformMatrix.
Implementowanie efektu reflektora
Teraz zaktualizujmy element kompozycyjny CameraPreviewContent, aby narysować efekt reflektora. Użyjemy elementu kompozycyjnego Canvas, aby narysować maskę 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 ) } } } } }
Działa to w następujący sposób:
- Zbieramy listę twarzy z modelu widoku.
- Aby nie rekomponować całego ekranu za każdym razem, gdy zmienia się lista wykrytych twarzy, używamy
derivedStateOf, aby śledzić, czy w ogóle wykryto jakieś twarze. Można go używać zAnimatedVisibility, aby animować kolorową nakładkę. surfaceRequestzawiera informacje potrzebne do przekształcenia współrzędnych czujnika na współrzędne bufora wSurfaceRequest.TransformationInfo. Używamy funkcjiproduceState, aby skonfigurować odbiornik w żądaniu powierzchni i wyczyścić go, gdy element 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, aż 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 Google Issue Tracker.
Wynik
Dzięki temu kodowi mamy w pełni funkcjonalny efekt reflektora, który wyróżnia wykryte twarze. Pełny fragment kodu znajdziesz tutaj.
Ten efekt to dopiero początek – dzięki możliwościom Compose możesz tworzyć niezliczone wizualnie oszałamiające efekty związane z aparatem. 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 podstawowym systemem aparatu. Dzięki animacjom, zaawansowanej grafice interfejsu, prostemu zarządzaniu stanem interfejsu i pełnej kontroli 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 do płynnego przechodzenia między różnymi interfejsami aparatu na urządzeniach składanych. Więcej informacji już wkrótce.
Fragmenty kodu w tym poście na 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. Umożliwiła to ciężka praca Yasitha Vidanaarachcha.
-
PoradnikiZ tego artykułu dowiesz się, jak używać interfejsu waitUntil API w Compose, aby czekać na spełnienie określonych warunków.
Jose Alcérreca • Czas czytania: 3 min -
PoradnikiWydajność 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 obserwujemy zmianę, w której pamięć urządzenia jest ważniejsza niż kiedykolwiek.
Alice Yuan, Ajesh Pai, Fung Lam • Czas czytania: 10 min -
r.r.
PoradnikiZ przyjemnością informujemy, że udostępniamy nowy zweryfikowany adres e-mail wydany 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 min
Otrzymuj co tydzień najnowsze informacje o tworzeniu aplikacji na Androida na swoją skrzynkę odbiorczą.