camerax中提供了camera-compose库,用于在compose中使用camerax来拍照和录视频。该库提供了一个CameraXViewfinder组件,这是一个可组合的适配器,它通过完成提供的SurfaceRequest来显示来自CameraX的帧。它是一个Viewfinder的封装器,它会在内部将CameraX SurfaceRequest转换为ViewfinderSurfaceRequest。此外,所有通常通过ViewfinderSurfaceRequest处理的交互都将从SurfaceRequest派生而来。
配置相关依赖
在gradle中配置camerax相关的依赖
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中申明,然后在使用的地方动态申请。
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
动态申请权限
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {}
launcher.launch(arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO))
CameraXViewfinder绑定相机并预览画面
CameraXViewfinder的使用非常简单,只需要传入一个SurfaceRequest参数就可以了。
@Composable
fun cameraLayout() {
CameraXViewfinder(
surfaceRequest = surfaceRequest,
modifier = Modifier.fillMaxSize()
)
}
surfaceRequest的获取需要绑定相机,并在预览中获取。首先使用ProcessCameraProvider将摄像头的生命周期绑定到应用程序进程内的任何LifecycleOwner上,一个进程中只能存在一个进程摄像头提供。程序重量级资源(例如已打开并正在运行的摄像头设备)的作用域将限定在bindToLifecycle提供的生命周期内。其他轻量级资源(例如静态摄像头特性)可以在首次使用getInstance检索此提供程序时被检索并缓存,并在进程的整个生命周期内保持有效。示例如下
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进行拍照,示例如下
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属性就可以设置闪光灯模式,例如开启闪光灯
//关闭闪光灯:ImageCapture.FLASH_MODE_OFF
//开启闪光灯:ImageCapture.FLASH_MODE_ON
//设置自动模式:ImageCapture.FLASH_MODE_AUTO
imageCapture?.flashMode = ImageCapture.FLASH_MODE_ON
缩放和聚焦涉及到CameraXViewfinder的手势处理,获取点击位置进行聚焦,以及双手捏合进行缩放处理。
@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中添加视图窗口,告诉相机我们的预览尺寸是多大,这样拍摄出来的照片,会根据我们提供的视图窗口比例进行裁剪。
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进行录制。需要注意的是录制视频不仅需要相机权限,还需要录音权限。示例如下
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正在录制时,获取录制的时间,以及录制的文件大小
//录制的时间,单位毫秒
val second = it.recordingStats.recordedDurationNanos / 1000000
//视频文件大小,单位MB
val size = it.recordingStats.numBytesRecorded / 1024 / 1024


612

被折叠的 条评论
为什么被折叠?



