Przeprowadź migrację z Aparatu 1 do aplikacji CameraX

Jeśli Twoja aplikacja korzysta z pierwotnej klasy Camera („Camera1”), która została wycofana w wersji Androida 5.0 (poziom API 21), zdecydowanie zalecamy przejście na nowy interfejs API aparatu na Androida. Android oferuje CameraX (standardowy, niezawodny interfejs API Jetpack do obsługi aparatu) i Camera2 (interfejs API frameworku na niskim poziomie). W większości przypadków zalecamy przeniesienie aplikacji do CameraX. Przyczyna jest następująca:

  • Łatwość obsługi: CameraX obsługuje szczegóły niskiego poziomu, dzięki czemu możesz mniej skupiać się na tworzeniu aparatu od podstaw, a bardziej na wyróżnieniu aplikacji.
  • CameraX radzi sobie z fragmentacją: CameraX zmniejsza długoterminowe koszty konserwacji i kodu związanego z konkretnym urządzeniem, zapewniając użytkownikom wyższą jakość. Więcej informacji znajdziesz w poście na blogu Zwiększenie zgodności z urządzeniami dzięki CameraX.
  • Funkcje zaawansowane: aplikacja CameraX została zaprojektowana tak, aby ułatwić Ci stosowanie zaawansowanych funkcji w aplikacji. Możesz na przykład łatwo stosować efekt bokeh, retusz twarzy, HDR (High Dynamic Range) i tryb nocny do swoich zdjęć za pomocą rozszerzeń CameraX.
  • Możliwość aktualizacji: w ciągu roku Android wprowadza nowe funkcje i poprawki błędów w CameraX. Dzięki migracji na CameraX Twoja aplikacja będzie korzystać z najnowszej technologii aparatu Androida w każdej wersji CameraX, a nie tylko w rocznych wersjach Androida.

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

W przypadku migracji czasami potrzebujesz większej elastyczności, aby zintegrować się z istniejącą bazą kodu. Cały kod CameraX w tym przewodniku ma implementację CameraController, która jest świetna, jeśli chcesz użyć CameraX w najprostszy sposób, a także implementację CameraProvider, która jest świetna, jeśli potrzebujesz większej elastyczności. Aby ułatwić Ci wybór odpowiedniej opcji, przedstawiamy korzyści płynące z każdej z nich:

CameraController

CameraProvider

Wymaga niewielkiej ilości kodu konfiguracyjnego Większa kontrola
Przekazanie aplikacji CameraX większej części procesu konfiguracji oznacza, że funkcje takie jak skupianie przez dotknięcie czy powiększanie przez zbliżenie działają automatycznie. Ponieważ konfiguracją zajmuje się deweloper aplikacji, masz więcej możliwości dostosowania konfiguracji, na przykład włączenia funkcji obracania obrazu wyjściowego lub ustawienia formatu obrazu wyjściowego w pliku ImageAnalysis.
Wymaganie PreviewView w przypadku podglądu aparatu umożliwia CameraX oferowanie płynnej integracji end-to-end, podobnie 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 „powierzchni” do podglądu aparatu daje większą elastyczność, np. możliwość użycia dotychczasowego kodu „powierzchni” jako danych wejściowych w innych częściach aplikacji.

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

Zanim przeprowadzisz migrację

Porównanie wykorzystania aplikacji CameraX i Camera1

Chociaż kod może wyglądać inaczej, zastosowane w Camera1 i CameraX koncepcje są bardzo podobne. CameraX umieszcza wspólne funkcje aparatu w ramach konkretnych zastosowań, dzięki czemu wiele zadań, które w aplikacji Camera1 należało do dewelopera, jest wykonywanych automatycznie przez CameraX. W aplikacji CameraX są 4 UseCase, które możesz stosować do różnych zadań związanych z aparatem: Preview, ImageCapture, VideoCaptureImageAnalysis.

Przykładem obsługi przez CameraX szczegółów niskiego poziomu dla deweloperów jest ViewPort, który jest udostępniany aktywnym UseCase. Dzięki temu wszystkie UseCase widzą dokładnie te same piksele. W aplikacji Camera1 musisz samodzielnie zarządzać tymi szczegółami, a z uwagi na zmienność formatów obrazu na czujnikach i ekranach aparatów na różnych urządzeniach może być trudno zapewnić, aby podgląd pasował do zrobionych zdjęć i filmów.

Innym przykładem jest CameraX, który automatycznie obsługuje wywołania zwrotne Lifecycle w przekazanej instancji Lifecycle. Oznacza to, że CameraX obsługuje połączenie aplikacji z kamerą przez cały cykl życia aktywności Androida, w tym w takich przypadkach: gdy aplikacja przechodzi na drugi plan, gdy nie jest już potrzebne wyświetlanie podglądu kamery, oraz gdy inna aktywność ma pierwszeństwo, np. przychodzący połączenie wideo.

Na koniec CameraX obsługuje obracanie i skalowanie bez potrzeby dodawania dodatkowego kodu. W przypadku Activity z odblokowaną orientacją konfiguracja UseCase jest wykonywana za każdym razem, gdy urządzenie zostanie obrócone, ponieważ system niszczy i ponownie tworzy Activity po zmianie orientacji. W rezultacie UseCases ustawia domyślnie dopasowanie obrotu do orientacji wyświetlacza. Więcej informacji o obrotach w CameraX

Zanim przejdziemy do szczegółów, przyjrzyjmy się ogólnie interfejsom API CameraXUseCase i aplikacji Camera1. (Koncepcje dotyczące aparatu X są w kolorze niebieskim, a koncepcje dotyczące aparatu 1 – w kolorze zielonym.)

CameraX

Konfiguracja CameraController / CameraProvider
Podgląd ImageCapture VideoCapture ImageAnalysis
Zarządzanie powierzchnią podglądu i jej ustawieniami w aparacie Ustaw PictureCallback i wywołaj takePicture() w Aparacie Zarządzanie konfiguracją aparatu i MediaRecorder w określonej kolejności niestandardowy kod analizy utworzony na podstawie interfejsu Surface w wersji podglądu;
Kod dla konkretnego urządzenia
Zarządzanie rotacją i skalowaniem urządzeń
Zarządzanie sesją kamery (wybór kamery, zarządzanie cyklem życia)

Camera1

Zgodność i wydajność w aplikacji CameraX

CameraX obsługuje urządzenia z Androidem 5.0 (poziom interfejsu API 21) lub nowszym. To ponad 98% dotychczasowych 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 fizycznych urządzeń z Androidem we wszystkich wersjach od 5.0. Możesz przejrzeć pełną listę urządzeń obecnie używanych w Test Lab.

CameraX używa Executor do obsługi zestawu kamer. Jeśli Twoja aplikacja ma określone wymagania dotyczące wątków, możesz ustawić własny wykonawca w CameraX. Jeśli nie jest ustawiony, CameraX tworzy i używa domyślnego wewnętrznego Executor. Wiele interfejsów API platformy, na której opiera się CameraX, wymaga blokowania komunikacji międzyprocesowej (IPC) z urządzeniami, na odpowiedź których czasami trzeba czekać setki milisekund. Z tego powodu CameraX wywołuje te interfejsy API tylko z wątków w tle, co zapewnia, że wątek główny nie jest zablokowany i interfejs użytkownika pozostaje płynny. Więcej informacji o wątkach

Jeśli na rynku docelowym Twojej aplikacji znajdują się urządzenia niskiej klasy, CameraX umożliwia skrócenie czasu konfiguracji za pomocą ogranicznika kamery. Proces łączenia z urządzeniami może zająć sporo czasu, zwłaszcza na urządzeniach niskiej klasy. Możesz więc określić zestaw kamer, których potrzebuje Twoja aplikacja. Aplikacja CameraX łączy się z tymi kamerami tylko podczas konfiguracji. Jeśli na przykład aplikacja korzysta tylko z tylnego aparatu, może ustawić tę konfigurację za pomocą DEFAULT_BACK_CAMERA, a następnie CameraX uniknie inicjowania przednich aparatów, aby zmniejszyć opóźnienie.

Koncepcje związane z tworzeniem aplikacji na Androida

W tym przewodniku zakładamy, że znasz podstawy programowania aplikacji na Androida. Oprócz podstaw warto poznać kilka pojęć, które pomogą Ci zrozumieć kod poniżej:

Przenoszenie w typowych sytuacjach

Z tej sekcji dowiesz się, 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 warto dodać do aplikacji aparatu, jest możliwość wyboru różnych kamer.

Camera1

W Camera1 możesz wywołać metodę Camera.open() bez parametrów, aby otworzyć pierwszy tylny aparat, lub możesz przekazać identyfikator całkowity aparatu, który chcesz otworzyć. 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: CameraController

W CameraX wybór aparatu jest obsługiwany przez klasę CameraSelector. CameraX ułatwia korzystanie z domyślnego aparatu. Możesz określić, czy chcesz użyć domyślnej przedniej kamery czy domyślnej tylnej kamery. Ponadto obiekt CameraControl w CameraX umożliwia łatwe ustawienie poziomu powiększenia w aplikacji. Jeśli aplikacja działa na urządzeniu obsługującym logiczne aparaty, przełączy się na odpowiedni obiektyw.

Oto kod CameraX do korzystania z domyślnego tylnego aparatu w ramach 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 (dostępne są opcje 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ć w CameraX, jeśli używasz CameraProvider, wywołując getAvailableCameraInfos(), co daje Ci obiekt CameraInfo do sprawdzania niektórych właściwości aparatu, takich jak isFocusMeteringSupported(). Następnie możesz przekształcić go w element CameraSelector, który będzie używany w sposób podobny do pokazanego w powyższych przykładach z metodą CameraInfo.getCameraSelector().

Więcej informacji o poszczególnych kamerach znajdziesz w klasie Camera2CameraInfo. Wywołaj funkcję getCameraCharacteristic() z kluczem do wybranych danych z kamery. Sprawdź klasę CameraCharacteristics, aby zobaczyć listę wszystkich kluczy, których możesz używać w zapytaniach.

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 do obsługi kamery musi w jakimś momencie wyświetlić obraz z kamery na ekranie. W przypadku Camera1 musisz prawidłowo zarządzać wywołaniami zwrotnymi cyklu życia, a także określić obrót i powiększenie podglądu.

Dodatkowo w Kamera1 musisz zdecydować, czy chcesz użyć jako powierzchni podglądu TextureView czy SurfaceView. Oba rozwiązania mają swoje wady, a w obu przypadkach Camera1 wymaga prawidłowego obsługiwania obracania i powiększania. Z drugiej strony, interfejs PreviewView w CameraX ma implementacje zarówno dla TextureView, jak i SurfaceView. CameraX wybiera najlepszą implementację na podstawie takich czynników jak typ urządzenia i wersja Androida, na której działa aplikacja. Jeśli którakolwiek z implementacji jest zgodna, możesz określić preferowany format za pomocą parametru PreviewView.ImplementationMode. Opcja COMPATIBLE używa TextureView do podglądu, 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 aparatu do aplikacji. Następnie, zanim rozpoczniesz podgląd obrazu na żywo, klasa Preview musi zostać przekazana 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 masz znacznie mniej elementów do zarządzania. Jeśli używasz reguły CameraController, musisz też użyć reguły PreviewView. Oznacza to, że Preview UseCase jest domyślnie ustawiony, 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

Dzięki funkcji CameraProvider w CameraX nie musisz używać funkcji PreviewView, ale znacznie upraszcza to konfigurowanie podglądu w porównaniu z Camera1. W tym przykładzie na potrzeby demonstracji użyto funkcji PreviewView, ale jeśli masz bardziej złożone potrzeby, możesz napisać niestandardową funkcję SurfaceProvider i przekazać ją do funkcji setSurfaceProvider().

W tym przypadku Preview UseCase nie jest domyślnie ustawiony 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()
    }
}

Dotknij, aby wyostrzyć

Gdy podgląd kamery jest widoczny na ekranie, można ustawić punkt ostrości, gdy użytkownik kliknie podgląd.

Camera1

Aby w aplikacji Camera1 wdrożyć funkcję „dotknij, aby ustawić ostrość”, musisz obliczyć optymalny punkt ostrości Area, aby wskazać, gdzie Camera ma się skupić. Ten element Area jest przekazywany do elementu setFocusAreas(). Musisz też ustawić zgodny tryb fokusa na urządzeniu Camera. Obszar skupienia ma zastosowanie tylko wtedy, gdy bieżący tryb skupienia 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 1–1000, która służy do ustalania priorytetów Areas, jeśli ustawiono kilka takich wartości. W tym przykładzie jest używana tylko jedna wartość Area, więc wartość wagi nie ma znaczenia. Współrzędne prostokąta mieszczą się w zakresie od -1000 do 1000. Punkt w lewym górnym rogu ma współrzędne (-1000, -1000). Punkt w prawym dolnym rogu ma współrzędne (1000, 1000). Kierunek jest określany względem orientacji czujnika, czyli tego, co widzi czujnik. Kierunek nie jest zależny od obracania ani lustrzanego odbicia Camera.setDisplayOrientation(), więc musisz przekształcić współrzędne zdarzenia dotykowego w 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 wyśrodkować”. Funkcję „dotknij, aby wyśrodkować” można włączyć i wyłączyć za pomocą metody setTapToFocusEnabled(), a wartość można sprawdzić za pomocą odpowiedniej metody gettera isTapToFocusEnabled().

Metoda getTapToFocusState() zwraca obiekt LiveData, który służy do śledzenia zmian stanu fokusa 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

Aby korzystać z funkcji „dotknij, aby wyśrodkować”, musisz najpierw skonfigurować CameraProvider. W tym przykładzie założono, że używasz tagu PreviewView. Jeśli nie, musisz dostosować logikę do zastosowania w przypadku niestandardowego Surface.

Oto, co należy zrobić, gdy używasz PreviewView:

  1. Skonfiguruj detektor gestów, aby obsługiwał zdarzenia dotyku.
  2. W przypadku zdarzenia dotknięcia utwórz MeteringPoint za pomocą funkcji MeteringPointFactory.createPoint().
  3. Utwórz FocusMeteringAction za pomocą MeteringPoint.
  4. Gdy obiekt CameraControl jest dostępny w Twoim Camera (zwrócony z bindToLifecycle()), wywołaj funkcję startFocusAndMetering(), przekazując jej argument FocusMeteringAction.
  5. (Opcjonalnie) Odpowiedz na FocusMeteringResult.
  6. PreviewView.setOnTouchListener() ustaw, aby wykrywanie gestów reagowało na zdarzenia dotykowe.
// 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
}

Rozciągnij, aby powiększyć

Powiększanie i pomniejszanie podglądu to kolejna powszechna metoda bezpośredniej manipulacji podglądem kamery. Wraz ze wzrostem liczby aparatów na urządzeniach użytkownicy oczekują, że obiektyw o najlepszej ogniskowej zostanie automatycznie wybrany w wyniku przybliżenia.

Camera1

Zdjęcia można powiększać na 2 sposoby. Metoda Camera.startSmoothZoom() animuje przejście od bieżącego poziomu powiększenia do podanego przez Ciebie poziomu powiększenia. Metoda Camera.Parameters.setZoom() przechodzi bezpośrednio do poziomu powiększenia, który podasz. Zanim użyjesz którejkolwiek z nich, zadzwoń odpowiednio do isSmoothZoomSupported() lub isZoomSupported(), aby upewnić się, że potrzebne metody powiększenia są dostępne w aparacie.

Aby zaimplementować funkcję powiększania przez zbliżanie, przykład używa setZoom(), ponieważ w miarę wykonywania przez użytkownika gestu zbliżania na powierzchni podglądu ciągle wywołuje on zdarzenia, które aktualizują poziom powiększenia natychmiast po każdym takim geście. Poniżej zdefiniowano klasę ZoomTouchListener, która powinna być ustawiona jako wywołanie zwrotne dla interfejsu dotykowego 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 wyostrzyć”, CameraController reaguje na zdarzenia dotykowe w PreviewView, aby automatycznie obsługiwać powiększanie przez rozciąganie. Możesz włączać i wyłączać funkcję powiększania za pomocą palców za pomocą metody setPinchToZoomEnabled(), a wartość sprawdzać za pomocą odpowiedniej metody gettera isPinchToZoomEnabled().

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

// 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ą gestów działała w aplikacji CameraProvider, musisz ją skonfigurować. Jeśli nie używasz funkcji PreviewView, musisz dostosować logikę do zastosowania w przypadku niestandardowej funkcji Surface.

Oto, co należy zrobić, gdy używasz PreviewView:

  1. Skonfiguruj detektor gestów przesunięcia, aby obsługiwać gesty ściśnięcia.
  2. Pobierz ZoomState z obiektu Camera.CameraInfo, gdzie zwracany jest obiekt Camera, gdy wywołasz bindToLifecycle().
  3. Jeśli element ZoomState ma wartość zoomRatio, zapisz ją jako bieżący współczynnik powiększenia. Jeśli w elementach ZoomState nie ma wartości zoomRatio, użyj domyślnego współczynnika powiększenia aparatu (1,0).
  4. Aby określić nowy współczynnik powiększenia, należy pomnożyć bieżący współczynnik powiększenia przez scaleFactor i przekazać go do CameraControl.setZoomRatio().
  5. PreviewView.setOnTouchListener() ustaw, aby wykrywanie gestów reagowało na zdarzenia dotykowe.
// 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

Z tej sekcji dowiesz się, jak wywołać zrobienie zdjęcia, niezależnie od tego, czy chcesz to zrobić po naciśnięciu przycisku migawki, po upływie czasu lub po innym zdarzeniu.

Camera1

W ramach usługi Camera1 najpierw definiujesz parametr Camera.PictureCallback, aby zarządzać danymi obrazu, gdy zostanie to poproszone. Oto prosty przykład funkcji 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() w obiekcie Camera. Ta metoda takePicture() ma 3 różne parametry dla różnych typów danych. Pierwszy parametr to ShutterCallback (niezdefiniowany w tym przykładzie). Drugi parametr to PictureCallback, który obsługuje dane w postaci nieskompresowanych danych z kamery. Trzeci parametr jest używany w tym przykładzie, 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

Funkcja CameraController w CameraX zachowuje prostotę Camera1 w zakresie rejestrowania obrazu, ponieważ implementuje własną metodę takePicture(). Tutaj zdefiniuj funkcję konfigurowania wpisu MediaStore i zrób zdjęcie, które ma zostać 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

Robienie zdjęć za pomocą CameraProvider działa prawie tak samo jak w przypadku CameraController, ale najpierw musisz utworzyć i powiązać ImageCapture UseCase, aby mieć obiekt, w którym można wywołać funkcję 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 chcesz zrobić zdjęcie, możesz zadzwonić do ImageCapture.takePicture(). Pełny przykład funkcji takePhoto() znajdziesz w tym dziale w kodzie 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ż do tej pory analizowane scenariusze. Każdy etap procesu musi być prawidłowo skonfigurowany, zwykle w określonej kolejności. Może też być konieczne sprawdzenie, czy film i dźwięk są zsynchronizowane, lub rozwiązanie dodatkowych problemów z urządzeniem.

Jak zobaczysz, CameraX ponownie zadba o wiele z tych złożonych kwestii.

Camera1

Nagrywanie filmów za pomocą Camera1 wymaga ostrożnego zarządzania metodami CameraMediaRecorder. Metody te muszą być wywoływane w określonej kolejności. Aby aplikacja działała prawidłowo, musisz postępować zgodnie z tym porządkiem:

  1. Otwórz aparat.
  2. Przygotuj i uruchom podgląd (jeśli aplikacja wyświetla nagrywany film, co zwykle ma miejsce).
  3. Odblokuj kamerę na potrzeby MediaRecorder, dzwoniąc na numer Camera.unlock().
  4. Skonfiguruj nagrywanie, wywołując te metody na MediaRecorder:
    1. Połącz instancję Camera z setCamera(camera).
    2. Będziesz dzwonić pod numer setAudioSource(MediaRecorder.AudioSource.CAMCORDER).
    3. Będziesz dzwonić pod numer 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. Będziesz dzwonić pod numer setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString()).
    6. Jeśli Twoja aplikacja ma podgląd wideo, zadzwoń pod numersetPreviewDisplay(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. Aby dokończyć konfigurowanie MediaRecorder, zadzwoń pod numer prepare().
  5. Aby rozpocząć nagrywanie, zadzwoń pod numer MediaRecorder.start().
  6. Aby zatrzymać nagrywanie, wywołaj te metody. Ponownie postępuj zgodnie z tą samą kolejnością:
    1. Będziesz dzwonić pod numer MediaRecorder.stop().
    2. Opcjonalnie możesz usunąć bieżącą konfigurację MediaRecorder, wywołując funkcję MediaRecorder.reset().
    3. Będziesz dzwonić pod numer MediaRecorder.release().
    4. Zablokuj kamerę, aby w przyszłości sesje MediaRecorder mogły z niej korzystać, wywołując Camera.lock().
  7. Aby zatrzymać podgląd, zadzwoń pod numer Camera.stopPreview().
  8. Aby uwolnić Camera, aby inne procesy mogły z niego korzystać, wywołaj funkcję Camera.release().

Oto wszystkie te czynności w połączeniu:

// 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 w ramach CameraX możesz niezależnie przełączać ImageCapture, VideoCaptureImageAnalysis UseCase, o ile tylko lista przypadków użycia może być używana jednocześnie. Domyślnie funkcje ImageCaptureImageAnalysis UseCase są włączone, więc nie musisz dzwonić do setEnabledUseCases(), aby zrobić zdjęcie.

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

// CameraX: Enable VideoCapture UseCase on CameraController.

cameraController.setEnabledUseCases(VIDEO_CAPTURE);

Aby rozpocząć nagrywanie filmu, możesz wywołać funkcję CameraController.startRecording(). Ta funkcja umożliwia zapisanie nagranego filmu w File, jak widać na przykładzie poniżej. Dodatkowo musisz przekazać Executor i klasę, która implementuje OnVideoSavedCallback, aby obsługiwać wywołania zwrotne powodzenia i błędu. Gdy nagranie ma się zakończyć, zadzwoń pod numer CameraController.stopRecording().

Uwaga: jeśli używasz CameraX 1.3.0-alpha02 lub nowszej wersji, dostępny jest dodatkowy parametr AudioConfig, który umożliwia włączenie lub wyłączenie 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 została usunięta metoda stopRecording(), a metoda 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óra umożliwia wyświetlanie treści w przypadku, gdy urządzenie nie może spełnić wymagań dotyczących jakości. Następnie powiązać instancję VideoCaptureCameraProvider za pomocą innych 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 tej chwili usługa Recorder jest dostępna w usłudze videoCapture.output. Użytkownik Recorder może nagrywać filmy, które są zapisywane w urządzeniu File, ParcelFileDescriptor lub MediaStore. W tym przykładzie użyto znacznika MediaStore.

W przypadku Recorder możesz użyć kilku metod, aby go przygotować. Wybierz opcję prepareRecording(), aby skonfigurować opcje wyjściowe MediaStore. Jeśli aplikacja ma uprawnienia do korzystania z mikrofonu urządzenia, zadzwoń też na numer withAudioEnabled(). Następnie wywołaj funkcję start(), aby rozpocząć nagrywanie, przekazując kontekst i słuchacza zdarzeń Consumer<VideoRecordEvent>, który będzie obsługiwać zdarzenia nagrywania wideo. Jeśli operacja się powiedzie, zwrócona wartość Recording może służyć do wstrzymywania, wznawiania i zatrzymywania 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

repozytorium GitHub z przykładowymi aplikacjami CameraX znajdziesz kilka kompletnych aplikacji. Te przykłady pokazują, jak scenariusze opisane w tym przewodniku pasują do pełnej aplikacji na Androida.

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