4. CameraX 预览实战:实现实时相机预览、配置分辨率与宽高比、处理屏幕旋转与镜像

CameraX 作为 Jetpack 相机库的核心组件,极大地简化了实时相机预览的实现。本章将深入剖析如何通过 PreviewView 与 Preview UseCase 构建稳定、高效的预览管道,并重点解决分辨率选择、宽高比适配、屏幕旋转与镜像翻转等实战中的关键问题。

4.1 预览管道架构与核心组件

CameraX 的预览流程围绕三个核心角色展开:Preview UseCase、PreviewView 与 SurfaceProvider。它们之间的协作关系如下图所示:

CameraX 预览管道架构 CameraX Lifecycle 绑定生命周期 Preview UseCase 配置 & 构建 SurfaceProvider 创建 Surface PreviewView 显示预览画面 关键流程: 1. Preview UseCase 通过 setSurfaceProvider() 注册 SurfaceProvider 2. CameraX 内部创建 Surface 并传递给相机 HAL → 3. 预览帧直接渲染到 PreviewView
核心原则: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)
    }
}
屏幕旋转处理流程 设备旋转 DisplayListener 更新 Rotation 重新绑定 旋转角度映射: Surface.ROTATION_0 → 0° (竖屏正立) Surface.ROTATION_90 → 90° (横屏左侧) Surface.ROTATION_180 → 180° (竖屏倒立) Surface.ROTATION_270 → 270° (横屏右侧)

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 帧的延迟。