第3章:PreviewView在Compose中的封装

PreviewView 是 CameraX 在传统 View 体系中提供的核心预览组件。在 Jetpack Compose 环境下,需要通过 AndroidView 将其嵌入到可组合函数中,并完成生命周期绑定与尺寸适配。本章将深入讲解这三步的核心原理与标准实现。

3.1 封装原理

  • AndroidView 桥梁:AndroidView 是 Compose 与 View 系统的互操作节点,允许在组合函数中创建并展示 View 实例。
  • 生命周期绑定:PreviewView 需要感知 Activity/Fragment 的生命周期以正确启动/停止相机预览,通过 ProcessCameraProvider.getInstance(context) 获取 provider 后,调用 cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview) 完成绑定。
  • 尺寸适配:PreviewView 的 scaleType 决定了预览画面在控件内的拉伸、裁剪或填充方式;在 Compose 中需通过 Modifier.fillMaxSize() 或自定义尺寸让 PreviewView 与容器对齐。

核心契约:PreviewView 的尺寸、生命周期所有者、CameraX 实例三者必须同时就绪,预览才能稳定输出。任何一环缺失都会导致黑屏、崩溃或预览错位。

3.2 实现流程

  1. 依赖注入:在 build.gradle.kts 中添加 CameraX 库依赖。
  2. 组合函数中创建 AndroidView,工厂函数内实例化 PreviewView。
  3. 在 update 回调(或 LaunchedEffect)中获取 ProcessCameraProvider,配置 Preview 用例并绑定到当前 LocalLifecycleOwner。
  4. 设置 PreviewView.scaleType 为 ScaleType.FILL_CENTER 或 FIT_CENTER 等,控制画面适配方式。
  5. 调用 cameraProvider.unbindAll() 做清理(可选)。

3.3 关键代码实现

以下是一个完整的 Compose 函数,演示了 PreviewView 的封装、生命周期绑定与尺寸适配:

@Composable
fun CameraPreviewView(
    context: Context = LocalContext.current,
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current
) {
    AndroidView(
        factory = { ctx ->
            PreviewView(ctx).apply {
                scaleType = PreviewView.ScaleType.FILL_CENTER
            }
        },
        modifier = Modifier.fillMaxSize(),
        update = { previewView ->
            val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
            cameraProviderFuture.addListener({
                val cameraProvider = cameraProviderFuture.get()
                val preview = Preview.Builder().build().apply {
                    setSurfaceProvider(previewView.surfaceProvider)
                }
                val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
                try {
                    cameraProvider.unbindAll()
                    cameraProvider.bindToLifecycle(
                        lifecycleOwner,
                        cameraSelector,
                        preview
                    )
                } catch (e: Exception) {
                    Log.e("CameraX", "绑定失败", e)
                }
            }, ContextCompat.getMainExecutor(context))
        }
    )
}

注意:update 回调会在重组时多次触发。建议将 cameraProvider 的获取和绑定操作放入 LaunchedEffect(Unit) 中执行,以避免重复绑定。上述代码仅为演示最简单路径,生产环境应加入 remember 或 DisposableEffect 进行生命周期管理。

3.4 生命周期绑定细节

绑定元素 作用 常见错误
LocalLifecycleOwner 提供当前界面的生命周期状态 使用 context as LifecycleOwner 强转导致 Activity 泄漏
ProcessCameraProvider 管理 CameraX 生命周期与用例绑定 未调用 unbindAll() 重复绑定导致异常
Preview.SurfaceProvider 将预览数据流输出到 PreviewView 的 Surface 在 onDestroy 后仍然持有 provider 引用

3.5 尺寸适配方案对比

ScaleType 效果 适用场景
FILL_CENTER 填充整个控件,裁切超出部分 全屏预览,需画面充满屏幕
FIT_CENTER 保持比例缩放到控件内,可能出现黑边 固定比例预览,避免裁剪
FILL_START 填充并从左上角对齐 异形屏或自定义布局
FILL_END 填充并从右下角对齐 底部对齐的悬浮预览

3.6 调试与常见错误

  • 黑屏:未获取到相机权限或 SurfaceProvider 未设置。检查 CameraProvider.bindToLifecycle 是否成功。
  • 预览画面拉伸:scaleType 与控件尺寸比例不匹配。推荐使用 FILL_CENTER 并配合 Modifier.aspectRatio(ratio) 固定比例。
  • 崩溃:”Camera is in use”:重复调用了 bindToLifecycle 而未先 unbindAll()。
  • 生命周期泄漏:ProcessCameraProvider 的监听器中直接持有 Activity 引用。确保使用 WeakReference 或 Lifecycle.Event 做清理。

性能警告:避免在 AndroidView 的 factory 中执行耗时操作(如 CAMERA 权限检查),应提前在 LaunchedEffect 中异步请求。PreviewView 实例复用可减少 Surface 重建开销,建议使用 remember 保持单个实例。

3.7 架构流程示意

Compose 中 PreviewView 封装架构 Compose UI AndroidView PreviewView LifecycleOwner CameraX Provider 说明:AndroidView 作为桥梁接收 Compose 的 modifier(尺寸约束)和生命周期(LifecycleOwner), 将 PreviewView 的 surfaceProvider 传给 CameraX Provider 绑定的 Preview 用例,形成完整预览链路。

3.8 稳定性与优化建议

  • 使用 DisposableEffect 管理资源:在 DisposableEffect 的 onDispose 中调用 cameraProvider.unbindAll(),确保离开界面时释放相机。
  • 异步初始化:ProcessCameraProvider.getInstance() 是异步操作,不要在 factory 中阻塞等待。
  • 预览帧率控制:如果预览卡顿,考虑设置 Preview.Builder().setTargetFrameRate(30) 限制帧率。
  • 避免重复创建 PreviewView:将 PreviewView 实例写在 remember 中,防止重组时重新构造 Surface。