Utilisez CameraX pour prendre des photos et enregistrer des vidéos dans Jetpack Compose

Dans l'histoire du développement d'Android, l'API de l'appareil photo a toujours été critiquée. Quiconque l'a utilisée sait que le sentiment intuitif est que la configuration est complexe, gonflée, difficile à utiliser et difficile à comprendre. On peut le voir sur le site officiel. Itinéraire d'itération de l'API à propos de Camera L'officiel essaie également d'améliorer continuellement l'expérience des développeurs sur Camera. L'API Camera est passée par CameraXetCamera2(obsolète),Cameratrois versions :

insérez la description de l'image ici

L'API Camera de première génération a été déclarée obsolète depuis la version 5.0, et l'API de Camera2 est particulièrement difficile à utiliser. Après que de nombreuses personnes l'utilisent, elle n'est pas aussi bonne que la précédente Camera , il y a donc CameraX , qui est en fait basé sur Camera2, mais il est utilisé Certaines optimisations plus humaines, qui font partie de la bibliothèque de composants Jetpack, sont actuellement la solution officielle de Camera. Par conséquent, si vous avez un nouveau projet impliquant l'API Camera ou prévoyez de mettre à niveau l'ancienne API Camera, il est recommandé d'utiliser CameraX directement.

Cet article explique comment utiliser CameraX dans Jetpack Compose.

Préparations CameraX

Ajoutez d'abord les dépendances :

dependencies {
    
     
  def camerax_version = "1.3.0-alpha04" 
  // implementation "androidx.camera:camera-core:${camerax_version}" // 可选,因为camera-camera2 包含了camera-core
  implementation "androidx.camera:camera-camera2:${camerax_version}" 
  implementation "androidx.camera:camera-lifecycle:${camerax_version}" 
  implementation "androidx.camera:camera-video:${camerax_version}" 
  implementation "androidx.camera:camera-view:${camerax_version}"  
  implementation "androidx.camera:camera-extensions:${camerax_version}"
}

Remarque, les dernières informations de version des bibliothèques ci-dessus peuvent être trouvées ici : https://developer.android.com/jetpack/androidx/releases/camera?hl=zh-cn

Étant donné que l'application d'autorisation de caméra est requise pour utiliser la caméra, une dépendance doit être ajoutée accompanist-permissionspour demander une autorisation dans Compose :

val accompanist_version = "0.31.2-alpha"
implementation "com.google.accompanist:accompanist-permissions:$accompanist_version"

Notez que les dernières informations sur la version des bibliothèques ci-dessus peuvent être trouvées ici : https://github.com/google/accompanist/releases

Pensez ensuite AndroidManifest.xmlà ajouter la déclaration de permission dans le :

<manifest .. >
    <uses-permission android:name="android.permission.CAMERA" />
    ..
</manifest>

CameraX a les exigences de version minimales suivantes :

  • API Android niveau 21
  • Composants d'architecture Android 1.1.1

Pour une activité sensible au cycle de vie, utilisez FragmentActivity ou AppCompatActivity.

Aperçu de la caméra CameraX

Regardons principalement comment CameraX effectue l'aperçu de la caméra

Créer un aperçu PreviewView

Étant donné que Jetpack Compose ne fournit pas directement un composant distinct pour l'aperçu de l'appareil photo, la solution consiste à utiliser AndroidViewce Composable pour intégrer le contrôle d'aperçu natif dans Compose pour l'affichage. code afficher comme ci-dessous:

@Composable
private fun CameraPreviewExample() {
    
    
    Scaffold(modifier = Modifier.fillMaxSize()) {
    
     innerPadding: PaddingValues ->
        AndroidView(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding),
            factory = {
    
     context ->
                PreviewView(context).apply {
    
    
                    setBackgroundColor(Color.White.toArgb())
                    layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
                    scaleType = PreviewView.ScaleType.FILL_START
                    implementationMode = PreviewView.ImplementationMode.COMPATIBLE
                }
            }
        )
    }
}

Voici un contrôle View PreviewViewnatif camera-viewde la bibliothèque, examinons plusieurs de ses méthodes de paramétrage :

1. : Cette méthode est utilisée pour définir le PreviewView.setImplementationMode()mode de mise en œuvre spécifique adapté à l'application

Mode de mise en œuvre

PreviewViewLe flux de prévisualisation peut être rendu sur la cible en utilisant l'un des modes suivantsView :

  • PERFORMANCEest le mode par défaut et affiche le flux vidéo PreviewViewen utilisant SurfaceView, mais revient à utiliser dans certains cas TextureView. SurfaceViewAvec une interface de dessin dédiée, l'objet est plus susceptible d'implémenter une superposition matérielle via un compositeur matériel interne, surtout s'il n'y a pas d'autres éléments d'interface tels que des boutons au-dessus de la vidéo de prévisualisation. Grâce au rendu à l'aide d'une superposition matérielle, les images vidéo sont contournées du chemin GPU, ce qui réduit la consommation d'énergie de la plate-forme et la latence.

  • COMPATIBLEmode, dans ce mode, PreviewViewsera utilisé TextureView. Contrairement à SurfaceView, cet objet n'a pas de surface de dessin dédiée. Par conséquent, la vidéo ne peut pas être affichée tant qu'elle n'a pas été rendue par fusion. Au cours de cette étape supplémentaire, l'application peut effectuer des traitements supplémentaires, tels que la mise à l'échelle et la rotation de la vidéo sans restriction.

Remarque : Car PERFORMANCEest le mode par défaut, si l'appareil ne le prend pas en charge SurfaceView, PreviewViewil reviendra à utiliser TextureView. Revient à lorsque le niveau d'API est 24ou inférieur et que le niveau de prise en charge du matériel de la caméra est CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACYou différent de la Preview.getTargetRotation()rotation de l'affichage . N'utilisez pas ce mode s'il est défini sur une valeur autre que la rotation du moniteur, car les rotations arbitraires ne sont pas prises en charge. N'utilisez pas ce mode si la vue d'aperçu doit être animée. Les animations ne sont pas prises en charge au niveau de l'API ou inférieur . En outre, pour l'état de flux d'aperçu fourni dans , l'état peut se produire plus tôt si ce mode est utilisé.PreviewViewPreviewViewTextureView
Preview.Builder.setTargetRotation(int)SurfaceView24SurfaceViewgetPreviewStreamStatePreviewView.StreamState.streaming

Évidemment, si c'est pour des raisons de performances, vous devez utiliser PERFORMANCEle mode, mais si c'est pour des raisons de compatibilité, il est préférable d'utiliser COMPATIBLEle mode.

2.PreviewView.setScaleType() : Cette méthode est utilisée pour définir le type de mise à l'échelle le plus adapté à l'application .

type de zoom

Lorsque la résolution de la vidéo d'aperçu PreviewViewdiffère des dimensions cibles, le contenu vidéo doit être recadré ou mis en boîte aux lettres pour s'adapter à la vue (en conservant le format d'image d'origine). A cet effet, PreviewViewsont prévus ScaleTypes:

  • FIT_CENTER, FIT_START, et FIT_ENDpour ajouter la boîte aux lettres . L'intégralité du contenu vidéo est redimensionnée (mise à l'échelle vers le haut ou vers le bas) à PreviewViewla plus grande taille pouvant être affichée dans la cible . Cependant, bien que l'intégralité de l'image vidéo soit affichée dans son intégralité, il peut y avoir des parties vierges de la capture d'écran. L'image vidéo est alignée sur le centre, le début ou la fin de la vue cible, selon celui des trois types de zoom que vous avez sélectionné ci-dessus.

  • FILL_CENTER, FILL_STARTet FILL_ENDpour le découpage . Si le rapport d'aspect de la vidéo PreviewViewne correspond pas au , seule une partie du contenu sera affichée, mais la vidéo remplira toujours l'intégralité du PreviewView.

CameraXLe type de mise à l'échelle par défaut utilisé est FILL_CENTER.

Remarque : L'objectif principal du type de zoom est d'empêcher l'étirement et la déformation de l'aperçu. Si vous utilisez l'API Camera ou Camera2 précédente, mon approche générale consiste à obtenir une liste des résolutions d'aperçu prises en charge par l'appareil photo et à sélectionner une résolution d'aperçu. . Alignez ensuite le rapport d'aspect de la commande SurfaceViewou TextureViewsur le rapport d'aspect de la résolution d'aperçu sélectionnée, de sorte qu'il n'y ait pas de problèmes d'étirement et de déformation pendant l'aperçu, et que l'effet final soit exactement le même que le type de zoom ci-dessus. Heureusement, maintenant avec la prise en charge officielle au niveau de l'API, les développeurs n'ont plus à faire ces choses gênantes manuellement.

Par exemple, l'image de gauche ci-dessous est l'effet d'affichage d'aperçu normal, tandis que l'image de droite est l'effet d'affichage d'aperçu de déformation étirée :

insérez la description de l'image ici

Ce genre d'expérience est très mauvais. Le plus gros problème est que ce que vous voyez est ce que vous obtenez (l'image ou le fichier vidéo enregistré est incohérent avec l'effet vu dans l'aperçu).

Prenez l'image 4:3 affichée sur l'écran d'aperçu 16:9 comme exemple, si aucun traitement n'est effectué, l'étirement et la déformation se produiront 100 % du temps :

insérez la description de l'image ici

L'image suivante montre l'effet des différents types de mise à l'échelle appliqués :

insérez la description de l'image ici

Il existe certaines restrictions d'utilisation PreviewView. Lorsque vous utilisez PreviewView, vous ne pouvez effectuer aucune des opérations suivantes :

  • Créé SurfaceTexturepour être réglé sur TextureViewet .Preview.SurfaceProvider
  • TextureViewExtrait de SurfaceTextureet Preview.SurfaceProviderdéfini sur le .
  • SurfaceViewObtenez-le à partir de Surfaceet Preview.SurfaceProvideractivez-le.

Si l'une des conditions ci-dessus se produit, Previewla diffusion d'images vers s'arrêtera PreviewView.

Liaison Lifecycle CameraController

Après la création PreviewView, l' étape suivante consiste à en définir un pour l' instance que nous avons créée CameraController, qui est une classe abstraite dont l' implémentation est LifecycleCameraController, puis nous pouvons CameraControllerlier l' instance créée au détenteur actuel du cycle de vie lifecycleOwner. code afficher comme ci-dessous:

@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun CameraPreviewExample() {
    
    
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val cameraController = remember {
    
     LifecycleCameraController(context) }

    Scaffold(modifier = Modifier.fillMaxSize()) {
    
     innerPadding: PaddingValues ->
        AndroidView(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding),
            factory = {
    
     context ->
                PreviewView(context).apply {
    
    
                    setBackgroundColor(Color.White.toArgb())
                    layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
                    scaleType = PreviewView.ScaleType.FILL_START
                    implementationMode = PreviewView.ImplementationMode.COMPATIBLE
                }.also {
    
     previewView ->
                    previewView.controller = cameraController
                    cameraController.bindToLifecycle(lifecycleOwner)
                }
            },
            onReset = {
    
    },
            onRelease = {
    
    
                cameraController.unbind()
            }
        )
    }
}

Notez que dans le code ci-dessus, nous dissocions le contrôleur de celui-cionRelease dans le callback . PreviewViewDe cette façon, nous pouvons nous assurer que AndroidViewles ressources de la caméra sont libérées lorsqu'elles ne sont plus utilisées.

demande d'accès

L'interface Composable de prévisualisation s'affiche uniquement après que l'application a obtenu l'autorisation de l'appareil photo, sinon une interface Composable d'espace réservé s'affiche. Le code de référence pour l'obtention de l'autorisation est le suivant :

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun ExampleCameraScreen() {
    
    
    val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
    LaunchedEffect(key1 = Unit) {
    
    
        if (!cameraPermissionState.status.isGranted && !cameraPermissionState.status.shouldShowRationale) {
    
    
            cameraPermissionState.launchPermissionRequest()
        }
    }
    if (cameraPermissionState.status.isGranted) {
    
     // 相机权限已授权, 显示预览界面
        CameraPreviewExample()
    } else {
    
     // 未授权,显示未授权页面
        NoCameraPermissionScreen(cameraPermissionState = cameraPermissionState)
    }
}

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun NoCameraPermissionScreen(cameraPermissionState: PermissionState) {
    
    
    // In this screen you should notify the user that the permission
    // is required and maybe offer a button to start another camera perission request
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
    
    
        val textToShow = if (cameraPermissionState.status.shouldShowRationale) {
    
    
            // 如果用户之前选择了拒绝该权限,应当向用户解释为什么应用程序需要这个权限
            "未获取相机授权将导致该功能无法正常使用。"
        } else {
    
    
            // 首次请求授权
            "该功能需要使用相机权限,请点击授权。"
        }
        Text(textToShow)
        Spacer(Modifier.height(8.dp))
        Button(onClick = {
    
     cameraPermissionState.launchPermissionRequest() }) {
    
     Text("请求权限") }
    }
}

Pour plus d'informations sur la façon de demander des autorisations dynamiques dans Compose, veuillez vous référer à Accompanist in Jetpack Compose , donc je n'entrerai pas dans les détails ici.

paramètres plein écran

Afin d'afficher la caméra en plein écran lors de la prévisualisation sans la barre d'état supérieure, vous pouvez ajouter le code suivant avant Activityla onCreate()méthode :setContent

if (isFullScreen) {
    
    
   requestWindowFeature(Window.FEATURE_NO_TITLE)
    //这个必须设置,否则不生效。
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
    
    
        window.attributes.layoutInDisplayCutoutMode =
            WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
    }
    WindowCompat.setDecorFitsSystemWindows(window, false)
    val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
    windowInsetsController.hide(WindowInsetsCompat.Type.statusBars()) // 隐藏状态栏
    windowInsetsController.hide(WindowInsetsCompat.Type.navigationBars()) // 隐藏导航栏
    //将底部的navigation操作栏弄成透明,滑动显示,并且浮在上面
    windowInsetsController.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
}

Habituellement ce code devrait fonctionner, sinon, essayez de modifier le thème theme :

// themes.xml
<?xml version="1.0" encoding="utf-8"?>
<resources> 
    <style name="Theme.MyComposeApplication" parent="android:Theme.Material.Light.NoActionBar.Fullscreen" >
        <item name="android:statusBarColor">@android:color/transparent</item>
        <item name="android:navigationBarColor">@android:color/transparent</item>
        <item name="android:windowTranslucentStatus">true</item>
    </style> 
</resources>

CameraX pour prendre des photos

Prendre des photos dans CameraX fournit principalement deux méthodes de surcharge :

  • takePicture(Executor, OnImageCapturedCallback): Cette méthode fournit une mémoire tampon pour l'image capturée.
  • takePicture(OutputFileOptions, Executor, OnImageSavedCallback): Cette méthode enregistre l'image capturée à l'emplacement de fichier fourni.

Ajoutons un FloatingActionButtonbouton CameraPreviewExamplequi déclenchera la fonction caméra lorsqu'il sera cliqué. code afficher comme ci-dessous:

@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun CameraPreviewExample() {
    
    
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val cameraController = remember {
    
     LifecycleCameraController(context) }

    Scaffold(
        modifier = Modifier.fillMaxSize(),
        floatingActionButton = {
    
    
            FloatingActionButton(onClick = {
    
     takePhoto(context, cameraController) }) {
    
    
                Icon(
                    imageVector = ImageVector.vectorResource(id = R.drawable.ic_camera_24),
                    contentDescription = "Take picture"
                )
            }
        },
        floatingActionButtonPosition = FabPosition.Center,
    ) {
    
     innerPadding: PaddingValues ->
        AndroidView(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding),
            factory = {
    
     context -> 
                PreviewView(context).apply {
    
    
                    setBackgroundColor(Color.White.toArgb())
                    layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
                    scaleType = PreviewView.ScaleType.FILL_START
                    implementationMode = PreviewView.ImplementationMode.COMPATIBLE
                }.also {
    
     previewView ->
                    previewView.controller = cameraController
                    cameraController.bindToLifecycle(lifecycleOwner)
                }
            },
            onReset = {
    
    },
            onRelease = {
    
    
                cameraController.unbind() 
            }
        )
    }
}
fun takePhoto(context: Context, cameraController: LifecycleCameraController) {
    
    
    val mainExecutor = ContextCompat.getMainExecutor(context)
    // Create time stamped name and MediaStore entry.
    val name = SimpleDateFormat(FILENAME, Locale.CHINA)
        .format(System.currentTimeMillis())
    val contentValues = ContentValues().apply {
    
    
        put(MediaStore.MediaColumns.DISPLAY_NAME, name)
        put(MediaStore.MediaColumns.MIME_TYPE, PHOTO_TYPE)
        if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
    
    
            val appName = context.resources.getString(R.string.app_name)
            put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/${
      
      appName}")
        }
    }
    // Create output options object which contains file + metadata
    val outputOptions = ImageCapture.OutputFileOptions
        .Builder(context.contentResolver, MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            contentValues)
        .build()
    cameraController.takePicture(outputOptions, mainExecutor, object : ImageCapture.OnImageSavedCallback {
    
    
            override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
    
    
                val savedUri = outputFileResults.savedUri
                Log.d(TAG, "Photo capture succeeded: $savedUri")
                context.notifySystem(savedUri)
            }
            override fun onError(exception: ImageCaptureException) {
    
    
                Log.e(TAG, "Photo capture failed: ${
      
      exception.message}", exception)
            }
        }
    )
    context.showFlushAnimation()
}

Dans le rappel de méthode OnImageSavedCallbackde , le fichier image enregistré onImageSavedpeut être obtenu , puis un traitement commercial supplémentaire peut être effectué.outputFileResultsUri

Si vous souhaitez exécuter vous-même la logique de sauvegarde après avoir pris une photo, ou juste pour l'affichage sans sauvegarde, vous pouvez utiliser un autre callback OnImageCapturedCallback:

fun takePhoto2(context: Context, cameraController: LifecycleCameraController) {
    
    
    val mainExecutor = ContextCompat.getMainExecutor(context)
    cameraController.takePicture(mainExecutor,  object : ImageCapture.OnImageCapturedCallback() {
    
    
        override fun onCaptureSuccess(image: ImageProxy) {
    
    
            Log.e(TAG, "onCaptureSuccess: ${
      
      image.imageInfo}")
            // Process the captured image here
            try {
    
    
                // The supported format is ImageFormat.YUV_420_888 or PixelFormat.RGBA_8888.
                val bitmap = image.toBitmap()
                Log.e(TAG, "onCaptureSuccess bitmap: ${
      
      bitmap.width} x ${
      
      bitmap.height}")
            } catch (e: Exception) {
    
    
                Log.e(TAG, "onCaptureSuccess Exception: ${
      
      e.message}")
            }
        }
    })
    context.showFlushAnimation()
}

Dans ce rappel, ImageProxy#toBitmapla méthode peut être utilisée pour convertir facilement les données brutes après avoir pris des photos en Bitmapvue de les afficher. Cependant, le format par défaut obtenu ici est ImageFormat.JPEGque toBitmapla conversion de la méthode échouera. Vous pouvez vous référer au code suivant pour le résoudre :

fun takePhoto2(context: Context, cameraController: LifecycleCameraController) {
    
    
    val mainExecutor = ContextCompat.getMainExecutor(context)
    cameraController.takePicture(mainExecutor,  object : ImageCapture.OnImageCapturedCallback() {
    
    
        override fun onCaptureSuccess(image: ImageProxy) {
    
    
            Log.e(TAG, "onCaptureSuccess: ${
      
      image.format}")
            // Process the captured image here
            try {
    
    
                var bitmap: Bitmap? = null
                // The supported format is ImageFormat.YUV_420_888 or PixelFormat.RGBA_8888.
                if (image.format == ImageFormat.YUV_420_888 || image.format == PixelFormat.RGBA_8888) {
    
    
                    bitmap = image.toBitmap()
                } else if (image.format == ImageFormat.JPEG) {
    
    
                    val planes = image.planes
                    val buffer = planes[0].buffer // 因为是ImageFormat.JPEG格式,所以 image.getPlanes()返回的数组只有一个,也就是第0个。
                    val size = buffer.remaining()
                    val bytes = ByteArray(size)
                    buffer.get(bytes, 0, size)
                    // ImageFormat.JPEG格式直接转化为Bitmap格式。
                    bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
                }
                if (bitmap != null) {
    
    
                    Log.e(TAG, "onCaptureSuccess bitmap: ${
      
      bitmap.width} x ${
      
      bitmap.height}")
                }
            } catch (e: Exception) {
    
    
                Log.e(TAG, "onCaptureSuccess Exception: ${
      
      e.message}")
            }
        }
    })
    context.showFlushAnimation()
}

Si le format YUV est obtenu ici, en plus d'appeler directement image.toBitmap()la méthode, l'officiel fournit également une classe d'outils qui peut YUV_420_888convertir le format en objet RGBdu format Bitmap, veuillez vous référer à YuvToRgbConverter.kt .

Le code complet de l'exemple ci-dessus :

@Composable
fun ExampleCameraNavHost() {
    
    
    val navController = rememberNavController()
    NavHost(navController, startDestination = "CameraScreen") {
    
    
        composable("CameraScreen") {
    
    
            ExampleCameraScreen(navController = navController)
        }
        composable("ImageScreen") {
    
    
            ImageScreen(navController = navController)
        }
    }
}

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun ExampleCameraScreen(navController: NavHostController) {
    
    
    val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA)
    LaunchedEffect(key1 = Unit) {
    
    
        if (!cameraPermissionState.status.isGranted && !cameraPermissionState.status.shouldShowRationale) {
    
    
            cameraPermissionState.launchPermissionRequest()
        }
    }
    if (cameraPermissionState.status.isGranted) {
    
     // 相机权限已授权, 显示预览界面
        CameraPreviewExample(navController)
    } else {
    
     // 未授权,显示未授权页面
        NoCameraPermissionScreen(cameraPermissionState = cameraPermissionState)
    }
}

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun NoCameraPermissionScreen(cameraPermissionState: PermissionState) {
    
    
    // In this screen you should notify the user that the permission
    // is required and maybe offer a button to start another camera perission request
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
    
    
        val textToShow = if (cameraPermissionState.status.shouldShowRationale) {
    
    
            // 如果用户之前选择了拒绝该权限,应当向用户解释为什么应用程序需要这个权限
            "未获取相机授权将导致该功能无法正常使用。"
        } else {
    
    
            // 首次请求授权
            "该功能需要使用相机权限,请点击授权。"
        }
        Text(textToShow)
        Spacer(Modifier.height(8.dp))
        Button(onClick = {
    
     cameraPermissionState.launchPermissionRequest() }) {
    
     Text("请求权限") }
    }
}

private const val TAG = "CameraXBasic"
private const val FILENAME = "yyyy-MM-dd-HH-mm-ss-SSS"
private const val PHOTO_TYPE = "image/jpeg"

@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun CameraPreviewExample(navController: NavHostController) {
    
    
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val cameraController = remember {
    
     LifecycleCameraController(context) }

    Scaffold(
        modifier = Modifier.fillMaxSize(),
        floatingActionButton = {
    
    
            FloatingActionButton(onClick = {
    
    
                 takePhoto(context, cameraController, navController)
                // takePhoto2(context, cameraController, navController)
                // takePhoto3(context, cameraController, navController)
            }) {
    
    
                Icon(
                    imageVector = ImageVector.vectorResource(id = R.drawable.ic_camera_24),
                    contentDescription = "Take picture"
                )
            }
        },
        floatingActionButtonPosition = FabPosition.Center,
    ) {
    
     innerPadding: PaddingValues ->
        AndroidView(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding),
            factory = {
    
     context ->
                cameraController.imageCaptureMode = CAPTURE_MODE_MINIMIZE_LATENCY
                PreviewView(context).apply {
    
    
                    setBackgroundColor(Color.White.toArgb())
                    layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
                    scaleType = PreviewView.ScaleType.FILL_CENTER
                    implementationMode = PreviewView.ImplementationMode.COMPATIBLE
                }.also {
    
     previewView ->
                    previewView.controller = cameraController
                    cameraController.bindToLifecycle(lifecycleOwner)
                }
            },
            onReset = {
    
    },
            onRelease = {
    
    
                cameraController.unbind()
            }
        )
    }
}

fun takePhoto(context: Context, cameraController: LifecycleCameraController, navController: NavHostController) {
    
    
    val mainExecutor = ContextCompat.getMainExecutor(context)
    // Create time stamped name and MediaStore entry.
    val name = SimpleDateFormat(FILENAME, Locale.CHINA)
        .format(System.currentTimeMillis())
    val contentValues = ContentValues().apply {
    
    
        put(MediaStore.MediaColumns.DISPLAY_NAME, name)
        put(MediaStore.MediaColumns.MIME_TYPE, PHOTO_TYPE)
        if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
    
    
            val appName = context.resources.getString(R.string.app_name)
            put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/${
      
      appName}")
        }
    }
    // Create output options object which contains file + metadata
    val outputOptions = ImageCapture.OutputFileOptions
        .Builder(context.contentResolver, MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            contentValues)
        .build()
    cameraController.takePicture(outputOptions, mainExecutor, object : ImageCapture.OnImageSavedCallback {
    
    
            override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
    
    
                val savedUri = outputFileResults.savedUri
                Log.d(TAG, "Photo capture succeeded: $savedUri")
                context.notifySystem(savedUri)

                navController.currentBackStackEntry?.savedStateHandle?.set("savedUri", savedUri)
                navController.navigate("ImageScreen")
            }
            override fun onError(exception: ImageCaptureException) {
    
    
                Log.e(TAG, "Photo capture failed: ${
      
      exception.message}", exception)
            }
        }
    )
    context.showFlushAnimation()
}

fun takePhoto2(context: Context, cameraController: LifecycleCameraController, navController: NavHostController) {
    
    
    val mainExecutor = ContextCompat.getMainExecutor(context)
    cameraController.takePicture(mainExecutor,  object : ImageCapture.OnImageCapturedCallback() {
    
    
        override fun onCaptureSuccess(image: ImageProxy) {
    
    
            Log.e(TAG, "onCaptureSuccess: ${
      
      image.format}")
            // Process the captured image here
            val scopeWithNoEffect = CoroutineScope(SupervisorJob())
            scopeWithNoEffect.launch {
    
    
                val savedUri = withContext(Dispatchers.IO) {
    
    
                    try {
    
    
                        var bitmap: Bitmap? = null
                        // The supported format is ImageFormat.YUV_420_888 or PixelFormat.RGBA_8888.
                        if (image.format == ImageFormat.YUV_420_888 || image.format == PixelFormat.RGBA_8888) {
    
    
                            bitmap = image.toBitmap()
                        } else if (image.format == ImageFormat.JPEG) {
    
    
                            val planes = image.planes
                            val buffer = planes[0].buffer // 因为是ImageFormat.JPEG格式,所以 image.getPlanes()返回的数组只有一个,也就是第0个。
                            val size = buffer.remaining()
                            val bytes = ByteArray(size)
                            buffer.get(bytes, 0, size)
                            // ImageFormat.JPEG格式直接转化为Bitmap格式。
                            bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
                        }
                        bitmap?.let {
    
    
                            // 保存bitmap到文件中
                            val photoFile = File(
                                context.getOutputDirectory(),
                                SimpleDateFormat(FILE_FORMAT, Locale.CHINA).format(System.currentTimeMillis()) + ".jpg"
                            )
                            BitmapUtilJava.saveBitmap(bitmap, photoFile.absolutePath, 100)
                            val savedUri = Uri.fromFile(photoFile)
                            savedUri
                        }
                    } catch (e: Exception) {
    
    
                        if (e is CancellationException) throw e
                        Log.e(TAG, "onCaptureSuccess Exception: ${
      
      e.message}")
                        null
                    }
                }
                mainExecutor.execute {
    
    
                	context.notifySystem(savedUri)
                    navController.currentBackStackEntry?.savedStateHandle?.set("savedUri", savedUri)
                    navController.navigate("ImageScreen")
                }
            }

        }
    })
    context.showFlushAnimation()
}
fun takePhoto3(context: Context, cameraController: LifecycleCameraController, navController: NavHostController) {
    
    
    val photoFile = File(
        context.getOutputDirectory(),
        SimpleDateFormat(FILENAME, Locale.CHINA).format(System.currentTimeMillis()) + ".jpg"
    )
    val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
    val mainExecutor = ContextCompat.getMainExecutor(context)
    cameraController.takePicture(outputOptions, mainExecutor, object: ImageCapture.OnImageSavedCallback {
    
    
        override fun onError(exception: ImageCaptureException) {
    
    
            Log.e(TAG, "Take photo error:", exception)
            onError(exception)
        }

        override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
    
    
            val savedUri = Uri.fromFile(photoFile)
            Log.d(TAG, "Photo capture succeeded: $savedUri")
            context.notifySystem(savedUri)

            navController.currentBackStackEntry?.savedStateHandle?.set("savedUri", savedUri)
            navController.navigate("ImageScreen")
        }
    })
    context.showFlushAnimation()
}

// flash 动画
private fun Context.showFlushAnimation() {
    
    
    // We can only change the foreground Drawable using API level 23+ API
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    
    
        // Display flash animation to indicate that photo was captured
        if (this is Activity) {
    
    
            val decorView = window.decorView
            decorView.postDelayed({
    
    
                decorView.foreground = ColorDrawable(android.graphics.Color.WHITE)
                decorView.postDelayed({
    
     decorView.foreground = null }, ANIMATION_FAST_MILLIS)
            }, ANIMATION_SLOW_MILLIS)
        }
    }
}
// 发送系统广播
private fun Context.notifySystem(savedUri: Uri?) {
    
    
    // 对于运行API级别>=24的设备,将忽略隐式广播,因此,如果您只针对24+级API,则可以删除此语句
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
    
    
        sendBroadcast(Intent(Camera.ACTION_NEW_PICTURE, savedUri))
    }
}
private fun Context.getOutputDirectory(): File {
    
    
    val mediaDir = externalMediaDirs.firstOrNull()?.let {
    
    
        File(it, resources.getString(R.string.app_name)).apply {
    
     mkdirs() }
    }

    return if (mediaDir != null && mediaDir.exists()) mediaDir else filesDir
}
// ImageScreen.kt 用于展示拍照结果的屏幕
@Composable
fun ImageScreen(navController: NavHostController) {
    
    
    val context = LocalContext.current
    var imageBitmap by remember {
    
     mutableStateOf<ImageBitmap?>(null) }
    val scope = rememberCoroutineScope()
    val savedUri = navController.previousBackStackEntry?.savedStateHandle?.get<Uri>("savedUri")
    savedUri?.run {
    
    
        scope.launch {
    
    
            withContext(Dispatchers.IO){
    
    
                val bitmap = BitmapUtilJava.getBitmapFromUri(context, savedUri)
                imageBitmap = BitmapUtilJava.scaleBitmap(bitmap, 1920, 1080).asImageBitmap()
            }
         }
        imageBitmap?.let {
    
    
            Image(it,
                contentDescription = null,
                modifier = Modifier.fillMaxWidth(),
                contentScale = ContentScale.Crop
            )
        }
    }
}
// 用到的几个工具类
public static void saveBitmap(Bitmap mBitmap, String filePath, int quality) {
    
    
	File f = new File(filePath);
	FileOutputStream fOut = null;
	try {
    
    
		fOut = new FileOutputStream(f);
	} catch (FileNotFoundException e) {
    
    
		e.printStackTrace();
	}
	mBitmap.compress(Bitmap.CompressFormat.JPEG, quality, fOut);
	try {
    
    
		if (fOut != null) {
    
    
			fOut.flush();
		}
	} catch (IOException e) {
    
    
		e.printStackTrace();
	}
}
/**
 * 宽高比取最大值缩放图片.
 *
 * @param bitmap     加载的图片
 * @param widthSize  缩放之后的图片宽度,一般就是屏幕的宽度.
 * @param heightSize 缩放之后的图片高度,一般就是屏幕的高度.
 */
public static Bitmap scaleBitmap(Bitmap bitmap, int widthSize, int heightSize) {
    
    
	int bmpW = bitmap.getWidth();
	int bmpH = bitmap.getHeight();
	float scaleW = ((float) widthSize) / bmpW;
	float scaleH = ((float) heightSize) / bmpH;
	//取宽高最大比例来缩放图片
	float max = Math.max(scaleW, scaleH);
	Matrix matrix = new Matrix();
	matrix.postScale(max, max);
	return Bitmap.createBitmap(bitmap, 0, 0, bmpW, bmpH, matrix, true);
}
/**
 * 根据Uri返回Bitmap对象
 * @param context
 * @param uri
 * @return
 */
public static Bitmap getBitmapFromUri(Context context, Uri uri){
    
    
	try {
    
    
		// 这种方式也可以
		// BitmapFactory.decodeStream(context.getContentResolver().openInputStream(uri));
		return MediaStore.Images.Media.getBitmap(context.getContentResolver(), uri);
	}catch (Exception e){
    
    
		e.printStackTrace();
		return null;
	}
}

Remarque : les opérations liées aux bitmaps sont des tâches chronophages et doivent être exécutées dans un planificateur de coroutines séparé du thread principal à l'aide de coroutines. En pratique, il doit être amélioré par lui-même s'il est utilisé dans un projet de production.

CameraProvider 与 CameraController

Tous les codes officiels de CameraX fournissent en fait CameraControllerdeux CameraProviderimplémentations. Choisissez si vous voulez la manière la plus simple d'utiliser CameraX CameraController, choisissez si vous avez besoin de plus de flexibilité CameraProvider.

Pour déterminer quelle implémentation vous convient le mieux, les avantages de chacune sont répertoriés ci-dessous :

CaméraContrôleur Fournisseur de caméra
Nécessite peu de code de configuration permettre un plus grand contrôle
Permet à CameraX de gérer une plus grande partie du processus de configuration, ce qui signifie que des fonctionnalités telles que toucher pour faire la mise au point et pincer pour zoomer fonctionnent automatiquement Étant donné que le développeur de l'application gère les paramètres, il existe davantage de possibilités de personnaliser la configuration, comme l'activation de la rotation de l'image de sortie ou la définition du format de l'image de sortie dans ImageAnalysis.
Nécessite PreviewView pour l'aperçu de la caméra, permettant à CameraX de fournir une intégration transparente de bout en bout, comme dans notre intégration ML Suite, qui prend les coordonnées de résultat du modèle d'apprentissage automatique (telles que les boîtes englobantes de visage) directement mappées aux coordonnées de prévisualisation Possibilité d'utiliser une "Surface" personnalisée pour l'aperçu de la caméra, permettant plus de flexibilité, comme l'utilisation de votre code "Surface" existant qui peut être utilisé comme entrée dans d'autres parties de l'application

Utilisez CameraProvider pour réaliser la fonction caméra

Pour un accès facile CameraProvider, vous pouvez d'abord définir une fonction d'extension :

private suspend fun Context.getCameraProvider(): ProcessCameraProvider {
    
    
    return ProcessCameraProvider.getInstance(this).await()
}

Prendre CameraProviderune photo en utilisant CameraControllerest très similaire à utiliser, la seule différence est qu'il nécessite un ImageCaptureobjet pour appeler takePicturedes méthodes sur :

private fun takePhoto(
    context: Context,
    imageCapture: ImageCapture,
    onImageCaptured: (Uri) -> Unit,
    onError: (ImageCaptureException) -> Unit
) {
    
    
    val photoFile = File(
        context.getOutputDirectory(),
        SimpleDateFormat(FILE_FORMAT, Locale.CHINA).format(System.currentTimeMillis()) + ".jpg"
    )
    val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
    val mainExecutor = ContextCompat.getMainExecutor(context)
    imageCapture.takePicture(outputOptions, mainExecutor, object: ImageCapture.OnImageSavedCallback {
    
    
        override fun onError(exception: ImageCaptureException) {
    
    
            Log.e(TAG, "Take photo error:", exception)
            onError(exception)
        }

        override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
    
    
            val savedUri = Uri.fromFile(photoFile)
            onImageCaptured(savedUri)
            context.notifySystem(savedUri)
        }
    })
    context.showFlushAnimation()
}

Indicatif d'appel :

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun CameraPreviewExample2(navController: NavHostController) {
    
    
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current

    val previewView = remember {
    
     PreviewView(context) }
    // Create Preview UseCase.
    val preview = remember {
    
    
        Preview.Builder().build().apply {
    
    
            setSurfaceProvider(previewView.surfaceProvider)
        }
    }
    val imageCapture: ImageCapture = remember {
    
     ImageCapture.Builder().build() }
    val cameraSelector = remember {
    
     CameraSelector.DEFAULT_BACK_CAMERA } // Select default back camera.
    var pCameraProvider: ProcessCameraProvider? = null
    
    LaunchedEffect(cameraSelector) {
    
    
        val cameraProvider = context.getCameraProvider()
        cameraProvider.unbindAll()  // Unbind UseCases before rebinding. 
        // Bind UseCases to camera. This function returns a camera
        // object which can be used to perform operations like zoom, flash, and focus.
        cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageCapture)
        pCameraProvider = cameraProvider
    }

    Scaffold(
        modifier = Modifier.fillMaxSize(),
        floatingActionButton = {
    
    
            FloatingActionButton(onClick = {
    
    
                takePhoto(
                    context,
                    imageCapture = imageCapture,
                    onImageCaptured = {
    
     savedUri ->
                        Log.d(TAG, "Photo capture succeeded: $savedUri")
                        context.notifySystem(savedUri)
                        navController.currentBackStackEntry?.savedStateHandle?.set("savedUri", savedUri)
                        navController.navigate("ImageScreen")
                    },
                    onError = {
    
    
                        Log.e(TAG, "Photo capture failed: ${
      
      it.message}", it)
                    }
                )
            }) {
    
    
                Icon(
                    imageVector = ImageVector.vectorResource(id = R.drawable.ic_camera_24),
                    contentDescription = "Take picture"
                )
            }
        },
        floatingActionButtonPosition = FabPosition.Center,
    ) {
    
     innerPadding: PaddingValues ->
        AndroidView(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding),
            factory = {
    
     previewView },
            onReset = {
    
    },
            onRelease = {
    
    
                pCameraProvider?.unbindAll()
            }
        )
    }
}

La plus grande différence ici est que LaunchedEffectl'API d'effet secondaire est utilisée et qu'une coroutine est lancée pour configurer cameraProviderles opérations liées à la liaison, car cameraProviderune fonction de suspension est utilisée lors de son obtention ici. Notez également qu'ici PreviewViewet sont mémorisés ImageCaptureà l'aide de remember, afin qu'ils Composablesurvivent à travers les réorganisations, plutôt que de recréer un nouvel objet à chaque fois qu'ils sont réorganisés.

Paramètres communs de CameraX

Définir le mode de prise de vue

Que vous utilisiez CameraControllerou , CameraProvidervous pouvez setCaptureMode()définir le mode de prise de vue via la méthode .

Les modes de prise de vue pris en charge par CameraX sont :

  • CAPTURE_MODE_MINIMIZE_LATENCY: Réduisez le temps de retard pour prendre des photos.
  • CAPTURE_MODE_MAXIMIZE_QUALITY: Améliorer la qualité d'image de la capture d'image.
  • CAPTURE_MODE_ZERO_SHUTTER_LAG: Mode Zero shutter lag, disponible depuis la 1.2. CAPTURE_MODE_MINIMIZE_LATENCYLorsque Zero Shutter Lag est activé, le temps de latence est considérablement réduit par rapport au mode de prise de vue par défaut , de sorte que vous ne manquez jamais une photo.

Le mode de prise de vue par défaut est CAPTURE_MODE_MINIMIZE_LATENCY. Voir la documentation de référence setCaptureMode() pour plus de détails .

Zero Shutter Lag utilise une mémoire tampon circulaire pour stocker les trois dernières images capturées. Lorsque l'utilisateur appuie sur le bouton de capture, CameraX appelle takePicture()et le tampon circulaire récupère l'image de capture dont l'horodatage est le plus proche de l'heure à laquelle le bouton a été enfoncé. CameraX retraite ensuite la session de capture pour générer une image à partir de cette image qui est enregistrée sur le disque au format JPEG.

Avant d'activer Zero Shutter Lag, utilisez pour isZslSupported()déterminer si l'appareil en question répond aux exigences suivantes :

Si l'appareil ne répond pas aux exigences minimales, CameraX reviendra à CAPTURE_MODE_MINIMIZE_LATENCY.

Le délai d'obturation zéro n'est disponible que pour les cas d'utilisation de capture d'image . Vous ne pouvez pas l'activer pour les cas d'utilisation de capture vidéo ou les extensions de caméra. Enfin, le décalage d'obturation zéro ne fonctionnera pas lorsque le flash est allumé ou en mode automatique, car l'utilisation du flash ajoute au temps de latence.

Exemple d'utilisation CameraControllerpour définir le mode de prise de vue :

cameraController.imageCaptureMode = CAPTURE_MODE_MINIMIZE_LATENCY

Exemple d'utilisation CameraProviderpour définir le mode de prise de vue :

val imageCapture: ImageCapture = remember {
    
    
   ImageCapture.Builder().setCaptureMode(ImageCapture.CAPTURE_MODE_ZERO_SHUTTER_LAG).build()
}

régler le flash

Le mode de flash par défaut est FLASH_MODE_OFF. Pour régler le mode flash, utilisez setFlashMode():

  • FLASH_MODE_ON: Le flash est toujours activé.
  • FLASH_MODE_AUTO: Lors d'une prise de vue dans un environnement peu éclairé, le flash s'active automatiquement.

Exemple d'utilisation CameraControllerpour régler le mode flash :

cameraController.imageCaptureFlashMode = ImageCapture.FLASH_MODE_AUTO

Exemple d'utilisation CameraProviderpour régler le mode flash :

ImageCapture.Builder() 
   .setFlashMode(FLASH_MODE_AUTO)
   .build()

sélectionner la caméra

Dans CameraX, la sélection de la caméra est CameraSelectorgérée par la classe. CameraX rend le cas courant d'utilisation de la caméra par défaut beaucoup plus facile. Vous pouvez spécifier si vous souhaitez utiliser la caméra frontale par défaut ou la caméra arrière par défaut.

Voici CameraControllerle code CameraX en utilisant la caméra arrière par défaut :

var cameraController = LifecycleCameraController(baseContext)
// val selector = CameraSelector.Builder()
//    .requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
val selector = CameraSelector.DEFAULT_BACK_CAMERA // 等价上面的代码
cameraController.cameraSelector = selector

Voici CameraProviderle code CameraX pour sélectionner la caméra frontale par défaut via :

val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA
cameraProvider.unbindAll() 
var camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, useCases)

appuyez pour faire la mise au point

Lorsqu'un aperçu de la caméra est affiché à l'écran, un contrôle courant consiste à définir la mise au point lorsque l'utilisateur appuie sur l'aperçu.

CameraControllerécoutera PreviewViewles événements tactiles pour gérer automatiquement la mise au point du robinet. Vous pouvez setTapToFocusEnabled()activer et désactiver le tap-to-focus via et getter isTapToFocusEnabled()vérifier sa valeur avec le correspondant.

getTapToFocusState()La méthode renvoie un LiveDataobjet qui suit CameraControllerles changements d'état du focus sur le .

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

Lors de l'utilisation de CameraProvider, une configuration est nécessaire pour que la mise au point par pression fonctionne correctement. Cet exemple suppose que vous utilisez PreviewView. S'il n'est pas utilisé, vous devrez ajuster la logique à appliquer à une surface personnalisée.

Lorsque vous utilisez PreviewView, suivez ces étapes :

  1. Définit le détecteur de geste utilisé pour gérer les événements tactiles.
  2. Pour les événements tactiles, utilisez pour MeteringPointFactory.createPoint()en créer un MeteringPoint.
  3. Pour MeteringPoint, créez-en un FocusMeteringAction.
  4. CameraControlPour les objets (renvoyés de ) sur Camera bindToLifecycle(), appelez-le startFocusAndMetering()afin qu'il soit transmis à FocusMeteringAction.
  5. (Facultatif) Réponse FocusMeteringResult.
  6. Configurez un détecteur de gestes pour PreviewView.setOnTouchListener()répondre aux événements tactiles dans un fichier .
// 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

Zoomer l'aperçu est une autre manipulation directe courante de l'aperçu de la caméra. Avec de plus en plus de caméras sur un appareil, les utilisateurs souhaitent également sélectionner automatiquement la caméra avec la meilleure mise au point après le zoom.

Comme pour le toucher pour la mise au point, les événements tactiles CameraControllersont surveillés PreviewViewpour gérer automatiquement le pincement pour zoomer. Vous pouvez setPinchToZoomEnabled()activer et désactiver le zoom par pincement avec et getter isPinchToZoomEnabled()vérifier sa valeur avec le correspondant.

getZoomState()La méthode renvoie un LiveDataobjet qui suit les modifications CameraControllersur le ZoomState.

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

Lors de l'utilisation de CameraProvider, une configuration est nécessaire pour que le pincement et le zoom fonctionnent correctement. Si vous n'utilisez pas PreviewView, vous devrez ajuster la logique pour appliquer custom Surface.

Lorsque vous utilisez PreviewView, suivez ces étapes :

  1. Définit le détecteur de mouvement de zoom utilisé pour gérer les événements de pincement.
  2. Camera.CameraInfoObtenue à partir de l'objet, une instance est renvoyée lorsque ZoomStatevous appelez .bindToLifecycle()Camera
  3. Si ZoomStatea zoomRatioune valeur, enregistrez-la en tant que zoom actuel. Si ZoomStateAucun zoomRatio, le zoom par défaut de la caméra ( 1.0) est utilisé.
  4. Obtient le facteur de zoom actuel scaleFactormultiplié par pour déterminer le nouveau facteur de zoom et le transmet à CameraControl.setZoomRatio().
  5. Configurez un détecteur de gestes pour PreviewView.setOnTouchListener()répondre aux événements tactiles dans un fichier .
// 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
}

CameraX capture la vidéo

Un système de capture enregistre généralement un flux vidéo et un flux audio, les compresse, multiplexe les deux flux et écrit les flux résultants sur le disque.

insérez la description de l'image ici

Présentation de l'API VideoCapture

Dans CameraX, la solution pour la capture vidéo est VideoCapturele cas d'utilisation :

insérez la description de l'image ici

La capture vidéo CameraX se compose de plusieurs composants architecturaux de haut niveau :

  • SurfaceProvider , représentant la source vidéo.
  • AudioSource , indiquant la source audio.
  • Deux encodeurs pour l'encodage et la compression vidéo/audio.
  • Un multiplexeur multimédia pour multiplexer deux flux.
  • Économiseur de fichiers pour écrire les résultats.

VideoCaptureL'API fait abstraction du moteur de capture complexe pour fournir une API plus simple et plus intuitive pour les applications.

VideoCaptureest un cas d'utilisation CameraX qui peut être utilisé seul ou en combinaison avec d'autres cas d'utilisation. Les combinaisons exactes prises en charge dépendent des capacités matérielles de la caméra, mais la combinaison de Previewet VideoCapturepour ce cas d'utilisation s'applique à tous les appareils.

Remarque : est implémenté dans la bibliothèque VideoCapturede CameraX , disponible dans et versions ultérieures. L'API CameraX n'est pas définitive et peut changer au fil du temps.camera-video1.1.0-alpha10VideoCapture

VideoCaptureL'API se compose des objets suivants qui peuvent communiquer avec l'application :

  • VideoCaptureest la classe de cas d'utilisation de niveau supérieur. Lier à VideoCapturevia et à d'autres cas d'utilisation de CameraX .CameraSelectorLifecycleOwner
  • Recorderest une implémentation VideoCaptureétroitement couplée à VideoOutput. RecorderUtilisé pour effectuer des opérations de capture vidéo et audio. Appliquer en Recordercréant un objet d'enregistrement.
  • PendingRecordingL'objet d'enregistrement est configuré, avec des options telles que l'activation de l'audio et la configuration des écouteurs d'événement. Vous devez utiliser Recorderpour créer PendingRecording. PendingRecordingRien ne sera enregistré.
  • RecordingL'opération d'enregistrement proprement dite sera effectuée. Vous devez utiliser PendingRecordingpour créer Recording.

La figure suivante montre la relation entre ces objets :

insérez la description de l'image ici
légende:

  1. Utilisez QualitySelectorcréer Recorder.
  2. Utilisez l'une de ces OutputOptionsconfigurations Recorder.
  3. withAudioEnabled()Utilisez pour activer l'audio si vous le souhaitez .
  4. Appelé avec VideoRecordEventl'auditeur start()pour commencer l'enregistrement.
  5. RecordingUtilisez pour pause()/resume()/stop()contrôler les opérations d'enregistrement.
  6. Répondre à l'intérieur de l'écouteur d'événement VideoRecordEvents.

Une liste détaillée des API se trouve dans current-txt dans le code source .

Tourner une vidéo avec CameraProvider

Si vous utilisez CameraProviderun VideoCapurecas d'utilisation lié, vous devez créer un VideoCaptureUseCase et transmettre un Recorderobjet. La qualité vidéo peut ensuite Recorder.Builderêtre définie via et éventuellement définie FallbackStrategyau cas où l'appareil ne répondrait pas aux spécifications de qualité requises. Enfin, VideoCapturel'instance est liée à avec d'autres UseCases CameraProvider.

Créer un objet QualitySelector

Les applications peuvent configurer la résolution vidéo via QualitySelectorl'objet Recorder.

CameraX Recorderprend en charge les qualités de résolution vidéo prédéfinies suivantes :

  • Qualité.UHD pour la taille vidéo 4K Ultra HD ( 2160p )
  • Qualité.FHD pour la taille vidéo Full HD ( 1080p )
  • Qualité.HD pour la taille de la vidéo HD ( 720p )
  • Qualité.SD pour les tailles de vidéo en définition standard ( 480p )

Veuillez noter que d'autres résolutions sont disponibles pour CameraX sur autorisation de l'application. La taille exacte de la vidéo pour chaque option dépend des capacités de votre caméra et de votre encodeur. Voir la documentation de CamcorderProfile pour plus de détails .

Vous pouvez le créer en utilisant l'une des méthodes suivantes QualitySelector:

  1. L'utilisation fromOrderedList()fournit plusieurs résolutions préférées et inclut une stratégie de secours au cas où aucune des résolutions préférées n'est prise en charge.

    CameraX peut déterminer la meilleure correspondance de secours en fonction des capacités de la caméra sélectionnée. Voir la spécificationQualitySelector FallbackStrategy pour plus de détails . Par exemple, le code suivant demande la résolution d'enregistrement la plus élevée prise en charge ; si aucune des résolutions demandées n'est prise en charge, CameraX est autorisé à choisir la résolution la plus proche :Quality.SD

val qualitySelector = QualitySelector.fromOrderedList(
         listOf(Quality.UHD, Quality.FHD, Quality.HD, Quality.SD),
         FallbackStrategy.lowerQualityOrHigherThan(Quality.SD))
  1. Interrogez d'abord les résolutions prises en charge par la caméra, puis QualitySelector::from()choisissez parmi les résolutions prises en charge avec :
val cameraInfo = cameraProvider.availableCameraInfos.filter {
    
    
    Camera2CameraInfo
    .from(it)
    .getCameraCharacteristic(CameraCharacteristics.LENS\_FACING) == CameraMetadata.LENS_FACING_BACK
}

val supportedQualities = QualitySelector.getSupportedQualities(cameraInfo[0])
val filteredQualities = arrayListOf (Quality.UHD, Quality.FHD, Quality.HD, Quality.SD)
                       .filter {
    
     supportedQualities.contains(it) }

// Use a simple ListView with the id of simple_quality_list_view
viewBinding.simpleQualityListView.apply {
    
    
    adapter = ArrayAdapter(context,
                           android.R.layout.simple_list_item_1,
                           filteredQualities.map {
    
     it.qualityToString() })

    // Set up the user interaction to manually show or hide the system UI.
    setOnItemClickListener {
    
     _, _, position, _ ->
        // Inside View.OnClickListener,
        // convert Quality.* constant to QualitySelector
        val qualitySelector = QualitySelector.from(filteredQualities[position])

        // Create a new Recorder/VideoCapture for the new quality
        // and bind to lifecycle
        val recorder = Recorder.Builder()
            .setQualitySelector(qualitySelector).build()

         // ...
    }
}

// A helper function to translate Quality to a string
fun Quality.qualityToString() : String {
    
    
    return when (this) {
    
    
        Quality.UHD -> "UHD"
        Quality.FHD -> "FHD"
        Quality.HD -> "HD"
        Quality.SD -> "SD"
        else -> throw IllegalArgumentException()
    }
}

Notez que QualitySelector.getSupportedQualities()la fonction renvoyée doit être applicable à VideoCaptureusecase ou à une combinaison de VideoCaptureet Previewusecase. Lors de la liaison avec ImageCapturele cas d'utilisation ou ImageAnalysis, CameraX peut toujours échouer à se lier si la caméra demandée ne prend pas en charge la combinaison souhaitée.

Créer et lier un objet VideoCapture

Une fois que vous avez QualitySelectorun , vous pouvez créer VideoCapturel'objet et effectuer la liaison. Notez que cette liaison est la même que pour les autres cas d'utilisation :

val recorder = Recorder.Builder()
    .setExecutor(cameraExecutor)
    .setQualitySelector(QualitySelector.from(Quality.FHD))
    .build()
val videoCapture = VideoCapture.withOutput(recorder)

try {
    
    
    // Bind use cases to camera
    cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, videoCapture)
} catch(exc: Exception) {
    
    
    Log.e(TAG, "Use case binding failed", exc)
}

RecorderLe format le plus approprié pour le système sera choisi. Le codec vidéo le plus courant est H.264 AVC, et son format de conteneur est MPEG-4.

Remarque : Il n'est actuellement pas possible de configurer le codec vidéo final et le format de conteneur.

Configurer et générer un objet d'enregistrement

Diverses configurations peuvent ensuite videoCapture.outputêtre effectuées sur la propriété pour générer un Recordingobjet qui peut être utilisé pour mettre en pause, reprendre ou arrêter l'enregistrement. Enfin, appelez start()pour démarrer l'enregistrement, en passant dans le contexte et Consumer<VideoRecordEvent>un écouteur d'événement pour gérer les événements d'enregistrement vidéo.

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")
    }
}
// Create MediaStoreOutputOptions for our recorder
val mediaStoreOutputOptions = MediaStoreOutputOptions
    .Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
    .setContentValues(contentValues)
    .build()
    
// 2. Configure Recorder and Start recording to the mediaStoreOutput.
val recording = videoCapture.output
    .prepareRecording(context, mediaStoreOutputOptions)
    .withAudioEnabled() // 启用音频
    .start(ContextCompat.getMainExecutor(this), captureListener) // 启动并注册录制事件监听

Options de sortie

RecorderLes types de configuration suivants sont pris en charge OutputOptions:

  • FileDescriptorOutputOptionspour capturer dans FileDescriptorun fichier .
  • FileOutputOptions, pour la capture dans un fichier.
  • MediaStoreOutputOptionspour la capture dans le MediaStore.

Quel que soit le OutputOptionstype de , vous pouvez setFileSizeLimit()définir une taille de fichier maximale via . D'autres options sont spécifiques aux types de sortie individuels, telles que ParcelFileDescriptorspécifiques à FileDescriptorOutputOptions.

Mettre en pause, reprendre et arrêter

Lorsque vous appelez start()la fonction, l'objet Recorderest renvoyé Recording. L'application peut utiliser cet Recordingobjet pour terminer la capture ou effectuer d'autres actions telles que la suspension ou la reprise. Vous pouvez mettre en pause, reprendre et arrêter une session en cours avec Recording:

  • pause(), pour mettre en pause l'enregistrement actuellement actif.
  • resume(), pour reprendre un enregistrement actif mis en pause.
  • stop(), qui termine l'enregistrement et efface tous les objets d'enregistrement associés.

stop()Notez que vous pouvez appeler pour terminer si l'enregistrement est en pause ou actif Recording.

RecorderUn Recordingobjet est pris en charge à la fois. RecordingAprès avoir appelé Recording.stop()ou sur l'objet précédent Recording.close(), vous pouvez démarrer un nouvel enregistrement.

 if (recording != null) {
    
    
      // Stop the current recording session.
      recording.stop()
      recording = null
      return
  }
  ..
  recording = ..

écouteur d'événement

Si vous vous êtes enregistré avec PendingRecording.start(), EventListenervous Recordingcommuniquerez VideoRecordEventen utilisant .

CameraX envoie un événement chaque fois que l'enregistrement démarre sur l'appareil photo correspondant VideoRecordEvent.Start.

  • VideoRecordEvent.StatusPour enregistrer des statistiques, telles que la taille du fichier actuel et la durée de l'enregistrement.
  • VideoRecordEvent.FinalizePour l'enregistrement des résultats, contiendra le fichier final URIet toutes les erreurs associées.

Une fois que votre application reçoit un indiquant une session d'enregistrement réussie Finalize, vous pouvez accéder à la vidéo capturée à partir de l'emplacement spécifié dans OutputOptionsle fichier .

 recording = videoCapture.output
     .prepareRecording(context, mediaStoreOutputOptions)
     .withAudioEnabled()
     .start(ContextCompat.getMainExecutor(context)) {
    
     recordEvent ->
         when(recordEvent) {
    
    
             is VideoRecordEvent.Start -> {
    
    

             }
             is VideoRecordEvent.Status -> {
    
    

             }
             is VideoRecordEvent.Pause -> {
    
    

             }
             is VideoRecordEvent.Resume -> {
    
    

             }
             is VideoRecordEvent.Finalize -> {
    
    
                 if (!recordEvent.hasError()) {
    
    
                     val msg = "Video capture succeeded: ${
      
      recordEvent.outputResults.outputUri}"
                     context.showToast(msg)
                     Log.d(TAG, msg)
                 } else {
    
    
                     recording?.close()
                     recording = null
                     Log.e(TAG, "video capture ends with error", recordEvent.cause)
                 }
             }
         }
     }

Exemple de code complet

CameraProviderVoici un exemple de code complet pour capturer une vidéo à l'aide de Compose :

// CameraProvider 拍摄视频示例
private const val TAG = "CameraXVideo"
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"

@Composable
fun CameraVideoExample(navController: NavHostController) {
    
    
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current

    val previewView = remember {
    
     PreviewView(context) }
    // Create Preview UseCase.
    val preview = remember {
    
    
        Preview.Builder().build().apply {
    
     setSurfaceProvider(previewView.surfaceProvider) }
    }
    var cameraSelector by remember {
    
     mutableStateOf(CameraSelector.DEFAULT_BACK_CAMERA) }
    // Create VideoCapture UseCase.
    val videoCapture = remember(cameraSelector) {
    
    
        val qualitySelector = QualitySelector.from(Quality.FHD)
        val recorder = Recorder.Builder()
            .setExecutor(ContextCompat.getMainExecutor(context))
            .setQualitySelector(qualitySelector)
            .build()
        VideoCapture.withOutput(recorder)
    }
    // Bind UseCases
    LaunchedEffect(cameraSelector) {
    
    
        try {
    
    
            val cameraProvider = context.getCameraProvider()
            cameraProvider.unbindAll()
            cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, videoCapture)
        } catch(exc: Exception) {
    
    
            Log.e(TAG, "Use case binding failed", exc)
        }
    }

    var recording: Recording? =  null
    var isRecording by remember {
    
     mutableStateOf(false) }
    var time by remember {
    
     mutableStateOf(0L) }

    Scaffold(
        modifier = Modifier.fillMaxSize(),
        floatingActionButton = {
    
    
            FloatingActionButton(onClick = {
    
    
                 if (!isRecording) {
    
    
                     isRecording = true
                     recording?.stop()
                     time = 0L
                     recording = startRecording(context, videoCapture,
                        onFinished = {
    
     savedUri ->
                            if (savedUri != Uri.EMPTY) {
    
    
                                val msg = "Video capture succeeded: $savedUri"
                                context.showToast(msg)
                                Log.d(TAG, msg)
                                navController.currentBackStackEntry?.savedStateHandle?.set("savedUri", savedUri)
                                navController.navigate("VideoPlayerScreen")
                            }
                        },
                        onProgress = {
    
     time = it },
                        onError = {
    
    
                            isRecording = false
                            recording?.close()
                            recording = null
                            time = 0L
                            Log.e(TAG, "video capture ends with error", it)
                        }
                    )
                 } else {
    
    
                     isRecording = false
                     recording?.stop()
                     recording = null
                     time = 0L
                 }
            }) {
    
    
                val iconId = if (!isRecording) R.drawable.ic_start_record_36
                    else R.drawable.ic_stop_record_36
                Icon(
                    imageVector = ImageVector.vectorResource(id = iconId),
                    tint = Color.Red,
                    contentDescription = "Capture Video"
                )
            }
        },
        floatingActionButtonPosition = FabPosition.Center,
    ) {
    
     innerPadding: PaddingValues ->
        Box(modifier = Modifier
            .padding(innerPadding)
            .fillMaxSize()) {
    
    
            AndroidView(
                modifier = Modifier.fillMaxSize(),
                factory = {
    
     previewView },
            )
            if (time > 0 && isRecording) {
    
    
                Text(text = "${
      
      SimpleDateFormat("mm:ss", Locale.CHINA).format(time)} s",
                    modifier = Modifier.align(Alignment.TopCenter),
                    color = Color.Red,
                    fontSize = 16.sp
                )
            }
            if (!isRecording) {
    
    
                IconButton(
                    onClick = {
    
    
                        cameraSelector = when(cameraSelector) {
    
    
                            CameraSelector.DEFAULT_BACK_CAMERA -> CameraSelector.DEFAULT_FRONT_CAMERA
                            else -> CameraSelector.DEFAULT_BACK_CAMERA
                        }
                    },
                    modifier = Modifier
                        .align(Alignment.TopEnd)
                        .padding(bottom = 32.dp)
                ) {
    
    
                    Icon(
                        painter = painterResource(R.drawable.ic_switch_camera),
                        contentDescription = "",
                        tint = Color.Green,
                        modifier = Modifier.size(36.dp)
                    )
                }
            }
        }
    }
}

@SuppressLint("MissingPermission")
private fun startRecording(
    context: Context,
    videoCapture: VideoCapture<Recorder>,
    onFinished: (Uri) -> Unit,
    onProgress: (Long) -> Unit,
    onError: (Throwable?) -> Unit
): Recording{
    
    
    // Create and start a new recording session.
    val name = SimpleDateFormat(FILENAME_FORMAT, Locale.CHINA)
        .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(context.contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
        .setContentValues(contentValues)
        .build()

    return videoCapture.output
        .prepareRecording(context, mediaStoreOutputOptions)
        .withAudioEnabled()  // 启用音频
        .start(ContextCompat.getMainExecutor(context)) {
    
     recordEvent ->
        when(recordEvent) {
    
    
            is VideoRecordEvent.Start -> {
    
    }
            is VideoRecordEvent.Status -> {
    
    
                val duration = recordEvent.recordingStats.recordedDurationNanos / 1000 / 1000
                onProgress(duration)
            }
            is VideoRecordEvent.Pause -> {
    
    }
            is VideoRecordEvent.Resume -> {
    
    }
            is VideoRecordEvent.Finalize -> {
    
    
                if (!recordEvent.hasError()) {
    
    
                    val savedUri = recordEvent.outputResults.outputUri
                    onFinished(savedUri)
                } else {
    
    
                    onError(recordEvent.cause)
                }
            }
        }
    }
}

private suspend fun Context.getCameraProvider(): ProcessCameraProvider {
    
    
    return ProcessCameraProvider.getInstance(this).await()
}

Configuration du routage et des autorisations :

@Composable
fun CameraVideoCaptureNavHost() {
    
    
    val navController = rememberNavController()
    NavHost(navController, startDestination = "CameraVideoScreen") {
    
    
        composable("CameraVideoScreen") {
    
    
            CameraVideoScreen(navController = navController)
        }
        composable("VideoPlayerScreen") {
    
    
            VideoPlayerScreen(navController = navController)
        }
    }

}

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CameraVideoScreen(navController: NavHostController) {
    
    
    val multiplePermissionsState = rememberMultiplePermissionsState(
        listOf(
            Manifest.permission.CAMERA,
            Manifest.permission.RECORD_AUDIO,
        )
    )
    LaunchedEffect(Unit) {
    
    
        if (!multiplePermissionsState.allPermissionsGranted) {
    
    
            multiplePermissionsState.launchMultiplePermissionRequest()
        }
    }
    if (multiplePermissionsState.allPermissionsGranted) {
    
    
        CameraVideoExample(navController)
    } else {
    
    
        NoCameraPermissionScreen(multiplePermissionsState)
    }
}

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun NoCameraPermissionScreen(permissionState: MultiplePermissionsState) {
    
    
    Column(modifier = Modifier.padding(10.dp)) {
    
    
        Text(
            getTextToShowGivenPermissions(
                permissionState.revokedPermissions, // 被拒绝/撤销的权限列表
                permissionState.shouldShowRationale
            ),
            fontSize = 16.sp
        )
        Spacer(Modifier.height(8.dp))
        Button(onClick = {
    
     permissionState.launchMultiplePermissionRequest() }) {
    
    
            Text("请求权限")
        }
    }
}

@OptIn(ExperimentalPermissionsApi::class)
private fun getTextToShowGivenPermissions(
    permissions: List<PermissionState>,
    shouldShowRationale: Boolean
): String {
    
    
    val size = permissions.size
    if (size == 0) return ""
    val textToShow = StringBuilder().apply {
    
     append("以下权限:\n") }
    for (i in permissions.indices) {
    
    
        textToShow.append(permissions[i].permission).apply {
    
    
            if (i == size - 1) append(" \n") else append(", ")
        }
    }
    textToShow.append(
        if (shouldShowRationale) {
    
    
            " 需要被授权,以保证应用功能正常使用."
        } else {
    
    
            " 未获得授权. 应用功能将不能正常使用."
        }
    )
    return textToShow.toString()
}

Afin de visualiser la vidéo enregistrée, nous utilisons la bibliothèque ExoPlayer de Google pour lire la vidéo dans un autre écran de routage , en ajoutant des dépendances :

implementation "com.google.android.exoplayer:exoplayer:2.18.7"
// 展示拍摄视频
@Composable
fun VideoPlayerScreen(navController: NavHostController) {
    
    
    val savedUri = navController.previousBackStackEntry?.savedStateHandle?.get<Uri>("savedUri")
    val context = LocalContext.current

    val exoPlayer = savedUri?.let {
    
    
        remember(context) {
    
    
            ExoPlayer.Builder(context).build().apply {
    
    
                setMediaItem(MediaItem.fromUri(savedUri))
                prepare()
            }
        }
    }

    DisposableEffect(
        Box(
            modifier = Modifier.fillMaxSize()
        ) {
    
    
            AndroidView(
                factory = {
    
     context ->
                    StyledPlayerView(context).apply {
    
    
                        player = exoPlayer
                        setShowFastForwardButton(false)
                        setShowNextButton(false)
                        setShowPreviousButton(false)
                        setShowRewindButton(false)
                        controllerHideOnTouch = true
                        controllerShowTimeoutMs = 200
                    }
                },
                modifier = Modifier.fillMaxSize()
            )
        }
    ) {
    
    
        onDispose {
    
    
            exoPlayer?.release()
        }
    }
}

Tourner une vidéo avec CameraController

Avec CameraX CameraController, vous pouvez basculer indépendamment ImageCapture, VideoCaptureet ImageAnalysisUseCase, à condition que ces UseCase puissent être utilisés en même temps . ImageCaptureet ImageAnalysisUseCase sont activés par défaut, vous n'avez donc pas besoin d'appeler setEnabledUseCases()pour prendre une photo.

Si CameraControllervous enregistrez une vidéo à l'aide de , vous devez d'abord setEnabledUseCases()autoriser VideoCaptureUseCase à l'aide de .

// CameraX: Enable VideoCapture UseCase on CameraController.
cameraController.setEnabledUseCases(VIDEO_CAPTURE);

Si vous souhaitez commencer à enregistrer une vidéo, vous pouvez appeler CameraController.startRecording()la fonction. Cette fonction enregistre la vidéo enregistrée sur File, comme illustré dans l'exemple suivant. De plus, vous devez transmettre a Executoret une OnVideoSavedCallbackclasse d'implémentation de pour gérer les rappels de réussite et d'erreur.

À partir de CameraX 1.3.0-alpha02, startRecording()renvoie un Recordingobjet qui peut être utilisé pour mettre en pause, reprendre et arrêter l'enregistrement vidéo. Vous pouvez également AudioConfigactiver ou désactiver la fonction d'enregistrement vidéo via le paramètre. Pour activer l'enregistrement, vous devez vous assurer que vous avez l'autorisation d'utiliser le microphone. Semblable à CameraProvider, un Consumer<VideoRecordEvent>type de rappel est également transmis pour surveiller les événements vidéo.

@SuppressLint("MissingPermission")
@androidx.annotation.OptIn(ExperimentalVideo::class)
private fun startStopVideo(context: Context, cameraController: LifecycleCameraController): Recording {
    
    
    // Define the File options for saving the video.
    val name = SimpleDateFormat(FILENAME_FORMAT, Locale.CHINA)
        .format(System.currentTimeMillis())+".mp4"

    val outputFileOptions = FileOutputOptions
        .Builder(File(context.filesDir, name))
        .build()

    // Call startRecording on the CameraController.
    return cameraController.startRecording(
        outputFileOptions,
        AudioConfig.create(true), // 开启音频
        ContextCompat.getMainExecutor(context),
    ) {
    
     videoRecordEvent ->
        when(videoRecordEvent) {
    
    
            is VideoRecordEvent.Start -> {
    
    }
            is VideoRecordEvent.Status -> {
    
    }
            is VideoRecordEvent.Pause -> {
    
    }
            is VideoRecordEvent.Resume -> {
    
    }
            is VideoRecordEvent.Finalize -> {
    
    
                if (!videoRecordEvent.hasError()) {
    
    
                    val savedUri = videoRecordEvent.outputResults.outputUri
                    val msg = "Video capture succeeded: $savedUri"
                    context.showToast(msg) 
                    Log.d(TAG, msg) 
                } else {
    
     
                    Log.d(TAG, "video capture ends with error", videoRecordEvent.cause)
                }
            }
        }
    }
}

Comme vous pouvez le voir, CameraControllerenregistrer une vidéo avec est beaucoup plus simple que d'utiliser CameraProvider.

Exemple de code complet

CameraControllerVoici un exemple de code complet pour capturer une vidéo à l'aide de Compose :

// CameraController 拍摄视频示例
private const val TAG = "CameraXVideo"
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"

@androidx.annotation.OptIn(ExperimentalVideo::class)
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun CameraVideoExample2(navController: NavHostController) {
    
    
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val cameraController = remember {
    
     LifecycleCameraController(context) }

    var recording: Recording? =  null
    var time by remember {
    
     mutableStateOf(0L) }

    Scaffold(
        modifier = Modifier.fillMaxSize(),
        floatingActionButton = {
    
    
            FloatingActionButton(onClick = {
    
    
                if (!cameraController.isRecording) {
    
    
                    recording?.stop()
                    time = 0L
                    recording = startRecording(context, cameraController,
                        onFinished = {
    
     savedUri ->
                            if (savedUri != Uri.EMPTY) {
    
    
                                val msg = "Video capture succeeded: $savedUri"
                                context.showToast(msg)
                                Log.d(TAG, msg)
                                navController.currentBackStackEntry?.savedStateHandle?.set("savedUri", savedUri)
                                navController.navigate("VideoPlayerScreen")
                            }
                        },
                        onProgress = {
    
     time = it },
                        onError = {
    
    
                            recording?.close()
                            recording = null
                            time = 0L
                            Log.e(TAG, "video capture ends with error", it)
                        }
                    )
                } else {
    
    
                    recording?.stop()
                    recording = null
                    time = 0L
                }
            }) {
    
    
                val iconId = if (!cameraController.isRecording) R.drawable.ic_start_record_36
                    else R.drawable.ic_stop_record_36
                Icon(
                    imageVector = ImageVector.vectorResource(id = iconId),
                    tint = Color.Red,
                    contentDescription = "Capture Video"
                )
            }
        },
        floatingActionButtonPosition = FabPosition.Center,
    ) {
    
     innerPadding: PaddingValues ->
        Box(modifier = Modifier
            .padding(innerPadding)
            .fillMaxSize()) {
    
    
            AndroidView(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(innerPadding),
                factory = {
    
     context ->
                    PreviewView(context).apply {
    
    
                        setBackgroundColor(Color.White.toArgb())
                        layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
                        scaleType = PreviewView.ScaleType.FILL_CENTER
                        implementationMode = PreviewView.ImplementationMode.COMPATIBLE
                    }.also {
    
     previewView ->
                        cameraController.cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
                        previewView.controller = cameraController
                        cameraController.bindToLifecycle(lifecycleOwner)
                        // cameraController.cameraInfo?.let {
    
    
                        //    val supportedQualities = QualitySelector.getSupportedQualities(it)
                        // }
                        cameraController.setEnabledUseCases(VIDEO_CAPTURE) // 启用 VIDEO_CAPTURE UseCase
                        cameraController.videoCaptureTargetQuality = Quality.FHD
                    }
                },
                onReset = {
    
    },
                onRelease = {
    
    
                    cameraController.unbind()
                }
            )
            if (time > 0 && cameraController.isRecording) {
    
    
                Text(text = "${
      
      SimpleDateFormat("mm:ss", Locale.CHINA).format(time)} s",
                    modifier = Modifier.align(Alignment.TopCenter),
                    color = Color.Red,
                    fontSize = 16.sp
                )
            }
            if (!cameraController.isRecording) {
    
    
                IconButton(
                    onClick = {
    
    
                        cameraController.cameraSelector = when(cameraController.cameraSelector) {
    
    
                            CameraSelector.DEFAULT_BACK_CAMERA -> CameraSelector.DEFAULT_FRONT_CAMERA
                            else -> CameraSelector.DEFAULT_BACK_CAMERA
                        }
                    },
                    modifier = Modifier
                        .align(Alignment.TopEnd)
                        .padding(bottom = 32.dp)
                ) {
    
    
                    Icon(
                        painter = painterResource(R.drawable.ic_switch_camera),
                        contentDescription = "",
                        tint = Color.Green,
                        modifier = Modifier.size(36.dp)
                    )
                }
            }
        }
    }
}

@SuppressLint("MissingPermission")
@androidx.annotation.OptIn(ExperimentalVideo::class)
private fun startRecording(
    context: Context,
    cameraController: LifecycleCameraController,
    onFinished: (Uri) -> Unit,
    onProgress: (Long) -> Unit,
    onError: (Throwable?) -> Unit,
): Recording {
    
    
    // Define the File options for saving the video.
    val name = SimpleDateFormat(FILENAME_FORMAT, Locale.CHINA)
        .format(System.currentTimeMillis())+".mp4"

    val outputFileOptions = FileOutputOptions
        .Builder(File(context.getOutputDirectory(), name))
        .build()

    // Call startRecording on the CameraController.
    return cameraController.startRecording(
        outputFileOptions,
        AudioConfig.create(true), // 开启音频
        ContextCompat.getMainExecutor(context),
    ) {
    
     videoRecordEvent ->
        when(videoRecordEvent) {
    
    
            is VideoRecordEvent.Start -> {
    
    }
            is VideoRecordEvent.Status -> {
    
    
                val duration = videoRecordEvent.recordingStats.recordedDurationNanos / 1000 / 1000
                onProgress(duration)
            }
            is VideoRecordEvent.Pause -> {
    
    }
            is VideoRecordEvent.Resume -> {
    
    }
            is VideoRecordEvent.Finalize -> {
    
    
                if (!videoRecordEvent.hasError()) {
    
    
                    val savedUri = videoRecordEvent.outputResults.outputUri
                    onFinished(savedUri)
                    context.notifySystem(savedUri, outputFileOptions.file)
                } else {
    
    
                    onError(videoRecordEvent.cause)
                }
            }
        }
    }
}
private fun Context.getOutputDirectory(): File {
    
    
    val mediaDir = externalMediaDirs.firstOrNull()?.let {
    
    
        File(it, resources.getString(R.string.app_name)).apply {
    
     mkdirs() }
    }

    return if (mediaDir != null && mediaDir.exists()) mediaDir else filesDir
}
// 发送系统广播
private fun Context.notifySystem(savedUri: Uri?, file: File) {
    
    
    // 对于运行API级别>=24的设备,将忽略隐式广播,因此,如果您只针对24+级API,则可以删除此语句
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
    
    
        sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, savedUri)) //刷新单个文件
    } else {
    
    
        MediaScannerConnection.scanFile(this, arrayOf(file.absolutePath), null, null)
    }
}

L'analyse d'image

Le cas d'utilisation d'analyse d'image fournit à votre application des images accessibles au processeur sur lesquelles vous pouvez effectuer un traitement d'image, une vision par ordinateur ou une inférence d'apprentissage automatique. L'application implémente analyze()une méthode qui exécute chaque image.

Pour utiliser Image Analytics dans votre application, procédez comme suit :

  • Construire ImageAnalysisdes cas d'utilisation.
  • créer ImageAnalysis.Analyzer.
  • Pour ImageAnalysisconfigurer l'analyseur.
  • Liez lifecycleOwner, cameraSelectoret ImageAnalysiscas d'utilisation à des cycles de vie. ( ProcessCameraProvider.bindToLifecycle())

Immédiatement après la liaison, CameraX enverra l'image à l'analyseur enregistré. Lorsque l'analyse est terminée, appelez ImageAnalysis.clearAnalyzer()ou dissociez le ImageAnalysiscas d'utilisation pour arrêter l'analyse.

Construire un cas d'utilisation ImageAnalysis

ImageAnalysisDes analyseurs (consommateurs d'images) peuvent être connectés à CameraX (producteurs d'images). Les applications peuvent utiliser ImageAnalysis.Builderpour créer ImageAnalysisdes objets. A l'aide de ImageAnalysis.Builder, l'application peut être configurée comme suit :

Paramètres de sortie d'image :

  • Format : CameraX est setOutputImageFormat(int)pris en charge via YUV_420_888et RGBA_8888. Le format par défaut est YUV_420_888.
  • Résolution et AspectRatio : Vous pouvez définir l'un de ces paramètres, mais notez que vous ne pouvez pas définir les deux valeurs en même temps.
  • angle de rotation .
  • Nom cible : utilisez ce paramètre pour le débogage.

Contrôle du flux d'images :

Voici l' ImageAnalysisexemple de code de construction :

private fun getImageAnalysis(): ImageAnalysis{
    
    
    val imageAnalysis = ImageAnalysis.Builder() 
        .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
        .setTargetResolution(Size(1280, 720))
        .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
        .build()
    val executor = Executors.newSingleThreadExecutor()
    imageAnalysis.setAnalyzer(executor, ImageAnalysis.Analyzer {
    
     imageProxy ->
        val rotationDegrees = imageProxy.imageInfo.rotationDegrees
        Log.e(TAG, "ImageAnalysis.Analyzer: imageProxy.format = ${
      
      imageProxy.format}")
        // insert your code here.
        if (imageProxy.format == ImageFormat.YUV_420_888 || imageProxy.format == PixelFormat.RGBA_8888) {
    
    
            val bitmap = imageProxy.toBitmap()
        }
        // ...
        // after done, release the ImageProxy object
        imageProxy.close()
    })
    return imageAnalysis
}
val imageAnalysis = getImageAnalysis() 
cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageCapture, imageAnalysis)

Remarque : AnalyzerLa méthode de rappel dans sera rappelée à chaque image lors de la prévisualisation.

Les applications peuvent définir la résolution ou le format d'image, mais pas les deux. La résolution de sortie exacte dépend de la taille (ou du rapport d'aspect) et des capacités matérielles demandées par l'application, et peut différer de la taille ou du rapport d'aspect demandé. Voir la documentation sur setTargetResolution() pour les algorithmes de correspondance de résolution

Les applications peuvent configurer les pixels de l'image de sortie pour qu'ils soient dans l'espace colorimétrique YUV (par défaut) ou RGBA. Lors du réglage du format de sortie RGBA, CameraX convertira en interne l'image de l'espace colorimétrique YUV vers l'espace colorimétrique RGBA, et regroupera les bits de l'image dans le ImageProxypremier plan (les deux autres plans ne sont pas utilisés) ByteBuffer, la séquence est la suivante :

ImageProxy.getPlanes()[0].buffer[0]: alpha
ImageProxy.getPlanes()[0].buffer[1]: red
ImageProxy.getPlanes()[0].buffer[2]: green
ImageProxy.getPlanes()[0].buffer[3]: blue
...

mode de fonctionnement

Lorsque le pipeline d'analyse de votre application ne peut pas répondre aux exigences de fréquence d'images de CameraX, vous pouvez configurer CameraX pour supprimer des images de l'une des manières suivantes :

  • Non bloquant (par défaut) : dans ce mode, l'exécuteur met toujours en cache la dernière image dans le tampon d'image (similaire à une file d'attente de profondeur 1), tandis que l'application analyse l'image précédente. Si CameraX reçoit une nouvelle image avant que l'application n'ait terminé le traitement, la nouvelle image est enregistrée dans le même tampon, écrasant la précédente. Notez que dans ce cas, ImageAnalysis.Builder.setImageQueueDepth()cela n'a aucun effet, le contenu du tampon est toujours écrasé. Vous pouvez activer ce mode non bloquant en utilisant STRATEGY_KEEP_ONLY_LATESTl'appel . setBackpressureStrategy()Consultez la documentation de référence pour STRATEGY_KEEP_ONLY_LATEST pour plus d'informations sur les effets liés à l'exécuteur .

  • Blocage : Dans ce mode, l'exécuteur interne peut ajouter plusieurs images à la file d'attente d'images interne et ne commencer à supprimer des images que lorsque la file d'attente est pleine. Le système bloque l'intégralité de l'appareil photo : si un appareil photo a plusieurs cas d'utilisation liés, lorsque CameraX traite ces images, le système les bloque toutes. Par exemple, si l'aperçu et l'analyse d'image sont liés à un appareil photo, l'aperçu correspondant sera également bloqué pendant que CameraX traite l'image. Vous pouvez activer le mode de blocage en passant STRATEGY_BLOCK_PRODUCERa setBackpressureStrategy()à . De plus, vous pouvez ImageAnalysis.Builder.setImageQueueDepth()configurer la profondeur de la file d'attente d'images à l'aide de .

Si l'analyseur a une faible latence et des performances élevées, auquel cas le temps total passé à analyser l'image est inférieur à la durée d'une image CameraX (par exemple, 16 ms à 60 ips), les deux modes de fonctionnement ci-dessus peuvent fournir un ensemble fluide expérience. Le mode de blocage est toujours utile dans certains cas, par exemple lorsqu'il s'agit de très brèves instabilités du système.

Si le profileur a une latence élevée et des performances élevées, une combinaison de mode de blocage et de longues files d'attente sera nécessaire pour compenser le retard. Notez cependant que l'application peut toujours traiter toutes les trames dans ce cas.

Le mode non bloquant peut être plus approprié si la latence de l'analyseur est élevée et prend du temps (l'analyseur ne peut pas traiter toutes les trames), car dans ce cas, le système doit supprimer des trames pour le chemin d'analyse, mais laisser d'autres cas d'utilisation liés simultanément Toutes les trames sont toujours visible.

Analyseur de kit ML (analyseur de kit d'apprentissage automatique)

La suite d'apprentissage automatique de Google fournit des API de vision d'apprentissage automatique sur l'appareil pour détecter les visages, scanner les codes-barres, étiqueter les images, etc. Avec l'aide de ML Kit Analyzer, vous pouvez intégrer plus facilement ML Kit aux applications CameraX.

Les analyseurs ML Kit sont ImageAnalysis.Analyzerdes implémentations de l'interface. Le profileur optimise l'utilisation avec ML Suite en remplaçant la résolution cible par défaut (si nécessaire), gère les transformations de coordonnées et transmet le cadre à ML Suite qui renvoie les résultats d'analyse agrégés.

Implémentation de l'analyseur de kit ML

Pour implémenter les profileurs ML Kit, il est recommandé d'utiliser CameraControllerla classe, qui peut être PreviewViewutilisée avec pour afficher les éléments de l'interface utilisateur. Lors de l'implémentation avec CameraController, l'analyseur ML Kit gère la transformation des coordonnées entre le ImageAnalysisflux brut et le pour vous. PreviewViewL'analyseur reçoit le système de coordonnées cible de CameraX, calcule la transformation des coordonnées et le transmet à la classe ML Kit Detectorpour analyse.

Pour utiliser un analyseur ML Kit avec CameraController, appelez setImageAnalysisAnalyzer()et transmettez-lui un nouvel objet analyseur ML Kit, incluant les éléments suivants dans son constructeur :

  • Une liste de suites d'apprentissage automatique Detector, CameraX sera appelée séquentiellement.

  • Le système de coordonnées cible utilisé pour déterminer les coordonnées de sortie de ML Kit :

    COORDINATE_SYSTEM_VIEW_REFERENCEDPreviewView: Les coordonnées transformées .
    COORDINATE_SYSTEM_ORIGINAL: les ImageAnalysiscoordonnées du flux d'origine.

  • Utilisé pour appeler Consumerle rappel et MlKitAnalyzer.Resulttransmettre le à l'application Executor.

  • Appelé par CameraX lorsqu'il y a une nouvelle sortie de kit ML Consumer.

L'utilisation du profileur ML Kit nécessite l'ajout de dépendances :

def camerax_version = "1.3.0-alpha04"
implementation "androidx.camera:camera-mlkit-vision:${camerax_version}"

Reconnaissance de code QR/code-barres

Ajoutez la bibliothèque de dépendances de codes-barres ML Kit :

implementation 'com.google.mlkit:barcode-scanning:17.1.0'

Voici un exemple d'utilisation :

private const val TAG = "MLKitAnalyzer"

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun MLKitAnalyzerCameraExample(navController: NavHostController) {
    
    
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val cameraController = remember {
    
     LifecycleCameraController(context) }

    AndroidView(
        modifier = Modifier.fillMaxSize() ,
        factory = {
    
     context ->
            PreviewView(context).apply {
    
    
                setBackgroundColor(Color.White.toArgb())
                layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
                scaleType = PreviewView.ScaleType.FILL_CENTER
                implementationMode = PreviewView.ImplementationMode.COMPATIBLE
            }.also {
    
     previewView ->
                previewView.controller = cameraController
                cameraController.bindToLifecycle(lifecycleOwner)
                cameraController.imageAnalysisBackpressureStrategy = ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST
                cameraController.setBarcodeAnalyzer(context) {
    
     result ->
                    navController.currentBackStackEntry?.savedStateHandle?.set("result", result)
                    navController.navigate("ResultScreen")
                }
            }
        },
        onReset = {
    
    },
        onRelease = {
    
    
            cameraController.unbind()
        }
    )
}

private fun LifecycleCameraController.setBarcodeAnalyzer(
    context: Context,
    onFound: (String?) -> Unit
) {
    
    
    // create BarcodeScanner object
    val options = BarcodeScannerOptions.Builder()
        .setBarcodeFormats(Barcode.FORMAT_QR_CODE,
            Barcode.FORMAT_AZTEC, Barcode.FORMAT_DATA_MATRIX, Barcode.FORMAT_PDF417,
            Barcode.FORMAT_CODABAR, Barcode.FORMAT_CODE_39, Barcode.FORMAT_CODE_93,
            Barcode.FORMAT_EAN_8, Barcode.FORMAT_EAN_13, Barcode.FORMAT_ITF,
            Barcode.FORMAT_UPC_A, Barcode.FORMAT_UPC_E
        )
        .build()
    val barcodeScanner = BarcodeScanning.getClient(options)

    setImageAnalysisAnalyzer(
        ContextCompat.getMainExecutor(context),
        MlKitAnalyzer(
            listOf(barcodeScanner),
            COORDINATE_SYSTEM_VIEW_REFERENCED,
            ContextCompat.getMainExecutor(context)
        ) {
    
     result: MlKitAnalyzer.Result? ->
            val value = result?.getValue(barcodeScanner)
            value?.let {
    
     list ->
                if (list.size > 0) {
    
    
                    list.forEach {
    
     barCode ->
                        Log.e(TAG, "format:${
      
      barCode.format}, displayValue:${
      
      barCode.displayValue}")
                        context.showToast("识别到:${
      
      barCode.displayValue}")
                    }
                    val res = list[0].displayValue
                    if (!res.isNullOrEmpty()) onFound(res) 
                }
            }
        }
    )
}

Dans l'exemple de code ci-dessus, l'analyseur ML Kit transmettra ce qui suit à la classe BarcodeScannerdeDetector :

  • COORDINATE_SYSTEM_VIEW_REFERENCEDUne transformation basée sur la représentation du système de coordonnées cible Matrix.
  • cadre de la caméra.

S'il BarcodeScannerrencontre des problèmes, il Detectorgénérera une erreur et l'analyseur ML Kit propagera cette erreur à votre application. En cas de succès, l'analyseur ML Kit renverra MLKitAnalyzer.Result#getValue(), dans ce cas Barcodeun objet.

Vous pouvez également barcode.valueTypeobtenir le type de la valeur via :

for (barcode in barcodes) {
    
    
    val bounds = barcode.boundingBox
    val corners = barcode.cornerPoints

    val rawValue = barcode.rawValue

    val valueType = barcode.valueType
    // See API reference for complete list of supported types
    when (valueType) {
    
    
        Barcode.TYPE_WIFI -> {
    
    
            val ssid = barcode.wifi!!.ssid
            val password = barcode.wifi!!.password
            val type = barcode.wifi!!.encryptionType
        }
        Barcode.TYPE_URL -> {
    
    
            val title = barcode.url!!.title
            val url = barcode.url!!.url
        }
    }
}

Vous pouvez également implémenter des analyseurs ML Kit à l'aide de la classe camera-coredans . ImageAnalysisCependant, comme ImageAnalysisn'est pas PreviewViewintégré à , vous devez gérer manuellement les transformations de coordonnées. Pour plus d'informations, consultez la documentation de référence de ML Kit Analyzer .

Configuration du routage et des autorisations :

@Composable
fun ExampleMLKitAnalyzerNavHost() {
    
    
    val navController = rememberNavController()
    NavHost(navController, startDestination = "MLKitAnalyzerCameraScreen") {
    
    
        composable("MLKitAnalyzerCameraScreen") {
    
    
            MLKitAnalyzerCameraScreen(navController = navController)
        }
        composable("ResultScreen") {
    
    
            ResultScreen(navController = navController)
        }
    }
}

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun MLKitAnalyzerCameraScreen(navController: NavHostController) {
    
    
    val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA)
    LaunchedEffect(Unit) {
    
    
        if (!cameraPermissionState.status.isGranted && !cameraPermissionState.status.shouldShowRationale) {
    
    
            cameraPermissionState.launchPermissionRequest()
        }
    }
    if (cameraPermissionState.status.isGranted) {
    
     // 相机权限已授权, 显示预览界面
        MLKitAnalyzerCameraExample(navController)
    } else {
    
     // 未授权,显示未授权页面
        NoCameraPermissionScreen(cameraPermissionState = cameraPermissionState)
    }
}

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun NoCameraPermissionScreen(cameraPermissionState: PermissionState) {
    
    
    // In this screen you should notify the user that the permission
    // is required and maybe offer a button to start another camera perission request
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
    
    
        val textToShow = if (cameraPermissionState.status.shouldShowRationale) {
    
    
            // 如果用户之前选择了拒绝该权限,应当向用户解释为什么应用程序需要这个权限
            "未获取相机授权将导致该功能无法正常使用。"
        } else {
    
    
            // 首次请求授权
            "该功能需要使用相机权限,请点击授权。"
        }
        Text(textToShow)
        Spacer(Modifier.height(8.dp))
        Button(onClick = {
    
     cameraPermissionState.launchPermissionRequest() }) {
    
     Text("请求权限") }
    }
}
// 展示识别结果
@Composable
fun ResultScreen(navController: NavHostController) {
    
    
    val result = navController.previousBackStackEntry?.savedStateHandle?.get<String>("result")
    result?.let {
    
    
        Box(modifier = Modifier.fillMaxSize()) {
    
    
            Text("$it", fontSize = 18.sp, modifier = Modifier.align(Alignment.Center))
        }
    }
}

Pour plus de contenu connexe, veuillez vous référer à : https://developers.google.cn/ml-kit/vision/barcode-scanning/android?hl=zh-cn

reconnaissance de texte

Ajoutez des dépendances :

dependencies {
    
    
  // To recognize Latin script
  implementation 'com.google.mlkit:text-recognition:16.0.0'
  // To recognize Chinese script
  implementation 'com.google.mlkit:text-recognition-chinese:16.0.0'
  // To recognize Devanagari script
  implementation 'com.google.mlkit:text-recognition-devanagari:16.0.0'
  // To recognize Japanese script
  implementation 'com.google.mlkit:text-recognition-japanese:16.0.0'
  // To recognize Korean script
  implementation 'com.google.mlkit:text-recognition-korean:16.0.0'
}

Exemple de code :

private fun LifecycleCameraController.setTextAnalyzer(
    context: Context,
    onFound: (String) -> Unit
) {
    
    
    var called = false
    // val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS) // 拉丁文
    val recognizer = TextRecognition.getClient(ChineseTextRecognizerOptions.Builder().build()) // 中文
    setImageAnalysisAnalyzer(
        ContextCompat.getMainExecutor(context),
        MlKitAnalyzer(
            listOf(recognizer),
            COORDINATE_SYSTEM_VIEW_REFERENCED,
            ContextCompat.getMainExecutor(context)
        ) {
    
     result: MlKitAnalyzer.Result? ->
            val value = result?.getValue(recognizer)
            value?.let {
    
     resultText ->
                val sb = StringBuilder()
                for (block in resultText.textBlocks) {
    
    
                    val blockText = block.text
                    sb.append(blockText).append("\n")
                    val blockCornerPoints = block.cornerPoints
                    val blockFrame = block.boundingBox
                    for (line in block.lines) {
    
    
                        val lineText = line.text
                        val lineCornerPoints = line.cornerPoints
                        val lineFrame = line.boundingBox
                        for (element in line.elements) {
    
    
                            val elementText = element.text
                            val elementCornerPoints = element.cornerPoints
                            val elementFrame = element.boundingBox
                        }
                    }
                }
                val res = sb.toString()
                if (res.isNotEmpty() && !called) {
    
    
                    Log.e(TAG, "$res")
                    context.showToast("识别到:$res")
                    onFound(res)
                    called = true
                }
            }
        }
    )
}

Un outil de reconnaissance de texte décompose le texte en blocs, lignes, éléments et symboles. Grosso modo:

  • Un bloc est un groupe de lignes de texte contiguës, telles que des paragraphes ou des colonnes.
  • Une ligne est un groupe de mots consécutifs sur le même axe,
  • L'élément est un groupe d'alphanumériques ("mots") consécutifs de l'alphabet latin qui fait référence à d'autres caractères dans la plupart des colonnes de l'alphabet latin
  • Le symbole est un caractère alphanumérique unique sur le même axe dans la plupart des langues latines, ou un caractère dans d'autres langues

La figure suivante présente ces exemples par ordre décroissant. Le premier bloc en surbrillance (indiqué en cyan) est un bloc de texte. Le deuxième ensemble de blocs bleus en surbrillance sont des lignes de texte. Enfin, le troisième ensemble de mots surlignés en bleu foncé est Word.

insérez la description de l'image ici

Pour tous les blocs, lignes, éléments et symboles détectés, l'API renvoie les cadres de délimitation, les coins, les informations de rotation, les scores de confiance, les langues reconnues et le texte reconnu.

Pour plus de contenu connexe, veuillez vous référer à : https://developers.google.cn/ml-kit/vision/text-recognition/v2/android?hl=zh-cn

reconnaissance de visage

Ajoutez des dépendances :

dependencies {
    
     
  // Use this dependency to bundle the model with your app
  implementation 'com.google.mlkit:face-detection:16.1.5'
}

Avant d'appliquer la détection de visage à une image, si vous souhaitez modifier les paramètres par défaut du détecteur de visage, utilisez l'objet FaceDetectorOptions pour spécifier ces paramètres. Vous pouvez modifier les paramètres suivants :

installation Fonction
setPerformanceMode PERFORMANCE_MODE_FAST(par défaut) / PERFORMANCE_MODE_ACCURATEPrioriser la vitesse ou la précision lors de la détection des visages.
setLandmarkMode LANDMARK_MODE_NONE(par défaut) / LANDMARK_MODE_ALLS'il faut essayer d'identifier les "repères" du visage : yeux, oreilles, nez, joues, bouche, etc.
setContourMode CONTOUR_MODE_NONE(par défaut) / CONTOUR_MODE_ALLS'il faut détecter le contour des traits du visage. Détecte uniquement les contours des visages les plus proéminents de l'image.
setClassificationMode CLASSIFICATION_MODE_NONE(par défaut) / CLASSIFICATION_MODE_ALLS'il faut classer les visages dans différentes catégories (par exemple, "sourire" vs "yeux ouverts").
setMinFaceSize float(Par défaut : 0.1f) Définit la taille de visage minimale souhaitée exprimée sous la forme du rapport entre la largeur de la tête et la largeur de l'image.
enableTracking false(par défaut) / trueAttribuer ou non des identifiants aux visages pour les utiliser dans le suivi des visages à travers les images.
Notez que lorsque la détection des contours est activée, un seul visage est détecté, donc le suivi des visages ne produira pas de résultats utiles. Pour cette raison, pour accélérer la détection, n'activez pas la détection des contours et le suivi du visage en même temps.

Par exemple:

// High-accuracy landmark detection and face classification
val highAccuracyOpts = FaceDetectorOptions.Builder()
        .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE)
        .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL)
        .setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL)
        .build()

// Real-time contour detection
val realTimeOpts = FaceDetectorOptions.Builder()
        .setContourMode(FaceDetectorOptions.CONTOUR_MODE_ALL)
        .build()

// 获取 FaceDetector 实例
val detector = FaceDetection.getClient(highAccuracyOpts)
// Or, to use the default option:
// val detector = FaceDetection.getClient();

Si l'opération de détection de visage réussit, le système transmet un tableau d'objets au success listener Face. Chaque Faceobjet représente un visage détecté dans l'image. Pour chaque visage, vous obtenez ses coordonnées de délimitation dans l'image d'entrée, ainsi que toute autre information que vous avez configurée pour rechercher le détecteur de visage. Par exemple:

for (face in faces) {
    
    
    val bounds = face.boundingBox
    val rotY = face.headEulerAngleY // Head is rotated to the right rotY degrees
    val rotZ = face.headEulerAngleZ // Head is tilted sideways rotZ degrees

    // If landmark detection was enabled (mouth, ears, eyes, cheeks, and nose available):
    val leftEar = face.getLandmark(FaceLandmark.LEFT_EYE)
    leftEar?.let {
    
    
        val leftEarPos = leftEar.position
    }

    // If contour detection was enabled:
    val leftEyeContour = face.getContour(FaceContour.LEFT_EYE)?.points
    val upperLipBottomContour = face.getContour(FaceContour.UPPER_LIP_BOTTOM)?.points

    // If classification was enabled:
    if (face.smilingProbability != null) {
    
    
        val smileProb = face.smilingProbability
    }
    if (face.rightEyeOpenProbability != null) {
    
    
        val rightEyeOpenProb = face.rightEyeOpenProbability
    }

    // If face tracking was enabled:
    if (face.trackingId != null) {
    
    
        val id = face.trackingId
    }
}

Exemple de code :

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun MLKitFaceDetectorExample() {
    
    
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val cameraController = remember {
    
     LifecycleCameraController(context) }
    var faces by remember {
    
     mutableStateOf(listOf<Face>()) }

    val bounds = remember(faces) {
    
    
        faces.map {
    
     face -> face.boundingBox }
    }

    val points = remember(faces) {
    
     getPoints(faces) }

    Box(modifier = Modifier.fillMaxSize()) {
    
    
        AndroidView(
            modifier = Modifier.fillMaxSize(),
            factory = {
    
     context ->
                PreviewView(context).apply {
    
    
                    setBackgroundColor(Color.White.toArgb())
                    layoutParams = LinearLayout.LayoutParams(
                        ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.MATCH_PARENT
                    )
                    scaleType = PreviewView.ScaleType.FILL_CENTER
                    implementationMode = PreviewView.ImplementationMode.COMPATIBLE
                }.also {
    
     previewView ->
                    previewView.controller = cameraController
                    cameraController.bindToLifecycle(lifecycleOwner)
                    cameraController.imageAnalysisBackpressureStrategy =
                        ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST
                    cameraController.setFaceDetectorAnalyzer(context) {
    
     faces = it }
                }
            },
            onReset = {
    
    },
            onRelease = {
    
    
                cameraController.unbind()
            }
        )
        Canvas(modifier = Modifier.fillMaxSize()) {
    
    
            bounds.forEach {
    
     rect ->
                drawRect(
                    Color.Red,
                    size = Size(rect.width().toFloat(), rect.height().toFloat()),
                    topLeft = Offset(x = rect.left.toFloat(), y = rect.top.toFloat()),
                    style = Stroke(width = 5f)
                )
            }
            points.forEach {
    
     point ->
                drawCircle(
                    Color.Green,
                    radius = 2.dp.toPx(),
                    center = Offset(x = point.x, y = point.y),
                )
            }
        }
    }
}

private fun LifecycleCameraController.setFaceDetectorAnalyzer(
    context: Context,
    onFound: (List<Face>) -> Unit
) {
    
    
    // High-accuracy landmark detection and face classification
    val highAccuracyOpts = FaceDetectorOptions.Builder()
        .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE)
        .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL)
        .enableTracking()
        .build()
    // Real-time contour detection
    val realTimeOpts = FaceDetectorOptions.Builder()
        .setContourMode(FaceDetectorOptions.CONTOUR_MODE_ALL)
        .build()
    val detector = FaceDetection.getClient(highAccuracyOpts)
    setImageAnalysisAnalyzer(
        ContextCompat.getMainExecutor(context),
        MlKitAnalyzer(
            listOf(detector),
            COORDINATE_SYSTEM_VIEW_REFERENCED,
            ContextCompat.getMainExecutor(context)
        ) {
    
     result: MlKitAnalyzer.Result? ->
            val value = result?.getValue(detector)
            value?.let {
    
     onFound(it) }
        }
    )
}

// All landmarks
private val landMarkTypes = intArrayOf(
    FaceLandmark.MOUTH_BOTTOM,
    FaceLandmark.MOUTH_RIGHT,
    FaceLandmark.MOUTH_LEFT,
    FaceLandmark.RIGHT_EYE,
    FaceLandmark.LEFT_EYE,
    FaceLandmark.RIGHT_EAR,
    FaceLandmark.LEFT_EAR,
    FaceLandmark.RIGHT_CHEEK,
    FaceLandmark.LEFT_CHEEK,
    FaceLandmark.NOSE_BASE
)

private fun getPoints(faces: List<Face>) : List<PointF> {
    
    
    val points = mutableListOf<PointF>()
    for (face in faces) {
    
    
        landMarkTypes.forEach {
    
     landMarkType ->
            face.getLandmark(landMarkType)?.let {
    
    
                points.add(it.position)
            }
        }
    }
    return points
}

Effet:

insérez la description de l'image ici

Pour plus de contenu connexe, veuillez vous référer à : https://developers.google.cn/ml-kit/vision/face-detection/android?hl=zh-cn

CameraX autres options de configuration avancées

CaméraXConfig

Pour plus de simplicité, CameraX a des configurations par défaut (telles que des exécuteurs et des gestionnaires internes) adaptées à la plupart des scénarios d'utilisation. Cependant, si votre application a des besoins particuliers ou souhaite personnaliser ces configurations, CameraXConfigl'interface peut être utilisée à cet effet.

Avec help CameraXConfig, les applications peuvent effectuer les opérations suivantes :

  • Utilisez pour setAvailableCameraLimiter()optimiser le délai de démarrage.
  • setCameraExecutor()Fournissez un exécuteur d'application à CameraX en utilisant .
  • Remplacez le gestionnaire de planificateur par défaut par setSchedulerHandler().
  • Utilisez setMinimumLoggingLevel()pour modifier le niveau de journalisation.

La procédure suivante montre comment utiliser CameraXConfig:

  1. Créez un objet avec votre configuration personnalisée CameraXConfig.
  2. ApplicationImplémente l'interface dans CameraXConfig.Provideret getCameraXConfig()renvoie CameraXConfigl'objet dans .
  3. Ajoutez Applicationla classe au AndroidManifest.xmlfichier.

Par exemple, l'exemple de code suivant limite la journalisation de CameraX aux messages d'erreur uniquement :

class CameraApplication : Application(), CameraXConfig.Provider {
    
    
   override fun getCameraXConfig(): CameraXConfig {
    
    
       return CameraXConfig.Builder.fromConfig(Camera2Config.defaultConfig())
           .setMinimumLoggingLevel(Log.ERROR).build()
   }
}

Si votre application a besoin de connaître la configuration de CameraX après sa configuration, conservez CameraXConfigune copie locale de l'objet.

limiteur de caméra

Lors du premier appel ProcessCameraProvider.getInstance(), CameraX énumère et interroge les caractéristiques des caméras disponibles sur l'appareil. Étant donné que CameraX doit communiquer avec des composants matériels, ce processus peut prendre beaucoup de temps pour chaque caméra, en particulier sur les appareils bas de gamme. Si votre application utilise uniquement une caméra spécifique sur l'appareil (telle que la caméra frontale par défaut), vous pouvez configurer CameraX pour qu'elle ignore les autres caméras, réduisant ainsi le délai de démarrage des caméras utilisées par votre application.

CameraXConfig.Builder.setAvailableCamerasLimiter()Si a est passé pour CameraSelectorfiltrer une caméra, CameraX supposera que la caméra n'existe pas au moment de l'exécution. Par exemple, le code suivant interdit à l'application d'utiliser uniquement la caméra arrière par défaut de l'appareil :

class MainApplication : Application(), CameraXConfig.Provider {
    
    
   override fun getCameraXConfig(): CameraXConfig {
    
    
       return CameraXConfig.Builder.fromConfig(Camera2Config.defaultConfig())
              .setAvailableCamerasLimiter(CameraSelector.DEFAULT_BACK_CAMERA)
              .build()
   }
}

fil

De nombreuses API de plate-forme sur lesquelles CameraX est construit nécessitent le blocage de la communication inter-processus (IPC) avec le matériel, ce qui peut parfois nécessiter des temps de réponse de centaines de millisecondes. Par conséquent, CameraX n'appelle ces API qu'à partir d'un thread d'arrière-plan, évitant ainsi le blocage du thread principal et gardant l'interface fluide. CameraX gère ces threads d'arrière-plan en interne, de sorte que ce type de comportement semble transparent. Cependant, certaines applications nécessitent un contrôle strict des threads. CameraXConfigPermet à une application de définir le thread d'arrière-plan utilisé par CameraXConfig.Builder.setCameraExecutor()et .CameraXConfig.Builder.setSchedulerHandler()

choisir automatiquement

CameraX fournit automatiquement des fonctionnalités dédiées en fonction de l'appareil sur lequel votre application est exécutée. Par exemple, si vous ne spécifiez pas de résolution ou si vous spécifiez une résolution non prise en charge, CameraX détermine automatiquement la meilleure résolution à utiliser. Toutes ces opérations sont gérées par la bibliothèque sans que vous ayez à écrire de code spécifique à l'appareil.

L'objectif de CameraX est d'initialiser avec succès une session de caméra. Cela signifie que CameraX réduit la résolution et le format d'image en fonction des capacités de l'appareil. Cela se produit pour les raisons suivantes :

  • L'appareil ne prend pas en charge la résolution demandée.
  • Il existe des problèmes de compatibilité avec l'appareil, comme un appareil plus ancien qui nécessite une certaine résolution pour fonctionner correctement.
  • Sur certains appareils, certains formats ne sont disponibles que dans certains formats d'image.
  • Pour l'encodage JPEG ou vidéo, l'appareil préfère le "mod16 le plus proche". Voir SCALER_STREAM_CONFIGURATION_MAP pour plus de détails .

Bien que CameraX crée et gère la session, vous devez toujours vérifier dans votre code la taille de l'image renvoyée par la sortie du cas d'utilisation et l'ajuster en conséquence.

Faire tourner

Par défaut, lors de la création du cas d'utilisation, la rotation de la caméra est définie pour correspondre à la rotation d'affichage par défaut. Par défaut, CameraX génère une sortie qui garantit que l'application correspond à ce que vous vous attendez à voir dans l'aperçu. Vous pouvez modifier l'angle de rotation en une valeur personnalisée pour prendre en charge les appareils à plusieurs affichages en transmettant l'orientation d'affichage actuelle lors de la configuration de l'objet de cas d'utilisation ou en transmettant l'orientation d'affichage dynamiquement après la création de l'objet de cas d'utilisation.

Votre application peut utiliser un paramètre de configuration pour définir la rotation cible. Ensuite, même pendant l'exécution du cycle de vie, l'application peut mettre à jour le paramètre de rotation à l'aide de méthodes dans l'API de cas d'utilisation telles que ImageAnalysis.setTargetRotation(). Vous pouvez faire ce qui précède pendant que l'application est verrouillée en mode portrait afin que vous n'ayez pas besoin de reconfigurer la rotation, mais les cas d'utilisation de photos ou d'analyses nécessitent une connaissance de la rotation actuelle de l'appareil. Par exemple, un cas d'utilisation peut nécessiter de connaître l'angle de rotation pour effectuer la détection de visage dans la bonne orientation ou pour définir une photo en orientation paysage ou portrait.

Les données des images capturées peuvent ne pas être stockées avec les informations de rotation. Les données Exif incluent des informations de rotation afin que l'application de la galerie affiche l'image dans l'orientation correcte de l'écran après l'enregistrement.

Pour afficher les données d'aperçu dans le bon sens, vous pouvez Preview.PreviewOutput()créer une transformation à l'aide de la sortie de métadonnées du fichier .

L'exemple de code suivant montre comment définir l'angle de rotation pour un événement d'orientation :

override fun onCreate() {
    
    
    val imageCapture = ImageCapture.Builder().build()

    val orientationEventListener = object : OrientationEventListener(this as Context) {
    
    
        override fun onOrientationChanged(orientation : Int) {
    
    
            // Monitors orientation values to determine the target rotation value
            val rotation : Int = when (orientation) {
    
    
                in 45..134 -> Surface.ROTATION_270
                in 135..224 -> Surface.ROTATION_180
                in 225..314 -> Surface.ROTATION_90
                else -> Surface.ROTATION_0
            }

            imageCapture.targetRotation = rotation
        }
    }
    orientationEventListener.enable()
}

Chaque cas d'utilisation fera soit directement pivoter les données d'image en fonction de l'angle de rotation défini, soit fournira à l'utilisateur des métadonnées de rotation des données d'image non pivotées.

  • Aperçu : fournit une sortie de métadonnées à utiliser en Preview.getTargetRotation()connaissant les paramètres de rotation pour la résolution cible.
  • ImageAnalysis : Fournit une sortie de métadonnées pour comprendre où les coordonnées du tampon d'image sont relatives aux coordonnées d'affichage.
  • ImageCapture : modifie les métadonnées Exif de l'image , le tampon ou les deux, pour refléter le paramètre de rotation. La valeur modifiée dépend de l'implémentation HAL.

Pour plus d'informations sur la rotation, veuillez vous référer à : https://developer.android.google.cn/training/camerax/orientation-rotation?hl=zh-cn

rectangle de découpage

Par défaut, le rectangle de découpage est le rectangle de tampon complet, qui peut être personnalisé avec ViewPortet . UseCaseGroupEn regroupant les cas d'utilisation et en définissant la fenêtre d'affichage, CameraX garantit que les rectangles de découpage de tous les cas d'utilisation d'un groupe pointent vers la même zone du capteur de la caméra.

L'extrait de code suivant montre comment utiliser ces deux classes :

val viewPort =  ViewPort.Builder(Rational(width, height), display.rotation).build()
val useCaseGroup = UseCaseGroup.Builder()
    .addUseCase(preview)
    .addUseCase(imageAnalysis)
    .addUseCase(imageCapture)
    .setViewPort(viewPort)
    .build()
cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, useCaseGroup)

ViewPortUtilisé pour spécifier le rectangle de tampon visible par l'utilisateur final. CameraX calcule le plus grand rectangle de découpage possible en fonction des propriétés de la fenêtre et du cas d'utilisation associé. En général, pour obtenir ce que vous voyez, vous devez configurer la fenêtre d'affichage en fonction de votre cas d'utilisation de l'aperçu. Un moyen simple d'obtenir la fenêtre consiste à utiliser PreviewView.

L'extrait de code suivant montre comment obtenir ViewPortun objet :

val viewport = findViewById<PreviewView>(R.id.preview_view).viewPort

Dans l'exemple précédent, l'application obtient le même contenu via ImageAnalysiset ImageCaptureque l'utilisateur final voit dans PreviewView(en supposant que PreviewViewle type de zoom de est défini sur la valeur par défaut FILL_CENTER). Une fois que le rectangle d'écrêtage et la rotation sont appliqués au tampon de sortie, l'image sera cohérente dans tous les cas d'utilisation, bien que la résolution puisse varier. Pour plus d'informations sur l'application des informations de transformation, consultez Sortie de transformation .

Sélectionnez une caméra disponible

CameraX sélectionne automatiquement le meilleur appareil photo en fonction des exigences de l'application et du cas d'utilisation. Si vous souhaitez utiliser un autre appareil que celui sélectionné automatiquement, plusieurs possibilités s'offrent à vous :

  • Utilisez CameraSelector.DEFAULT_FRONT_CAMERApour demander la caméra frontale par défaut.
  • Utilisez CameraSelector.DEFAULT_BACK_CAMERApour demander la caméra arrière par défaut.
  • Utilisez CameraSelector.Builder.addCameraFilter()pour CameraCharacteristicsfiltrer la liste des appareils disponibles.

Remarque : Les appareils photo doivent être reconnus par le système et apparaître dans CameraManager.getCameraIdList()avant de pouvoir être utilisés.

En outre, chaque fabricant d'équipement d'origine (OEM) doit choisir de prendre en charge ou non les appareils photo externes. PackageManager.FEATURE_CAMERA_EXTERNALPar conséquent, assurez-vous de vérifier qu'il est activé avant d'essayer d'utiliser une caméra externe .

L'exemple de code suivant montre comment créer un CameraSelectorpour affecter la sélection de périphérique :

fun selectExternalOrBestCamera(provider: ProcessCameraProvider):CameraSelector? {
    
    
   val cam2Infos = provider.availableCameraInfos.map {
    
    
       Camera2CameraInfo.from(it)
   }.sortedByDescending {
    
    
       // HARDWARE_LEVEL is Int type, with the order of:
       // LEGACY < LIMITED < FULL < LEVEL_3 < EXTERNAL
       it.getCameraCharacteristic(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)
   }

   return when {
    
    
       cam2Infos.isNotEmpty() -> {
    
    
           CameraSelector.Builder()
               .addCameraFilter {
    
    
                   it.filter {
    
     camInfo ->
                       // cam2Infos[0] is either EXTERNAL or best built-in camera
                       val thisCamId = Camera2CameraInfo.from(camInfo).cameraId
                       thisCamId == cam2Infos[0].cameraId
                   }
               }.build()
       }
       else -> null
    }
}

// create a CameraSelector for the USB camera (or highest level internal camera)
val selector = selectExternalOrBestCamera(processCameraProvider)
processCameraProvider.bindToLifecycle(this, selector, preview, analysis)

Sélectionnez plusieurs caméras en même temps

À partir de CameraX 1.3 , vous pouvez également sélectionner plusieurs caméras en même temps. Par exemple, vous pouvez configurer les caméras avant et arrière pour prendre des photos ou enregistrer des vidéos des deux points de vue simultanément.

Lors de l'utilisation de la fonction de caméra simultanée, l'appareil peut exécuter deux caméras avec des orientations d'objectif différentes en même temps, ou exécuter deux caméras arrière en même temps. Le bloc de code suivant montre comment bindToLifecycleconfigurer deux caméras lors de l'appel et ConcurrentCameraobtenir deux Cameraobjets à partir de l'objet renvoyé.

// Build ConcurrentCameraConfig
val primary = ConcurrentCamera.SingleCameraConfig(
    primaryCameraSelector,
    useCaseGroup,
    lifecycleOwner
)

val secondary = ConcurrentCamera.SingleCameraConfig(
    secondaryCameraSelector,
    useCaseGroup,
    lifecycleOwner
)

val concurrentCamera = cameraProvider.bindToLifecycle(
    listOf(primary, secondary)
)

val primaryCamera = concurrentCamera.cameras[0]
val secondaryCamera = concurrentCamera.cameras[1]

résolution de la caméra

Vous pouvez choisir de laisser CameraX définir la résolution de l'image en fonction des capacités de l'appareil, du niveau matériel pris en charge par l'appareil, du cas d'utilisation et de la combinaison de rapport d'aspect fournie. Vous pouvez également définir une résolution cible spécifique ou un format d'image spécifique dans les cas d'utilisation où la configuration correspondante est prise en charge.

résolution automatique

CameraX peut cameraProcessProvider.bindToLifecycle()déterminer automatiquement le meilleur paramètre de résolution en fonction du cas d'utilisation spécifié dans . Dans la mesure du possible, bindToLifecycle()spécifiez tous les cas d'utilisation qui doivent s'exécuter simultanément dans une seule session d'un seul appel. CameraX détermine la résolution en fonction de l'ensemble de cas d'utilisation groupés, en tenant compte du niveau matériel pris en charge par l'appareil, ainsi que des variations spécifiques à l'appareil (l'appareil dépasse ou ne respecte pas la configuration de diffusion disponible). Ceci est fait pour garantir que l'application s'exécute sur une variété d'appareils avec un minimum de chemins de code spécifiques à l'appareil.

Le format d'image par défaut pour les cas d'utilisation de capture d'image et d'analyse d'image est 4:3.

Pour les cas d'utilisation avec des rapports d'aspect configurables, laissez l'application spécifier le rapport d'aspect souhaité en fonction de la conception de l'interface utilisateur. CameraX génère une sortie dans le rapport d'aspect demandé, correspondant aussi étroitement que possible au rapport d'aspect pris en charge par l'appareil. Si aucune résolution de correspondance exacte n'est prise en charge, la résolution qui satisfait le plus de critères est choisie. C'est-à-dire que l'application déterminera comment la caméra est affichée dans l'application, et CameraX déterminera le réglage de résolution optimal de la caméra pour répondre aux exigences spécifiques des différents appareils.

Par exemple, une application peut effectuer l'une des actions suivantes :

  • Spécifiez la résolution cible de 4:3ou pour le cas d'utilisation16:9
  • Spécifiez une résolution personnalisée, CameraX essaiera de trouver la résolution la plus proche de cette résolution
  • ImageCaptureSpécifie le rapport d'aspect de recadrage pour

CameraX sélectionne automatiquement Camera2la résolution de l'interface interne. Le tableau ci-dessous montre ces résolutions :

insérez la description de l'image ici

spécifier la résolution

Lors de la création d'un cas d'utilisation à l'aide setTargetResolution(Size resolution)de la méthode, vous pouvez définir une résolution spécifique, comme illustré dans l'exemple de code suivant :

val imageAnalysis = ImageAnalysis.Builder()
    .setTargetResolution(Size(1280, 720))
    .build()

Vous ne pouvez pas définir le format d'image cible et la résolution cible pour le même cas d'utilisation. Si c'est le cas, il sera lancé lors de la construction de l'objet de configuration IllegalArgumentException.

Après avoir fait pivoter la taille prise en charge par l'angle de rotation cible, exprimez la résolution dans un système de coordonnées Size. Par exemple, un appareil dont l'orientation naturelle de l'écran est portrait et utilise un angle de rotation cible naturel peut spécifier s'il demande une image portrait, 480x640tandis que le même appareil lorsqu'il est tourné 90et cible une orientation d'écran paysage peut le spécifier 640x480.

La résolution cible tente de définir une limite inférieure sur la résolution de l'image. La résolution réelle de l'image est la résolution disponible la plus proche et sa taille n'est pas inférieure à la résolution cible déterminée par la mise en œuvre de la caméra.

Cependant, s'il n'y a pas de résolution égale ou supérieure à la résolution cible, la résolution disponible la plus proche inférieure à la résolution cible est choisie. Les résolutions avec le même rapport d'aspect que celui fourni Sizeont priorité sur les résolutions avec des rapports d'aspect différents.

CameraX applique la résolution la plus appropriée sur demande. Spécifiez uniquement si le besoin principal est de répondre aux exigences de rapport d'aspect, et setTargetAspectRatioCameraX déterminera une résolution spécifique appropriée en fonction de l'appareil. À utiliser si l'exigence principale de votre application est de spécifier une résolution pour améliorer l'efficacité du traitement d'image (telle que le traitement d'images de petite ou moyenne taille en fonction des capacités de traitement de l'appareil) setTargetResolution(Size resolution).

Remarque : Si vous utilisez setTargetResolution(), vous pouvez obtenir des tampons avec des proportions qui ne correspondent pas à d'autres cas d'utilisation. Si les proportions doivent correspondre, vérifiez les dimensions de la mémoire tampon renvoyées par les deux cas d'utilisation, et découpez ou mettez à l'échelle l'une pour qu'elle corresponde à l'autre.
Si votre application nécessite des résolutions exactes, consultez createCaptureSession()le tableau ci-dessous pour déterminer les résolutions maximales prises en charge pour chaque niveau matériel. Pour voir les résolutions spécifiques prises en charge par votre appareil actuel, voir StreamConfigurationMap.getOutputSizes(int).

Si votre application s'exécute sur Android 10 ou version ultérieure, vous pouvez utiliser isSessionConfigurationSupported()des fichiers SessionConfiguration.

Contrôler la sortie de la caméra

Non seulement CameraX vous permet de configurer en option la sortie de la caméra pour chaque cas d'utilisation individuel, mais il implémente également les interfaces suivantes pour prendre en charge les opérations de caméra courantes dans tous les cas d'utilisation groupés :

  • Avec CameraControl, vous pouvez configurer les fonctionnalités courantes de la caméra.
  • Avec CameraInfo, vous pouvez interroger l'état de ces fonctions génériques de caméra.

Les CameraControlfonctionnalités de caméra suivantes sont prises en charge :

  • Zoom
  • lampe de poche
  • Mise au point et mesure (appuyez pour faire la mise au point)
  • la compensation d'exposition

Obtenir des instances de CameraControl et CameraInfo

Récupère les instances de et en utilisant l'objet ProcessCameraProvider.bindToLifecycle()renvoyé par . Le code suivant montre un exemple :CameraCameraControlCameraInfo

val camera = processCameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview)

// For performing operations that affect all outputs.
val cameraControl = camera.cameraControl
// For querying information and states.
val cameraInfo = camera.cameraInfo

Par exemple, vous pouvez bindToLifecycle()soumettre une opération de zoom et d'autres CameraControlopérations après avoir appelé . Si vous arrêtez ou détruisez l'instance utilisée pour lier la caméra activity, CameraControlaucune autre opération ne peut être effectuée et un échec sera renvoyé ListenableFuture.

REMARQUE : Si LifecycleOwnerest arrêté ou détruit, Camerail sera éteint, après quoi tous les changements d'état pour le zoom, la torche, la mise au point et la mesure, et les commandes de compensation d'exposition reviendront à leurs valeurs par défaut.

Zoom

CameraControlDeux méthodes de modification du niveau de zoom sont proposées :

  • setZoomRatio() est utilisé pour définir le zoom par taux de zoom.

    Le rapport doit être compris entre CameraInfo.getZoomState().getValue().getMinZoomRatio()et CameraInfo.getZoomState().getValue().getMaxZoomRatio(). Sinon, la fonction renvoie failed ListenableFuture.

  • setLinearZoom() définit l'opération de zoom actuelle avec une valeur de zoom linéaire comprise entre et 0.1.0

L'avantage du zoom linéaire est qu'il permet au champ de vision (FOV) de s'adapter à mesure que le zoom change. Par conséquent, le zoom linéaire fonctionne bien avec Sliderla vue.

CameraInfo.getZoomState()renverra l'état actuel du zoom LiveData. Cette valeur change lorsque la caméra est initialisée ou lorsque le niveau de zoom est défini à l'aide setZoomRatio()de ou . setLinearZoom()L'appel de l'une ou l'autre méthode définit les valeurs ZoomState.getZoomRatio()et prises en charge ZoomState.getLinearZoom(). Ceci est utile si vous souhaitez que le texte de l'échelle de zoom apparaisse à côté du curseur de zoom. Les deux peuvent être mis à jour en observant simplement ZoomState LiveData, sans cast.

Le renvoyé par ces deux API ListenableFuturedonne à l'application la possibilité de recevoir des notifications lorsque des demandes répétées avec la valeur de zoom spécifiée sont terminées. De plus, si vous définissez une nouvelle valeur de zoom alors que l'opération de zoom précédente est toujours en cours, l'opération de zoom précédente ListenableFutureéchouera immédiatement.

lampe de poche

CameraControl.enableTorch(boolean)La torche peut être activée ou désactivée (application torche).

CameraInfo.getTorchState()Peut être utilisé pour interroger l'état actuel de la lampe de poche. Vous pouvez CameraInfo.hasFlashUnit()déterminer si la fonction torche est disponible en vérifiant la valeur renvoyée par . Si la torche n'est pas disponible, l'appel CameraControl.enableTorch(boolean)entraînera la ListenableFuturefin immédiate du retour avec un résultat d'échec, définissant l'état de la torche sur TorchState.OFF.

Lorsqu'elle est activée, la torche reste allumée lors de la prise de photos et de vidéos, quel que soit le réglage du mode flash. ImageCapturene fonctionnera que lorsque la lampe de poche est désactivée flashMode.

Mise au point et mesure

CameraControl.startFocusAndMetering()La zone de mesure AF/AE/AWB peut être définie en fonction des FocusMeteringActionparamètres spécifiés pour déclencher la mise au point automatique et la mesure de l'exposition. Il existe de nombreuses applications d'appareil photo qui implémentent la fonctionnalité "appuyer pour faire la mise au point" de cette façon.

Point de mesure

Tout d'abord, utilisez MeteringPointFactory.createPoint(float x, float y, float size)Créer MeteringPoint. Représente un seul point sur MeteringPointla caméra . SurfaceIl est stocké sous forme normalisée afin de pouvoir être facilement converti en coordonnées de capteur pour spécifier les zones AF/AE/AWB.

MeteringPointa une taille comprise entre 0et 1, avec une taille par défaut de 0.15f. Lors de l'appel MeteringPointFactory.createPoint(float x, float y, float size), CameraX sizecrée une zone rectangulaire (x, y)centrée sur le fourni.

Le code suivant montre comment créer MeteringPoint:

// Use PreviewView.getMeteringPointFactory if PreviewView is used for preview.
previewView.setOnTouchListener((view, motionEvent) ->  {
    
    
val meteringPoint = previewView.meteringPointFactory
    .createPoint(motionEvent.x, motionEvent.y)
}

// Use DisplayOrientedMeteringPointFactory if SurfaceView / TextureView is used for
// preview. Please note that if the preview is scaled or cropped in the View,
// it’s the application's responsibility to transform the coordinates properly
// so that the width and height of this factory represents the full Preview FOV.
// And the (x,y) passed to create MeteringPoint might need to be adjusted with
// the offsets.
val meteringPointFactory = DisplayOrientedMeteringPointFactory(
     surfaceView.display,
     camera.cameraInfo,
     surfaceView.width,
     surfaceView.height
)

// Use SurfaceOrientedMeteringPointFactory if the point is specified in
// ImageAnalysis ImageProxy.
val meteringPointFactory = SurfaceOrientedMeteringPointFactory(
     imageWidth,
     imageHeight,
     imageAnalysis)

Si startFocusAndMetering et FocusMeteringAction
doivent être appelés startFocusAndMetering(), l'application doit être construite FocusMeteringAction, qui contient un ou plusieurs MeteringPoints, ce dernier est composé de modes de mesure optionnels tels que FLAG_AF, FLAG_AE et FLAG_AWB. Le code suivant illustre cette utilisation :

val meteringPoint1 = meteringPointFactory.createPoint(x1, x1)
val meteringPoint2 = meteringPointFactory.createPoint(x2, y2)
val action = FocusMeteringAction.Builder(meteringPoint1) // default AF|AE|AWB
      // Optionally add meteringPoint2 for AF/AE.
      .addPoint(meteringPoint2, FLAG_AF | FLAG_AE)
      // The action is canceled in 3 seconds (if not set, default is 5s).
      .setAutoCancelDuration(3, TimeUnit.SECONDS)
      .build()

val result = cameraControl.startFocusAndMetering(action)
// Adds listener to the ListenableFuture if you need to know the focusMetering result.
result.addListener({
    
    
   // result.get().isFocusSuccessful returns if the auto focus is successful or not.
}, ContextCompat.getMainExecutor(this)

startFocusAndMetering()Accepte un , comme indiqué dans le code ci-dessus FocusMeteringAction, qui en contient un pour les zones de mesure AF/AE/AWB MeteringPoint, et un autre pour AF et AE uniquement MeteringPoint.

En interne, CameraX le convertira en Camera2 MeteringRectangleset définira les CONTROL_AF_REGIONS/CONTROL_AE_REGIONS/CONTROL_AWB_REGIONSparamètres correspondants à la demande de capture.

Étant donné que tous les appareils ne prennent pas en charge AF/AE/AWB et plusieurs zones, CameraX fait de son mieux FocusMeteringAction. CameraX utilisera le nombre maximum pris en charge MeteringPointet les utilisera séquentiellement dans l'ordre dans lequel les points de mesure ont été ajoutés. MeteringPointCameraX ignore tous les ajouts au-delà du nombre maximum pris en charge . Par exemple, si vous fournissez 3 pour MeteringPointsur une plate-forme qui ne prend en charge que 2 , CameraX n'utilisera que les 2 premiers et ignorera le dernier .FocusMeteringActionMeteringPointMeteringPointMeteringPoint

la compensation d'exposition

La compensation d'exposition est utile lorsque l'application nécessite un réglage fin de la valeur d'exposition (EV) autre que le résultat de la sortie d'exposition automatique (AE). CameraX combinera les valeurs de compensation d'exposition comme suit pour déterminer l'exposition souhaitée pour les conditions d'image actuelles :

Exposure = ExposureCompensationIndex * ExposureCompensationStep

CameraX fournit Camera.CameraControl.setExposureCompensationIndex()une fonction pour régler la compensation d'exposition sur une valeur indexée.

Lorsque la valeur de l'index est positive, l'image sera éclaircie ; lorsque la valeur de l'index est négative, l'image sera assombrie. CameraInfo.ExposureState.exposureCompensationRange()Les applications peuvent interroger les plages prises en charge, comme décrit dans la section suivante . setExposureCompensationIndex()Le ListenableFuture renvoyé se termine lorsque la valeur est activée avec succès dans une demande de capture si la valeur correspondante est prise en charge, ou entraîne l' ListenableFutureexécution immédiate du retour avec un échec si l'index spécifié est en dehors de la plage prise en charge.

CameraX ne conserve que la dernière setExposureCompensationIndex()requête en attente. Appeler cette fonction plusieurs fois alors que la requête précédente n'a pas été exécutée entraînera l'annulation de la requête.

L'extrait de code suivant définit l'indice de compensation d'exposition et enregistre un rappel pour savoir quand une demande de modification d'exposition est exécutée :

camera.cameraControl.setExposureCompensationIndex(exposureCompensationIndex)
   .addListener({
    
    
      // Get the current exposure compensation index, it might be
      // different from the asked value in case this request was
      // canceled by a newer setting request.
      val currentExposureIndex = camera.cameraInfo.exposureState.exposureCompensationIndex
   }, mainExecutor)

Camera.CameraInfo.getExposureState()Les actuels peuvent être récupérés ExposureState, notamment :

  • Prise en charge des contrôles de compensation d'exposition.
  • L'indice de correction d'exposition actuel.
  • Plage d'index de compensation d'exposition.
  • L'étape de compensation d'exposition utilisée pour calculer la valeur de compensation d'exposition.

Par exemple, le code suivant initialise le paramètre Exposition avec la ExposureStatevaleur courante SeekBar:

val exposureState = camera.cameraInfo.exposureState
binding.seekBar.apply {
    
    
   isEnabled = exposureState.isExposureCompensationSupported
   max = exposureState.exposureCompensationRange.upper
   min = exposureState.exposureCompensationRange.lower
   progress = exposureState.exposureCompensationIndex
}

API d'extensions CameraX

CameraX fournit une API d'extensions pour accéder aux extensions mises en œuvre par les fabricants d'appareils sur divers appareils Android. Voir Extensions de caméra pour une liste des modes d'extension pris en charge .

Pour obtenir la liste des appareils prenant en charge l'extension, consultez Appareils pris en charge .

Architecture étendue

Le schéma suivant montre l'architecture de l'extension de caméra.

insérez la description de l'image ici

Les applications CameraX peuvent utiliser des extensions via l'API CameraX Extensions. L'API CameraX Extensions peut être utilisée pour gérer les requêtes pour les extensions disponibles, configurer les sessions de caméra d'extension et communiquer avec les bibliothèques OEM d'extensions de caméra. Cela permet à votre application d'utiliser des fonctionnalités telles que Nuit, HDR, Auto, Bokeh ou Face Photo Fix.

dépendances

L'API CameraX Extensions est camera-extensionsimplémentée dans la bibliothèque. Ces extensions dépendent des modules principaux de CameraX ( core, camera2, lifecycle).

dependencies {
    
    
  def camerax_version = "1.3.0-alpha04"
  implementation "androidx.camera:camera-core:${camerax_version}"
  implementation "androidx.camera:camera-camera2:${camerax_version}"
  implementation "androidx.camera:camera-lifecycle:${camerax_version}"
  //the CameraX Extensions library
  implementation "androidx.camera:camera-extensions:${camerax_version}"
    ...
}

Une extension qui permet la capture et la prévisualisation d'images

Avant d'utiliser l'API Extensions, utilisez ExtensionsManager#getInstanceAsync(Context, CameraProvider)la méthode pour récupérer ExtensionsManagerune instance. De cette façon, vous pouvez demander des informations sur la disponibilité étendue. Récupérez ensuite les extensions activées CameraSelector. Si la méthode est appelée CameraSelectoravec l'extension enabled bindToLifecycle(), le mode étendu sera appliqué pour les cas d'utilisation de capture d'image et de prévisualisation.

Remarque : Avec les extensions activées sur ImageCaptureet Preview, si vous utilisez et ImageCapturecomme arguments de , vous pouvez être limité dans le nombre de caméras que vous pouvez choisir. Une exception est levée si aucune caméra prenant en charge l'extension n'est trouvée .PreviewbindToLifecycle()ExtensionsManager#getExtensionEnabledCameraSelector()

Pour une extension permettant d'implémenter le cas d'utilisation de capture et d'aperçu d'image, consultez l'exemple de code suivant :

import androidx.camera.extensions.ExtensionMode
import androidx.camera.extensions.ExtensionsManager

override fun onCreate(savedInstanceState: Bundle?) {
    
    
    super.onCreate(savedInstanceState)

    val lifecycleOwner = this

    val cameraProviderFuture = ProcessCameraProvider.getInstance(applicationContext)
    cameraProviderFuture.addListener({
    
    
        // Obtain an instance of a process camera provider
        // The camera provider provides access to the set of cameras associated with the device.
        // The camera obtained from the provider will be bound to the activity lifecycle.
        val cameraProvider = cameraProviderFuture.get()

        val extensionsManagerFuture =
            ExtensionsManager.getInstanceAsync(applicationContext, cameraProvider)
        extensionsManagerFuture.addListener({
    
    
            // Obtain an instance of the extensions manager
            // The extensions manager enables a camera to use extension capabilities available on
            // the device.
            val extensionsManager = extensionsManagerFuture.get()

            // Select the camera
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

            // Query if extension is available.
            // Not all devices will support extensions or might only support a subset of
            // extensions.
            if (extensionsManager.isExtensionAvailable(cameraSelector, ExtensionMode.NIGHT)) {
    
    
                // Unbind all use cases before enabling different extension modes.
                try {
    
    
                    cameraProvider.unbindAll()

                    // Retrieve a night extension enabled camera selector
                    val nightCameraSelector =
                        extensionsManager.getExtensionEnabledCameraSelector(
                            cameraSelector,
                            ExtensionMode.NIGHT
                        )

                    // Bind image capture and preview use cases with the extension enabled camera
                    // selector.
                    val imageCapture = ImageCapture.Builder().build()
                    val preview = Preview.Builder().build()
                    // Connect the preview to receive the surface the camera outputs the frames
                    // to. This will allow displaying the camera frames in either a TextureView
                    // or SurfaceView. The SurfaceProvider can be obtained from the PreviewView.
                    preview.setSurfaceProvider(surfaceProvider)

                    // Returns an instance of the camera bound to the lifecycle
                    // Use this camera object to control various operations with the camera
                    // Example: flash, zoom, focus metering etc.
                    val camera = cameraProvider.bindToLifecycle(
                        lifecycleOwner,
                        nightCameraSelector,
                        imageCapture,
                        preview
                    )
                } catch (e: Exception) {
    
    
                    Log.e(TAG, "Use case binding failed", e)
                }
            }
        }, ContextCompat.getMainExecutor(this))
    }, ContextCompat.getMainExecutor(this))
}

Désactiver l'extension

Pour désactiver les extensions de fournisseur, dissociez tous les cas d'utilisation, puis reliez la capture d'image et prévisualisez les cas d'utilisation à l'aide du sélecteur de caméra habituel. Par exemple, utilisez pour CameraSelector.DEFAULT_BACK_CAMERArelier à la caméra arrière.

Résumé de la comparaison entre CameraX et Camera1

Bien que le code de CameraX et Camera1 puisse sembler différent, les concepts de base de CameraX et Camera1 sont très similaires. Dans CameraX, diverses tâches de fonction de caméra courantes : Preview, ImageCapture, VideoCaptureet ImageAnalysissont toutes résumées en tant que cas d'utilisation UseCase , de sorte que de nombreuses tâches laissées aux développeurs dans Camera1 peuvent être automatiquement gérées par CameraX.

insérez la description de l'image ici

Un exemple de la façon dont CameraX gère les détails de bas niveau pour le développeur est UseCasepartagé entre valid ViewPort. Cela garantit que tous UseCaseles pixels vus sont exactement les mêmes. Dans Camera1, vous devrez gérer ces détails vous-même, et étant donné que les appareils ont des rapports d'aspect différents pour chaque capteur et écran d'appareil photo, il peut être difficile de s'assurer que l'aperçu correspond aux photos et vidéos capturées.

Comme autre exemple, CameraX gère automatiquement les rappels Lifecycle pour l'instance Lifecycle que vous lui transmettez. Cela signifie que 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 : fermeture de l'appareil photo après que votre application passe en arrière-plan, suppression de l'aperçu de l'appareil photo lorsque l'écran n'en a plus besoin et mise en pause de l'appareil photo. aperçu tandis que d'autres activités (telles qu'une invitation à un appel vidéo) sont prioritaires au premier plan.

De plus, CameraX gère la rotation et la mise à l'échelle sans que vous ayez à faire de code supplémentaire. Si l'orientation de l'activité n'est pas verrouillée, le système la définira à chaque rotation de l'appareil, UseCasecar le système détruit et recrée l'activité lorsque l'orientation de l'écran change. De cette façon, les cas d'utilisation utiliseront par défaut leur rotation cible à chaque fois pour correspondre à l'orientation de l'écran.

insérez la description de l'image ici


Référence : https://developer.android.google.cn/training/camerax?hl=zh-cn

Je suppose que tu aimes

Origine blog.csdn.net/lyabc123456/article/details/131181550
conseillé
Classement