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 chiamatafindViewById()
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:- Aggiungi
implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0")
al tuo file Gradle. - Inserisci qualsiasi codice CameraX che restituisce un
ListenableFuture
in unlaunch
blocco o funzione di sospensione. - Aggiungi una chiamata
await()
alla chiamata di funzione che restituisce unListenableFuture
. - Per una comprensione più approfondita del funzionamento delle coroutine, consulta la guida su come avviare una coroutine.
- Aggiungi
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
:
- Configura un rilevatore di gesti per gestire gli eventi di tocco.
- Con l'evento tocco, crea un
MeteringPoint
utilizzandoMeteringPointFactory.createPoint()
. - Con
MeteringPoint
, crea unFocusMeteringAction
. - Con l'oggetto
CameraControl
inCamera
(restituito dabindToLifecycle()
), chiamastartFocusAndMetering()
passando il valoreFocusMeteringAction
. - (Facoltativo) Rispondi al
FocusMeteringResult
. - 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
:
- Configura un rilevatore di gesti di scala per gestire gli eventi di pizzicamento.
- Recupera
ZoomState
dall'oggettoCamera.CameraInfo
, dove l'istanzaCamera
viene restituita quando chiamibindToLifecycle()
. - Se
ZoomState
ha un valorezoomRatio
, salvalo come rapporto di zoom corrente. Se non è presentezoomRatio
suZoomState
, utilizza il rapporto di zoom predefinito della videocamera (1,0). - Moltiplica il rapporto di zoom corrente con
scaleFactor
per determinare il nuovo rapporto di zoom e trasmetterlo aCameraControl.setZoomRatio()
. - 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:
- Apri la fotocamera.
- Prepara e avvia un'anteprima (se la tua app mostra il video in fase di registrazione, come di solito accade).
- Sblocca la videocamera per l'utilizzo da parte di
MediaRecorder
chiamandoCamera.unlock()
. - Configura la registrazione chiamando questi metodi su
MediaRecorder
:- Connetti l'istanza
Camera
asetCamera(camera)
. - Chiama il numero
setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
. - Chiama il numero
setVideoSource(MediaRecorder.VideoSource.CAMERA)
. - Chiama
setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P))
per impostare la qualità. ConsultaCamcorderProfile
per tutte le opzioni di qualità. - Chiama il numero
setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString())
. - Se la tua app ha un'anteprima del video, chiama
setPreviewDisplay(preview?.holder?.surface)
. - Chiama il numero
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
. - Chiama il numero
setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT)
. - Chiama il numero
setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT)
. - Chiama
prepare()
per completare la configurazione diMediaRecorder
.
- Connetti l'istanza
- Per avviare la registrazione, chiama
MediaRecorder.start()
. - Per interrompere la registrazione, chiama questi metodi. Anche in questo caso, segui esattamente questo ordine:
- Chiama il numero
MediaRecorder.stop()
. - Se vuoi, rimuovi la configurazione
MediaRecorder
corrente chiamandoMediaRecorder.reset()
. - Chiama il numero
MediaRecorder.release()
. - Blocca la videocamera in modo che possa essere utilizzata per le sessioni
MediaRecorder
future chiamandoCamera.lock()
.
- Chiama il numero
- Per interrompere l'anteprima, chiama
Camera.stopPreview()
. - Infine, per rilasciare
Camera
in modo che possa essere utilizzato da altri processi, chiamaCamera.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: VideoCaptureprivate 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.