Przeprowadź migrację z Aparatu 1 do aplikacji CameraX

Jeśli Twoja aplikacja korzysta z oryginalnej klasy Camera („Camera1”), która jest wycofana od Androida 5.0 (poziom API 21), zdecydowanie zalecamy przejście na nowoczesny interfejs API aparatu na Androidzie. Android oferuje CameraX (standardowy, niezawodny interfejs API aparatu Jetpack) i Camera2 (interfejs API niskiego poziomu). W większości przypadków zalecamy migrację aplikacji do CameraX. Przyczyna jest następująca:

  • Łatwość użycia: CameraX obsługuje szczegóły niskiego poziomu, dzięki czemu możesz mniej skupiać się na tworzeniu funkcji aparatu od podstaw, a bardziej na wyróżnianiu swojej aplikacji.
  • CameraX obsługuje fragmentację: CameraX zmniejsza długoterminowe koszty konserwacji i kod specyficzny dla urządzenia, zapewniając użytkownikom wyższą jakość. Więcej informacji znajdziesz w naszym poście na blogu na temat lepszej zgodności urządzeń z CameraX.
  • Zaawansowane możliwości: CameraX została starannie zaprojektowana, aby ułatwić wdrożenie w aplikacji zaawansowanych funkcji. Na przykład dzięki rozszerzeniom CameraX możesz łatwo zastosować do zdjęć efekt bokeh, retusz twarzy, HDR (High Dynamic Range) i tryb nocny, który rozjaśnia zdjęcia robione w słabym oświetleniu.
  • Możliwość aktualizacji: Android udostępnia nowe funkcje i poprawki błędów w CameraX w ciągu roku. Dzięki migracji do CameraX Twoja aplikacja będzie korzystać z najnowszej technologii aparatu na Androidzie w każdej wersji CameraX, a nie tylko w corocznych wersjach Androida.

W tym przewodniku znajdziesz typowe scenariusze dotyczące aplikacji aparatu. Każdy scenariusz zawiera implementację Camera1 i CameraX do porównania.

W przypadku migracji czasami potrzebujesz większej elastyczności, aby zintegrować kod z istniejącą bazą kodu. Wszystkie fragmenty kodu CameraX w tym przewodniku mają implementację CameraController, która jest przydatna, jeśli chcesz używać CameraX w prostszy sposób, oraz implementację CameraProvider, która jest przydatna, jeśli potrzebujesz większej elastyczności. Aby pomóc Ci zdecydować, która opcja jest dla Ciebie odpowiednia, przedstawiamy korzyści każdej z nich:

CameraController

CameraProvider

Wymaga niewielkiego kodu konfiguracji Zapewnia większą kontrolę
Pozwolenie CameraX na obsługę większej części procesu konfiguracji oznacza, że funkcje takie jak dotknięcie w celu ustawienia ostrości i szczypanie w celu powiększenia działają automatycznie. Konfiguracją zajmuje się deweloper aplikacji, więc jest więcej możliwości dostosowania ustawień, np. włączenia obracania obrazu wyjściowego lub ustawienia jego formatu w ImageAnalysis.
Wymaganie PreviewView w przypadku podglądu z kamery umożliwia CameraX płynną integrację kompleksową, tak jak w przypadku integracji z ML Kit, która może mapować współrzędne wyników modelu ML (np. ramki ograniczające twarz) bezpośrednio na współrzędne podglądu. Możliwość używania niestandardowej klasy `Surface` do podglądu z kamery zapewnia większą elastyczność, np. możliwość używania istniejącego kodu klasy `Surface`, który może być danymi wejściowymi dla innych części aplikacji.

Jeśli napotkasz problemy podczas migracji, skontaktuj się z nami na grupie dyskusyjnej CameraX.

Przed migracją

Porównanie użycia CameraX i Camera1

Chociaż kod może wyglądać inaczej, podstawowe koncepcje w Camera1 i CameraX są bardzo podobne. CameraX abstrakcyjnie przekształca typowe funkcje aparatu w przypadki użycia, dzięki czemu wiele zadań, które w Camera1 były pozostawione programiście, jest automatycznie obsługiwanych przez CameraX. W CameraX są 4 UseCase, których możesz używać do różnych zadań związanych z kamerą: Preview, ImageCapture, VideoCaptureImageAnalysis.

Przykładem tego, jak CameraX obsługuje szczegóły niskiego poziomu za deweloperów, jest ViewPort, który jest udostępniany aktywnym UseCase. Dzięki temu wszystkie UseCase będą widzieć dokładnie te same piksele. W przypadku Camera1 musisz samodzielnie zarządzać tymi szczegółami. Ze względu na różne formaty obrazu na urządzeniach dopasowanie podglądu do zarejestrowanych multimediów jest trudne.

Inny przykład: CameraX automatycznie obsługuje wywołania zwrotne Lifecycle w instancji Lifecycle, którą podasz. Dzięki tej architekturze CameraX obsługuje połączenie aplikacji z aparatem przez cały cykl życia działania Androida, w tym w tych przypadkach: zamykanie aparatu, gdy aplikacja przechodzi w tło; usuwanie podglądu z kamery, gdy ekran nie wymaga już jego wyświetlania; wstrzymywanie podglądu z kamery, gdy inna Activity ma wyższy priorytet, np. przychodząca rozmowa wideo.

Na koniec CameraX obsługuje obracanie i skalowanie bez konieczności pisania dodatkowego kodu. W przypadku Activity z odblokowaną orientacją konfiguracja UseCase jest przeprowadzana za każdym razem, gdy urządzenie zostanie obrócone, ponieważ system niszczy i ponownie tworzy Activity po zmianie orientacji. W rezultacie UseCasesustawienie docelowego obrotu jest domyślnie dopasowywane do orientacji wyświetlacza. Więcej informacji o obracaniu w CameraX

Zanim przejdziemy do szczegółów, zobaczmy, jak wygląda CameraX UseCase i jak odnosi się do niej aplikacja Camera1. (Pojęcia związane z CameraX są oznaczone kolorem niebieskim, a pojęcia związane z Camera1 – kolorem zielonym).

CameraX

Konfiguracja CameraController / CameraProvider
Podgląd ImageCapture VideoCapture ImageAnalysis
Zarządzanie podglądem i ustawianie go w Aparacie Ustaw PictureCallback i wywołaj takePicture() w klasie Camera Zarządzanie konfiguracją kamery i klasy MediaRecorder w określonej kolejności Niestandardowy kod analizy utworzony na podstawie podglądu powierzchni
Kod urządzenia
Zarządzanie obracaniem i skalowaniem urządzeń
Zarządzanie sesją kamery (wybór kamery, zarządzanie cyklem życia)

Camera1

Zgodność i wydajność w CameraX

CameraX obsługuje urządzenia z Androidem 5.0 (poziom interfejsu API 21) lub nowszym. To ponad 98% wszystkich urządzeń z Androidem. Biblioteka CameraX została zaprojektowana tak, aby automatycznie obsługiwać różnice między urządzeniami, co zmniejsza potrzebę stosowania w aplikacji kodu specyficznego dla danego urządzenia. Ponadto w naszym laboratorium testowym CameraX testujemy ponad 150 urządzeń fizycznych z Androidem w wersji 5.0 i nowszej. Pełną listę urządzeń w Laboratorium znajdziesz tutaj.

CameraX używa Executor do obsługi stosu aparatu. Jeśli Twoja aplikacja ma określone wymagania dotyczące wątków, możesz ustawić własny moduł wykonawczy w CameraX. Jeśli nie zostanie ustawiony, CameraX utworzy i użyje zoptymalizowanego domyślnego wewnętrznego Executor. Wiele interfejsów API platformy, na których opiera się CameraX, wymaga blokowania komunikacji międzyprocesowej (IPC) ze sprzętem, co czasami może trwać setki milisekund. Dlatego CameraX wywołuje te interfejsy API tylko z wątków w tle, co zapewnia, że 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 urządzenia z niższej półki, CameraX umożliwia skrócenie czasu konfiguracji za pomocą ogranicznika kamery. Proces łączenia z komponentami sprzętowymi może zająć sporo czasu, zwłaszcza na urządzeniach z niższej półki. Możesz więc określić zestaw kamer, których potrzebuje Twoja aplikacja. CameraX łączy się z tymi kamerami tylko podczas konfiguracji. Jeśli na przykład aplikacja używa tylko tylnych aparatów, może ustawić tę konfigurację za pomocą DEFAULT_BACK_CAMERA, a wtedy CameraX nie będzie inicjować przednich aparatów, aby zmniejszyć opóźnienie.

Pojęcia związane z programowaniem aplikacji na Androida

Zakładamy, że masz ogólną wiedzę na temat programowania na Androida. Oprócz podstawowych informacji warto poznać te pojęcia, zanim przejdziesz do poniższego kodu:

Przenoszenie typowych scenariuszy

W tej sekcji znajdziesz informacje o tym, jak przenieść typowe scenariusze z Camera1 do CameraX. Każdy scenariusz obejmuje implementację Camera1, implementację CameraXCameraProvider i implementację CameraXCameraController.

Wybieranie kamery

Jedną z pierwszych rzeczy, które możesz zaoferować w aplikacji aparatu, jest możliwość wyboru różnych aparatów.

Camera1

W przypadku Camera1 możesz wywołać funkcję Camera.open() bez parametrów, aby otworzyć pierwszy tylny aparat, lub przekazać identyfikator liczbowy aparatu, który chcesz otworzyć. Oto przykład:

// 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: CameraController

W CameraX wybór aparatu jest obsługiwany przez klasę CameraSelector. CameraX upraszcza typowy przypadek używania domyślnego aparatu. Możesz określić, czy chcesz używać domyślnego przedniego czy tylnego aparatu. Dodatkowo obiekt CameraControl CameraX umożliwia ustawienie poziomu powiększenia w aplikacji. Jeśli aplikacja działa na urządzeniu obsługującym kamery logiczne, przełączy się na odpowiedni obiektyw.

Oto kod CameraX do używania domyślnego tylnego aparatu z właściwością 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 za pomocą właściwości CameraProvider (przedniego lub tylnego aparatu można używać z właściwością 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 preceding "Android development concepts"
// section.
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 mieć kontrolę nad tym, która kamera jest wybrana, możesz to zrobić w CameraX, używając CameraProvider przez wywołanie getAvailableCameraInfos(), co daje obiekt CameraInfo do sprawdzania określonych właściwości kamery, takich jak isFocusMeteringSupported(). Następnie możesz przekształcić go w CameraSelector, aby użyć go w sposób pokazany w poprzednich przykładach z metodą CameraInfo.getCameraSelector().

Więcej informacji o poszczególnych kamerach możesz uzyskać, korzystając z klasy Camera2CameraInfo. Wywołaj funkcję getCameraCharacteristic(), podając klucz do danych z kamery, które chcesz uzyskać. Sprawdź CameraCharacteristics class, aby zobaczyć listę wszystkich kluczy, o które możesz wysyłać zapytania.

Oto przykład użycia niestandardowej funkcji checkFocalLength(), którą możesz 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świetlanie podglądu

Większość aplikacji aparatu musi w pewnym momencie wyświetlać przekaz z kamery na ekranie. W przypadku Camera1 musisz prawidłowo zarządzać wywołaniami zwrotnymi cyklu życia, a także określać rotację i skalowanie podglądu.

W przypadku Camera1 musisz też zdecydować, czy jako powierzchnię podglądu chcesz używać TextureView czy SurfaceView. Obie opcje mają swoje wady i zalety, a w każdym przypadku Camera1 wymaga prawidłowego obsługiwania rotacji i skalowania. PreviewView CameraX ma natomiast implementacje bazowe zarówno dla TextureView, jak 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 obie implementacje są kompatybilne, możesz zadeklarować preferencje za pomocą parametru PreviewView.ImplementationMode. Opcja COMPATIBLE używa TextureView w podglądzie, a wartość PERFORMANCE używa SurfaceView (jeśli to możliwe).

Camera1

Aby wyświetlić podgląd, musisz napisać własną klasę Preview z implementacją interfejsu android.view.SurfaceHolder.Callback, który służy do przekazywania danych obrazu z sprzętu aparatu do aplikacji. Zanim rozpoczniesz podgląd obrazu na żywo, musisz przekazać Preview do obiektu Camera.

// 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: CameraController

W CameraX deweloper ma znacznie mniej do zarządzania. Jeśli używasz zasady CameraController, musisz też użyć zasady PreviewView. Oznacza to, że Preview UseCase jest domyślnie uwzględniane, co znacznie ułatwia konfigurację:

// 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 CameraX nie musisz używać PreviewView, ale nadal znacznie upraszcza to konfigurację podglądu w porównaniu z Camera1. Na potrzeby demonstracji w tym przykładzie użyto PreviewView, ale w razie bardziej złożonych potrzeb możesz napisać niestandardową funkcję SurfaceProvider, aby przekazać ją do setSurfaceProvider().

W tym przypadku symbol Preview UseCase nie jest domyślny, 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 preceding "Android development concepts"
// section.
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()
    }
}

Dotknij, aby ustawić ostrość

Gdy podgląd z kamery jest widoczny na ekranie, typowym sposobem sterowania jest ustawianie punktu ostrości, gdy użytkownik kliknie podgląd.

Camera1

Aby wdrożyć funkcję dotknij, aby ustawić ostrość w interfejsie Camera1, musisz obliczyć optymalną ostrość Area, aby wskazać, gdzie Camera ma próbować ustawić ostrość. Ten Area jest przekazywany do setFocusAreas(). Musisz też ustawić zgodny tryb zaznaczania w Camera. Obszar ostrości ma wpływ 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 to prostokąt o określonej wadze. Waga to wartość z zakresu od 1 do 1000. Służy do określania priorytetu ostrości Areas, jeśli ustawiono ich kilka. W tym przykładzie użyto tylko 1 Area, więc wartość wagi nie ma znaczenia. Współrzędne prostokąta mieszczą się w zakresie od –1000 do 1000. Lewy górny punkt to (-1000, -1000). Prawy dolny punkt ma współrzędne (1000, 1000). Kierunek jest określany względem orientacji czujnika, czyli tego, co on widzi. Kierunek nie zależy od obrotu ani odbicia lustrzanego elementu Camera.setDisplayOrientation(), 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: CameraController

CameraController nasłuchuje zdarzeń dotyku PreviewView, aby automatycznie obsługiwać funkcję dotknij, aby ustawić ostrość. Funkcję dotknij, aby ustawić ostrość, możesz włączyć i wyłączyć za pomocą metody setTapToFocusEnabled(), a jej wartość sprawdzić za pomocą odpowiedniego gettera isTapToFocusEnabled().

Metoda getTapToFocusState() zwraca obiekt LiveData do śledzenia zmian stanu fokusu na elemencie 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

W przypadku korzystania z CameraProvider wymagana jest konfiguracja, aby włączyć funkcję dotknięcia w celu ustawienia ostrości. W tym przykładzie założono, że używasz PreviewView. Jeśli nie, musisz dostosować logikę do swojego niestandardowego Surface.

Jeśli używasz PreviewView, wykonaj te czynności:

  1. Skonfiguruj detektor gestów do obsługi zdarzeń kliknięcia.
  2. Za pomocą zdarzenia kliknięcia utwórz MeteringPoint, używając MeteringPointFactory.createPoint().
  3. Za pomocą MeteringPoint utwórz FocusMeteringAction.
  4. W przypadku obiektu CameraControl na urządzeniu Camera (zwróconego przez bindToLifecycle()) wywołaj startFocusAndMetering(), przekazując FocusMeteringAction.
  5. (Opcjonalnie) Odpowiedz na FocusMeteringResult.
  6. Ustaw detektor gestów tak, aby reagował na zdarzenia dotyku w 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
// preceding "Android development concepts" section.
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-zoom scenario for scaleGestureDetector definition.
    var didConsume = scaleGestureDetector.onTouchEvent(event)
    if (!scaleGestureDetector.isInProgress) {
        didConsume = gestureDetector.onTouchEvent(event)
    }
    didConsume
}

Ściągnij, aby powiększyć

Powiększanie i pomniejszanie podglądu to kolejna popularna metoda bezpośredniego manipulowania podglądem z kamery. Wraz ze wzrostem liczby aparatów w urządzeniach użytkownicy oczekują, że w wyniku powiększenia automatycznie zostanie wybrany obiektyw o najlepszej ogniskowej.

Camera1

W przypadku Camera1 możesz powiększać obraz na 2 sposoby. Metoda Camera.startSmoothZoom() animuje przejście od bieżącego poziomu powiększenia do poziomu, który podasz. Metoda Camera.Parameters.setZoom() przechodzi bezpośrednio do poziomu powiększenia, który podasz. Zanim zaczniesz korzystać z jednej z tych funkcji, zadzwoń pod numer isSmoothZoomSupported() lub isZoomSupported(), aby sprawdzić, czy na aparacie są dostępne odpowiednie metody powiększania.

W tym przykładzie do wdrożenia funkcji powiększania za pomocą gestu uszczypnięcia użyto setZoom(), ponieważ odbiornik dotyku na powierzchni podglądu stale wywołuje zdarzenia podczas wykonywania gestu uszczypnięcia, więc za każdym razem natychmiast aktualizuje poziom powiększenia. Klasa ZoomTouchListener jest zdefiniowana w dalszej części tej sekcji. Należy ją ustawić jako wywołanie zwrotne dla odbiornika zdarzeń dotykowych powierzchni 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: CameraController

Podobnie jak w przypadku funkcji dotknij, aby ustawić ostrość, CameraController nasłuchuje zdarzeń dotykowych w obiektywie PreviewView, aby automatycznie obsługiwać powiększanie przez uszczypnięcie. Funkcję powiększania za pomocą gestu uszczypnięcia możesz włączać i wyłączać za pomocą metody setPinchToZoomEnabled(), a jej wartość możesz sprawdzać za pomocą odpowiedniej metody pobierającej isPinchToZoomEnabled().

Metoda getZoomState() zwraca obiekt LiveData do śledzenia zmian w ZoomState na 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 funkcja powiększania za pomocą gestu szczypania działała w przypadku CameraProvider, wymagana jest konfiguracja. Jeśli nie korzystasz z PreviewView, musisz dostosować logikę do własnego Surface.

Jeśli używasz PreviewView, wykonaj te czynności:

  1. Skonfiguruj detektor gestów skalowania, aby obsługiwać zdarzenia uszczypnięcia.
  2. Pobierz ZoomState z obiektu Camera.CameraInfo, gdzie instancja Camera jest zwracana po wywołaniu funkcji 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 wartości zoomRatio, użyj domyślnego współczynnika powiększenia kamery (1,0).
  4. Pomnóż bieżący współczynnik powiększenia przez scaleFactor, aby określić nowy współczynnik powiększenia, i przekaż go do CameraControl.setZoomRatio().
  5. Ustaw detektor gestów tak, aby reagował na zdarzenia dotyku w 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-zoom scenario for gestureDetector definition.
        didConsume = gestureDetector.onTouchEvent(event)
    }
    didConsume
}

Robienie zdjęcia

Z tej sekcji dowiesz się, jak wywołać zrobienie zdjęcia – czy ma to nastąpić po naciśnięciu przycisku migawki, po upływie czasu odliczania czy po innym wybranym przez Ciebie zdarzeniu.

Camera1

W przypadku Camera1 najpierw definiujesz Camera.PictureCallback, aby zarządzać danymi obrazu, gdy są one wymagane. Oto prosty przykład 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)
    }
}

Następnie, gdy chcesz zrobić zdjęcie, wywołujesz metodę takePicture() na instancji Camera. Ta takePicture() metoda ma 3 różne parametry dla różnych typów danych. Pierwszy parametr dotyczy ShutterCallback (który nie jest zdefiniowany w tym przykładzie). Drugi parametr służy do obsługi surowych (nieskompresowanych) danych z kamery przez PictureCallback. W tym przykładzie używamy trzeciego parametru, ponieważ jest to PictureCallback do obsługi danych obrazu JPEG.

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

camera?.takePicture(null, null, picture)

CameraX: CameraController

CameraController CameraX zachowuje prostotę Camera1 w zakresie robienia zdjęć, implementując własną metodę takePicture(). Zdefiniuj tutaj funkcję, która skonfiguruje wpis MediaStore i zrobi zdjęcie, które zostanie tam zapisane.

// 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

Zrobienie zdjęcia za pomocą CameraProvider działa prawie tak samo jak w przypadku CameraController, ale najpierw musisz utworzyć i powiązać ImageCapture UseCase, aby mieć obiekt, na którym można 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)

Gdy zechcesz zrobić zdjęcie, możesz wywołać ImageCapture.takePicture(). Pełny przykład funkcji takePhoto() znajdziesz w sekcji z kodem CameraController.

// 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ż opisane dotychczas scenariusze. Każda część procesu musi być prawidłowo skonfigurowana, zwykle w określonej kolejności. Może też być konieczne sprawdzenie, czy obraz i dźwięk są zsynchronizowane, lub rozwiązanie problemów z niezgodnościami urządzeń.

Jak zobaczysz, CameraX ponownie zajmuje się wieloma z tych złożonych kwestii.

Camera1

Przechwytywanie wideo za pomocą Camera1 wymaga starannego zarządzania elementami CameraMediaRecorder, a metody muszą być wywoływane w określonej kolejności. Aby aplikacja działała prawidłowo, musisz wykonać te czynności w podanej kolejności:

  1. Uruchom aparat.
  2. Przygotuj i uruchom podgląd (jeśli aplikacja wyświetla nagrywany film, co zwykle ma miejsce).
  3. Odblokuj aparat, aby mógł być używany przez MediaRecorder, dzwoniąc pod numer Camera.unlock().
  4. Skonfiguruj nagrywanie, wywołując te metody w MediaRecorder:
    1. Połącz instancję Camera z usługą setCamera(camera).
    2. Zadzwoń: setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
    3. Zadzwoń: setVideoSource(MediaRecorder.VideoSource.CAMERA)
    4. Zadzwoń pod numer setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P)), aby ustawić jakość. Wszystkie opcje jakości znajdziesz w sekcji CamcorderProfile.
    5. Zadzwoń: setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString())
    6. Jeśli Twoja aplikacja ma podgląd wideo, wywołaj funkcję setPreviewDisplay(preview?.holder?.surface).
    7. Zadzwoń: setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
    8. Zadzwoń: setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT)
    9. Zadzwoń: setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT)
    10. Zadzwoń pod numer 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. Ponownie postępuj w tej kolejności:
    1. Zadzwoń: MediaRecorder.stop()
    2. Opcjonalnie możesz usunąć bieżącą konfigurację MediaRecorder, wywołując MediaRecorder.reset().
    3. Zadzwoń: MediaRecorder.release()
    4. Zablokuj kamerę, aby przyszłe sesje MediaRecorder mogły jej używać, wywołując Camera.lock().
  7. Aby zatrzymać podgląd, zadzwoń pod numer Camera.stopPreview().
  8. Na koniec, aby zwolnić Camera, tak aby inne procesy mogły go używać, wywołaj funkcję Camera.release().

Oto wszystkie te czynności w jednym miejscu:

// 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: CameraController

Za pomocą interfejsu CameraController CameraX możesz niezależnie przełączać ImageCapture, VideoCaptureImageAnalysis UseCase, o ile lista UseCase może być używana jednocześnie. ImageCapture iImageAnalysis UseCase są domyślnie włączone, dlatego nie musisz wywoływać setEnabledUseCases(), aby zrobić zdjęcie.

Aby użyć CameraController do nagrywania filmów, musisz najpierw użyć setEnabledUseCases(), aby zezwolić na VideoCapture UseCase.

// CameraX: Enable VideoCapture UseCase on CameraController.

cameraController.setEnabledUseCases(VIDEO_CAPTURE);

Aby rozpocząć nagrywanie wideo, możesz wywołać funkcję CameraController.startRecording(). Ta funkcja może zapisać nagrany film w File, jak widać na poniższym przykładzie. Musisz też przekazać Executor i klasę, która implementuje interfejs OnVideoSavedCallback, aby obsługiwać wywołania zwrotne dotyczące powodzenia i błędów. Gdy nagrywanie ma się zakończyć, wywołaj funkcję 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 filmie. Aby włączyć nagrywanie dźwięku, musisz mieć uprawnienia do korzystania z mikrofonu. Dodatkowo w wersji 1.3.0-alpha02 usunięto metodę stopRecording(), a metoda startRecording() zwraca obiekt Recording, którego można używać 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. Na Recorder.Builder możesz ustawić jakość wideo i opcjonalnie FallbackStrategy, który obsługuje przypadki, gdy urządzenie nie spełnia oczekiwanych specyfikacji jakości. Następnie powiąż instancję VideoCaptureCameraProvider za pomocą pozostałych 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)

W tym momencie do usługi videoCapture.output można uzyskać dostęp w usłudze Recorder. Recorder może rozpocząć nagrywanie filmów, które są zapisywane na urządzeniu File, ParcelFileDescriptor lub MediaStore. W tym przykładzie użyto metody MediaStore.

Na Recorder jest kilka sposobów na przygotowanie połączenia. Call prepareRecording(), aby ustawić MediaStore opcje wyjściowe. Jeśli aplikacja ma uprawnienia do korzystania z mikrofonu urządzenia, wywołaj też funkcję withAudioEnabled(). Następnie wywołaj funkcję start(), aby rozpocząć nagrywanie, przekazując kontekst i Consumer<VideoRecordEvent> detektor zdarzeń do obsługi zdarzeń nagrywania wideo. Jeśli operacja się powiedzie, zwrócony identyfikator Recording można wykorzystać 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 GitHub z przykładami dotyczącymi aparatu znajdziesz kilka kompletnych aplikacji CameraX. Te przykłady pokazują, jak scenariusze opisane w tym przewodniku pasują do pełnoprawnej aplikacji na Androida.

Jeśli potrzebujesz dodatkowej pomocy przy migracji na CameraX lub masz pytania dotyczące pakietu interfejsów API aparatu na Androida, skontaktuj się z nami na grupie dyskusyjnej CameraX.