4. CameraX 预览实战:实现实时相机预览、配置分辨率与宽高比、处理屏幕旋转与镜像
CameraX 作为 Jetpack 相机库的核心组件,极大地简化了实时相机预览的实现。本章将深入剖析如何通过 PreviewView 与 Preview UseCase 构建稳定、高效的预览管道,并重点解决分辨率选择、宽高比适配、屏幕旋转与镜像翻转等实战中的关键问题。
4.1 预览管道架构与核心组件
CameraX 的预览流程围绕三个核心角色展开:Preview UseCase、PreviewView 与 SurfaceProvider。它们之间的协作关系如下图所示:
核心原则:Preview UseCase 不直接持有 Surface,而是通过 SurfaceProvider 回调机制解耦了预览管道的创建与销毁。这使得 CameraX 能够在生命周期变化时自动管理 Surface 的释放与重建,开发者无需手动处理。
4.2 实现基础实时预览
以下代码展示了在 Fragment 中集成 CameraX 预览的最小实现。关键步骤包括:绑定生命周期、构建 Preview UseCase、配置 PreviewView。
// 在 Fragment 的 onViewCreated 中
val previewView: PreviewView = binding.previewView
// 1. 获取 CameraProvider
val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
// 2. 构建 Preview UseCase
val preview = Preview.Builder()
.build()
// 3. 为 Preview 设置 SurfaceProvider(由 PreviewView 提供)
preview.setSurfaceProvider(previewView.surfaceProvider)
// 4. 选择后置摄像头
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
// 5. 绑定到生命周期
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
viewLifecycleOwner,
cameraSelector,
preview
)
} catch (e: Exception) {
Log.e(TAG, "预览绑定失败", e)
}
}, ContextCompat.getMainExecutor(requireContext()))
注意:务必在
onDestroyView 中调用 cameraProvider.unbindAll() 或依赖 viewLifecycleOwner 自动解绑,避免内存泄漏。同时,ProcessCameraProvider 是单例,重复获取不会产生额外开销。
4.3 配置分辨率与宽高比
CameraX 提供了灵活的分辨率配置策略。开发者可以通过 setTargetResolution() 和 setTargetAspectRatio() 控制输出尺寸,但需理解两者的优先级与协作关系。
| 配置方法 | 作用 | 优先级 | 典型值 |
|---|---|---|---|
setTargetResolution(Size) |
指定目标分辨率,CameraX 会选择最接近的可用尺寸 | 高(覆盖宽高比) | Size(1920, 1080) |
setTargetAspectRatio(int) |
指定目标宽高比,CameraX 自动选择匹配的尺寸 | 低(被分辨率覆盖) | AspectRatio.RATIO_16_9 |
最佳实践:推荐优先使用
setTargetAspectRatio() 配合 PreviewView 的 setScaleType() 实现自适应布局。仅在需要精确控制输出大小时(如视频录制)才使用 setTargetResolution()。
以下代码展示了如何根据屏幕尺寸动态选择宽高比:
fun configurePreviewWithAspectRatio(previewView: PreviewView): Preview {
// 获取 PreviewView 的宽高比
val viewWidth = previewView.width.toFloat()
val viewHeight = previewView.height.toFloat()
val aspectRatio = if (viewWidth > viewHeight) {
AspectRatio.RATIO_16_9 // 横屏
} else {
AspectRatio.RATIO_4_3 // 竖屏
}
return Preview.Builder()
.setTargetAspectRatio(aspectRatio)
.build()
}
4.4 处理屏幕旋转与镜像
屏幕旋转会导致预览画面方向错误,而前置摄像头的镜像翻转则影响用户体验。CameraX 通过 setTargetRotation() 和 PreviewView 的镜像属性解决这两个问题。
4.4.1 屏幕旋转适配
CameraX 会自动监听设备旋转,但需要开发者显式设置目标旋转角度,以确保预览输出方向正确。推荐在 ConfigurationChanged 或使用 DisplayListener 时更新:
// 获取当前显示旋转
val display = previewView.display
val rotation = display?.rotation ?: Surface.ROTATION_0
// 构建 Preview 时设置目标旋转
val preview = Preview.Builder()
.setTargetRotation(rotation)
.build()
// 当屏幕旋转时重新绑定(简化示例)
val listener = object : DisplayListener {
override fun onDisplayChanged(displayId: Int) {
val newRotation = previewView.display?.rotation ?: return
// 重新创建 Preview 并绑定
rebindPreview(newRotation)
}
}
4.4.2 前置摄像头镜像处理
默认情况下,前置摄像头的预览画面是镜像的(如同镜子)。CameraX 的 PreviewView 提供了 setScaleX() 方法进行水平翻转:
// 检测是否为前置摄像头
val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA
// 在绑定成功后,对 PreviewView 进行镜像翻转
previewView.scaleX = -1f // 水平镜像
// 如果需要同时处理旋转,建议在布局中固定 PreviewView 方向
// 或使用 setScaleY(-1f) 实现垂直镜像
镜像与旋转的叠加:当同时需要处理旋转和镜像时,建议通过
PreviewView 的 setScaleX() 和 setScaleY() 组合实现。注意:setTargetRotation() 只影响相机输出方向,不影响 PreviewView 的显示镜像。
4.5 综合配置示例
以下代码整合了分辨率、宽高比、旋转与镜像的完整配置:
fun setupCameraPreview(
context: Context,
lifecycleOwner: LifecycleOwner,
previewView: PreviewView,
useFrontCamera: Boolean = false
) {
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
// 1. 获取当前旋转
val rotation = previewView.display?.rotation ?: Surface.ROTATION_0
// 2. 构建 Preview
val preview = Preview.Builder()
.setTargetAspectRatio(AspectRatio.RATIO_16_9)
.setTargetRotation(rotation)
.build()
// 3. 设置 SurfaceProvider
preview.setSurfaceProvider(previewView.surfaceProvider)
// 4. 选择摄像头
val cameraSelector = if (useFrontCamera) {
CameraSelector.DEFAULT_FRONT_CAMERA
} else {
CameraSelector.DEFAULT_BACK_CAMERA
}
// 5. 绑定
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview)
// 6. 前置摄像头镜像处理
if (useFrontCamera) {
previewView.scaleX = -1f
} else {
previewView.scaleX = 1f
}
}, ContextCompat.getMainExecutor(context))
}
4.6 常见问题与避坑指南
- 预览黑屏:检查
PreviewView是否在布局中设置了固定宽高(如match_parent),且surfaceProvider在onResume之后设置。 - 宽高比不匹配:使用
PreviewView.setScaleType(ScaleType.FILL_CENTER)裁剪多余部分,或ScaleType.FIT_CENTER保留完整画面(可能出现黑边)。 - 旋转后画面拉伸:确保
setTargetRotation()与PreviewView的布局方向一致。竖屏应用应固定android:screenOrientation="portrait"。 - 前置摄像头镜像失效:在
bindToLifecycle之后设置scaleX,避免被 CameraX 内部重置。
性能提示:CameraX 预览默认使用
SurfaceView 作为后端(API ≥ 24),具有更低的延迟。若需在预览上叠加自定义绘制(如滤镜),可考虑 TextureView 模式,但会增加 1-2 帧的延迟。