3. 相机预览链路搭建
相机预览链路是相机应用最基础也最容易出问题的一环。很多开发者第一次接入 CameraX 或 Camera2 时,代码能编译、能打开相机,但屏幕上要么黑屏,要么画面拉伸变形,要么旋转 90 度,要么首帧迟迟不出现。这些现象几乎都指向同一个核心问题:预览数据的承载视图选错了,或者视图与相机输出之间的尺寸、方向、生命周期没有对齐。本节先把三种主流预览视图的底层机制讲清楚,再给出一条可复现的首帧渲染实战路径。
三种预览视图的本质差异
SurfaceView、TextureView、PreviewView 并不是三个平级的“控件选项”,它们处在不同的抽象层级上。理解这一点,后面的选型就不会纠结。
SurfaceView 拥有一个独立于应用窗口的 Surface,由 SurfaceFlinger 单独合成。相机把预览帧直接写入这个 Surface,不经过应用主线程的 View 绘制流程。它的优点是功耗低、延迟小、对高分辨率和高帧率友好;缺点是它本质上是“挖洞”显示,视图层级、圆角、透明度、动画变换支持很差,早期版本在滚动列表中还会出现黑边或残影。
TextureView 则把相机输出当作一个 OpenGL 纹理,再通过硬件加速的 View 绘制流程合成到窗口里。它支持平移、缩放、旋转、透明度等常规 View 变换,能很好地嵌入复杂布局。代价是每一帧都要经过一次纹理合成,功耗和延迟高于 SurfaceView,在 4K 或高帧率场景下更容易掉帧。
PreviewView 是 CameraX 提供的封装视图。它内部会根据设备能力、API 级别和当前配置,自动在 SurfaceView 与 TextureView 之间选择实现,并内置了预览画面的缩放与旋转适配逻辑。开发者只需要设置 scaleType 和 implementationMode,就能获得基本正确的预览比例。
PreviewView 的两种实现模式
PreviewView 通过 implementationMode 暴露底层选择:PERFORMANCE 优先使用 SurfaceView,COMPATIBLE 优先使用 TextureView。默认是 PERFORMANCE。如果你的预览界面需要做圆角裁剪、叠加动画、或者放在 RecyclerView 里频繁复用,建议显式设为 COMPATIBLE,否则可能在部分机型上遇到黑边或裁剪异常。
另一个关键属性是 scaleType。FILL_CENTER 会保持宽高比并填满整个 PreviewView,超出部分被裁掉,适合全屏预览;FIT_CENTER 会完整显示画面,可能留下黑边,适合需要看到完整取景范围的场景。很多“画面被拉伸”的问题,其实是 PreviewView 的尺寸和相机输出宽高比不一致,而 scaleType 又没有正确设置导致的。
首帧渲染实战:从绑定到画面出现
下面以 CameraX + PreviewView 为例,走一遍首帧渲染的完整链路。假设我们要在 onCreate 中完成初始化,在 onResume 中启动相机。
// 1. 布局中声明 PreviewView
<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:scaleType="fillCenter"
app:implementationMode="performance" />
// 2. 在 onCreate 中构建 Preview 用例
Preview preview = new Preview.Builder()
.setTargetAspectRatio(AspectRatio.RATIO_16_9)
.build();
// 3. 绑定生命周期并连接 PreviewView 的 SurfaceProvider
preview.setSurfaceProvider(previewView.getSurfaceProvider());
// 4. 在 onResume 中启动相机
CameraProviderFuture future = ProcessCameraProvider.getInstance(this);
future.addListener(() -> {
CameraProvider provider = future.get();
provider.unbindAll();
provider.bindToLifecycle(this, CameraSelector.DEFAULT_BACK_CAMERA, preview);
}, ContextCompat.getMainExecutor(this));
这段代码看起来简单,但首帧能否顺利出现,取决于几个隐藏条件。第一,PreviewView 必须已经完成布局,getSurfaceProvider() 才能返回有效的 SurfaceProvider。如果在 onCreate 中视图还没测量就调用,可能拿到空 Surface。第二,bindToLifecycle 的 LifecycleOwner 必须是已经进入 STARTED 或 RESUMED 状态的对象,否则相机会等待生命周期推进后才真正打开。第三,setTargetAspectRatio 设置的宽高比要和 PreviewView 的实际显示比例匹配,否则会出现裁剪或黑边。
bindToLifecycle 返回成功。更可靠的方式是监听 PreviewView 的 previewStreamState,当状态变为 STREAMING 时,说明第一帧已经到达并开始显示。也可以在 Preview 用例上设置 OnPreviewOutputUpdateListener,在回调中记录时间戳,用于测量从启动相机到首帧出现的耗时。
常见首帧问题与排查方向
如果预览黑屏,先检查相机权限是否授予、bindToLifecycle 是否在 onResume 之后调用、以及是否在 onPause 中正确解绑。如果画面旋转错误,需要同时考虑 Sensor Orientation 和当前屏幕旋转角度:后置摄像头通常需要将预览旋转 90 度,前置摄像头还需要额外做镜像处理。PreviewView 内部已经处理了大部分旋转逻辑,但如果你直接用 SurfaceView 或 TextureView 接 Camera2,就必须自己计算 Matrix 变换。
如果画面拉伸,优先检查 PreviewView 的宽高比和相机输出宽高比是否一致。例如 PreviewView 是 1080×1920 的竖屏全屏,而相机输出是 1920×1080 的横屏 16:9,此时使用 FILL_CENTER 会裁掉左右两侧,使用 FIT_CENTER 则上下留黑边。正确做法是让 PreviewView 的布局比例也接近 16:9,或者接受裁剪并确认取景范围符合预期。
onCreate 中直接调用 bindToLifecycle 并期望立刻看到画面。相机启动是异步过程,涉及 HAL 打开、流配置、首帧出流等多个阶段。在低端设备上,从调用到首帧显示可能需要 300ms 到 1s 以上。建议在相机启动期间显示一个占位图或加载指示,避免用户看到长时间黑屏。
选型决策表
| 场景 | 推荐视图 | 理由 |
|---|---|---|
| 普通拍照/录像预览 | PreviewView | 自动适配,代码量最少,首帧逻辑最省心 |
| 高帧率/高分辨率预览 | SurfaceView | 独立合成,延迟和功耗更低 |
| 需要圆角、动画、叠加变换 | TextureView | 支持完整 View 变换,嵌入布局灵活 |
| 列表/复用容器中的预览 | PreviewView(COMPATIBLE) | 避免 SurfaceView 在复用时的黑边和残影 |
总结一下:PreviewView 是大多数场景的默认答案,SurfaceView 适合追求极致性能,TextureView 适合需要复杂视觉变换。首帧渲染的关键不在于写了多少代码,而在于视图 Surface 是否就绪、生命周期是否对齐、宽高比和旋转是否匹配。把这三点检查清楚,黑屏、拉伸、旋转问题基本都能定位到具体原因。