Przeprowadź migrację z Aparatu 1 do aplikacji CameraX

Jeśli Twoja aplikacja używa oryginalnej klasy Camera („Camera1”), która została wycofana od czasu Androida 5.0 (poziom interfejsu API 21), zdecydowanie zalecamy zaktualizowanie go do nowoczesnego interfejsu API aparatu w Androidzie. Android oferuje CameraX (standardowy, niezawodny interfejs API Jetpack) i Camera2 (niski poziom interfejsu API platformy). W większości przypadków zalecamy przeniesienie aplikacji do CameraX. Oto dlaczego tak się dzieje:

  • Łatwość obsługi: aplikacja CameraX zajmuje się detalami niskopoziomowymi, więc nie musisz skupiać się na tworzeniu od podstaw obsługi kamery, a więcej na wyróżnianiu aplikacji.
  • Aplikacja CameraX obsługuje fragmentację za Ciebie: aplikacja CameraX obniża długoterminowe koszty utrzymania i zmniejsza kod związany z konkretnym urządzeniem, zapewniając użytkownikom lepsze wrażenia. Więcej informacji na ten temat znajdziesz w poście na blogu na temat lepszej zgodności urządzeń z aparatem CameraX.
  • Zaawansowane funkcje: Aparat CameraX został zaprojektowany tak, aby można było w łatwy sposób wdrożyć zaawansowane funkcje w aplikacji. Dzięki rozszerzeniom CameraX możesz łatwo stosować np. Bokeh, retusz twarzy, HDR (High Dynamic Range) oraz naświetlać zdjęcia przy słabym oświetleniu.
  • Możliwość aktualizacji: Android w ciągu roku udostępnia nowe funkcje i poprawki błędów w aplikacji CameraX. Dzięki migracji do CameraX Twoja aplikacja będzie korzystać z najnowszej technologii aparatów na Androidzie w każdej wersji CameraX, a nie tylko w kolejnych rocznych wersjach Androida.

W tym przewodniku zamieściliśmy typowe scenariusze korzystania z aparatu. Każdy scenariusz obejmuje implementację aparatu Camera1 i implementację CameraX do porównania.

W przypadku migracji czasami potrzebujesz dodatkowej elastyczności, aby zintegrować ją z istniejącą bazą kodu. Cały kod CameraX w tym przewodniku zawiera implementację CameraController – to świetne rozwiązanie, jeśli potrzebujesz najprostszego sposobu korzystania z aplikacji CameraX i implementacji CameraProvider. Aby zdecydować, która opcja jest dla Ciebie najlepsza, oto zalety każdej z nich:

Kontroler aparatu

Dostawca aparatu

Wymaga niewielkiego kodu konfiguracji Większa kontrola
Jeśli pozwolisz aplikacji CameraX na przeprowadzanie większej części procesu konfiguracji, funkcje takie jak ustawianie ostrości przez dotknięcie i rozsuwanie palcem, by powiększyć obraz, działają automatycznie Konfiguracją zajmuje się to programista aplikacji, dlatego masz więcej możliwości dostosowania konfiguracji, na przykład włączenie rotacji obrazu wyjściowego lub ustawienie formatu obrazu wyjściowego w ImageAnalysis
Wymaganie atrybutu PreviewView na potrzeby podglądu aparatu pozwala CameraX zapewniać płynną integrację, na przykład w ramach integracji z pakietem ML Kit, który może mapować współrzędne wyników modelu ML (takie jak ramki ograniczające twarz) bezpośrednio na współrzędne podglądu Możliwość użycia niestandardowej „powierzchni” do wyświetlania podglądu aparatu zapewnia większą elastyczność, na przykład przez wykorzystanie istniejącego kodu „Surface”, który może być danymi wejściowymi do innych części aplikacji.

Jeśli masz problem z migracją, skontaktuj się z nami na grupie dyskusyjnej CameraX.

Przed migracją

Porównanie wykorzystania aplikacji CameraX i Aparat 1

Kod może wyglądać inaczej, ale podstawowe pojęcia w Aparacie 1 i Aparacie X są bardzo podobne. CameraX uwzględnia w swoich zastosowaniach typowe funkcje kamery, dzięki czemu wiele zadań pozostawionych deweloperowi w Aparacie 1 jest realizowanych automatycznie. W aplikacji CameraX są dostępne 4 obiekty UseCase, których można używać do różnych zadań związanych z kamerą: Preview, ImageCapture, VideoCapture i ImageAnalysis.

Jednym z przykładów dla programistów obsługujących niskopoziomowe informacje o Aparacie X jest ViewPort, który jest udostępniany przez aktywne UseCase. Dzięki temu wszystkie elementy UseCase widzą dokładnie te same piksele. W przypadku Aparatu 1 musisz samodzielnie zarządzać tymi szczegółami, a ze względu na zmienność współczynników proporcji w przypadku czujników i ekranów w aparatach urządzeń uzyskanie dopasowania podglądu do zdjęć i filmów może być trudne.

W innym przykładzie CameraX automatycznie obsługuje wywołania zwrotne Lifecycle w przekazywanej instancji Lifecycle. Oznacza to, że CameraX zarządza połączeniem aplikacji z kamerą przez cały cykl życia aktywności na Androidzie. Obejmuje to między innymi: zamknięcie aparatu, gdy aplikacja działa w tle, usunięcie podglądu aparatu, gdy ekran nie wymaga już jego wyświetlania, oraz wstrzymywanie podglądu, gdy inna aktywność ma pierwszeństwo, np. inna rozmowa wideo.

Aparat CameraX obsługuje obroty i skalowanie bez żadnego dodatkowego kodu. W przypadku obiektu Activity z odblokowaną orientacją konfiguracja UseCase jest wykonywana przy każdym obróceniu urządzenia, ponieważ system niszczy i odtwarza Activity po zmianie orientacji. Powoduje to, że ustawienie UseCases powoduje, że rotacja docelowa jest za każdym razem dopasowywana do domyślnej orientacji wyświetlacza. Więcej informacji o obrotach w Aparacie X

Zanim przejdziemy do szczegółów, przyjrzyjmy się bliżej UseCasesprawcom aplikacji CameraX i tym, jak może ona wyglądać w przypadku aplikacji Camera1. (Pojęcia związane z Aparatem X są zapisane w kolorze niebieskim, a koncepcje Camera1 są zielone).

Aparat X

Konfiguracja kontrolera CameraController / CameraProvider
Podgląd Robienie zdjęć Nagrywanie filmów Analiza obrazu
Zarządzaj powierzchnią podglądu i ustawiaj ją w Aparacie Ustaw funkcję PictureCallback i wywołaj metodę TakePicture() w aparacie Zarządzanie konfiguracją aparatu i usługi MediaRecorder w określonej kolejności Niestandardowy kod analizy utworzony na platformie Surface
Kod urządzenia
Zarządzanie rotacją i skalowaniem urządzeń
Zarządzanie sesjami kamery (wybór kamery, zarządzanie cyklem życia)

Aparat 1

Zgodność i wydajność w aplikacji CameraX

CameraX obsługuje urządzenia z Androidem 5.0 (poziom interfejsu API 21) lub nowszym. Dotyczy to ponad 98% obecnych urządzeń z Androidem. CameraX została opracowana, aby automatycznie obsługiwać różnice między urządzeniami, co zmniejsza konieczność umieszczania w aplikacji kodu. Dodatkowo od wersji 5.0 w Laboratorium CameraX testujemy ponad 150 urządzeń fizycznych, na wszystkie wersje Androida. Możesz przejrzeć pełną listę urządzeń objętych obecnie Laboratorium.

CameraX używa Executor do sterowania stosem kamer. Możesz ustawić własnego wykonawcy w Aparacie X, jeśli aplikacja ma określone wymagania dotyczące wątków. Jeśli zasada jest nieskonfigurowana, CameraX tworzy i używa zoptymalizowanego wewnętrznego Executor. Wiele interfejsów API platformy, na których stworzono CameraX, wymaga blokowania komunikacji międzyprocesowej (IPC) za pomocą sprzętu, którego działanie może czasem trwać setki milisekund. Z tego powodu CameraX wywołuje te interfejsy API tylko z wątków w tle, dzięki czemu wątek główny nie jest blokowany, a interfejs pozostaje płynny. Więcej informacji o wątkach

Jeśli rynek docelowy Twojej aplikacji obejmuje słabsze urządzenia, CameraX zapewnia skrócenie czasu konfiguracji dzięki ograniczaniu kamery. Ponieważ proces łączenia z komponentami sprzętowymi może trwać dość długo, zwłaszcza na słabszych urządzeniach, możesz określić zestaw kamer, których potrzebuje Twoja aplikacja. CameraX łączy się z tymi kamerami tylko podczas ich konfiguracji. Jeśli na przykład aplikacja używa tylko tylnych aparatów, można ustawić tę konfigurację za pomocą ustawienia DEFAULT_BACK_CAMERA. Wtedy CameraX nie inicjuje przednich aparatów, by zmniejszyć opóźnienie.

Pojęcia związane z programowaniem Androida

W tym przewodniku przyjęto ogólną wiedzę o programowaniu aplikacji na Androida. Oprócz podstawowych informacji oto kilka koncepcji, które warto poznać przed zagłębieniem się w kod:

  • Funkcja View Binding generuje klasę powiązania dla plików układu XML, dzięki czemu możesz łatwo odwoływać się do widoków w działaniach, jak w kilku fragmentach kodu poniżej. Istnieją pewne różnice między powiązaniem widoku a elementem findViewById() (wcześniejszym sposobem odwoływania się do widoków), ale w poniższym kodzie powinno być możliwe zastąpienie wierszy powiązania widoku podobnym wywołaniem findViewById().
  • Korutyny asynchroniczne to wzorzec projektu równoczesności dodany w Kotlin 1.3, który może być używany do obsługi metod CameraX zwracających wartość ListenableFuture. Teraz jest to łatwiejsze dzięki bibliotece Concurrent Jetpack od wersji 1.1.0. Aby dodać konfigurację asynchroniczną do aplikacji:
    1. Dodaj implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0") do pliku Gradle.
    2. Umieść kod CameraX, który zwraca wartość ListenableFuture, w bloku launch lub funkcji zawieszenia.
    3. Dodaj do wywołania funkcji await() wywołanie funkcji, które zwraca wartość ListenableFuture.
    4. Dokładniejsze informacje o tym, jak działają współprogramy, znajdziesz w przewodniku Uruchamianie współprogramowania.

Migracja typowych scenariuszy

W tej sekcji wyjaśniamy, jak przenieść typowe scenariusze z Aparatu 1 do CameraX. Każdy scenariusz obejmuje: Aparat1, Aparat CameraProvider i Aparat CameraController.

Wybieram kamerę

Jedną z pierwszych rzeczy, jakie warto zaoferować w aplikacji aparatu, jest możliwość wyboru różnych kamer.

Aparat 1

W aplikacji Aparat 1 możesz wywołać metodę Camera.open() bez parametrów, aby otworzyć pierwszy aparat tylny, lub przekazać w postaci liczby całkowitej identyfikator kamery, którą chcesz uruchomić. Oto przykład, jak to może wyglądać:

// Camera1: select a camera from id.

// Note: opening the camera is a non-trivial task, and it shouldn't be
// called from the main thread, unlike CameraX calls, which can be
// on the main thread since CameraX kicks off background threads
// internally as needed.

private fun safeCameraOpen(id: Int): Boolean {
    return try {
        releaseCameraAndPreview()
        camera = Camera.open(id)
        true
    } catch (e: Exception) {
        Log.e(TAG, "failed to open camera", e)
        false
    }
}

private fun releaseCameraAndPreview() {
    preview?.setCamera(null)
    camera?.release()
    camera = null
}

CameraX: kontroler kamery

W aplikacji CameraX wybór kamery jest obsługiwany przez klasę CameraSelector. CameraX to prosty przypadek użycia domyślnej kamery. Możesz wybrać domyślny przedni lub tylny aparat. Dodatkowo obiekt CameraControl w aplikacji CameraX umożliwia łatwe ustawienie poziomu powiększenia w aplikacji. Jeśli aplikacja działa na urządzeniu, które obsługuje aparaty logiczne, przełączy się ona na odpowiedni obiektyw.

Oto kod aparatu CameraX, który pozwala użyć domyślnego tylnego aparatu na urządzeniu CameraController:

// CameraX: select a camera with CameraController

var cameraController = LifecycleCameraController(baseContext)
val selector = CameraSelector.Builder()
    .requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
cameraController.cameraSelector = selector

CameraX: CameraProvider

Oto przykład wyboru domyślnego przedniego aparatu w CameraProvider (przedni lub tylny aparat może być używany z funkcjami CameraController lub CameraProvider):

// CameraX: select a camera with CameraProvider.

// Use await() within a suspend function to get CameraProvider instance.
// For more details on await(), see the "Android development concepts"
// section above.
private suspend fun startCamera() {
    val cameraProvider = ProcessCameraProvider.getInstance(this).await()

    // Set up UseCases (more on UseCases in later scenarios)
    var useCases:Array = ...

    // Set the cameraSelector to use the default front-facing (selfie)
    // camera.
    val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA

    try {
        // Unbind UseCases before rebinding.
        cameraProvider.unbindAll()

        // Bind UseCases to camera. This function returns a camera
        // object which can be used to perform operations like zoom,
        // flash, and focus.
        var camera = cameraProvider.bindToLifecycle(
            this, cameraSelector, useCases)

    } catch(exc: Exception) {
        Log.e(TAG, "UseCase binding failed", exc)
    }
})

...

// Call startCamera in the setup flow of your app, such as in onViewCreated.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    ...

    lifecycleScope.launch {
        startCamera()
    }
}

Jeśli chcesz kontrolować, która kamera jest wybrana, możesz to zrobić również w AparacieX, jeśli używasz CameraProvider, wywołując getAvailableCameraInfos(). Otrzymasz obiekt CameraInfo do sprawdzenia określonych właściwości kamery, np. isFocusMeteringSupported(). Następnie możesz przekonwertować go na CameraSelector, aby używać go jak w powyższych przykładach za pomocą metody CameraInfo.getCameraSelector().

Więcej informacji o poszczególnych kamerach możesz uzyskać dzięki klasie Camera2CameraInfo. Wywołaj getCameraCharacteristic() i użyj klucza odpowiadającego danym kamery, których potrzebujesz. W klasie CameraCharacteristics znajdziesz listę wszystkich kluczy, o które możesz wysyłać zapytania.

Oto przykład użycia niestandardowej funkcji checkFocalLength(), którą można zdefiniować samodzielnie:

// CameraX: get a cameraSelector for first camera that matches the criteria
// defined in checkFocalLength().

val cameraInfo = cameraProvider.getAvailableCameraInfos()
    .first { cameraInfo ->
        val focalLengths = Camera2CameraInfo.from(cameraInfo)
            .getCameraCharacteristic(
                CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS
            )
        return checkFocalLength(focalLengths)
    }
val cameraSelector = cameraInfo.getCameraSelector()

Wyświetlam podgląd

W większości aplikacji do obsługi kamery musi być jakiś moment, żeby obraz z kamery pojawił się na ekranie. Korzystając z Aparatu1, musisz prawidłowo zarządzać wywołaniami zwrotnymi cyklu życia, a także określić rotację i skalowanie na potrzeby podglądu.

Dodatkowo w Aparacie 1 musisz określić, czy do wyświetlania podglądu chcesz użyć TextureView czy SurfaceView. Obie opcje mają swoje kompromisy. W obu przypadkach usługa Camera1 wymaga prawidłowej obsługi obrotu i skalowania. Z kolei funkcja PreviewView firmy CameraX ma implementacje bazowe dla komponentów TextureView i SurfaceView. CameraX decyduje, która implementacja jest najlepsza na podstawie takich czynników jak typ urządzenia i wersja Androida, na której działa aplikacja. Jeśli 1 z nich jest zgodna, możesz zadeklarować swoje preferencje w PreviewView.ImplementationMode. Opcja COMPATIBLE używa TextureView do wyświetlania podglądu, a wartość PERFORMANCE używa właściwości SurfaceView (w miarę możliwości).

Aparat 1

Aby wyświetlić podgląd, musisz napisać własną klasę Preview z implementacją interfejsu android.view.SurfaceHolder.Callback, który umożliwia przekazywanie do aplikacji danych obrazu ze sprzętu aparatu. Zanim będzie można rozpocząć podgląd obrazu na żywo, do obiektu Camera należy przekazać klasę Preview.

// Camera1: set up a camera preview.

class Preview(
        context: Context,
        private val camera: Camera
) : SurfaceView(context), SurfaceHolder.Callback {

    private val holder: SurfaceHolder = holder.apply {
        addCallback(this@Preview)
        setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS)
    }

    override fun surfaceCreated(holder: SurfaceHolder) {
        // The Surface has been created, now tell the camera
        // where to draw the preview.
        camera.apply {
            try {
                setPreviewDisplay(holder)
                startPreview()
            } catch (e: IOException) {
                Log.d(TAG, "error setting camera preview", e)
            }
        }
    }

    override fun surfaceDestroyed(holder: SurfaceHolder) {
        // Take care of releasing the Camera preview in your activity.
    }

    override fun surfaceChanged(holder: SurfaceHolder, format: Int,
                                w: Int, h: Int) {
        // If your preview can change or rotate, take care of those
        // events here. Make sure to stop the preview before resizing
        // or reformatting it.
        if (holder.surface == null) {
            return  // The preview surface does not exist.
        }

        // Stop preview before making changes.
        try {
            camera.stopPreview()
        } catch (e: Exception) {
            // Tried to stop a non-existent preview; nothing to do.
        }

        // Set preview size and make any resize, rotate or
        // reformatting changes here.

        // Start preview with new settings.
        camera.apply {
            try {
                setPreviewDisplay(holder)
                startPreview()
            } catch (e: Exception) {
                Log.d(TAG, "error starting camera preview", e)
            }
        }
    }
}

class CameraActivity : AppCompatActivity() {
    private lateinit var viewBinding: ActivityMainBinding
    private var camera: Camera? = null
    private var preview: Preview? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)

        // Create an instance of Camera.
        camera = getCameraInstance()

        preview = camera?.let {
            // Create the Preview view.
            Preview(this, it)
        }

        // Set the Preview view as the content of the activity.
        val cameraPreview: FrameLayout = viewBinding.cameraPreview
        cameraPreview.addView(preview)
    }
}

CameraX: kontroler kamery

W aplikacji CameraX nie trzeba zajmować się niczym deweloperem. Jeśli używasz właściwości CameraController, musisz też użyć właściwości PreviewView. Oznacza to, że właściwość Preview UseCase jest domniemana, przez co konfiguracja jest znacznie mniej pracochłonna:

// CameraX: set up a camera preview with a CameraController.

class MainActivity : AppCompatActivity() {
    private lateinit var viewBinding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)

        // Create the CameraController and set it on the previewView.
        var cameraController = LifecycleCameraController(baseContext)
        cameraController.bindToLifecycle(this)
        val previewView: PreviewView = viewBinding.cameraPreview
        previewView.controller = cameraController
    }
}

CameraX: CameraProvider

W przypadku CameraProvider w Aparacie X nie musisz używać PreviewView, ale i tak znacznie upraszcza to konfigurowanie podglądu w porównaniu z Aparatem 1. Na potrzeby prezentacji używany jest w tym przykładzie PreviewView, ale jeśli masz bardziej złożone potrzeby, możesz napisać niestandardową regułę SurfaceProvider, która będzie przekazywana do funkcji setSurfaceProvider().

W tym przypadku Preview UseCase nie jest domniemany, tak jak w przypadku CameraController, więc musisz go skonfigurować:

// CameraX: set up a camera preview with a CameraProvider.

// Use await() within a suspend function to get CameraProvider instance.
// For more details on await(), see the "Android development concepts"
// section above.
private suspend fun startCamera() {
    val cameraProvider = ProcessCameraProvider.getInstance(this).await()

    // Create Preview UseCase.
    val preview = Preview.Builder()
        .build()
        .also {
            it.setSurfaceProvider(
                viewBinding.viewFinder.surfaceProvider
            )
        }

    // Select default back camera.
    val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

    try {
        // Unbind UseCases before rebinding.
        cameraProvider.unbindAll()

        // Bind UseCases to camera. This function returns a camera
        // object which can be used to perform operations like zoom,
        // flash, and focus.
        var camera = cameraProvider.bindToLifecycle(
            this, cameraSelector, useCases)

    } catch(exc: Exception) {
        Log.e(TAG, "UseCase binding failed", exc)
    }
})

...

// Call startCamera() in the setup flow of your app, such as in onViewCreated.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    ...

    lifecycleScope.launch {
        startCamera()
    }
}

Ustawianie ostrości przez dotknięcie

Gdy podgląd z kamery jest widoczny na ekranie, często ustawiany jest punkt ostrości, gdy użytkownik kliknie podgląd.

Aparat 1

Aby wdrożyć ostrość dotykiem w Aparacie 1, musisz obliczyć optymalną ostrość (Area), aby wskazać, gdzie Camera ma starać się ustawić ostrość. Ta kolumna Area jest przekazywana do setFocusAreas(). Musisz też ustawić zgodny tryb pełnej koncentracji na urządzeniu Camera. Obszar ostrości działa tylko wtedy, gdy bieżący tryb ostrości to FOCUS_MODE_AUTO, FOCUS_MODE_MACRO, FOCUS_MODE_CONTINUOUS_VIDEO lub FOCUS_MODE_CONTINUOUS_PICTURE.

Każdy element Area jest prostokątem o określonej wadze. Waga ma wartość z zakresu od 1 do 1000 i jest używana do określania priorytetów elementu Areas, jeśli ustawiono wiele. W tym przykładzie użyto tylko jednej właściwości Area, więc wartość wagi nie ma znaczenia. Współrzędne prostokąta mają zakres od -1000 do 1000. Punkt w lewym górnym rogu to (-1000, -1000). Prawy dolny punkt to (1000, 1000). Kierunek zależy od orientacji czujnika, czyli tego, co widzi. Obrót ani odbicie lustrzane elementu Camera.setDisplayOrientation() nie mają wpływu na kierunek, więc musisz przekonwertować współrzędne zdarzenia dotknięcia na współrzędne czujnika.

// Camera1: implement tap-to-focus.

class TapToFocusHandler : Camera.AutoFocusCallback {
    private fun handleFocus(event: MotionEvent) {
        val camera = camera ?: return
        val parameters = try {
            camera.getParameters()
        } catch (e: RuntimeException) {
            return
        }

        // Cancel previous auto-focus function, if one was in progress.
        camera.cancelAutoFocus()

        // Create focus Area.
        val rect = calculateFocusAreaCoordinates(event.x, event.y)
        val weight = 1  // This value's not important since there's only 1 Area.
        val focusArea = Camera.Area(rect, weight)

        // Set the focus parameters.
        parameters.setFocusMode(Parameters.FOCUS_MODE_AUTO)
        parameters.setFocusAreas(listOf(focusArea))

        // Set the parameters back on the camera and initiate auto-focus.
        camera.setParameters(parameters)
        camera.autoFocus(this)
    }

    private fun calculateFocusAreaCoordinates(x: Int, y: Int) {
        // Define the size of the Area to be returned. This value
        // should be optimized for your app.
        val focusAreaSize = 100

        // You must define functions to rotate and scale the x and y values to
        // be values between 0 and 1, where (0, 0) is the upper left-hand side
        // of the preview, and (1, 1) is the lower right-hand side.
        val normalizedX = (rotateAndScaleX(x) - 0.5) * 2000
        val normalizedY = (rotateAndScaleY(y) - 0.5) * 2000

        // Calculate the values for left, top, right, and bottom of the Rect to
        // be returned. If the Rect would extend beyond the allowed values of
        // (-1000, -1000, 1000, 1000), then crop the values to fit inside of
        // that boundary.
        val left = max(normalizedX - (focusAreaSize / 2), -1000)
        val top = max(normalizedY - (focusAreaSize / 2), -1000)
        val right = min(left + focusAreaSize, 1000)
        val bottom = min(top + focusAreaSize, 1000)

        return Rect(left, top, left + focusAreaSize, top + focusAreaSize)
    }

    override fun onAutoFocus(focused: Boolean, camera: Camera) {
        if (!focused) {
            Log.d(TAG, "tap-to-focus failed")
        }
    }
}

CameraX: kontroler kamery

CameraController nasłuchuje zdarzeń dotyku, które ma PreviewView, aby automatycznie ustawić ostrość za pomocą dotknięcia. Klikanie, aby ustawić fokus, możesz włączać i wyłączać za pomocą funkcji setTapToFocusEnabled() oraz sprawdzać wartość przy użyciu odpowiedniej metody pobierania isTapToFocusEnabled().

Metoda getTapToFocusState() zwraca obiekt LiveData do śledzenia zmian stanu zaznaczenia na CameraController.

// CameraX: track the state of tap-to-focus over the Lifecycle of a PreviewView,
// with handlers you can define for focused, not focused, and failed states.

val tapToFocusStateObserver = Observer { state ->
    when (state) {
        CameraController.TAP_TO_FOCUS_NOT_STARTED ->
            Log.d(TAG, "tap-to-focus init")
        CameraController.TAP_TO_FOCUS_STARTED ->
            Log.d(TAG, "tap-to-focus started")
        CameraController.TAP_TO_FOCUS_FOCUSED ->
            Log.d(TAG, "tap-to-focus finished (focus successful)")
        CameraController.TAP_TO_FOCUS_NOT_FOCUSED ->
            Log.d(TAG, "tap-to-focus finished (focused unsuccessful)")
        CameraController.TAP_TO_FOCUS_FAILED ->
            Log.d(TAG, "tap-to-focus failed")
    }
}

cameraController.getTapToFocusState().observe(this, tapToFocusStateObserver)

CameraX: CameraProvider

Korzystanie z CameraProvider wymaga pewnej konfiguracji, aby ułatwić sobie pracę z ekranem dotykowym. W tym przykładzie założono, że używasz PreviewView. Jeśli nie, musisz dostosować tę zasadę, aby zastosować ją do niestandardowego elementu Surface.

Gdy używasz PreviewView:

  1. Skonfiguruj wykrywanie gestów do obsługi zdarzeń kliknięcia.
  2. Za pomocą zdarzenia kliknięcia utwórz MeteringPoint za pomocą MeteringPointFactory.createPoint().
  3. Za pomocą MeteringPoint utwórz FocusMeteringAction.
  4. Z obiektem CameraControl na urządzeniu Camera (zwróconym z bindToLifecycle()) wywołaj startFocusAndMetering(), przekazując FocusMeteringAction.
  5. (Opcjonalnie) Odpowiedz na FocusMeteringResult.
  6. Skonfiguruj czujnik gestów tak, aby reagował na zdarzenia dotyku w komponencie PreviewView.setOnTouchListener().
// CameraX: implement tap-to-focus with CameraProvider.

// Define a gesture detector to respond to tap events and call
// startFocusAndMetering on CameraControl. If you want to use a
// coroutine with await() to check the result of focusing, see the
// "Android development concepts" section above.
val gestureDetector = GestureDetectorCompat(context,
    object : SimpleOnGestureListener() {
        override fun onSingleTapUp(e: MotionEvent): Boolean {
            val previewView = previewView ?: return
            val camera = camera ?: return
            val meteringPointFactory = previewView.meteringPointFactory
            val focusPoint = meteringPointFactory.createPoint(e.x, e.y)
            val meteringAction = FocusMeteringAction
                .Builder(meteringPoint).build()
            lifecycleScope.launch {
                val focusResult = camera.cameraControl
                    .startFocusAndMetering(meteringAction).await()
                if (!result.isFocusSuccessful()) {
                    Log.d(TAG, "tap-to-focus failed")
                }
            }
        }
    }
)

...

// Set the gestureDetector in a touch listener on the PreviewView.
previewView.setOnTouchListener { _, event ->
    // See pinch-to-zooom scenario for scaleGestureDetector definition.
    var didConsume = scaleGestureDetector.onTouchEvent(event)
    if (!scaleGestureDetector.isInProgress) {
        didConsume = gestureDetector.onTouchEvent(event)
    }
    didConsume
}

Ściągnij palce, aby powiększyć

Powiększanie i pomniejszanie podglądu to inna częsta metoda bezpośredniego manipulacji podglądem z aparatu. Ze względu na rosnącą liczbę aparatów dostępnych na urządzeniach użytkownicy oczekują, że w wyniku powiększenia zostanie automatycznie wybrany obiektyw o najlepszej ogniskowej.

Aparat 1

Istnieją 2 sposoby powiększania za pomocą Aparatu1. Metoda Camera.startSmoothZoom() animuje się od bieżącego poziomu powiększenia do obecnego poziomu powiększenia. Metoda Camera.Parameters.setZoom() przeskakuje bezpośrednio do przekazanego poziomu powiększenia. Zanim użyjesz którejś z tych opcji, wywołaj odpowiednio metodę isSmoothZoomSupported() lub isZoomSupported(), by mieć pewność, że powiązane metody powiększenia są dostępne w kamerze.

Do wdrożenia funkcji ściągania i rozciągania palcem w tym przykładzie używany jest setZoom(), ponieważ detektor dotyku na powierzchni podglądu stale uruchamia zdarzenia w miarę wykonywania gestu ściągnięcia palcami, więc za każdym razem natychmiast aktualizuje poziom powiększenia. Klasa ZoomTouchListener jest zdefiniowana poniżej i powinna być ustawiona jako wywołanie zwrotne dla odbiornika dotyku obszaru podglądu.

// Camera1: implement pinch-to-zoom.

// Define a scale gesture detector to respond to pinch events and call
// setZoom on Camera.Parameters.
val scaleGestureDetector = ScaleGestureDetector(context,
    object : ScaleGestureDetector.OnScaleGestureListener {
        override fun onScale(detector: ScaleGestureDetector): Boolean {
            val camera = camera ?: return false
            val parameters = try {
                camera.parameters
            } catch (e: RuntimeException) {
                return false
            }

            // In case there is any focus happening, stop it.
            camera.cancelAutoFocus()

            // Set the zoom level on the Camera.Parameters, and set
            // the Parameters back onto the Camera.
            val currentZoom = parameters.zoom
            parameters.setZoom(detector.scaleFactor * currentZoom)
        camera.setParameters(parameters)
            return true
        }
    }
)

// Define a View.OnTouchListener to attach to your preview view.
class ZoomTouchListener : View.OnTouchListener {
    override fun onTouch(v: View, event: MotionEvent): Boolean =
        scaleGestureDetector.onTouchEvent(event)
}

// Set a ZoomTouchListener to handle touch events on your preview view
// if zoom is supported by the current camera.
if (camera.getParameters().isZoomSupported()) {
    view.setOnTouchListener(ZoomTouchListener())
}

CameraX: kontroler kamery

Podobnie jak w przypadku klikania, aby ustawić ostrość, CameraController nasłuchuje zdarzeń dotknięcia obiektu PreviewView, aby automatycznie obsługiwać powiększenie przez ściąganie palców. Możesz włączyć lub wyłączyć ściąganie palcami, aby powiększyć, za pomocą funkcji setPinchToZoomEnabled(), a także sprawdzić wartość za pomocą odpowiedniej metody pobierania isPinchToZoomEnabled().

Metoda getZoomState() zwraca obiekt LiveData do śledzenia zmian w ZoomState w CameraController.

// CameraX: track the state of pinch-to-zoom over the Lifecycle of
// a PreviewView, logging the linear zoom ratio.

val pinchToZoomStateObserver = Observer { state ->
    val zoomRatio = state.getZoomRatio()
    Log.d(TAG, "ptz-zoom-ratio $zoomRatio")
}

cameraController.getZoomState().observe(this, pinchToZoomStateObserver)

CameraX: CameraProvider

Aby przybliżyć obraz przez ściąganie palców na urządzeniu CameraProvider, musisz przeprowadzić konfigurację. Jeśli nie korzystasz z tabeli PreviewView, musisz dostosować zasady, aby zastosować ją do niestandardowego elementu Surface.

Gdy używasz PreviewView:

  1. Skonfiguruj wykrywanie gestów skalowania do obsługi zdarzeń ściągnięcia.
  2. Pobierz ZoomState z obiektu Camera.CameraInfo, który zwraca wystąpienie Camera podczas wywoływania bindToLifecycle().
  3. Jeśli ZoomState ma wartość zoomRatio, zapisz ją jako bieżący współczynnik powiększenia. Jeśli na urządzeniu ZoomState nie ma wskaźnika zoomRatio, użyj domyślnej szybkości powiększenia (1,0).
  4. Pobierz iloczyn bieżącego współczynnika powiększenia z scaleFactor, aby określić nowy współczynnik powiększenia, i prześlij go do CameraControl.setZoomRatio().
  5. Skonfiguruj czujnik gestów tak, aby reagował na zdarzenia dotyku w komponencie PreviewView.setOnTouchListener().
// CameraX: implement pinch-to-zoom with CameraProvider.

// Define a scale gesture detector to respond to pinch events and call
// setZoomRatio on CameraControl.
val scaleGestureDetector = ScaleGestureDetector(context,
    object : SimpleOnGestureListener() {
        override fun onScale(detector: ScaleGestureDetector): Boolean {
            val camera = camera ?: return
            val zoomState = camera.cameraInfo.zoomState
            val currentZoomRatio: Float = zoomState.value?.zoomRatio ?: 1f
            camera.cameraControl.setZoomRatio(
                detector.scaleFactor * currentZoomRatio
            )
        }
    }
)

...

// Set the scaleGestureDetector in a touch listener on the PreviewView.
previewView.setOnTouchListener { _, event ->
    var didConsume = scaleGestureDetector.onTouchEvent(event)
    if (!scaleGestureDetector.isInProgress) {
        // See pinch-to-zooom scenario for gestureDetector definition.
        didConsume = gestureDetector.onTouchEvent(event)
    }
    didConsume
}

Robienie zdjęcia

W tej sekcji dowiesz się, jak wywołać zdjęcie – niezależnie od tego, czy musisz to zrobić po naciśnięciu przycisku migawki, po upływie minutnika czy też w przypadku jakiegokolwiek innego zdarzenia związanego z wyborem.

Aparat 1

W Aparacie 1 musisz najpierw zdefiniować Camera.PictureCallback, aby zarządzać danymi zdjęcia, gdy o to poprosi. Oto prosty przykład użycia właściwości PictureCallback do obsługi danych obrazu JPEG:

// Camera1: define a Camera.PictureCallback to handle JPEG data.

private val picture = Camera.PictureCallback { data, _ ->
    val pictureFile: File = getOutputMediaFile(MEDIA_TYPE_IMAGE) ?: run {
        Log.d(TAG,
              "error creating media file, check storage permissions")
        return@PictureCallback
    }

    try {
        val fos = FileOutputStream(pictureFile)
        fos.write(data)
        fos.close()
    } catch (e: FileNotFoundException) {
        Log.d(TAG, "file not found", e)
    } catch (e: IOException) {
        Log.d(TAG, "error accessing file", e)
    }
}

Za każdym razem, gdy zechcesz zrobić zdjęcie, wywołaj metodę takePicture() w swojej instancji Camera. Ta metoda takePicture() ma 3 różne parametry dla różnych typów danych. Pierwszy parametr odnosi się do właściwości ShutterCallback (w tym przykładzie nie zdefiniowano jej). Drugi parametr służy do obsługi nieprzetworzonych (nieskompresowanych) danych kamery przez PictureCallback. W tym przykładzie jest używany trzeci parametr, ponieważ to parametr PictureCallback służący do obsługi danych obrazów JPEG.

// Camera1: call takePicture on Camera instance, passing our PictureCallback.

camera?.takePicture(null, null, picture)

CameraX: kontroler kamery

Funkcja CameraController w Aparacie X zachowuje prostotę obsługi robienia zdjęć za pomocą Aparatu 1, implementując własną metodę takePicture(). Tutaj zdefiniuj funkcję umożliwiającą skonfigurowanie wpisu MediaStore i zrobienie zdjęcia do zapisania w tym miejscu.

// CameraX: define a function that uses CameraController to take a photo.

private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"

private fun takePhoto() {
   // Create time stamped name and MediaStore entry.
   val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
              .format(System.currentTimeMillis())
   val contentValues = ContentValues().apply {
       put(MediaStore.MediaColumns.DISPLAY_NAME, name)
       put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
       if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
           put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
       }
   }

   // Create output options object which contains file + metadata.
   val outputOptions = ImageCapture.OutputFileOptions
       .Builder(context.getContentResolver(),
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
       .build()

   // Set up image capture listener, which is triggered after photo has
   // been taken.
   cameraController.takePicture(
       outputOptions,
       ContextCompat.getMainExecutor(this),
       object : ImageCapture.OnImageSavedCallback {
           override fun onError(e: ImageCaptureException) {
               Log.e(TAG, "photo capture failed", e)
           }

           override fun onImageSaved(
               output: ImageCapture.OutputFileResults
           ) {
               val msg = "Photo capture succeeded: ${output.savedUri}"
               Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
               Log.d(TAG, msg)
           }
       }
   )
}

CameraX: CameraProvider

Robienie zdjęć za pomocą funkcji CameraProvider działa prawie tak samo jak w przypadku CameraController, ale musisz najpierw utworzyć i powiązać obiekt ImageCapture UseCase, aby otrzymać obiekt, który chcesz wywołać takePicture():

// CameraX: create and bind an ImageCapture UseCase.

// Make a reference to the ImageCapture UseCase at a scope that can be accessed
// throughout the camera logic in your app.
private var imageCapture: ImageCapture? = null

...

// Create an ImageCapture instance (can be added with other
// UseCase definitions).
imageCapture = ImageCapture.Builder().build()

...

// Bind UseCases to camera (adding imageCapture along with preview here, but
// preview is not required to use imageCapture). This function returns a camera
// object which can be used to perform operations like zoom, flash, and focus.
var camera = cameraProvider.bindToLifecycle(
    this, cameraSelector, preview, imageCapture)

Jeśli chcesz zrobić zdjęcie, zadzwoń pod numer ImageCapture.takePicture(). Pełny przykład funkcji takePhoto() znajdziesz w kodzie CameraController w tej sekcji.

// CameraX: define a function that uses CameraController to take a photo.

private fun takePhoto() {
    // Get a stable reference of the modifiable ImageCapture UseCase.
    val imageCapture = imageCapture ?: return

    ...

    // Call takePicture on imageCapture instance.
    imageCapture.takePicture(
        ...
    )
}

Nagrywanie filmu

Nagrywanie filmu jest znacznie bardziej skomplikowane niż przedstawiane do tej pory scenariusze. Każdy etap procesu musi być prawidłowo skonfigurowany – zwykle w określonej kolejności. Możesz też sprawdzić, czy obraz i dźwięk są zsynchronizowane, lub rozwiązać ewentualne dodatkowe niespójności na urządzeniach.

Jak widzisz, CameraX ponownie w dużej mierze radzi sobie z tym złożonością.

Aparat 1

Nagrywanie filmów za pomocą Aparatu 1 wymaga starannego zarządzania elementami Camera i MediaRecorder, a metody muszą być wywoływane w określonej kolejności. Aby aplikacja działała prawidłowo, musisz wykonać te czynności:

  1. Otwórz aparat.
  2. Przygotuj i otwórz podgląd (jeśli aplikacja pokazuje nagrywany film – zwykle tak jest).
  3. Odblokuj aparat, aby MediaRecorder mógł go używać, dzwoniąc pod numer Camera.unlock().
  4. Skonfiguruj nagranie, wywołując w MediaRecorder te metody:
    1. Połącz instancję Camera z usługą setCamera(camera).
    2. Będziesz dzwonić pod numer setAudioSource(MediaRecorder.AudioSource.CAMCORDER).
    3. Będziesz dzwonić pod numer setVideoSource(MediaRecorder.VideoSource.CAMERA).
    4. Wywołaj setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P)), aby ustawić jakość. Wszystkie opcje jakości znajdziesz w sekcji CamcorderProfile.
    5. Będziesz dzwonić pod numer setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString()).
    6. Jeśli aplikacja udostępnia podgląd filmu, zadzwoń pod numer setPreviewDisplay(preview?.holder?.surface).
    7. Będziesz dzwonić pod numer setOutputFormat(MediaRecorder.OutputFormat.MPEG_4).
    8. Będziesz dzwonić pod numer setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT).
    9. Będziesz dzwonić pod numer setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT).
    10. Wywołaj prepare(), aby dokończyć konfigurację urządzenia MediaRecorder.
  5. Aby rozpocząć nagrywanie, zadzwoń pod numer MediaRecorder.start().
  6. Aby zatrzymać nagrywanie, wywołaj te metody. Tutaj też powtórz te czynności:
    1. Będziesz dzwonić pod numer MediaRecorder.stop().
    2. Opcjonalnie usuń bieżącą konfigurację MediaRecorder, wywołując metodę MediaRecorder.reset().
    3. Będziesz dzwonić pod numer MediaRecorder.release().
    4. Zablokuj kamerę, by mogły jej używać przyszłe sesje MediaRecorder, wywołując metodę Camera.lock().
  7. Aby wyłączyć podgląd, zadzwoń pod numer Camera.stopPreview().
  8. Aby na koniec zwolnić funkcję Camera, tak aby mogły jej używać inne procesy, wywołaj Camera.release().

Oto wszystkie te etapy:

// Camera1: set up a MediaRecorder and a function to start and stop video
// recording.

// Make a reference to the MediaRecorder at a scope that can be accessed
// throughout the camera logic in your app.
private var mediaRecorder: MediaRecorder? = null
private var isRecording = false

...

private fun prepareMediaRecorder(): Boolean {
    mediaRecorder = MediaRecorder()

    // Unlock and set camera to MediaRecorder.
    camera?.unlock()

    mediaRecorder?.run {
        setCamera(camera)

        // Set the audio and video sources.
        setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
        setVideoSource(MediaRecorder.VideoSource.CAMERA)

        // Set a CamcorderProfile (requires API Level 8 or higher).
        setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH))

        // Set the output file.
        setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString())

        // Set the preview output.
        setPreviewDisplay(preview?.holder?.surface)

        setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
        setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT)
        setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT)

        // Prepare configured MediaRecorder.
        return try {
            prepare()
            true
        } catch (e: IllegalStateException) {
            Log.d(TAG, "preparing MediaRecorder failed", e)
            releaseMediaRecorder()
            false
        } catch (e: IOException) {
            Log.d(TAG, "setting MediaRecorder file failed", e)
            releaseMediaRecorder()
            false
        }
    }
    return false
}

private fun releaseMediaRecorder() {
    mediaRecorder?.reset()
    mediaRecorder?.release()
    mediaRecorder = null
    camera?.lock()
}

private fun startStopVideo() {
    if (isRecording) {
        // Stop recording and release camera.
        mediaRecorder?.stop()
        releaseMediaRecorder()
        camera?.lock()
        isRecording = false

        // This is a good place to inform user that video recording has stopped.
    } else {
        // Initialize video camera.
        if (prepareVideoRecorder()) {
            // Camera is available and unlocked, MediaRecorder is prepared, now
            // you can start recording.
            mediaRecorder?.start()
            isRecording = true

            // This is a good place to inform the user that recording has
            // started.
        } else {
            // Prepare didn't work, release the camera.
            releaseMediaRecorder()

            // Inform user here.
        }
    }
}

CameraX: kontroler kamery

Za pomocą CameraController w AparacieX możesz przełączać obiekty ImageCapture, VideoCapture i ImageAnalysis UseCase niezależnie od tego, o ile lista przypadków użycia może być używana równocześnie. Urządzenia ImageCapture i ImageAnalysis UseCase są domyślnie włączone, dlatego nie musisz dzwonić pod numer setEnabledUseCases(), żeby zrobić zdjęcie.

Aby używać urządzenia CameraController do nagrywania wideo, musisz najpierw użyć setEnabledUseCases(), by zezwolić na UseCase VideoCapture.

// CameraX: Enable VideoCapture UseCase on CameraController.

cameraController.setEnabledUseCases(VIDEO_CAPTURE);

Aby rozpocząć nagrywanie filmu, możesz wywołać funkcję CameraController.startRecording(). Ta funkcja może zapisać nagrany film w elemencie File, jak widać w przykładzie poniżej. Dodatkowo musisz przekazać obiekt Executor i klasę, która implementuje OnVideoSavedCallback do obsługi wywołań zwrotnych zakończonych powodzeniem i błędów. Po zakończeniu nagrywania wywołaj metodę CameraController.stopRecording().

Uwaga: jeśli używasz CameraX w wersji 1.3.0-alpha02 lub nowszej, dostępny jest dodatkowy parametr AudioConfig, który umożliwia włączanie i wyłączanie nagrywania dźwięku w filmach. Aby włączyć nagrywanie dźwięku, musisz mieć uprawnienia do korzystania z mikrofonu. Dodatkowo w wersjach 1.3.0-alfa02 metoda stopRecording() usuwana jest, a startRecording() zwraca obiekt Recording, który można wykorzystać do wstrzymywania, wznawiania i zatrzymywania nagrywania filmu.

// CameraX: implement video capture with CameraController.

private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"

// Define a VideoSaveCallback class for handling success and error states.
class VideoSaveCallback : OnVideoSavedCallback {
    override fun onVideoSaved(outputFileResults: OutputFileResults) {
        val msg = "Video capture succeeded: ${outputFileResults.savedUri}"
        Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
        Log.d(TAG, msg)
    }

    override fun onError(videoCaptureError: Int, message: String,
                         cause: Throwable?) {
        Log.d(TAG, "error saving video: $message", cause)
    }
}

private fun startStopVideo() {
    if (cameraController.isRecording()) {
        // Stop the current recording session.
        cameraController.stopRecording()
        return
    }

    // Define the File options for saving the video.
    val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
        .format(System.currentTimeMillis())

    val outputFileOptions = OutputFileOptions
        .Builder(File(this.filesDir, name))
        .build()

    // Call startRecording on the CameraController.
    cameraController.startRecording(
        outputFileOptions,
        ContextCompat.getMainExecutor(this),
        VideoSaveCallback()
    )
}

CameraX: CameraProvider

Jeśli używasz CameraProvider, musisz utworzyć VideoCapture UseCase i przekazać obiekt Recorder. W Recorder.Builder możesz ustawić jakość filmu i opcjonalnie FallbackStrategy, który obsługuje przypadki, gdy urządzenie nie spełnia wymagań dotyczących jakości. Następnie powiąż instancję VideoCapture z CameraProvider z pozostałymi elementami UseCase.

// CameraX: create and bind a VideoCapture UseCase with CameraProvider.

// Make a reference to the VideoCapture UseCase and Recording at a
// scope that can be accessed throughout the camera logic in your app.
private lateinit var videoCapture: VideoCapture
private var recording: Recording? = null

...

// Create a Recorder instance to set on a VideoCapture instance (can be
// added with other UseCase definitions).
val recorder = Recorder.Builder()
    .setQualitySelector(QualitySelector.from(Quality.FHD))
    .build()
videoCapture = VideoCapture.withOutput(recorder)

...

// Bind UseCases to camera (adding videoCapture along with preview here, but
// preview is not required to use videoCapture). This function returns a camera
// object which can be used to perform operations like zoom, flash, and focus.
var camera = cameraProvider.bindToLifecycle(
    this, cameraSelector, preview, videoCapture)

Obecnie dostęp do Recorder jest dostępny w usłudze videoCapture.output. Recorder może uruchamiać nagrania wideo zapisane w usłudze File, ParcelFileDescriptor lub MediaStore. W tym przykładzie użyto właściwości MediaStore.

W Recorder można wywołać kilka metod, aby go przygotować. Wywołaj prepareRecording(), aby ustawić opcje wyjściowe elementu MediaStore. Jeśli aplikacja ma uprawnienia do korzystania z mikrofonu urządzenia, zadzwoń też pod numer withAudioEnabled(). Następnie wywołaj start(), aby rozpocząć nagrywanie, przekazując kontekst i detektor zdarzeń Consumer<VideoRecordEvent> do obsługi zdarzeń nagrywania wideo. Jeśli operacja się uda, zwróconego Recording można użyć do wstrzymania, wznowienia lub zatrzymania nagrywania.

// CameraX: implement video capture with CameraProvider.

private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"

private fun startStopVideo() {
   val videoCapture = this.videoCapture ?: return

   if (recording != null) {
       // Stop the current recording session.
       recording.stop()
       recording = null
       return
   }

   // Create and start a new recording session.
   val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
       .format(System.currentTimeMillis())
   val contentValues = ContentValues().apply {
       put(MediaStore.MediaColumns.DISPLAY_NAME, name)
       put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
       if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
           put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
       }
   }

   val mediaStoreOutputOptions = MediaStoreOutputOptions
       .Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
       .setContentValues(contentValues)
       .build()

   recording = videoCapture.output
       .prepareRecording(this, mediaStoreOutputOptions)
       .withAudioEnabled()
       .start(ContextCompat.getMainExecutor(this)) { recordEvent ->
           when(recordEvent) {
               is VideoRecordEvent.Start -> {
                   viewBinding.videoCaptureButton.apply {
                       text = getString(R.string.stop_capture)
                       isEnabled = true
                   }
               }
               is VideoRecordEvent.Finalize -> {
                   if (!recordEvent.hasError()) {
                       val msg = "Video capture succeeded: " +
                           "${recordEvent.outputResults.outputUri}"
                       Toast.makeText(
                           baseContext, msg, Toast.LENGTH_SHORT
                       ).show()
                       Log.d(TAG, msg)
                   } else {
                       recording?.close()
                       recording = null
                       Log.e(TAG, "video capture ends with error",
                             recordEvent.error)
                   }
                   viewBinding.videoCaptureButton.apply {
                       text = getString(R.string.start_capture)
                       isEnabled = true
                   }
               }
           }
       }
}

Dodatkowe materiały

W naszym repozytorium na GitHubie z przykładami aparatu znajdziesz kompletne aplikacje CameraX. Te przykłady pokazują, jak scenariusze zawarte w tym przewodniku pasują do w pełni funkcjonalnej aplikacji na Androida.

Jeśli potrzebujesz dodatkowej pomocy przy migracji do CameraX lub masz pytania związane z interfejsem Android Camera API, skontaktuj się z nami w grupie dyskusyjnej poświęconej CameraX.