Esegui la migrazione da Camera1 a CameraX

Se la tua app utilizza la classe Camera originale ("Fotocamera1"), che è stata ritirata a partire da Android 5.0 (livello API 21), ti consigliamo vivamente di passare a un'API Android Camera moderna. Android offre FotocameraX (un'API per fotocamera Jetpack standardizzata e robusta) e Fotocamera2 (un'API framework di basso livello). Nella maggior parte dei casi, consigliamo di eseguire la migrazione dell'app a CameraX. Di seguito le ragioni:

  • Facilità d'uso: CameraX gestisce i dettagli di basso livello, così puoi concentrarti meno sulla creazione di un'esperienza con la fotocamera da zero e più sulla differenziazione dell'app.
  • FotocameraX gestisce la frammentazione al posto tuo: CameraX riduce i costi di manutenzione a lungo termine e il codice specifico per dispositivo, offrendo esperienze di qualità superiore agli utenti. Per ulteriori informazioni, leggi il nostro post del blog Migliore compatibilità dei dispositivi con CameraX.
  • Funzionalità avanzate: CameraX è stata progettata con cura per semplificare l'integrazione di funzionalità avanzate nella tua app. Ad esempio, puoi applicare facilmente le funzionalità Bokeh, ritocco viso, HDR (High Dynamic Range) e scarsa luminosità Modalità di acquisizione notturna alle tue foto con le estensioni CameraX.
  • Updatabilità: Android rilascia nuove funzionalità e correzioni di bug per CameraX nel corso dell'anno. Se esegui la migrazione a CameraX, la tua app riceve la tecnologia della fotocamera Android più recente a ogni release di CameraX, non solo per le release annuali della versione di Android.

In questa guida troverai scenari comuni relativi alle applicazioni della fotocamera. Ogni scenario include un'implementazione di Camera1 e un'implementazione di CameraX per il confronto.

Quando si tratta di migrazione, a volte è necessaria una maggiore flessibilità per l'integrazione con un codebase esistente. Tutto il codice di CameraX in questa guida include un'implementazione di CameraController, ideale se vuoi il modo più semplice per usare CameraX, e anche un'implementazione di CameraProvider, ideale se hai bisogno di maggiore flessibilità. Per aiutarti a scegliere quella più adatta a te, ecco i vantaggi di ognuna:

Controller della videocamera

Fornitore Fotocamera

Richiede poco codice di configurazione Consente un maggiore controllo
Consentendo a CameraX di gestire una parte maggiore del processo di configurazione, funzionalità come il tocco per mettere a fuoco e il pizzico per lo zoom verranno attivate automaticamente Poiché lo sviluppatore dell'app gestisce la configurazione, ci sono più opportunità di personalizzare la configurazione, ad esempio attivare la rotazione dell'immagine di output o impostare il formato dell'immagine di output in ImageAnalysis
La richiesta di PreviewView per l'anteprima della fotocamera consente a CameraX di offrire un'integrazione end-to-end senza interruzioni, come nell'integrazione del nostro ML Kit, che può mappare le coordinate dei risultati del modello ML (come i riquadri di delimitazione dei volti) direttamente sulle coordinate di anteprima La possibilità di utilizzare una "Surface" personalizzata per l'anteprima della fotocamera consente una maggiore flessibilità, ad esempio l'utilizzo del codice "Surface" esistente che potrebbe essere un input per altre parti della tua app

Se non riesci a eseguire la migrazione, contattaci tramite il Gruppo di discussione di CameraX.

Prima di eseguire la migrazione

Confronta l'utilizzo di CameraX con Camera1

Anche se il codice può avere un aspetto diverso, i concetti di base di Camera1 e FotocameraX sono molto simili. CameraX astrae le funzionalità più comuni della videocamera nei casi d'uso e, di conseguenza, molte attività lasciate allo sviluppatore in Camera1 vengono gestite automaticamente da CameraX. In CameraX sono disponibili quattro elementi UseCase che puoi utilizzare per diverse attività della fotocamera: Preview, ImageCapture, VideoCapture e ImageAnalysis.

Un esempio di CameraX che gestisce i dettagli di basso livello per gli sviluppatori è il ViewPort condiviso tra gli UseCase attivi. In questo modo tutti i UseCase vedono esattamente gli stessi pixel. In Fotocamera1 devi gestire autonomamente questi dettagli e, considerata la variabilità delle proporzioni tra i sensori e gli schermi della fotocamera dei dispositivi, può essere difficile assicurarti che l'anteprima corrisponda a foto e video acquisiti.

Per fare un altro esempio, CameraX gestisce automaticamente i callback Lifecycle nell'istanza Lifecycle in cui la passi. Ciò significa che CameraX gestisce la connessione dell'app alla videocamera durante l'intero ciclo di vita dell'attività Android, inclusi i seguenti casi: chiusura della videocamera quando l'app entra in background, rimozione dell'anteprima della videocamera quando non viene più visualizzata sullo schermo e messa in pausa dell'anteprima della fotocamera quando un'altra attività ha la precedenza, ad esempio una videochiamata in arrivo.

Infine, CameraX gestisce la rotazione e la scalabilità senza richiedere alcun codice aggiuntivo da parte tua. Nel caso di un oggetto Activity con orientamento sbloccato, la configurazione di UseCase viene eseguita ogni volta che il dispositivo viene ruotato, poiché il sistema distrugge e ricrea il Activity quando cambia l'orientamento. In questo modo UseCases imposta ogni volta la rotazione target in modo che corrisponda all'orientamento del display per impostazione predefinita. Scopri di più sulle rotazioni in FotocameraX.

Prima di entrare nei dettagli, diamo uno sguardo generale alle funzionalità UseCase di CameraX e al legame tra un'app Camera1. (I concetti di CameraX sono in blu e quelli di Camera1 sono in verde).

FotocameraX

CameraController / Configurazione CameraProvider
Anteprima Acquisizione immagine Acquisizione video Analisi di immagini
Gestisci la Surface di anteprima e impostala sulla Fotocamera Imposta PictureCallback e chiama takePicture() sulla fotocamera Gestisci la configurazione della videocamera e di MediaRecorder in un ordine specifico Codice di analisi personalizzato basato sulla Surface di anteprima
Codice specifico per dispositivo
Gestione della rotazione e della scalabilità dei dispositivi
Gestione della sessione della videocamera (selezione della videocamera, gestione del ciclo di vita)

Fotocamera1

Compatibilità e prestazioni in CameraX

CameraX supporta dispositivi con Android 5.0 (livello API 21) e versioni successive. oltre il 98% dei dispositivi Android esistenti. CameraX è progettata per gestire automaticamente le differenze tra i dispositivi, riducendo la necessità di utilizzare un codice specifico per dispositivo nell'app. Inoltre, nel nostro CameraX Test Lab testiamo oltre 150 dispositivi fisici su tutte le versioni di Android a partire dalla 5.0. Puoi consultare l'elenco completo dei dispositivi attualmente in Test Lab.

CameraX utilizza un elemento Executor per controllare lo stack di fotocamere. Puoi impostare un tuo esecutore personalizzato su CameraX se la tua app ha requisiti specifici di threading. Se non viene configurato, CameraX crea e utilizza un'impostazione Executor interna predefinita ottimizzata. Molte delle API della piattaforma su cui è stata sviluppata CameraX richiedono il blocco della comunicazione tra processi (IPC) con hardware la cui risposta a volte può richiedere centinaia di millisecondi. Per questo motivo, CameraX chiama queste API solo dai thread in background, il che garantisce che il thread principale non sia bloccato e che l'interfaccia utente rimanga scorrevole. Scopri di più sui thread.

Se il mercato di destinazione della tua app include dispositivi di fascia bassa, CameraX offre un modo per ridurre i tempi di configurazione con un limitatore per fotocamera. Poiché il processo di connessione ai componenti hardware può richiedere un tempo non banale, soprattutto sui dispositivi di fascia bassa, puoi specificare il set di videocamere di cui la tua app ha bisogno. CameraX si connette a queste videocamere solo durante la configurazione. Ad esempio, se l'applicazione utilizza solo fotocamere posteriori, può impostare questa configurazione con DEFAULT_BACK_CAMERA, dopodiché CameraX evita di inizializzare le fotocamere anteriori per ridurre la latenza.

Concetti di sviluppo di Android

Questa guida presuppone una conoscenza generale dello sviluppo di Android. Oltre alle nozioni di base, ecco un paio di concetti che è utile comprendere prima di passare al codice riportato di seguito:

  • L'associazione delle viste genera una classe di associazione per i file di layout XML, che ti consente di fare riferimento facilmente alle tue viste in Attività, come avviene in diversi snippet di codice riportati di seguito. Esistono alcune differenze tra l'associazione della vista e findViewById() (il metodo precedente per fare riferimento alle viste), ma nel codice seguente dovresti essere in grado di sostituire le righe di associazione della vista con una chiamata findViewById() simile.
  • Le coroutine asincrone sono un modello di progettazione di contemporaneità aggiunto in Kotlin 1.3 che può essere utilizzato per gestire i metodi CameraX che restituiscono un valore ListenableFuture. Ciò è reso più semplice con la libreria Jetpack Concurrent a partire dalla versione 1.1.0. Per aggiungere una coroutine asincrona alla tua app:
    1. Aggiungi implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0") al file Gradle.
    2. Inserisci qualsiasi codice CameraX che restituisca ListenableFuture in un blocco launch o in una funzione di sospensione.
    3. Aggiungi una chiamata await() alla chiamata funzione che restituisce ListenableFuture.
    4. Per una comprensione più approfondita di come funzionano le coroutine, consulta la guida Iniziare una coroutine.

Esegui la migrazione degli scenari comuni

Questa sezione spiega come eseguire la migrazione degli scenari comuni da Fotocamera1 a CameraX. Ogni scenario riguarda un'implementazione di Camera1, CameraProvider di CameraX e un'implementazione di CameraController di CameraX.

Selezione di una videocamera

Nell'applicazione della fotocamera, una delle prime opzioni che proponiamo è un modo per selezionare diverse videocamere.

Fotocamera1

In Fotocamera1, puoi chiamare Camera.open() senza parametri per aprire la prima fotocamera posteriore oppure passare un ID intero per la videocamera che vuoi aprire. Ecco un esempio di come potrebbe essere strutturata:

// 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: controller della videocamera

In CameraX, la selezione della fotocamera viene gestita dalla classe CameraSelector. CameraX semplifica l'uso comune della fotocamera predefinita. Puoi specificare se vuoi la fotocamera anteriore predefinita o quella posteriore. Inoltre, l'oggetto CameraControl di CameraX ti consente di impostare facilmente il livello di zoom della tua app, quindi se l'app è in esecuzione su un dispositivo che supporta le fotocamere logiche, passerà all'obiettivo corretto.

Ecco il codice CameraX per utilizzare la fotocamera posteriore predefinita con CameraController:

// CameraX: select a camera with CameraController

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

FotocameraX: CameraProvider

Ecco un esempio di selezione della fotocamera anteriore predefinita con CameraProvider (la fotocamera anteriore o posteriore può essere utilizzata con CameraController o 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()
    }
}

Se vuoi controllare quale videocamera è selezionata, puoi farlo anche in FotocameraX se utilizzi CameraProvider chiamando getAvailableCameraInfos(), che ti fornisce un oggetto CameraInfo per controllare determinate proprietà della videocamera, come isFocusMeteringSupported(). Puoi quindi convertirlo in CameraSelector da utilizzare come negli esempi precedenti con il metodo CameraInfo.getCameraSelector().

Puoi ottenere ulteriori dettagli su ogni videocamera utilizzando il corso Camera2CameraInfo. Chiama il numero getCameraCharacteristic() con una chiave per i dati della videocamera che ti interessano. Controlla la classe CameraCharacteristics per un elenco di tutte le chiavi su cui puoi eseguire query.

Ecco un esempio di utilizzo di una funzione checkFocalLength() personalizzata che potresti definire personalmente:

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

Visualizzazione di un'anteprima

La maggior parte delle applicazioni per videocamere deve mostrare il feed della videocamera sullo schermo in un determinato momento. Con Camera1, devi gestire correttamente i callback del ciclo di vita e determinare anche la rotazione e il ridimensionamento dell'anteprima.

Inoltre, in Fotocamera1 devi decidere se utilizzare TextureView o SurfaceView come piattaforma di anteprima. Entrambe le opzioni presentano compromessi e, in entrambi i casi, Fotocamera1 richiede di gestire correttamente la rotazione e la scalabilità. PreviewView di CameraX, invece, ha implementazioni sottostanti sia per TextureView che per SurfaceView. CameraX decide quale implementazione è migliore in base a fattori quali il tipo di dispositivo e la versione di Android su cui è in esecuzione la tua app. Se una delle due implementazioni è compatibile, puoi dichiarare la tua preferenza con PreviewView.ImplementationMode. L'opzione COMPATIBLE utilizza un valore TextureView per l'anteprima e il valore PERFORMANCE utilizza un valore SurfaceView (se possibile).

Fotocamera1

Per mostrare un'anteprima, devi scrivere la tua classe Preview con un'implementazione dell'interfaccia android.view.SurfaceHolder.Callback, che viene utilizzata per trasferire i dati immagine dall'hardware della fotocamera all'applicazione. Quindi, prima di poter avviare l'anteprima dell'immagine live, è necessario passare la classe Preview all'oggetto 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: controller della videocamera

In CameraX c'è molto meno da gestire per gli sviluppatori. Se utilizzi CameraController, devi utilizzare anche PreviewView. Ciò significa che è implicito Preview UseCase, rendendo molto meno efficiente la configurazione:

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

FotocameraX: CameraProvider

Con CameraProvider di CameraX, non è necessario utilizzare PreviewView, ma semplifica notevolmente la configurazione dell'anteprima rispetto a Camera1. A fini dimostrativi, questo esempio utilizza un PreviewView, ma puoi scrivere una SurfaceProvider personalizzata da passare in setSurfaceProvider() se hai esigenze più complesse.

In questo caso, il valore di Preview UseCase non è implicito come lo è con CameraController, quindi devi configurarlo:

// 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()
    }
}

Tocca per mettere a fuoco

Quando l'anteprima della fotocamera è sullo schermo, un controllo comune consiste nell'impostare il punto di messa a fuoco quando l'utente tocca l'anteprima.

Fotocamera1

Per implementare la funzionalità Tocca per mettere a fuoco nella Fotocamera1, devi calcolare la messa a fuoco ottimale Area per indicare il punto in cui Camera deve tentare di mettere a fuoco. Questo Area viene passato in setFocusAreas(). Inoltre, devi impostare una modalità di messa a fuoco compatibile su Camera. L'area di messa a fuoco ha effetto solo se la modalità di messa a fuoco corrente è FOCUS_MODE_AUTO, FOCUS_MODE_MACRO, FOCUS_MODE_CONTINUOUS_VIDEO o FOCUS_MODE_CONTINUOUS_PICTURE.

Ogni Area è un rettangolo con il peso specificato. La ponderazione è un valore compreso tra 1 e 1000 e viene utilizzata per dare la priorità a Areas, se sono impostate più opzioni. Questo esempio utilizza solo un'istruzione Area, quindi il valore del peso non è importante. Le coordinate del rettangolo sono comprese tra -1000 e 1000. Il punto in alto a sinistra è (-1000, -1000). Il punto in basso a destra è (1000, 1000). La direzione è relativa all'orientamento del sensore, ovvero a ciò che vede il sensore. La direzione non è influenzata dalla rotazione o dal mirroring di Camera.setDisplayOrientation(), quindi devi convertire le coordinate degli eventi touch nelle coordinate del sensore.

// 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: controller della videocamera

CameraController ascolta gli eventi tocco di PreviewView per gestire automaticamente il tocco per mettere a fuoco. Puoi attivare e disattivare il tocco per mettere a fuoco con setTapToFocusEnabled() e controllare il valore con il getter corrispondente isTapToFocusEnabled().

Il metodo getTapToFocusState() restituisce un oggetto LiveData per il monitoraggio delle modifiche allo stato di impostazione dello stato attivo su 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)

FotocameraX: CameraProvider

Quando si utilizza un CameraProvider, sono necessarie alcune configurazioni per mettere a fuoco tocca per mettere a fuoco. Questo esempio presuppone l'utilizzo di PreviewView. In caso contrario, devi adattare la logica da applicare al tuo Surface personalizzato.

Ecco la procedura da seguire per utilizzare PreviewView:

  1. Configura un rilevatore di gesti per gestire gli eventi di tocco.
  2. Con l'evento di tocco, crea un MeteringPoint utilizzando MeteringPointFactory.createPoint().
  3. Con il MeteringPoint, crea un FocusMeteringAction.
  4. Con l'oggetto CameraControl su Camera (restituito da bindToLifecycle()), chiama startFocusAndMetering(), passando la FocusMeteringAction.
  5. (Facoltativo) Rispondi alla FocusMeteringResult.
  6. Imposta il rilevatore di gesti per rispondere agli eventi di tocco in PreviewView.setOnTouchListener().
// CameraX: implement tap-to-focus with CameraProvider.

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

...

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

Pizzica per eseguire lo zoom

Aumentare e diminuire lo zoom di un'anteprima è un'altra manipolazione diretta comune dell'anteprima della fotocamera. Con il numero crescente di fotocamere sui dispositivi, gli utenti si aspettano anche che venga selezionato automaticamente l'obiettivo con la lunghezza focale migliore in seguito allo zoom.

Fotocamera1

Esistono due modi per eseguire lo zoom con Fotocamera1. Il metodo Camera.startSmoothZoom() consente di passare dal livello di zoom corrente a quello passato. Il metodo Camera.Parameters.setZoom() passa direttamente al livello di zoom inserito. Prima di utilizzare uno dei due, chiama rispettivamente isSmoothZoomSupported() o isZoomSupported() per assicurarti che i relativi metodi di zoom necessari siano disponibili sulla Fotocamera.

Per implementare la funzionalità di pizzicatura per eseguire lo zoom, in questo esempio viene utilizzato setZoom() perché l'ascoltatore al tocco sulla superficie di anteprima attiva continuamente eventi man mano che si esegue il gesto di pizzicatura, quindi aggiorna immediatamente il livello di zoom ogni volta. La classe ZoomTouchListener viene definita di seguito e deve essere impostata come callback per il listener touch della superficie di anteprima.

// 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: controller della videocamera

Come per il tocco per mettere a fuoco, CameraController ascolta gli eventi touch di PreviewView per gestire automaticamente lo zoom tramite pizzico. Puoi abilitare e disabilitare la funzionalità Pizzica per eseguire lo zoom con setPinchToZoomEnabled() e verificare il valore con il getter corrispondente isPinchToZoomEnabled().

Il metodo getZoomState() restituisce un oggetto LiveData per il monitoraggio delle modifiche a ZoomState nella 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)

FotocameraX: CameraProvider

Per utilizzare la funzionalità Pizzica per eseguire lo zoom con CameraProvider, è necessaria una configurazione. Se non utilizzi PreviewView, devi adattare la logica in modo da applicarla alla Surface personalizzata.

Ecco la procedura da seguire per utilizzare PreviewView:

  1. Configura un rilevatore dei gesti di ridimensionamento per gestire gli eventi di pizzicatura.
  2. Ottieni ZoomState dall'oggetto Camera.CameraInfo, dove viene restituita l'istanza Camera quando chiami bindToLifecycle().
  3. Se ZoomState ha un valore zoomRatio, salvalo come rapporto di zoom corrente. Se non è presente zoomRatio su ZoomState, utilizza la velocità di zoom predefinita della videocamera (1,0).
  4. Prendi il prodotto del rapporto di zoom corrente con il scaleFactor per determinare il nuovo rapporto di zoom e trasmettilo a CameraControl.setZoomRatio().
  5. Imposta il rilevatore di gesti per rispondere agli eventi di tocco in PreviewView.setOnTouchListener().
// CameraX: implement pinch-to-zoom with CameraProvider.

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

...

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

Scatto di una foto

Questa sezione mostra come attivare l'acquisizione di foto, a prescindere dal fatto che sia necessario premere il pulsante di scatto, dopo che è trascorso un timer o in qualsiasi altro evento di tua scelta.

Fotocamera1

In Fotocamera1, devi prima definire un Camera.PictureCallback per gestire i dati relativi all'immagine quando vengono richiesti. Ecco un semplice esempio di PictureCallback per la gestione dei dati di immagini 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)
    }
}

Ogni volta che vuoi scattare una foto, chiama il metodo takePicture() sull'istanza Camera. Questo metodo takePicture() ha tre diversi parametri per tipi di dati diversi. Il primo parametro è per un elemento ShutterCallback (non definito in questo esempio). Il secondo parametro serve a PictureCallback per gestire i dati non elaborati (non compressi) della videocamera. Il terzo parametro è quello utilizzato in questo esempio, poiché è un PictureCallback per gestire i dati dell'immagine JPEG.

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

camera?.takePicture(null, null, picture)

CameraX: controller della videocamera

CameraController di CameraX mantiene la semplicità di Camera1 per l'acquisizione di immagini implementando un proprio metodo takePicture(). Qui, definisci una funzione per configurare una voce MediaStore e scattare una foto da salvare lì.

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

FotocameraX: CameraProvider

Scattare una foto con CameraProvider funziona quasi nello stesso modo che con CameraController, ma devi prima creare e associare un ImageCapture UseCase per avere un oggetto su cui chiamare 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)

Poi, ogni volta che vuoi scattare una foto, puoi chiamare ImageCapture.takePicture(). Consulta il codice CameraController in questa sezione per un esempio completo della funzione takePhoto().

// 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(
        ...
    )
}

Registrazione di un video

Registrare un video è notevolmente più complicato rispetto agli scenari esaminati finora. Ogni parte del processo deve essere impostata correttamente, solitamente in un ordine particolare. Inoltre, potrebbe essere necessario verificare che il video e l'audio siano sincronizzati o risolvere ulteriori incongruenze con il dispositivo.

Come vedrai, di nuovo CameraX gestisce gran parte di questa complessità al posto tuo.

Fotocamera1

L'acquisizione video con Fotocamera1 richiede un'attenta gestione di Camera e MediaRecorder e i metodi devono essere richiamati in un ordine particolare. Affinché l'applicazione funzioni correttamente, devi seguire questo ordine:

  1. Apri la fotocamera.
  2. Prepara e avvia un'anteprima (se la tua app mostra il video registrato, come avviene di solito).
  3. Sblocca la fotocamera per utilizzarla entro il giorno MediaRecorder chiamando il numero Camera.unlock().
  4. Configura la registrazione chiamando questi metodi su MediaRecorder:
    1. Connetti la tua istanza Camera a setCamera(camera).
    2. Chiama il numero setAudioSource(MediaRecorder.AudioSource.CAMCORDER).
    3. Chiama il numero setVideoSource(MediaRecorder.VideoSource.CAMERA).
    4. Chiama setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P)) per impostare la qualità. Consulta la pagina CamcorderProfile per conoscere tutte le opzioni relative alla qualità.
    5. Chiama il numero setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString()).
    6. Se la tua app dispone di un'anteprima del video, chiama setPreviewDisplay(preview?.holder?.surface).
    7. Chiama il numero setOutputFormat(MediaRecorder.OutputFormat.MPEG_4).
    8. Chiama il numero setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT).
    9. Chiama il numero setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT).
    10. Chiama prepare() per finalizzare la configurazione del tuo MediaRecorder.
  5. Per avviare la registrazione, chiama il numero MediaRecorder.start().
  6. Per interrompere la registrazione, chiama questi metodi. Segui sempre questo ordine esatto:
    1. Chiama il numero MediaRecorder.stop().
    2. Se vuoi, rimuovi l'attuale configurazione di MediaRecorder chiamando MediaRecorder.reset().
    3. Chiama il numero MediaRecorder.release().
    4. Blocca la videocamera in modo che le sessioni MediaRecorder future possano utilizzarla chiamando Camera.lock().
  7. Per interrompere l'anteprima, chiama Camera.stopPreview().
  8. Infine, per rilasciare Camera in modo che altri processi possano utilizzarlo, chiama Camera.release().

Ecco la combinazione di tutti i passaggi:

// 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: controller della videocamera

Con CameraController di CameraX, puoi attivare/disattivare ImageCapture, VideoCapture e ImageAnalysis UseCase in modo indipendente, a patto che l'elenco di casi d'uso possa essere utilizzato contemporaneamente. Gli UseCase ImageCapture e ImageAnalysis sono attivi per impostazione predefinita, motivo per cui non hai dovuto chiamare setEnabledUseCases() per scattare una foto.

Per utilizzare un CameraController per la registrazione video, devi prima usare setEnabledUseCases() per consentire l'UseCase di VideoCapture.

// CameraX: Enable VideoCapture UseCase on CameraController.

cameraController.setEnabledUseCases(VIDEO_CAPTURE);

Quando vuoi iniziare a registrare un video, puoi chiamare la funzione CameraController.startRecording(). Questa funzione può salvare il video registrato in un File, come puoi vedere nell'esempio riportato di seguito. Inoltre, devi passare un Executor e una classe che implementi OnVideoSavedCallback per gestire i callback di successo e di errore. Quando la registrazione dovrebbe terminare, chiama il numero CameraController.stopRecording().

Nota: se utilizzi CameraX 1.3.0-alpha02 o versioni successive, è disponibile un parametro aggiuntivo AudioConfig che consente di attivare o disattivare la registrazione audio del video. Per attivare la registrazione audio, devi assicurarti di disporre delle autorizzazioni di accesso al microfono. Inoltre, il metodo stopRecording() viene rimosso nella versione 1.3.0-alpha02 e startRecording() restituisce un oggetto Recording che può essere utilizzato per mettere in pausa, ripristinare e interrompere la registrazione video.

// 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()
    )
}

FotocameraX: CameraProvider

Se utilizzi un CameraProvider, devi creare un VideoCapture UseCase e passare in un oggetto Recorder. Su Recorder.Builder puoi impostare la qualità video e, facoltativamente, un FallbackStrategy, che gestisce i casi in cui un dispositivo non sia in grado di soddisfare le specifiche sulla qualità desiderate. Quindi associa l'istanza VideoCapture a CameraProvider con gli altri 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)

A questo punto, è possibile accedere a Recorder nella proprietà videoCapture.output. Recorder può avviare registrazioni video salvate in un File, ParcelFileDescriptor o MediaStore. In questo esempio viene utilizzato MediaStore.

Su Recorder, esistono diversi metodi per prepararla. Richiama prepareRecording() per impostare le opzioni di output di MediaStore. Se la tua app è autorizzata a utilizzare il microfono del dispositivo, chiama anche withAudioEnabled(). Quindi, chiama start() per iniziare a registrare, passando in un contesto e un listener di eventi Consumer<VideoRecordEvent> per gestire gli eventi di registrazione video. Se l'operazione ha esito positivo, l'elemento Recording restituito può essere utilizzato per mettere in pausa, riprendere o interrompere la registrazione.

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

Risorse aggiuntive

Abbiamo diverse app CameraX complete nel nostro repository GitHub di esempi di videocamere. Questi esempi mostrano come gli scenari di questa guida si adattano a un'app Android completa.

Se vuoi ricevere ulteriore assistenza per la migrazione a CameraX o hai domande sulla suite di API Android Camera, contattaci tramite il Gruppo di discussione CameraX.