Esegui la migrazione da Camera1 a CameraX

Se la tua app utilizza la classe Camera originale ("Camera1"), che è stata ritirata a partire da Android 5.0 (livello API 21), ti consigliamo vivamente di eseguire l'aggiornamento a un'API fotocamera Android moderna. Android offre CameraX (un'API fotocamera Jetpack standardizzata e solida) e Camera2 (un'API framework di basso livello). Nella maggior parte dei casi, ti consigliamo di eseguire la migrazione dell'app a CameraX. Ecco perché:

  • Facilità d'uso:CameraX gestisce i dettagli di basso livello, in modo che tu possa concentrarti meno sulla creazione di un'esperienza con la fotocamera da zero e di più sulla differenziazione della tua app.
  • CameraX gestisce la frammentazione per te: CameraX riduce i costi di manutenzione a lungo termine e il codice specifico per il dispositivo, offrendo agli utenti esperienze di qualità superiore. Per saperne di più, leggi il post del blog Migliore compatibilità dei dispositivi con CameraX.
  • Funzionalità avanzate: CameraX è progettata con cura per semplificare l'incorporamento di funzionalità avanzate nella tua app. Ad esempio, puoi applicare facilmente Bokeh, Ritocco viso, HDR (High Dynamic Range) e la modalità di acquisizione notturna con luminosità in condizioni di scarsa illuminazione alle tue foto con le estensioni CameraX.
  • Aggiornabilità: Android rilascia nuove funzionalità e correzioni di bug per CameraX durante l'anno. Con la migrazione a CameraX, la tua app riceve la tecnologia della videocamera Android più recente con ogni release di CameraX, non solo con le release annuali della versione di Android.

In questa guida troverai scenari comuni per le applicazioni della videocamera. 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 codebase esistente. Tutto il codice CameraX in questa guida ha un'implementazione CameraController, ideale se vuoi un modo più semplice per utilizzare CameraX, e anche un'implementazione CameraProvider, ideale se hai bisogno di maggiore flessibilità. Per aiutarti a decidere quale sia la soluzione più adatta a te, ecco i vantaggi di ciascuna:

CameraController

CameraProvider

Richiede un codice di configurazione minimo Consente un maggiore controllo
Se consenti a CameraX di gestire una parte maggiore della procedura di configurazione, funzionalità come il tocco per mettere a fuoco e il pizzico per zoomare funzionano automaticamente. Poiché lo sviluppatore di app gestisce la configurazione, ci sono più opportunità per personalizzare la configurazione, ad esempio attivando la rotazione dell'immagine di output o impostando il formato dell'immagine di output in ImageAnalysis.
Richiedere PreviewView per l'anteprima della videocamera consente a CameraX di offrire un'integrazione end-to-end perfetta, come nella nostra integrazione di ML Kit, che può mappare le coordinate dei risultati del modello ML (come i riquadri di selezione del volto) direttamente sulle coordinate dell'anteprima La possibilità di utilizzare una `Surface` personalizzata per l'anteprima della videocamera offre maggiore flessibilità, ad esempio l'utilizzo del codice `Surface` esistente che potrebbe essere un input per altre parti dell'app

Se hai difficoltà a eseguire la migrazione, contattaci nel gruppo di discussione di CameraX.

Prima di eseguire la migrazione

Confrontare l'utilizzo di CameraX e Camera1

Anche se il codice potrebbe avere un aspetto diverso, i concetti di base di Camera1 e CameraX sono molto simili. CameraX astrae le funzionalità comuni della fotocamera in casi d'uso e, di conseguenza, molte attività che erano lasciate allo sviluppatore in Camera1 vengono gestite automaticamente da CameraX. In CameraX sono presenti 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 Camera1, devi gestire questi dettagli personalmente. Date le proporzioni variabili dei dispositivi, è difficile abbinare l'anteprima ai contenuti multimediali acquisiti.

Come altro esempio, CameraX gestisce automaticamente i callback Lifecycle nell'istanza Lifecycle che fornisci. Grazie a questa architettura, 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 la tua app va in background; rimozione dell'anteprima della videocamera quando lo schermo non richiede più la visualizzazione; e pausa dell'anteprima della videocamera quando un'altra attività prende la precedenza in primo piano, ad esempio una videochiamata in arrivo.

Infine, CameraX gestisce la rotazione e il ridimensionamento senza che tu debba scrivere codice aggiuntivo. Nel caso di un Activity con orientamento sbloccato, la configurazione di UseCase viene eseguita ogni volta che il dispositivo viene ruotato, poiché il sistema elimina e ricrea l'Activity in caso di modifiche dell'orientamento. Di conseguenza, l'impostazione UseCases imposta la rotazione del target in modo che corrisponda all'orientamento del display per impostazione predefinita ogni volta. Scopri di più sulle rotazioni in CameraX.

Prima di entrare nei dettagli, ecco una panoramica di alto livello delle UseCasedi CameraX e del loro rapporto con un'app Camera1. (I concetti di CameraX sono in blu e quelli 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 basato sulla superficie di anteprima
Codice specifico per dispositivo
Gestione della rotazione e del ridimensionamento dei dispositivi
Gestione sessione videocamera (selezione videocamera, gestione del ciclo di vita)

Fotocamera1

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 nella tua app. Inoltre, testiamo oltre 150 dispositivi fisici su tutte le versioni di Android a partire dalla 5.0 nel nostro CameraX Test Lab. Puoi consultare l'elenco completo dei dispositivi in Test Lab.

CameraX utilizza un Executor per gestire lo stack della fotocamera. 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 della piattaforma su cui è basato CameraX richiedono la comunicazione interprocesso (IPC) di blocco con l'hardware, che a volte può richiedere centinaia di millisecondi per rispondere. Per questo motivo, CameraX chiama queste API solo da thread in background, il che garantisce che il thread principale non venga bloccato e che la UI 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 limite per la videocamera. Poiché il processo di connessione ai componenti hardware può richiedere un tempo non trascurabile, 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 le fotocamere posteriori, può impostare questa configurazione con DEFAULT_BACK_CAMERA e poi CameraX evita di inizializzare le fotocamere frontali per ridurre la latenza.

Concetti di sviluppo Android

Questa guida presuppone una familiarità generale con lo sviluppo per Android. Oltre alle basi, ecco un paio di concetti utili da comprendere prima di esaminare il seguente codice:

  • View Binding genera una classe di binding per i file di layout XML, consentendoti di fare riferimento alle visualizzazioni nelle attività, come mostrato in diversi snippet di codice successivi. Esistono alcune differenze tra il binding delle visualizzazioni e findViewById() (il modo precedente per fare riferimento alle visualizzazioni), ma nel codice seguente 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 dalla 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 tuo file Gradle.
    2. Inserisci qualsiasi codice CameraX che restituisce un ListenableFuture in un blocco launch o in una 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 Avviare una coroutine.

Eseguire la migrazione degli scenari comuni

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

Selezionare una videocamera

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

Fotocamera1

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 apparire:

// 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 viene gestita dalla classe CameraSelector. CameraX semplifica il caso comune dell'utilizzo della fotocamera predefinita. Puoi specificare se vuoi la fotocamera anteriore predefinita o quella posteriore predefinita. Inoltre, l'oggetto CameraControl di CameraX ti consente di impostare il livello di zoom per la tua app, quindi se la tua app viene eseguita su un dispositivo che supporta le fotocamere logiche, passerà all'obiettivo corretto.

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 (è possibile utilizzare la fotocamera anteriore o posteriore 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 preceding "Android development concepts"
// section.
private suspend fun startCamera() {
    val cameraProvider = ProcessCameraProvider.getInstance(this).await()

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

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

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

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

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

...

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

    ...

    lifecycleScope.launch {
        startCamera()
    }
}

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

Puoi ottenere maggiori dettagli su ogni videocamera utilizzando la classe Camera2CameraInfo. Chiama getCameraCharacteristic() con una chiave per i dati della videocamera che ti interessano. Controlla 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 potresti definire tu stesso:

// 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 devi anche determinare la rotazione e il ridimensionamento per l'anteprima.

Inoltre, in Camera1 devi decidere se utilizzare un TextureView o un SurfaceView come superficie di anteprima. Entrambe le opzioni presentano compromessi e, in entrambi i casi, Camera1 richiede di gestire correttamente la rotazione e il ridimensionamento. PreviewView di CameraX, invece, ha implementazioni sottostanti sia per TextureView che per SurfaceView. CameraX decide quale implementazione è la migliore in base a fattori quali il tipo di dispositivo e la versione di Android su cui viene eseguita 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, mentre il valore PERFORMANCE utilizza un 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 passare i dati immagine dall'hardware della fotocamera all'applicazione. Prima di poter avviare l'anteprima dell'immagine live, 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 deve gestire molte meno cose. Se utilizzi un CameraController, devi utilizzare anche PreviewView. Ciò significa che Preview UseCase è implicito, il che rende la configurazione molto meno impegnativa:

// 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 devi utilizzare PreviewView, ma semplifica comunque notevolmente la configurazione dell'anteprima 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.

In questo caso, Preview UseCase non è implicito come in 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 preceding "Android development concepts"
// section.
private suspend fun startCamera() {
    val cameraProvider = ProcessCameraProvider.getInstance(this).await()

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

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

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

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

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

...

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

    ...

    lifecycleScope.launch {
        startCamera()
    }
}

Tocca per mettere a fuoco

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

Fotocamera1

Per implementare la messa a fuoco con tocco in Camera1, devi calcolare la messa a fuoco ottimale Area per indicare dove Camera deve tentare di mettere a fuoco. Questo Area viene trasferito a setFocusAreas(). Inoltre, devi impostare una modalità di messa a fuoco compatibile su Camera. L'area di interesse ha effetto solo se la modalità Niente distrazioni attuale è 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à alla messa a fuoco Areas se ne sono impostate più di una. Questo esempio utilizza un solo Area, quindi il valore della ponderazione non è importante. 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 la messa a fuoco con un tocco. Puoi attivare e disattivare la messa a fuoco con un tocco con setTapToFocusEnabled() e controllare il valore con il getter corrispondente isTapToFocusEnabled().

Il metodo getTapToFocusState() restituisce un oggetto LiveData per monitorare le modifiche allo 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)

CameraX: CameraProvider

Quando si utilizza un CameraProvider, è necessaria una configurazione per attivare la messa a fuoco con tocco. 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 quando utilizzi 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 MeteringPoint, crea un FocusMeteringAction.
  4. Con l'oggetto CameraControl sul tuo Camera (restituito da bindToLifecycle()), chiama startFocusAndMetering(), passando FocusMeteringAction.
  5. (Facoltativo) Rispondi a 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
// preceding "Android development concepts" section.
val gestureDetector = GestureDetectorCompat(context,
    object : SimpleOnGestureListener() {
        override fun onSingleTapUp(e: MotionEvent): Boolean {
            val previewView = previewView ?: return
            val camera = camera ?: return
            val meteringPointFactory = previewView.meteringPointFactory
            val focusPoint = meteringPointFactory.createPoint(e.x, e.y)
            val meteringAction = FocusMeteringAction
                .Builder(meteringPoint).build()
            lifecycleScope.launch {
                val focusResult = camera.cameraControl
                    .startFocusAndMetering(meteringAction).await()
                if (!result.isFocusSuccessful()) {
                    Log.d(TAG, "tap-to-focus failed")
                }
            }
        }
    }
)

...

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

Pizzica per eseguire lo zoom

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

Fotocamera1

Esistono due modi per eseguire lo zoom utilizzando Camera1. Il metodo Camera.startSmoothZoom() anima dal livello di zoom attuale al livello di zoom che passi. Il metodo Camera.Parameters.setZoom() passa direttamente al livello di zoom che hai inserito. Prima di utilizzarne uno, chiama isSmoothZoomSupported() o isZoomSupported(), rispettivamente, per assicurarti che i metodi di zoom correlati di cui hai bisogno siano disponibili sulla tua videocamera.

Per implementare il gesto di pizzicare per ingrandire, questo esempio utilizza setZoom() perché il listener di tocco sulla superficie di anteprima attiva continuamente gli eventi man mano che viene eseguito il gesto di pizzicare, quindi aggiorna immediatamente il livello di zoom ogni volta. La classe ZoomTouchListener è definita più avanti in questa sezione e devi impostarla come callback per il listener di 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

Simile alla messa a fuoco con tocco, CameraController ascolta gli eventi di tocco di PreviewView per gestire automaticamente il pizzico per zoomare. Puoi attivare e disattivare il pizzico per zoomare con setPinchToZoomEnabled() e controllare il valore con il getter corrispondente isPinchToZoomEnabled().

Il metodo getZoomState() restituisce un oggetto LiveData per il monitoraggio delle modifiche apportate a 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 utilizzare il gesto di pizzicare per zoomare con CameraProvider, è necessaria una configurazione. Se non utilizzi PreviewView, devi adattare la logica in modo che si applichi al tuo Surface personalizzato.

Ecco i passaggi da seguire quando utilizzi PreviewView:

  1. Configura un rilevatore di gesti di ridimensionamento per gestire gli eventi di pizzicamento.
  2. Ottieni 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 la velocità di zoom predefinita della videocamera (1.0).
  4. Prendi il prodotto del rapporto di zoom attuale con scaleFactor per determinare il nuovo rapporto di zoom e passalo a CameraControl.setZoomRatio().
  5. Imposta il rilevatore di gesti in modo che risponda agli eventi 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-zoom scenario for gestureDetector definition.
        didConsume = gestureDetector.onTouchEvent(event)
    }
    didConsume
}

Scattare una foto

Questa sezione mostra come attivare l'acquisizione di foto, sia che tu debba farlo premendo un pulsante di scatto, dopo lo scadere di un timer o in qualsiasi altro evento di tua scelta.

Fotocamera1

In Camera1, devi prima definire 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 parametri diversi 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 della videocamera non elaborati (non compressi). 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.

// 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(). Per un esempio completo della funzione takePhoto(), consulta il codice CameraController in questa sezione.

// 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 è molto più complicato degli scenari esaminati finora. Ogni parte del processo deve essere configurata correttamente, di solito in un ordine particolare. Inoltre, potresti dover verificare che il video e l'audio siano sincronizzati o risolvere ulteriori problemi di coerenza del dispositivo.

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

Fotocamera1

L'acquisizione video tramite Camera1 richiede una gestione attenta di Camera e MediaRecorder e i metodi devono essere chiamati in un ordine particolare. Per il corretto funzionamento dell'applicazione, devi seguire questo ordine:

  1. Apri la fotocamera.
  2. Prepara e avvia un'anteprima (se la tua app mostra il video in fase di registrazione, il che di solito accade).
  3. Sblocca la videocamera per l'utilizzo da parte di MediaRecorder chiamando il numero Camera.unlock().
  4. Configura la registrazione chiamando questi metodi su MediaRecorder:
    1. Collega 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 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 il numero prepare() per finalizzare la configurazione di MediaRecorder.
  5. Per iniziare la registrazione, chiama il numero MediaRecorder.start().
  6. Per interrompere la registrazione, chiama questi metodi. Ancora una volta, segui questo ordine esatto:
    1. Chiama il numero MediaRecorder.stop().
    2. (Facoltativo) Rimuovi la configurazione MediaRecorder attuale chiamando MediaRecorder.reset().
    3. Chiama il numero MediaRecorder.release().
    4. Blocca la videocamera in modo che le future sessioni di MediaRecorder possano utilizzarla chiamando Camera.lock().
  7. Per interrompere l'anteprima, chiama il numero Camera.stopPreview().
  8. Infine, per rilasciare il 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 ImageCapture, VideoCapture e ImageAnalysis UseCase in modo indipendente, a condizione che l'elenco di UseCase possa essere utilizzato contemporaneamente. ImageCapture e ImageAnalysis UseCase sono attivi per impostazione predefinita, motivo per cui non è stato necessario chiamare setEnabledUseCases() per scattare una foto.

Per utilizzare un CameraController per la registrazione video, devi prima utilizzare 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 seguente. Inoltre, devi superare un Executor e una classe che implementa OnVideoSavedCallback per gestire i callback di successo e di errore. Quando la registrazione deve terminare, chiama il numero CameraController.stopRecording().

Nota:se utilizzi CameraX 1.3.0-alpha02 o versioni successive, è disponibile un parametro AudioConfig aggiuntivo che ti consente di attivare o disattivare la registrazione audio nel video. Per attivare la registrazione audio, devi assicurarti di disporre delle autorizzazioni 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à desiderate. Poi 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. L'Recorder può avviare registrazioni video che vengono salvate in un File, ParcelFileDescriptor o MediaStore. Questo esempio utilizza MediaStore.

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

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