Intégration de CameraX avec Jetpack Compose et PreviewView pour la capture photo

CameraX, un composant essentiel de la bibliothèque Jetpack, simplifie considérablement l'intégration des fonctionnalités de l'appareil photo dans les applications Android. Cet article détaille la création d'une application de capture d'images en combinant la puissance de CameraX avec Jetpack Compose et le composant d'interface classique PreviewView. L'implémentation cible Android SDK 34 et illustre l'interopérabilité entre les vues traditionnelles et les interfaces déclaratives modernes.

Configuration des dépendances

Pour débuter, il est nécessaire d'ajouter les modules CameraX ainsi que la bibliothèque Accompanist pour la gestion des permissions dans le fichier build.gradle.kts de votre module :

dependencies {
    val cameraXVersion = "1.3.1"
    implementation("androidx.camera:camera-core:$cameraXVersion")
    implementation("androidx.camera:camera-camera2:$cameraXVersion")
    implementation("androidx.camera:camera-lifecycle:$cameraXVersion")
    implementation("androidx.camera:camera-view:$cameraXVersion")
    
    val accompanistVer = "0.32.0"
    implementation("com.google.accompanist:accompanist-permissions:$accompanistVer")
}

Gestion des permissions de l'appareil photo

Avant d'accéder au flux de la caméra, l'application doit déclarer les fonctionnalités et permissions requises dans le manifeste Android :

<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-permission android:name="android.permission.CAMERA" />

En utilisant Jetpack Compose, la gestion des permissions d'exécution est fluidifiée grâce à la fonction rememberPermissionState d'Accompanist. L'état de la permission est évalué et une requête est déclenchée de manière asynchrone via LaunchedEffect.

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CameraAppEntryPoint() {
    val camState = rememberPermissionState(android.Manifest.permission.CAMERA)
    
    LaunchedEffect(Unit) {
        if (!camState.status.isGranted && !camState.status.shouldShowRationale) {
            camState.launchPermissionRequest()
        }
    }

    if (camState.status.isGranted) {
        CameraPreviewAndCapture()
    } else {
        PermissionDeniedUI(camState)
    }
}

Interface de refus de permission

Si l'utilisateur n'a pas accordé l'accès, une interface dédiée s'affiche pour lui expliquer la nécessité de la permission et lui proposer de réessayer.

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun PermissionDeniedUI(state: PermissionState) {
    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        val infoText = if (state.status.shouldShowRationale) {
            "L'accès à l'appareil photo est indispensable pour cette fonctionnalité."
        } else {
            "Veuillez accorder la permission requise pour continuer."
        }
        Text(text = infoText, textAlign = TextAlign.Center)
        Spacer(modifier = Modifier.height(16.dp))
        Button(onClick = { state.launchPermissionRequest() }) {
            Text("Accorder l'accès")
        }
    }
}

Prévisualisation du flux caméra avec PreviewView

Le composant PreviewView est une vue classique optimisée pour afficher, recadrer et faire pivoter le flux de l'appareil photo. Pour l'intégrer dans une interface Compose, l'enveloppeur AndroidView est indispensable.

Exemples de ScaleType

Deux configurations majeures doivent être maîtrisées :

  • ScaleType (Type de mise à l'échelle) : Détermine comment le flux s'adapte au conteneur. Des options comme FIT_CENTER ajoutent des bandes noires pour conserver le ratio, tandis que FILL_CENTER (le défaut) recadre l'image pour remplir tout l'espace.
  • ImplementationMode (Mode de rendu) : Le mode PERFORMANCE utilise SurfaceView pour un rendu matériel direct, idéal pour réduire la latence. Le mode COMPATIBLE s'appuie sur TextureView, permettant des transformations complexes au prix d'une consommation légèrement supérieure.

Voici l'implémentation du bloc de prévisualsiation :

@Composable
fun CameraPreviewAndCapture() {
    val ctx = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val cameraCtrl = remember { LifecycleCameraController(ctx) }
    val mainExecutor = ContextCompat.getMainExecutor(ctx)

    Box(modifier = Modifier.fillMaxSize()) {
        AndroidView(
            factory = { context ->
                PreviewView(context).apply {
                    layoutParams = ViewGroup.LayoutParams(
                        ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.MATCH_PARENT
                    )
                    setBackgroundColor(android.graphics.Color.BLACK)
                    implementationMode = PreviewView.ImplementationMode.PERFORMANCE
                    scaleType = PreviewView.ScaleType.FIT_CENTER
                    controller = cameraCtrl
                    cameraCtrl.bindToLifecycle(lifecycleOwner)
                }
            },
            onRelease = {
                cameraCtrl.unbind()
            }
        )
        
        FloatingActionButton(
            onClick = { savePhoto(cameraCtrl, mainExecutor, ctx) },
            modifier = Modifier.align(Alignment.BottomCenter).padding(32.dp)
        ) {
            Icon(Icons.Default.Camera, contentDescription = "Prendre une photo")
        }
    }
}

Capture et sauvegarde de la photo

CameraX offre l'API takePicture pour capturer des images. Bien qu'il soit possible de récupérer directement un flux en mémoire, la persistance de l'image sur le stockage de l'appareil nécessite l'utilisation de OutputFileOptions combiné à OnImageSavedCallback.

La fonction suivante gère la création d'un fichier temporaire dans le répertoire cache de l'application et délègue l'écriture asynchrone à l'exécuteur principal.

fun savePhoto(
    controller: LifecycleCameraController, 
    executor: Executor, 
    context: Context
) {
    val photoFile = File(context.cacheDir, "capture_${System.currentTimeMillis()}.jpg")
    val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()

    controller.takePicture(
        outputOptions,
        executor,
        object : ImageCapture.OnImageSavedCallback {
            override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
                Log.i("CameraXApp", "Image sauvegardée avec succès : ${outputFileResults.savedUri}")
            }

            override fun onError(exception: ImageCaptureException) {
                Log.e("CameraXApp", "Échec de la sauvegarde de l'image", exception)
            }
        }
    )
}

Une fois le cliché pris, le fichier est stocké dans le dossier cache. L'URI retourné par outputFileResults.savedUri permet d'accéder directement à cette ressource pour un traitement ultérieur ou un partage.

Étiquettes: Android CameraX Jetpack Compose kotlin PreviewView

Publié le 7 juin à 17h06