一文理解Jetpack——CameraX

857 阅读4分钟

屏幕截图 2024-05-02 102507.png

在 Android 开发中,摄像头几乎在每一个 App 都需要使用到。但是由于 Android 碎片化的问题,要在不同的机型上保持一致体验非常麻烦。 Google 为了解决这个问题,就创建了 CameraX 这个组件库。我们开发者只需要关心 CameraX 的 api 即可,CameraX 内部会对不同的机型进行适配处理。

要使用 CameraX,需要引入如下的依赖:

def camerax_version = '1.1.0'
implementation "androidx.camera:camera-core:$camerax_version"
implementation "androidx.camera:camera-camera2:$camerax_version"
implementation "androidx.camera:camera-lifecycle:$camerax_version"
implementation "androidx.camera:camera-view:$camerax_version"

申请权限

在使用 Camera 之前,我们需要先申请相应的权限。Camera 相关的权限如下所示:

<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
   android:maxSdkVersion="28" />

预览

CameraX 中,我们需要使用 PreviewView 组件来显示预览图。我们首先需要将 PreviewView 添加到布局文件,代码如下:

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/camera_container"
    android:background="@android:color/black"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.camera.view.PreviewView
        android:id="@+id/view_finder"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

布局文件添加好后,我们就可以开始实现预览效果了,代码示例如下:

private fun startCamera() {
   
   val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

   // addListener 是为了监听 CameraX 初始化完成
   cameraProviderFuture.addListener({
       //  用来绑定生命周期组件,让 CameraX 具有生命周期感知能力
       val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

       // 预览组件
       val preview = Preview.Builder()
          .build()
          .also {
              // 绑定我们在布局中加入的 PreviewView
              it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
          }

       // 设置选中的摄像头,这里选中的是后置摄像头
       val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

       try {
           // 解除之前的绑定
           cameraProvider.unbindAll()

           // CameraX 把不同的功能,如 预览、拍照等功能封装成不同的 UseCase
           // 这样我们就可以根据业务的需要,来绑定不同的 UseCase 来实现具体的功能
           cameraProvider.bindToLifecycle(
               this, cameraSelector, preview)

       } catch(exc: Exception) {
           Log.e(TAG, "Use case binding failed", exc)
       }
       // ContextCompat.getMainExecutor 表示在主线程执行初始化
   }, ContextCompat.getMainExecutor(this))
}

上面的代码就是我们使用 CameraX 的固定流程了。如果需要添加其他的功能,比如拍照、拍视频等,只需要添加对应的 UseCase 就可以了。

切换摄像头

CameraX 中如果需要切换摄像头,我们需要替换 cameraSelector 的值,然后重新绑定。代码示例如下,这里使用协程来实现。

private var cameraProvider: ProcessCameraProvider? = null
private var currentLensFacing: Int = CameraSelector.LENS_FACING_BACK

// 切换摄像头
fun switchCamera() {
    val cameraProvider = cameraProvider ?: return
    val lensFacing = when {
            hasBackCamera() -> CameraSelector.LENS_FACING_BACK
            hasFrontCamera() -> CameraSelector.LENS_FACING_FRONT
            else -> throw IllegalStateException("Back and front camera are unavailable")
        }
     currentLensFacing = lensFacing
     bindUseCases()
}

private suspend fun startCamera() {
       // 使用 await 等待初始化,而不是 addListener
       cameraProvider = cameraProviderFuture.get().await()

       bindUseCases()
}

// 绑定 UseCase
private fun bindUseCases() {

   val cameraProvider = cameraProvider ?: return
    
   val preview = Preview.Builder()
      .build()
      .also {
          it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
      }
   // 设置当前需要切换的摄像头
   val cameraSelector = currentLensFacing

   try {
       // 解除之前的绑定
       cameraProvider.unbindAll()

       // CameraX 把不同的功能,如 预览、拍照等功能封装成不同的 UseCase
       // 这样我们就可以根据业务的需要,来绑定不同的 UseCase 来实现具体的功能
       cameraProvider.bindToLifecycle(
           this, cameraSelector, preview)

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

为了避免摄像头不存在的异常情况,我们还需要提前判断摄像头是否存在,代码如下:

// 后置摄像头是否存在
private fun hasBackCamera(): Boolean {
    return cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ?: false
}

// 前置摄像头是否存在
private fun hasFrontCamera(): Boolean {
    return cameraProvider?.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) ?: false
}

拍照

CameraX 中拍照片,我们需要添加 ImageCapture 这个 UseCase 并绑定,代码示例如下:

private var imageCapture: ImageCapture? = null

private fun bindUseCases() {

   ...
   // 增加拍照的 UseCase
   imageCapture = ImageCapture.Builder()
        .build()

   try {
       ...
       // 增加 imageCapture 的绑定
       cameraProvider.bindToLifecycle(
           this, cameraSelector, preview, imageCapture)

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

完成绑定后,我们就可以使用创建的 imageCapture 对象来实现拍照操作了,代码示例如下:

private val cameraExecutor = Executors.newSingleThreadExecutor()

private fun takePhoto() {
   val imageCapture = imageCapture ?: return

   // 创建用于保存图片的 MediaStore 内容值
   val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
              .format(System.currentTimeMillis())
   val contentValues = ContentValues().apply {
       put(MediaStore.MediaColumns.DISPLAY_NAME, name)
       put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
       if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
           put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
       }
   }

   // 设置所需的输出内容。这里将输出保存在 MediaStore 中
   val outputOptions = ImageCapture.OutputFileOptions
           .Builder(contentResolver,
                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                    contentValues)
           .build()

   // 执行拍照操作
   imageCapture.takePicture(
       outputOptions,
       cameraExecutor, // 设置在子线程执行
       object : ImageCapture.OnImageSavedCallback {
           override fun onError(exc: ImageCaptureException) {
               // 失败
               Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
           }

           override fun
               onImageSaved(output: ImageCapture.OutputFileResults){
               // 图片保存成功
           }
       }
   )
}

让预览界面与相机捕获界面一致

由于摄像头支持的拍摄的比例(一般是 4:3)与屏幕的比例不一致,会导致拍照图与预览图结果不一样。如果你想要让预览图与拍照图一致,需要指定分辨率。代码示例如下:

private fun bindUseCases() {

   ...
   
    // 相机设置为全屏分辨率
    val metrics = windowManager.getCurrentWindowMetrics().bounds
    val screenAspectRatio = aspectRatio(metrics.width(), metrics.height())
    val rotation = fragmentCameraBinding.viewFinder.display.rotation

    // 通过 setTargetAspectRatio 来设置相同的宽高比
    // 如果设置的宽高比当前Camera不支持,CameraX 会找到最接近的宽高比来处理
    preview = Preview.Builder()
            .setTargetAspectRatio(screenAspectRatio)
            .setTargetRotation(rotation)
            .build()

    imageCapture = ImageCapture.Builder()
            .setTargetAspectRatio(screenAspectRatio)
            .setTargetRotation(rotation)
            .build()
   
}

录像

如果需要在 CameraX 中录像,我要添加绑定 VideoCapture 这个 UseCase ,代码示例如下:

private lateinit var videoCapture: VideoCapture<Recorder>

private fun bindUseCases() {
    ...
    
    // 构建一个记录器,它可以:
    // - 将视频/音频录制到 MediaStore 、File、ParcelFileDescriptor
    // - 用于创建录音
    val recorder = Recorder.Builder()
        .setQualitySelector(QualitySelector.from(Quality.HIGHEST))
        .build()
    videoCapture = VideoCapture.withOutput(recorder)
}

绑定完成后,就可以使用 VideoCapture 来录像了,代码示例如下:

private var recording: Recording? = null

private fun captureVideo() {
   val videoCapture = this.videoCapture ?: return

   val curRecording = recording
   if (curRecording != null) {
       // 停止录像
       curRecording.stop()
       recording = null
       return
   }

   // 创建 MediaStore 视频内容对象,将系统时间戳作为视频的名字
   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")
       }
   }
   // 设置所需的输出内容。将输出保存在 MediaStore 中
   val mediaStoreOutputOptions = MediaStoreOutputOptions
       .Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
       .setContentValues(contentValues)
       .build()
   
   // 开始录像
   recording = videoCapture.output
       .prepareRecording(this, mediaStoreOutputOptions)
       .start(ContextCompat.getMainExecutor(this)) { recordEvent ->
           when(recordEvent) {
               is VideoRecordEvent.Start -> {
                   // 录制开始
               }
               is VideoRecordEvent.Pause -> {
                   // 暂停
               }
               is VideoRecordEvent.Resume -> {
                   // 恢复
               }
               is VideoRecordEvent.Finalize -> {
                   // 录制结束
                   if (!recordEvent.hasError()) {
                       // 录制失败
                   } else {
                       // 录
                   }
               }
           }
       }
}

参考