在Compose中使用camerax进行拍照和录视频

camerax中提供了camera-compose库,用于在compose中使用camerax来拍照和录视频。该库提供了一个CameraXViewfinder组件,这是一个可组合的适配器,它通过完成提供的SurfaceRequest来显示来自CameraX的帧。它是一个Viewfinder的封装器,它会在内部将CameraX SurfaceRequest转换为ViewfinderSurfaceRequest。此外,所有通常通过ViewfinderSurfaceRequest处理的交互都将从SurfaceRequest派生而来。

配置相关依赖

在gradle中配置camerax相关的依赖

kotlin 复制代码
dependencies {
  implementation("androidx.camera:camera-core:1.5.1")
  implementation("androidx.camera:camera-camera2:1.5.1")
  implementation("androidx.camera:camera-compose:1.5.1")
  implementation("androidx.camera:camera-lifecycle:1.5.1")
  implementation("androidx.camera:camera-video:1.5.1")
}

core库提供了camerax的核心,camera2是camerax的具体实现,compose库提供了CameraXViewfinder组件,lifecycle用于管理生命周期,video用于录制视频。

权限申请

拍摄照片需要相机权限,如果是录视频,还需要录音权限。先在AndroidManifest中申明,然后在使用的地方动态申请。

xml 复制代码
<uses-permission android:name="android.permission.CAMERA" />
  <uses-permission android:name="android.permission.RECORD_AUDIO" />

动态申请权限

kotlin 复制代码
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {}
launcher.launch(arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO))

CameraXViewfinder绑定相机并预览画面

CameraXViewfinder的使用非常简单,只需要传入一个SurfaceRequest参数就可以了。

kotlin 复制代码
@Composable
fun cameraLayout() {
  CameraXViewfinder(
    surfaceRequest = surfaceRequest,
    modifier = Modifier.fillMaxSize()
  )
}

surfaceRequest的获取需要绑定相机,并在预览中获取。首先使用ProcessCameraProvider将摄像头的生命周期绑定到应用程序进程内的任何LifecycleOwner上,一个进程中只能存在一个进程摄像头提供。程序重量级资源(例如已打开并正在运行的摄像头设备)的作用域将限定在bindToLifecycle提供的生命周期内。其他轻量级资源(例如静态摄像头特性)可以在首次使用getInstance检索此提供程序时被检索并缓存,并在进程的整个生命周期内保持有效。示例如下

kotlin 复制代码
fun cameraLayout() {
  val context = LocalContext.current
  val lifecycleOwner = LocalLifecycleOwner.current
  var surfaceRequest by remember { mutableStateOf<SurfaceRequest?>(null) }
  LaunchedEffect(Unit) {
    val group = UseCaseGroup.Builder().addUseCase(Preview.Builder().build().apply {
      setSurfaceProvider {
        surfaceRequest = it
      }
    }).build()
    val processCameraProvider = ProcessCameraProvider.awaitInstance(context)
    processCameraProvider.bindToLifecycle(lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, group).also {
      try {
        awaitCancellation()
      } finally {
        processCameraProvider.unbindAll()
      }
    }
  }
  surfaceRequest?.also {
    CameraXViewfinder(
      surfaceRequest = it,
      modifier = Modifier.fillMaxSize()
    )
  }
}

UseCaseGroup用于管理一组用例集合,当用例组绑定到生命周期时,它会将所有用例绑定到同一个生命周期。用例组内的用例通常共享一些公共属性,例如由视口定义的视野范围。camerax使用不同的用例来管理相机,例如提供相机预览能力的Preview用例,用于拍照输出的ImageCapture用例,提供视图窗口的ViewPort用例,以及提供录制视频能力的VideoCapture用例。最后processCameraProvider调用bindToLifecycle绑定生命周期,这里使用CameraSelector.DEFAULT_BACK_CAMERA后置摄像头进行预览。

使用ImageCapture进行拍照

要进行拍照,首先需要在UseCaseGroup中添加ImageCapture用例,然后使用ImageCapture进行拍照,示例如下

kotlin 复制代码
class CameraViewModel(application: Application) : AndroidViewModel(application) {
  private var imageCapture: ImageCapture? = null
  var cameraControl: CameraControl? = null
    private set

  suspend fun bindToCamera(lifecycleOwner: LifecycleOwner, surface: Preview.SurfaceProvider) {
    val processCameraProvider = ProcessCameraProvider.awaitInstance(application)
    val group = UseCaseGroup.Builder().addUseCase(Preview.Builder().build().apply {
      surfaceProvider = surface
    }).addUseCase(ImageCapture.Builder().build().also { imageCapture = it }).build()
    processCameraProvider.bindToLifecycle(lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, group).also {
      cameraControl = it.cameraControl
      try {
        awaitCancellation()
      } finally {
        processCameraProvider.unbindAll()
        cameraControl = null
      }
    }
  }

  fun takePhoto() {
    val imageCapture = imageCapture ?: return
    val name = SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault()).format(System.currentTimeMillis())
    val dir = File(application.cacheDir, Environment.DIRECTORY_PICTURES)
    dir.takeUnless { it.exists() }?.mkdirs()
    val options = ImageCapture.OutputFileOptions.Builder(File(dir, "DIC_${name}.jpg"))
      .setMetadata(ImageCapture.Metadata()).build()
    imageCapture.takePicture(options, CameraXExecutors.ioExecutor(), object : ImageCapture.OnImageSavedCallback {
        override fun onImageSaved(output: ImageCapture.OutputFileResults) {
          val path = output.savedUri?.toString()
          Log.i("camera","照片路径:$path")
        }
    })
  }
}

就这么几行代码,传入照片路径,就能进行拍照了。在options中,我们可以通过setMetadata给照片添加无数据,例如设置位置,是否水平或垂直翻转等。

拍照时设置闪光灯缩放和聚焦

设置闪光灯比较简单,在前面绑定生命周期后,可以获取到CameraControl对象,直接通过CameraControl对象的flashMode属性就可以设置闪光灯模式,例如开启闪光灯

kotlin 复制代码
//关闭闪光灯:ImageCapture.FLASH_MODE_OFF
//开启闪光灯:ImageCapture.FLASH_MODE_ON
//设置自动模式:ImageCapture.FLASH_MODE_AUTO
imageCapture?.flashMode = ImageCapture.FLASH_MODE_ON

缩放和聚焦涉及到CameraXViewfinder的手势处理,获取点击位置进行聚焦,以及双手捏合进行缩放处理。

kotlin 复制代码
@Composable
fun cameraLayout() {
  var cameraZoom by remember { mutableStateOf(1F) }
  var surfaceRequest by remember { mutableStateOf<SurfaceRequest?>(null) }
  val coordinateTransformer = remember { MutableCoordinateTransformer() }
  surfaceRequest?.also { request ->
    CameraXViewfinder(
      surfaceRequest = request,
      modifier = Modifier.fillMaxSize()
        .pointerInput(Unit) {
          detectTapGestures {
            with(coordinateTransformer) {
              it.transform()
            }.let {
              SurfaceOrientedMeteringPointFactory(
                request.resolution.width.toFloat(),
                request.resolution.height.toFloat()
              ).createPoint(it.x, it.y)
            }.also {
              val action = FocusMeteringAction.Builder(it, FocusMeteringAction.FLAG_AF.or(FocusMeteringAction.FLAG_AE))
                .setAutoCancelDuration(3, TimeUnit.SECONDS).build()
              viewModel.cameraControl?.startFocusAndMetering(action)?.addListener({ }, CameraXExecutors.ioExecutor())
            }
          }
        }
        .pointerInput(Unit) {
          detectTransformGestures { _, _, zoom, _ ->
            cameraZoom *= zoom
            viewModel.cameraControl?.setZoomRatio(cameraZoom)
          }
        },
      coordinateTransformer = coordinateTransformer
    )
  }
}

处理照片与预览范围不一致

当我们浏览本地拍摄的照片时,会发现照片的实现范围可能会比预览时的范围大,如果没有特殊要求,这个差别可能不会有什么影响,但如果我们需要对拍摄的照片进行处理,比如加水印,或进行裁剪,影响就比较大了,会出现水印位置不对,裁剪位置不对等问题。其实处理这些问题非常简单,根本原因是拍摄的宽度比与预览的宽高比不一致导致的。我们可以在UseCaseGroup中添加视图窗口,告诉相机我们的预览尺寸是多大,这样拍摄出来的照片,会根据我们提供的视图窗口比例进行裁剪。

kotlin 复制代码
suspend fun bindToCamera(lifecycleOwner: LifecycleOwner, viewSize: Size, surface: Preview.SurfaceProvider) {
  val processCameraProvider = ProcessCameraProvider.awaitInstance(application)
  val group = UseCaseGroup.Builder().addUseCase(Preview.Builder().build().apply {
    setSurfaceProvider{}
    surfaceProvider = surface
  }).addUseCase(ImageCapture.Builder().setFlashMode(flashMode).build().also { imageCapture = it })
    .setViewPort(ViewPort.Builder(Rational(viewSize.width.toInt(), viewSize.height.toInt()), Surface.ROTATION_0).build()
    ).build()
  processCameraProvider.bindToLifecycle(lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, group).also {
    cameraControl = it.cameraControl
    try {
      awaitCancellation()
    } finally {
      processCameraProvider.unbindAll()
      cameraControl = null
    }
  }
}

使用VideoCapture录制视频

在camerax中录制视频也非常简单,首先在UseCaseGroup中添加VideoCapture用例,然后用VideoCapture进行录制。需要注意的是录制视频不仅需要相机权限,还需要录音权限。示例如下

kotlin 复制代码
class CameraViewModel(application: Application) : AndroidViewModel(application) {
  private var videoCapture: VideoCapture<Recorder>? = null
  private val tag = javaClass.simpleName
  
  suspend fun bindToCamera(lifecycleOwner: LifecycleOwner, viewSize: Size, surface: Preview.SurfaceProvider) {
    val processCameraProvider = ProcessCameraProvider.awaitInstance(application)
    val group = UseCaseGroup.Builder().addUseCase(Preview.Builder().build().apply {
      setSurfaceProvider{}
      surfaceProvider = surface
    }).addUseCase(ImageCapture.Builder().setFlashMode(flashMode).build().also { imageCapture = it })
      .addUseCase(VideoCapture.withOutput(Recorder.Builder().setQualitySelector(QualitySelector.from(Quality.FHD)).build()).also {
        videoCapture = it
      })
      .setViewPort(ViewPort.Builder(Rational(viewSize.width.toInt(), viewSize.height.toInt()), Surface.ROTATION_0).build()
      ).build()
    processCameraProvider.bindToLifecycle(lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, group).also {
      cameraControl = it.cameraControl
      try {
        awaitCancellation()
      } finally {
        processCameraProvider.unbindAll()
        cameraControl = null
      }
    }
  }
  fun startOrStopRecordVideo() {
    if (recording == null) {
      recording = videoCapture?.takeIf { checkPermission(Manifest.permission.RECORD_AUDIO) }?.let { capture ->
        val name = SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault()).format(System.currentTimeMillis())
        val dir = File(application.cacheDir, Environment.DIRECTORY_MOVIES).also { it.takeUnless { it.exists() }?.mkdirs() }
        capture.output.prepareRecording(application, FileOutputOptions.Builder(File(dir, "${name}.mp4")).build()).withAudioEnabled()
          .start(CameraXExecutors.ioExecutor()) {
            when (it) {
              is VideoRecordEvent.Start -> Log.w(tag, "开始录制")
              is VideoRecordEvent.Finalize -> {
                if (it.hasError()) {
                  Log.w(tag, "录制失败: ${it.cause?.message}")
                } else {
                  Log.i(tag, "结束录制: ${it.outputResults.outputUri}")
                }
              }
              is VideoRecordEvent.Status -> {
                Log.w(tag, "正在录制")
              }
            }
          }
      }
    } else {
      recording?.stop()
      recording = null
    }
  }
}

我们可以通过VideoRecordEvent来获取视频的录制信息,例如当状态为VideoRecordEvent.Status正在录制时,获取录制的时间,以及录制的文件大小

kotlin 复制代码
//录制的时间,单位毫秒
val second = it.recordingStats.recordedDurationNanos / 1000000
//视频文件大小,单位MB
val size = it.recordingStats.numBytesRecorded / 1024 / 1024

相关推荐
此去正年少19 小时前
编写adb脚本工具对Android设备上的闪退问题进行监控分析
android·adb·logcat·ndk·日志监控
落羽凉笙19 小时前
Python基础(4)| 玩转循环结构:for、while与嵌套循环全解析(附源码)
android·开发语言·python
十幺卜入20 小时前
Unity3d C# 基于安卓真机调试日志抓取拓展包(Android Logcat)
android·c#·unity 安卓调试·unity 安卓模拟·unity排查问题
frontend_frank20 小时前
脱离 Electron autoUpdater:uni-app跨端更新:Windows+Android统一实现方案
android·前端·javascript·electron·uni-app
薛晓刚20 小时前
MySQL的replace使用分析
android·adb
DengDongQi21 小时前
Jetpack Compose 滚轮选择器
android
stevenzqzq21 小时前
Android Studio Logcat 基础认知
android·ide·android studio·日志
代码不停21 小时前
MySQL事务
android·数据库·mysql
朝花不迟暮21 小时前
使用Android Studio生成apk,卡在Running Gradle task ‘assembleDebug...解决方法
android·ide·android studio
yngsqq21 小时前
使用VS(.NET MAUI)开发第一个安卓APP
android·.net