Si votre application utilise la classe Camera
d'origine ("Camera1"), qui est obsolète depuis Android 5.0 (niveau d'API 21), nous vous recommandons vivement de passer à une API d'appareil photo Android moderne. Android propose CameraX (API d'appareil photo Jetpack robuste et standardisée) et Camera2 (API framework de bas niveau). Dans la grande majorité des cas, nous vous conseillons de migrer votre application vers CameraX. Pourquoi ?
- Simplicité d'utilisation : CameraX gère les détails de bas niveau. Vous pouvez donc passer plus de temps à concevoir une application qui se démarque et moins de temps à créer une expérience photo en partant de zéro.
- Fragmentation automatique : CameraX réduit les coûts de maintenance à long terme et le code spécifique à l'appareil, offrant ainsi des expériences de meilleure qualité aux utilisateurs. Pour en savoir plus à ce sujet, consultez notre article de blog intitulé Better Device Compatibility with CameraX (Améliorer la compatibilité des appareils avec CameraX).
- Fonctionnalités avancées : CameraX a été soigneusement conçu pour faciliter l'intégration de fonctionnalités avancées dans votre application. Par exemple, vous pouvez aisément appliquer des fonctionnalités Bokeh, Retouche du visage, HDR (High Dynamic Range) et le mode de capture nocturne à faible luminosité à vos photos grâce aux extensions CameraX.
- Facilité de mise à jour : Android publie de nouvelles fonctionnalités et corrections de bugs tout au long de l'année pour CameraX. En migrant vers CameraX, votre application bénéficie des dernières technologies photo d'Android à chaque version de CameraX, et pas seulement à la sortie des versions annuelles d'Android.
Dans ce guide, vous trouverez des scénarios courants pour les applications photo. Chaque scénario comprend une implémentation de Camera1 et une implémentation de CameraX à titre de comparaison.
Lors d'une migration, vous avez parfois besoin de plus de flexibilité pour intégrer un codebase existant. Tous les codes CameraX figurant dans ce guide comportent une implémentation CameraController
(idéale si vous souhaitez utiliser CameraX le plus simplement possible) et une implémentation CameraProvider
(idéale si vous avez besoin de plus de flexibilité). Pour vous aider à choisir la solution la mieux adaptée à vos besoins, découvrez leurs avantages ci-dessous :
CameraController |
CameraProvider |
Code de configuration minime | Davantage de contrôle |
Permettre à CameraX de gérer une plus grande partie du processus de configuration signifie que des fonctionnalités comme la mise au point en appuyant sur l'aperçu et le pincement pour zoomer fonctionnent automatiquement |
Étant donné que le développeur de l'application gère la configuration, il dispose de davantage de possibilités pour personnaliser la configuration (par exemple, activer la rotation de l'image de sortie ou définir le format de l'image de sortie dans ImageAnalysis )
|
L'affichage obligatoire du PreviewView pour l'aperçu de l'appareil photo permet à CameraX d'offrir une intégration parfaite et complète. C'est déjà le cas avec l'intégration de ML Kit, qui peut mapper les coordonnées des résultats du modèle de ML (les cadres de délimitation des visages, par exemple) directement sur les coordonnées de l'aperçu
|
La possibilité d'utiliser une "Surface" personnalisée pour l'aperçu de l'appareil photo offre plus de flexibilité, par exemple lorsque vous avez recours au code "Surface" existant comme entrée pour d'autres parties de votre application |
Si vous rencontrez des difficultés pour effectuer la migration, contactez-nous via le groupe de discussion CameraX.
Avant la migration
Comparer l'utilisation de CameraX et Camera1
Bien que le code puisse sembler différent, les concepts sous-jacents de Camera1 et de CameraX sont très similaires. CameraX extrait les fonctionnalités courantes de l'appareil photo dans des cas d'utilisation. Par conséquent, de nombreuses tâches laissées au développeur dans Camera1 sont gérées automatiquement par CameraX. Il existe quatre UseCase
dans CameraX, que vous pouvez utiliser pour diverses tâches d'appareil photo : Preview
. ImageCapture
, VideoCapture
et ImageAnalysis
.
Un exemple de gestion des détails de bas niveau par CameraX pour les développeurs est le ViewPort
partagé entre les UseCase
actifs. Cela permet de garantir que tous les UseCase
voient exactement les mêmes pixels.
Dans Camera1, vous devez gérer vous-même ces détails. En raison des différents formats de capteurs et d'écrans des appareils photo, il peut être difficile de vous assurer que l'aperçu correspond aux photos et aux vidéos prises.
Autre exemple : CameraX gère automatiquement les rappels Lifecycle
au niveau de l'instance Lifecycle
que vous lui transmettez. Autrement dit, CameraX gère la connexion de votre application à l'appareil photo tout au long du cycle de vie de l'activité Android, y compris dans les cas suivants : arrêt de l'appareil photo lorsque l'application passe en arrière-plan, suppression de l'aperçu de l'appareil photo lorsque l'écran n'a plus besoin de l'afficher, et interruption de l'aperçu de l'appareil photo lorsqu'une autre activité est prioritaire, comme un appel vidéo entrant.
Enfin, CameraX gère la rotation et la mise à l'échelle sans que vous ayez besoin de code supplémentaire. Dans le cas d'une Activity
dont l'orientation est déverrouillée, la configuration du UseCase
est appliquée à chaque rotation de l'appareil, car le système détruit et recrée l'Activity
lors des changements d'orientation. Par conséquent, les UseCases
définissent à chaque fois leur rotation cible pour qu'elle corresponde à l'orientation de l'écran par défaut.
En savoir plus sur les rotations dans CameraX
Avant d'entrer dans les détails, voici un aperçu général des UseCase
de CameraX et de la manière dont une application Camera1 fonctionnerait. Notez que les concepts de CameraX sont en bleu et les concepts de Camera1 sont en vert.
CameraX |
|||
Configuration de CameraController/CameraProvider | |||
↓ | ↓ | ↓ | ↓ |
Preview | ImageCapture | VideoCapture | ImageAnalysis |
⁞ | ⁞ | ⁞ | ⁞ |
Gérer la surface d'aperçu et la configurer sur l'appareil photo | Définir PictureCallback et appeler takePicture() sur l'appareil photo | Gérer la configuration de l'appareil photo et de MediaRecorder dans un ordre spécifique | Code d'analyse personnalisé basé sur la surface d'aperçu |
↑ | ↑ | ↑ | ↑ |
Code spécifique à l'appareil | |||
↑ | |||
Gestion de la rotation et de la mise à l'échelle des appareils | |||
↑ | |||
Gestion des sessions photo (sélection de l'appareil photo, gestion du cycle de vie) | |||
Camera1 |
Compatibilité et performances dans CameraX
CameraX est compatible avec les appareils équipés d'Android 5.0 (niveau d'API 21) ou version ultérieure, soit plus de 98 % des appareils Android existants. CameraX est conçu pour gérer automatiquement les différences entre les appareils, ce qui réduit le besoin de code spécifique à l'appareil dans votre application. De plus, nous testons plus de 150 appareils physiques sur toutes les versions d'Android depuis la version 5.0 dans le cadre de notre Test Lab de CameraX. Vous pouvez consulter la liste complète des appareils actuellement disponibles dans le Test Lab.
CameraX utilise un Executor
pour piloter la pile de l'appareil photo. Vous pouvez spécifier votre propre exécuteur sur CameraX si votre application a des exigences spécifiques en termes de threads. Si vous ne le spécifiez pas, CameraX crée et utilise par défaut un Executor
interne optimisé. De nombreuses API de la plate-forme sur lesquelles CameraX repose requièrent un dispositif de communication inter-processus (IPC) avec du matériel dont la réponse peut parfois prendre des centaines de millisecondes. Pour cette raison, CameraX n'appelle ces API qu'à partir de threads en arrière-plan, ce qui garantit que le thread principal n'est pas bloqué et que l'UI reste fluide.
En savoir plus sur les threads
Si le marché cible de votre application inclut des appareils d'entrée de gamme, CameraX offre un moyen de réduire le temps de configuration à l'aide d'un limiteur d'appareil photo. Étant donné que le processus de connexion aux composants matériels peut prendre du temps, en particulier sur les appareils d'entrée de gamme, vous pouvez spécifier l'ensemble d'appareils photo dont votre application a besoin. CameraX ne se connectera à ces appareils photo que lors de la configuration. Par exemple, si l'application n'utilise que les appareils photo arrière, elle peut définir cette configuration avec DEFAULT_BACK_CAMERA
. Ensuite, CameraX évitera d'initialiser les appareils photo frontaux pour réduire la latence.
Concepts du développement Android
Ce guide part du principe que vous possédez des connaissances générales en développement Android. Au-delà des principes de base, voici quelques concepts qu'il est utile de comprendre avant de passer au code :
- La liaison de vue génère une classe de liaison pour vos fichiers de mise en page XML, ce qui vous permet de référencer facilement les vues dans les activités, comme illustré dans plusieurs extraits de code ci-dessous. Il existe des différences entre la liaison de vue et
findViewById()
(méthode précédente permettant de référencer des vues), mais dans le code ci-dessous, vous devriez pouvoir remplacer les lignes de liaison des vues par un appelfindViewById()
similaire. - Les coroutines asynchrones représentent un modèle de conception de simultanéité ajouté dans Kotlin 1.3. Elles peuvent être utilisées pour gérer les méthodes CameraX qui renvoient un
ListenableFuture
. Le processus est simplifié grâce à la bibliothèque Jetpack Concurrent à partir de la version 1.1.0. Pour ajouter une coroutine asynchrone à votre application, procédez comme suit :- Ajoutez
implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0")
à votre fichier Gradle. - Placez le code CameraX renvoyant un
ListenableFuture
dans un bloclaunch
ou une fonction de suspension. - Ajoutez un appel
await()
à l'appel de fonction qui renvoie unListenableFuture
. - Pour mieux comprendre le fonctionnement des coroutines, consultez le guide Démarrer une coroutine.
- Ajoutez
Migrer des scénarios courants
Cette section explique comment migrer des scénarios courants de Camera1 vers CameraX.
Chaque scénario couvre une implémentation de Camera1, une implémentation de CameraX CameraProvider
et une implémentation de CameraX CameraController
.
Sélectionner un appareil photo
Dans votre application photo, vous pouvez d'abord proposer un moyen de sélectionner différents appareils photo.
Camera1
Dans Camera1, vous pouvez appeler Camera.open()
sans paramètre pour ouvrir le premier appareil photo arrière, ou transmettre un ID entier pour l'appareil photo que vous voulez ouvrir. Voici un exemple :
// 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
Dans CameraX, la sélection de l'appareil photo est gérée par la classe CameraSelector
. CameraX facilite l'utilisation de l'appareil photo par défaut, qui est un cas courant. Vous pouvez indiquer si vous souhaitez utiliser l'appareil photo avant par défaut ou l'appareil photo arrière par défaut. De plus, l'objet CameraControl
de CameraX vous permet de définir facilement le niveau de zoom de votre application. Par conséquent, si elle s'exécute sur un appareil compatible avec les appareils photo logiques, elle passera sur l'objectif approprié.
Voici le code CameraX permettant d'utiliser l'appareil photo arrière par défaut avec 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
Voici un exemple de sélection de l'appareil photo avant par défaut avec un CameraProvider
(l'appareil photo avant ou arrière peut être utilisé avec un CameraController
ou un 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() } }
Si vous souhaitez contrôler l'appareil photo sélectionné, cela est également possible dans CameraX si vous utilisez un CameraProvider
en appelant getAvailableCameraInfos()
. Vous générez ainsi un objet CameraInfo
permettant de vérifier certaines propriétés de l'appareil photo, telles que isFocusMeteringSupported()
.
Vous pouvez ensuite le convertir en CameraSelector
pour l'utiliser comme dans les exemples ci-dessus avec la méthode CameraInfo.getCameraSelector()
.
Pour obtenir plus d'informations sur chaque appareil photo, utilisez la classe Camera2CameraInfo
. Appelez getCameraCharacteristic()
avec une touche correspondant aux données d'appareil photo souhaitées. Consultez la classe CameraCharacteristics
pour obtenir la liste de toutes les touches que vous pouvez interroger.
Voici un exemple utilisant une fonction checkFocalLength()
personnalisée que vous pouvez définir vous-même :
// 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()
Afficher un aperçu
La plupart des applications photo doivent à un moment donné afficher le flux de l'appareil photo à l'écran. Avec Camera1, vous devez gérer correctement les rappels de cycle de vie et déterminer la rotation ainsi que la mise à l'échelle de l'aperçu.
De plus, dans Camera1, vous devez choisir entre utiliser un TextureView
ou un SurfaceView
comme surface d'aperçu.
Ces deux options impliquent des compromis. Dans les deux cas, Camera1 nécessite que vous gériez la rotation et la mise à l'échelle de manière appropriée. En revanche, le PreviewView
de CameraX comporte des implémentations sous-jacentes pour un TextureView
et un SurfaceView
.
CameraX détermine l'implémentation la plus adaptée en fonction de facteurs tels que le type d'appareil et la version d'Android sur laquelle votre application est exécutée. Si l'une de ces deux implémentations est compatible, vous pouvez déclarer votre préférence avec PreviewView.ImplementationMode
.
L'option COMPATIBLE
utilise un TextureView
pour l'aperçu, et la PERFORMANCE
utilise un SurfaceView
(si possible).
Camera1
Pour afficher un aperçu, vous devez écrire votre propre classe Preview
avec une implémentation de l'interface android.view.SurfaceHolder.Callback
, qui sert à transmettre les données d'image depuis les composants matériels de l'appareil photo à l'application. Ensuite, avant de pouvoir lancer l'aperçu de l'image en direct, la classe Preview
doit être transmise à l'objet 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
En tant que développeur, vous avez beaucoup moins à gérer dans CameraX. Si vous utilisez un CameraController
, vous devez également utiliser un PreviewView
. Autrement dit, le UseCase
Preview
est implicite, ce qui rend la configuration beaucoup moins fastidieuse :
// 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
Avec le CameraProvider
de CameraX, vous n'avez pas besoin d'utiliser de PreviewView
et cela simplifie considérablement la configuration de l'aperçu par rapport à Camera1. À des fins de démonstration, cet exemple utilise un PreviewView
, mais vous pouvez écrire un SurfaceProvider
personnalisé à transmettre au setSurfaceProvider()
si vos besoins sont plus complexes.
Ici, le UseCase
Preview
n'est pas implicite comme avec le CameraController
. Vous devez donc le configurer :
// 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() } }
Appuyer pour effectuer la mise au point
Lorsque l'aperçu de l'appareil photo s'affiche à l'écran, une commande courante consiste à effectuer la mise au point lorsque l'utilisateur appuie sur l'aperçu.
Camera1
Pour implémenter la fonctionnalité de mise au point via l'appui sur l'aperçu dans Camera1, vous devez calculer la zone de mise au point optimale (Area
) pour indiquer où le Camera
doit tenter d'effectuer cette opération. Cet Area
est transmis dans setFocusAreas()
. De plus, vous devez définir un mode de mise au point compatible sur le Camera
. La zone de mise au point n'a d'effet que si le mode de mise au point actuel est FOCUS_MODE_AUTO
, FOCUS_MODE_MACRO
, FOCUS_MODE_CONTINUOUS_VIDEO
ou FOCUS_MODE_CONTINUOUS_PICTURE
.
Chaque Area
est un rectangle avec une pondération spécifiée. La pondération est une valeur comprise entre 1 et 1 000, utilisée pour définir la priorité des zones de mise au point (Areas
) si plusieurs sont définies. Comme cet exemple n'utilise qu'un seul Area
, la valeur de pondération n'a pas d'importance. Les coordonnées de la plage du rectangle vont de -1 000 à 1 000. Le point supérieur gauche correspond à (-1 000, -1 000).
Le point inférieur droit correspond à (1 000, 1 000). La direction dépend de l'orientation du capteur, c'est-à-dire de ce qu'il perçoit. L'orientation n'est pas affectée par la rotation ou la duplication d'écran de Camera.setDisplayOrientation()
. Vous devez donc convertir les coordonnées de l'événement tactile en coordonnées de capteur.
// 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
Le CameraController
écoute les événements tactiles du PreviewView
pour gérer automatiquement la mise au point via l'appui sur l'aperçu. Vous pouvez activer ou désactiver la fonctionnalité de mise au point via l'appui sur l'aperçu avec setTapToFocusEnabled()
, puis vérifier la valeur avec le getter isTapToFocusEnabled()
correspondant.
La méthode getTapToFocusState()
renvoie un objet LiveData
pour suivre les modifications de l'état de mise au point sur le 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
Avec un CameraProvider
, une configuration est nécessaire pour que la mise au point via un appui sur l'aperçu fonctionne. Cet exemple suppose que vous utilisez PreviewView
. Dans le cas contraire, vous devez adapter la logique à appliquer à votre Surface
personnalisée.
Voici la procédure à suivre lorsque vous utilisez le PreviewView
:
- Configurez un détecteur de gestes pour gérer les événements de type "Appuyer".
- Avec cet événement, créez un
MeteringPoint
à l'aide deMeteringPointFactory.createPoint()
. - Avec le
MeteringPoint
, créez unFocusMeteringAction
. - Avec l'objet
CameraControl
sur votreCamera
(renvoyé parbindToLifecycle()
), appelezstartFocusAndMetering()
en transmettant leFocusMeteringAction
. - (Facultatif) Répondez au
FocusMeteringResult
. - Configurez le détecteur de gestes pour qu'il réponde aux événements tactiles dans
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 }
Pincer pour zoomer
Faire un zoom avant ou arrière dans un aperçu est une autre manipulation directe couramment utilisée dans l'aperçu de l'appareil photo. Comme de plus en plus d'appareils photo sont intégrés aux appareils, les utilisateurs s'attendent également à ce que l'objectif offrant la meilleure distance focale soit automatiquement sélectionné une fois la fonctionnalité de zoom déclenchée.
Camera1
Il existe deux façons de zoomer à l'aide de Camera1. La méthode Camera.startSmoothZoom()
s'exécute du niveau de zoom actuel au niveau de zoom que vous transmettez. La méthode Camera.Parameters.setZoom()
passe directement au niveau de zoom que vous transmettez. Avant d'utiliser l'une de ces méthodes, appelez respectivement isSmoothZoomSupported()
ou isZoomSupported()
pour vous assurer que les méthodes de zoom associées dont vous avez besoin sont disponibles sur votre appareil photo.
Pour implémenter le pincement pour zoomer, cet exemple utilise setZoom()
, car l'écouteur tactile de la surface d'aperçu déclenche en continu des événements lorsque le geste de pincement se produit. Par conséquent, il met immédiatement à jour le niveau de zoom à chaque fois. La classe ZoomTouchListener
est spécifiée ci-dessous et doit être définie comme rappel de l'écouteur tactile de la surface d'aperçu.
// 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
Semblable à la fonctionnalité de mise au point via l'appui sur l'aperçu, le CameraController
écoute les événements tactiles de PreviewView afin de gérer le pincement pour zoomer automatiquement. Vous pouvez activer et désactiver la fonctionnalité Pincer pour zoomer avec setPinchToZoomEnabled()
, puis vérifier la valeur avec le getter isPinchToZoomEnabled()
correspondant.
La méthode getZoomState()
renvoie un objet LiveData
pour suivre les modifications apportées au ZoomState
sur le 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
Pour que le pincement permette de zoomer avec le CameraProvider
, vous devez configurer certains paramètres. Si vous n'utilisez pas le PreviewView
, vous devez adapter la logique à votre Surface
personnalisé.
Voici la procédure à suivre lorsque vous utilisez le PreviewView
:
- Configurez un détecteur de gestes à l'échelle pour gérer les événements de pincement.
- Obtenez la valeur
ZoomState
à partir de l'objetCamera.CameraInfo
, où l'instanceCamera
est renvoyée lorsque vous appelezbindToLifecycle()
. - Si le
ZoomState
a une valeurzoomRatio
, enregistrez-la sous le format de zoom actuel. S'il n'y a pas dezoomRatio
pour leZoomState
, utilisez le ratio de zoom par défaut de l'appareil photo (1.0). - Utilisez le produit du rapport de zoom actuel avec le
scaleFactor
pour déterminer le nouveau ratio de zoom, puis transmettez ce résultat dansCameraControl.setZoomRatio()
. - Configurez le détecteur de gestes pour qu'il réponde aux événements tactiles dans
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 }
Prendre une photo
Cette section explique comment déclencher la prise de photos, que ce soit lorsque vous appuyez sur le bouton de l'obturateur, à la fin du retardateur ou lors de tout autre événement de votre choix.
Camera1
Dans Camera1, vous définissez d'abord un Camera.PictureCallback
pour gérer les données d'image lorsqu'elles sont demandées. Voici un exemple simple de PictureCallback
pour gérer les données d'une image 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) } }
Ensuite, chaque fois que vous souhaitez prendre une photo, appelez la méthode takePicture()
sur votre instance Camera
. Cette méthode takePicture()
comporte trois paramètres distincts pour différents types de données. Le premier paramètre correspond à un objet ShutterCallback
(qui n'est pas défini dans cet exemple). Le deuxième paramètre permet à un PictureCallback
de gérer les données brutes (non compressées) de l'appareil photo. Le troisième paramètre est celui utilisé dans cet exemple, car il s'agit d'un PictureCallback
qui gère les données de l'image JPEG.
// Camera1: call takePicture on Camera instance, passing our PictureCallback. camera?.takePicture(null, null, picture)
CameraX : CameraController
Le CameraController
de CameraX conserve la simplicité de Camera1 pour la capture d'image en implémentant sa propre méthode takePicture()
. Définissez ici une fonction permettant de configurer une entrée MediaStore
et de prendre une photo qui y sera enregistrée.
// 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
Prendre une photo avec le CameraProvider
fonctionne presque de la même manière qu'avec le CameraController
, mais vous devez d'abord créer et lier un UseCase
ImageCapture
pour avoir un objet avec lequel appeler 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)
Chaque fois que vous souhaitez prendre une photo, vous pouvez appeler ImageCapture.takePicture()
. Consultez le code CameraController
de cette section pour voir un exemple complet de la fonction 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( ... ) }
Enregistrer une vidéo
Enregistrer une vidéo est bien plus complexe que les scénarios vus jusqu'à présent. Chaque partie du processus doit être configurée correctement, généralement dans un ordre particulier. Vous devrez peut-être également vérifier que la vidéo et l'audio sont synchronisés ou traiter des incohérences supplémentaires au niveau des appareils.
Comme vous le verrez, CameraX gère une grande partie de cette complexité pour vous.
Camera1
La capture vidéo à l'aide de Camera1 nécessite de gérer minutieusement le Camera
et le MediaRecorder
, et les méthodes doivent être appelées dans un ordre spécifique. Vous devez suivre cet ordre pour que votre application fonctionne comme prévu :
- Ouvrez l'appareil photo.
- Préparez et démarrez un aperçu (si votre application montre la vidéo en cours d'enregistrement, ce qui est généralement le cas).
- Déverrouillez l'appareil photo pour que
MediaRecorder
puisse l'utiliser en appelantCamera.unlock()
. - Configurez l'enregistrement en appelant les méthodes suivantes sur
MediaRecorder
:- Connectez votre instance
Camera
àsetCamera(camera)
. - Appelez
setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
. - Appelez
setVideoSource(MediaRecorder.VideoSource.CAMERA)
. - Appelez
setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P))
pour définir la qualité. Consultez la section surCamcorderProfile
pour vous familiariser avec toutes les options de qualité. - Appelez
setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString())
. - Si votre application dispose d'un aperçu de la vidéo, appelez
setPreviewDisplay(preview?.holder?.surface)
. - Appelez
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
. - Appelez
setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT)
. - Appelez
setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT)
. - Appelez
prepare()
pour finaliser la configuration de votreMediaRecorder
.
- Connectez votre instance
- Pour commencer l'enregistrement, appelez
MediaRecorder.start()
. - Pour arrêter l'enregistrement, appelez ces méthodes. Là encore, suivez l'ordre exact :
- Appelez
MediaRecorder.stop()
. - Vous pouvez également supprimer la configuration
MediaRecorder
actuelle en appelantMediaRecorder.reset()
. - Appelez
MediaRecorder.release()
. - Verrouillez l'appareil photo pour que les futures sessions
MediaRecorder
puissent l'utiliser en appelantCamera.lock()
.
- Appelez
- Pour arrêter l'aperçu, appelez
Camera.stopPreview()
. - Enfin, pour libérer le
Camera
afin que d'autres processus puissent l'utiliser, appelezCamera.release()
.
Voici toutes ces étapes combinées :
// 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
Avec le CameraController
de CameraX, vous pouvez activer/désactiver les UseCase
s ImageCapture
, VideoCapture
et ImageAnalysis
de manière indépendante, tant que la liste des UseCases peut être utilisée simultanément.
Les UseCase
s ImageCapture
et ImageAnalysis
sont activés par défaut. Vous n'avez donc pas besoin d'appeler setEnabledUseCases()
pour prendre une photo.
Si vous souhaitez utiliser un CameraController
pour enregistrer une vidéo, vous devez d'abord recourir à setEnabledUseCases()
pour autoriser le UseCase
VideoCapture
.
// CameraX: Enable VideoCapture UseCase on CameraController. cameraController.setEnabledUseCases(VIDEO_CAPTURE);
Lorsque vous souhaitez commencer à enregistrer une vidéo, vous pouvez appeler la fonction CameraController.startRecording()
. Cette fonction permet de stocker la vidéo enregistrée dans un File
, comme vous pouvez le voir dans l'exemple ci-dessous. En outre, vous devez transmettre un Executor
et une classe qui implémente OnVideoSavedCallback
pour gérer les rappels de réussite et d'erreur. À la fin de l'enregistrement, appelez CameraController.stopRecording()
.
Remarque : Si vous utilisez CameraX 1.3.0-alpha02 ou une version ultérieure, un paramètre AudioConfig
supplémentaire vous permet d'activer ou de désactiver l'enregistrement audio au niveau de la vidéo. Pour activer l'enregistrement audio, vous devez vous assurer que vous disposez des autorisations d'accès au micro.
De plus, la méthode stopRecording()
est supprimée dans la version 1.3.0-alpha02, et startRecording()
renvoie un objet Recording
qui peut être utilisé pour suspendre, reprendre et arrêter l'enregistrement vidéo.
// 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
Si vous utilisez un CameraProvider
, vous devez créer un UseCase
VideoCapture
et transmettre un objet Recorder
. Sur le Recorder.Builder
, vous pouvez définir la qualité vidéo et, éventuellement, un FallbackStrategy
, qui gère les cas où un appareil ne remplit pas les spécifications de qualité souhaitées. Liez ensuite l'instance VideoCapture
au CameraProvider
avec vos autres UseCase
s.
// 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)
À ce stade, le Recorder
est accessible sur la propriété videoCapture.output
. Le Recorder
peut démarrer des enregistrements vidéo stockés dans un File
, un ParcelFileDescriptor
ou un MediaStore
. Cet exemple utilise MediaStore
.
Sur le Recorder
, il existe plusieurs méthodes à appeler pour le préparer. Appelez prepareRecording()
pour définir les options de sortie de MediaStore
. Si votre application est autorisée à utiliser le micro de l'appareil, appelez également withAudioEnabled()
.
Ensuite, appelez start()
pour commencer l'enregistrement, en transmettant un contexte et un écouteur d'événements Consumer<VideoRecordEvent>
afin de gérer les événements d'enregistrement vidéo. En cas de succès, la réponse Recording
renvoyée pourra être utilisée pour suspendre, reprendre ou arrêter l'enregistrement.
// 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 } } } } }
Ressources supplémentaires
Le dépôt GitHub d'exemples d'appareils photo contient plusieurs applications CameraX complètes. Ces exemples permettent de comprendre comment les scénarios de ce guide s'intègrent à une application Android complète.
Si vous avez besoin d'aide supplémentaire pour passer à CameraX ou si vous avez des questions concernant la suite d'API Android Camera, contactez-nous via le groupe de discussion CameraX.