Esegui la migrazione da Camera1 a CameraX

Se la tua app utilizza la classe Camera originale ("Camera1"), ritirata da Android 5.0 (livello API 21), consigliamo vivamente di eseguire l'aggiornamento a un'API della fotocamera Android moderna. Android offre CameraX (un'API per fotocamere Jetpack robusta e standardizzata) e Camera2 (un'API di framework a 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, in modo da poter concentrarti meno sulla creazione di un'esperienza con la fotocamera da zero e più sulla differenziazione della tua app.
  • CameraX gestisce la frammentazione per te: riduce i costi di manutenzione a lungo termine e il codice specifico del dispositivo, offrendo agli utenti esperienze di qualità superiore. Per saperne di più, consulta il nostro post del blog sulla migliore compatibilità dei dispositivi con CameraX.
  • Funzionalità avanzate: CameraX è progettato con attenzione per semplificare l'integrazione di funzionalità avanzate nella tua app. Ad esempio, puoi applicare facilmente effetti Bokeh, ritocco del viso, HDR (High Dynamic Range) e luminosità in condizioni di scarsa illuminazione alla modalità di acquisizione notturna delle tue foto con le Estensioni CameraX.
  • Aggiornamento: Android rilascia nuove funzionalità e correzioni di bug per CameraX throughout the year. Se esegui la migrazione a CameraX, la tua app riceve la tecnologia della fotocamera Android più recente con ogni release di CameraX, non solo con le release annuali della versione di Android.

In questa guida sono riportati scenari comuni per le applicazioni per videocamere. 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 una base di codice esistente. Tutto il codice CameraX in questa guida ha un'implementazione CameraController, ideale se vuoi utilizzare CameraX nel modo più semplice, e un'implementazione CameraProvider, ideale se hai bisogno di maggiore flessibilità. Per aiutarti a decidere qual è la soluzione più adatta a te, ecco i vantaggi di ciascuna:

CameraController

CameraProvider

Richiede poco codice di configurazione Consente un maggiore controllo
Se consenti a CameraX di gestire una parte maggiore della procedura di configurazione, le funzionalità come il tocco per mettere a fuoco e lo zoom con pizzico funzionano automaticamente Poiché la configurazione è gestita dallo sviluppatore dell'app, esistono più opportunità per personalizzare la configurazione, ad esempio attivare la rotazione delle immagini 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 di ML Kit che può mappare le coordinate del risultato del modello ML (come le bounding box del volto) direttamente sulle coordinate di anteprima La possibilità di utilizzare un "Surface" personalizzato 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 dell'app

Se non riesci a eseguire la migrazione, contattaci nel gruppo di discussione CameraX.

Prima di eseguire la migrazione

Confrontare l'utilizzo di CameraX con quello di Camera1

Anche se il codice può sembrare diverso, i concetti alla base di Camera1 e CameraX sono molto simili. CameraX estrae le funzionalità comuni della fotocamera in casi d'uso, e di conseguenza molte attività lasciate allo sviluppatore in Camera1 sono gestita automaticamente da CameraX. In CameraX sono disponibili quattro UseCase che puoi utilizzare per una serie di attività della fotocamera: Preview, ImageCapture, VideoCapture e ImageAnalysis.

Un esempio di gestione dei dettagli di basso livello da parte di CameraX per gli sviluppatori è il ViewPort condiviso tra UseCase attivi. In questo modo, tutti i UseCase vedono esattamente gli stessi pixel. In Fotocamera 1 devi gestire questi dettagli autonomamente e, data la variabilità dei rapporti di aspetto tra i sensori e gli schermi delle fotocamere dei dispositivi, può essere complicato assicurarti che l'anteprima corrisponda alle foto e ai video acquisiti.

Come altro esempio, CameraX gestisce automaticamente i callback Lifecycle sull'istanza Lifecycle che le passi. Ciò significa che CameraX gestisce la connessione della tua app alla videocamera durante l'intero ciclo di vita dell'attività Android, inclusi i seguenti casi: chiusura della videocamera quando l'app passa in background; rimozione dell'anteprima della videocamera quando lo schermo non richiede più la sua visualizzazione; messa in pausa dell'anteprima della videocamera quando un'altra attività ha la precedenza in primo piano, ad esempio una videochiamata in arrivo.

Infine, CameraX gestisce la rotazione e la scalatura senza che tu debba scrivere codice aggiuntivo. Nel caso di un Activity con orientamento sbloccato, la configurazione del UseCase viene eseguita ogni volta che il dispositivo viene ruotato, poiché il sistema distrugge e ricrea il Activity quando cambia l'orientamento. Di conseguenza, la rotazione target di UseCases viene impostata automaticamente in base all'orientamento del display. Scopri di più sulle rotazioni in CameraX.

Prima di entrare nei dettagli, ecco un'analisi generale delle API CameraXUseCase e del rapporto con un'app Camera1. I concetti di CameraX sono in blu e i concetti di Camera1 sono in verde.

CameraX

Configurazione di CameraController / CameraProvider
Anteprima ImageCapture VideoCapture ImageAnalysis
Gestire la superficie di anteprima e impostarla sulla videocamera Imposta PictureCallback e chiama takePicture() su Fotocamera Gestire la configurazione di Camera e MediaRecorder in un ordine specifico Codice di analisi personalizzato creato sulla base della piattaforma di anteprima
Codice specifico per il dispositivo
Gestione della rotazione e del ridimensionamento dei dispositivi
Gestione della sessione della videocamera (selezione della videocamera, gestione del ciclo di vita)

Camera1

Compatibilità e prestazioni in CameraX

CameraX supporta i dispositivi con Android 5.0 (livello API 21) e versioni successive. Ciò rappresenta oltre il 98% dei dispositivi Android esistenti. CameraX è progettato per gestire automaticamente le differenze tra i dispositivi, riducendo la necessità di codice specifico per il 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 esaminare l'elenco completo dei dispositivi attualmente in Test Lab.

CameraX utilizza un Executor per gestire la serie di fotocamere. Puoi impostare il tuo executor su CameraX se la tua app ha requisiti di threading specifici. Se non viene impostato, CameraX crea e utilizza un Executor interno predefinito ottimizzato. Molte delle API di piattaforma su cui è basata CameraX richiedono la comunicazione interprocessuale (IPC) bloccante con hardware che a volte può richiedere centinaia di millisecondi per rispondere. Per questo motivo, CameraX chiama queste API solo dai thread in background, il che garantisce che il thread principale non venga bloccato e che l'interfaccia utente rimanga fluida. 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 della fotocamera. Poiché la procedura di connessione ai componenti hardware può richiedere molto tempo, in particolare sui dispositivi di fascia bassa, puoi specificare l'insieme di videocamere di cui ha bisogno la tua app. 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 e CameraX evita di inizializzare le fotocamere anteriori per ridurre la latenza.

Concetti di sviluppo Android

Questa guida presuppone una conoscenza generale dello sviluppo Android. Oltre alle nozioni di base, ecco un paio di concetti utili da comprendere prima di esaminare il codice riportato di seguito:

  • View Binding genera una classe di binding per i tuoi file di layout XML, consentendoti di fare facilmente riferimento alle tue visualizzazioni in Activities, come avviene in diversi snippet di codice di seguito. Esistono alcune differenze tra il binding delle visualizzazioni e findViewById() (il precedente modo per fare riferimento alle visualizzazioni), ma nel codice riportato di seguito dovresti essere in grado di sostituire le righe di binding delle visualizzazioni con una chiamata findViewById() simile.
  • Le coroutine asincrone sono un pattern di progettazione della concorrenza aggiunto in Kotlin 1.3 che può essere utilizzato per gestire i metodi CameraX che restituiscono un ListenableFuture. Questa operazione è semplificata con la libreria Concurrent di Jetpack a partire dalla versione 1.1.0. Per aggiungere una coroutine asincrona all'app:
    1. Aggiungi implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0") al tuo file Gradle.
    2. Inserisci qualsiasi codice CameraX che restituisce un ListenableFuture in un launch blocco o funzione di sospensione.
    3. Aggiungi una chiamata await() alla chiamata di funzione che restituisce un ListenableFuture.
    4. Per una comprensione più approfondita del funzionamento delle coroutine, consulta la guida su come avviare una coroutine.

Eseguire la migrazione di scenari comuni

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

Selezione di una videocamera

Nell'applicazione della videocamera, una delle prime cose che potresti offrire è un modo per selezionare videocamere diverse.

Camera1

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

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

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

Ecco il codice CameraX per utilizzare la fotocamera posteriore predefinita con un 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

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 fotocamera è selezionata, questa operazione è possibile anche in CameraX se utilizzi CameraProvider chiamando getAvailableCameraInfos(), che ti fornisce un oggetto CameraInfo per controllare determinate proprietà della fotocamera come isFocusMeteringSupported(). Puoi quindi convertirlo in un CameraSelector da utilizzare come negli esempi precedenti con il metodo CameraInfo.getCameraSelector().

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

Ecco un esempio che utilizza una funzione checkFocalLength() personalizzata che puoi definire autonomamente:

// 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 a un certo punto. Con Camera1, devi gestire correttamente i callback del ciclo di vita e anche determinare la rotazione e la scala per l'anteprima.

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

Camera1

Per mostrare un'anteprima, devi scrivere la tua classe Preview con un'implementazione dell'interfaccia android.view.SurfaceHolder.Callback, utilizzata per trasmettere i dati delle immagini dall'hardware della fotocamera all'applicazione. Prima di poter avviare l'anteprima delle immagini in tempo reale, la classe Preview deve essere passata 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: CameraController

In CameraX, lo sviluppatore ha molto meno da gestire. Se utilizzi un CameraController, devi utilizzare anche PreviewView. Ciò significa che il valore Preview UseCase è implicito, semplificando notevolmente 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
    }
}

CameraX: CameraProvider

Con CameraProvider di CameraX non è necessario utilizzare PreviewView, ma la configurazione dell'anteprima è comunque molto semplificata rispetto a Camera1. A scopo dimostrativo, questo esempio utilizza un PreviewView, ma puoi scrivere un SurfaceProvider personalizzato da passare a setSurfaceProvider() se hai esigenze più complesse.

Qui, Preview UseCase non è implicito come 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 è impostare il punto di fuoco quando l'utente tocca l'anteprima.

Camera1

Per implementare il tocco per mettere a fuoco in Camera1, devi calcolare la messa a fuoco ottimale Area per indicare dove Camera deve tentare di mettere a fuoco. Questo valore Area viene passato a setFocusAreas(). Inoltre, devi impostare una modalità di messa a fuoco compatibile sul Camera. L'area di interesse 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 un peso specificato. Il peso è un valore compreso tra 1 e 1000 e viene utilizzato per dare la priorità all'attenzione Areas se ne sono impostati più di uno. Questo esempio utilizza un solo Area, quindi il valore della ponderazione non ha importanza. Le coordinate del rettangolo vanno da -1000 a 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 dell'evento tocco in 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: CameraController

CameraController ascolta gli eventi touch 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 monitorare le modifiche allo stato di messa a fuoco 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)

CameraX: CameraProvider

Quando utilizzi un CameraProvider, è necessaria una certa configurazione per attivare la funzionalità Tocca per mettere in primo piano. Questo esempio presuppone che tu stia utilizzando PreviewView. In caso contrario, devi adattare la logica da applicare al tuo Surface personalizzato.

Ecco i passaggi da seguire per utilizzare PreviewView:

  1. Configura un rilevatore di gesti per gestire gli eventi di tocco.
  2. Con l'evento tocco, crea un MeteringPoint utilizzando MeteringPointFactory.createPoint().
  3. Con MeteringPoint, crea un FocusMeteringAction.
  4. Con l'oggetto CameraControl in Camera (restituito da bindToLifecycle()), chiama startFocusAndMetering() passando il valore FocusMeteringAction.
  5. (Facoltativo) Rispondi al FocusMeteringResult.
  6. Imposta il rilevatore di gesti in modo che risponda agli eventi 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
}

Zoom regolato pizzicando con le dita

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

Camera1

Esistono due modi per aumentare lo zoom utilizzando la fotocamera 1. Il metodo Camera.startSmoothZoom() anima dal livello di zoom attuale a quello specificato. Il metodo Camera.Parameters.setZoom() passa direttamente al livello di zoom specificato. Prima di utilizzare uno dei due, chiama rispettivamente isSmoothZoomSupported() o isZoomSupported() per assicurarti che i metodi di zoom correlati di cui hai bisogno siano disponibili sulla videocamera.

Per implementare lo zoom con due dita, questo esempio utilizza setZoom() perché l'ascoltatore di tocco sulla superficie di anteprima attiva continuamente eventi durante il gesto di pinch, quindi aggiorna il livello di zoom immediatamente ogni volta. La classe ZoomTouchListener è definita di seguito e deve essere impostata come callback per l'ascoltatore tocco 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: CameraController

Analogamente al tocco per la messa a fuoco, CameraController ascolta gli eventi di tocco di PreviewView per gestire automaticamente lo zoom con due dita. Puoi attivare e disattivare la funzionalità di zoom con pinch con setPinchToZoomEnabled() e controllare il valore con il getter corrispondente isPinchToZoomEnabled().

Il metodo getZoomState() restituisce un oggetto LiveData per monitorare le modifiche apportate al ZoomState su CameraController.

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

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

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

CameraX: CameraProvider

Per attivare lo zoom con due dita con CameraProvider, è necessaria una configurazione. Se non utilizzi PreviewView, devi adattare la logica da applicare al Surface personalizzato.

Ecco i passaggi da seguire per utilizzare PreviewView:

  1. Configura un rilevatore di gesti di scala per gestire gli eventi di pizzicamento.
  2. Recupera ZoomState dall'oggetto Camera.CameraInfo, dove l'istanza Camera viene restituita quando chiami bindToLifecycle().
  3. Se ZoomState ha un valore zoomRatio, salvalo come rapporto di zoom corrente. Se non è presente zoomRatio su ZoomState, utilizza il rapporto di zoom predefinito della videocamera (1,0).
  4. Moltiplica il rapporto di zoom corrente con scaleFactor per determinare il nuovo rapporto di zoom e trasmetterlo a CameraControl.setZoomRatio().
  5. Imposta il rilevatore di gesti in modo che risponda 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
}

Scattare una foto

Questa sezione mostra come attivare l'acquisizione di foto, se devi farlo premendo il pulsante di scatto, dopo il tempo di un timer o su qualsiasi altro evento scelto da te.

Camera1

In Camera1, definisci innanzitutto un Camera.PictureCallback per gestire i dati delle immagini quando vengono richiesti. Ecco un semplice esempio di PictureCallback per la gestione dei dati delle 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)
    }
}

Poi, ogni volta che vuoi scattare una foto, chiami il metodo takePicture() sulla tua istanza Camera. Questo metodo takePicture() ha tre diversi parametri per tipi di dati diversi. Il primo parametro è per un ShutterCallback (che non è definito in questo esempio). Il secondo parametro è per un PictureCallback per gestire i dati non elaborati (non compressi) della fotocamera. Il terzo parametro è quello utilizzato in questo esempio, poiché è un PictureCallback per gestire i dati delle immagini JPEG.

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

camera?.takePicture(null, null, picture)

CameraX: CameraController

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 al suo interno.

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

Scattare una foto con CameraProvider funziona quasi esattamente come 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

La registrazione di un video è molto più complessa rispetto agli scenari esaminati finora. Ogni parte del processo deve essere configurata correttamente, in genere in un ordine specifico. Inoltre, potresti dover verificare che il video e l'audio siano sincronizzati o gestire ulteriori incoerenze del dispositivo.

Come vedrai, CameraX gestisce di nuovo gran parte di questa complessità per te.

Camera1

L'acquisizione video utilizzando la fotocamera 1 richiede una gestione attenta di Camera e MediaRecorder e i metodi devono essere invocati in un ordine specifico. Devi seguire questo ordine affinché la tua applicazione funzioni correttamente:

  1. Apri la fotocamera.
  2. Prepara e avvia un'anteprima (se la tua app mostra il video in fase di registrazione, come di solito accade).
  3. Sblocca la videocamera per l'utilizzo da parte di MediaRecorder chiamando Camera.unlock().
  4. Configura la registrazione chiamando questi metodi su MediaRecorder:
    1. Connetti l'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 CamcorderProfile per tutte le opzioni di qualità.
    5. Chiama il numero setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString()).
    6. Se la tua app ha 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 completare la configurazione di MediaRecorder.
  5. Per avviare la registrazione, chiama MediaRecorder.start().
  6. Per interrompere la registrazione, chiama questi metodi. Anche in questo caso, segui esattamente questo ordine:
    1. Chiama il numero MediaRecorder.stop().
    2. Se vuoi, rimuovi la configurazione MediaRecorder corrente chiamando MediaRecorder.reset().
    3. Chiama il numero MediaRecorder.release().
    4. Blocca la videocamera in modo che possa essere utilizzata per le sessioni MediaRecorder future chiamando Camera.lock().
  7. Per interrompere l'anteprima, chiama Camera.stopPreview().
  8. Infine, per rilasciare Camera in modo che possa essere utilizzato da altri processi, chiama Camera.release().

Ecco tutti i passaggi combinati:

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

Con CameraController di CameraX, puoi attivare/disattivare i ImageCapture, VideoCapture e ImageAnalysis UseCase in modo indipendente, a condizione che l'elenco di UseCases possa essere utilizzato contemporaneamente. I UseCase ImageCapture e ImageAnalysis sono abilitati per impostazione predefinita, per questo motivo non è stato necessario chiamare setEnabledUseCases() per scattare una foto.

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

// 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 implementa OnVideoSavedCallback per gestire i callback di successo e di errore. Quando la registrazione deve terminare, chiama CameraController.stopRecording().

Nota:se utilizzi CameraX 1.3.0-alpha02 o versioni successive, è disponibile un parametro aggiuntivo AudioConfig che ti consente di attivare o disattivare la registrazione audio sul 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, riprendere 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()
    )
}

CameraX: CameraProvider

Se utilizzi un CameraProvider, devi creare un VideoCapture UseCase e passare un oggetto Recorder. Su Recorder.Builder, puoi impostare la qualità video e, facoltativamente, un FallbackStrategy, che gestisce i casi in cui un dispositivo non è in grado di soddisfare le specifiche di qualità che preferisci. 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. Il Recorder può avviare registrazioni video che vengono salvate in un File, ParcelFileDescriptor o MediaStore. Questo esempio utilizza MediaStore.

In Recorder, esistono diversi metodi per chiamare per prepararlo. Chiama prepareRecording() per impostare le opzioni di output MediaStore. Se la tua app ha l'autorizzazione per utilizzare il microfono del dispositivo, chiama anche withAudioEnabled(). Quindi, chiama start() per avviare la registrazione, passando un contesto e un ascoltatore di eventi Consumer<VideoRecordEvent> per gestire gli eventi di registrazione video. Se l'operazione è andata a buon fine, il valore 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 app per la fotocamera. Questi esempi mostrano come gli scenari descritti in questa guida si inseriscono in un'app Android completa.

Se hai bisogno di ulteriore assistenza per la migrazione a CameraX o hai domande sulla suite di API Android Camera, contattaci nel gruppo di discussione CameraX.