13. 前后摄像头切换:检测摄像头数量、切换CameraDevice、保持预览连续性
摄像头切换,这个功能听起来简单吧?不就是关掉一个,再开另一个嘛。但真做起来,坑还挺多的。我早期做的一个项目,切换摄像头时画面会黑一下,用户反馈说「这体验也太糙了」。嗯,后来我花了不少精力才把这个问题彻底解决。
今天我们就来聊聊,怎么优雅地实现前后摄像头切换。核心就三件事:检测摄像头数量、切换CameraDevice、保持预览连续性。
13.1 检测摄像头数量
你想想看,如果设备只有一个摄像头,你硬要切到后置,那不就崩了吗?所以第一步,先搞清楚设备上有几个摄像头,分别是什么朝向。
Android 提供了 CameraManager.getCameraIdList() 来获取所有摄像头 ID。然后通过 CameraCharacteristics 查询朝向信息。
// 获取摄像头管理器
CameraManager cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
// 获取所有摄像头 ID
String[] cameraIdList = cameraManager.getCameraIdList();
// 遍历,找出前置和后置
String frontCameraId = null;
String backCameraId = null;
for (String cameraId : cameraIdList) {
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
if (facing != null) {
if (facing == CameraCharacteristics.LENS_FACING_FRONT) {
frontCameraId = cameraId;
} else if (facing == CameraCharacteristics.LENS_FACING_BACK) {
backCameraId = cameraId;
}
}
}
13.2 切换 CameraDevice 的核心流程
切换摄像头,说白了就是:关闭当前 CameraDevice → 打开目标 CameraDevice → 重新创建 CaptureSession。但这里有个关键点——预览的 Surface 要不要重建?
我个人习惯是:复用 TextureView 的 Surface。只要 Surface 不销毁,预览就能无缝衔接。但要注意,前后摄像头的分辨率可能不同,所以你需要重新配置一下。
13.2.1 关闭旧摄像头
private void closeCamera() {
if (mCaptureSession != null) {
mCaptureSession.close();
mCaptureSession = null;
}
if (mCameraDevice != null) {
mCameraDevice.close();
mCameraDevice = null;
}
}
这里有个细节:close() 是异步的,但 Android 保证关闭完成后,你再打开新的摄像头是安全的。我一般会在 onClosed 回调里做下一步操作,但实际项目中,直接顺序调用也没问题。
13.2.2 打开新摄像头
private void openCamera(String cameraId) {
CameraManager cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
try {
cameraManager.openCamera(cameraId, new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice camera) {
mCameraDevice = camera;
// 创建 CaptureSession
createCaptureSession();
}
@Override
public void onDisconnected(@NonNull CameraDevice camera) {
camera.close();
}
@Override
public void onError(@NonNull CameraDevice camera, int error) {
camera.close();
}
}, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
13.3 保持预览连续性
这是最头疼的部分。切换摄像头时,画面会短暂中断。怎么让这个中断「无感」?
我的做法是:在切换期间,保持 TextureView 的 Surface 不变,并且用最后一帧画面「冻结」住。等新摄像头准备好后,再恢复预览。
具体来说:
- 切换前,调用
TextureView.getBitmap()保存当前帧(可选)。 - 关闭旧摄像头,但不销毁 TextureView。
- 打开新摄像头,使用同一个 Surface 创建 CaptureSession。
- 新 Session 创建成功后,开始预览。
这样用户看到的只是「画面停顿了一下」,而不是黑屏或闪白。
private void switchCamera() {
// 1. 保存当前帧(可选)
// Bitmap lastFrame = mTextureView.getBitmap();
// 2. 关闭旧摄像头
closeCamera();
// 3. 切换摄像头 ID
String newCameraId = isFrontCamera ? backCameraId : frontCameraId;
isFrontCamera = !isFrontCamera;
// 4. 打开新摄像头
openCamera(newCameraId);
}
onSurfaceTextureDestroyed 返回 true。否则 Surface 会被销毁,你就得重建了。
13.4 处理预览方向
前后摄像头的传感器方向可能不同。前置摄像头通常是镜像的,后置是正常的。如果你不做处理,画面可能会「倒立」或「左右翻转」。
我一般用 CameraCharacteristics.SENSOR_ORIENTATION 来获取旋转角度,然后配合 TextureView.setTransform() 做校正。
private void configureTransform(int viewWidth, int viewHeight) {
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(mCameraId);
Integer sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
// 计算旋转矩阵
Matrix matrix = new Matrix();
// ... 根据 sensorOrientation 和 view 尺寸计算
mTextureView.setTransform(matrix);
}
sensorOrientation 是 270°,而我只考虑了 90° 的情况。所以一定要根据实际值做适配。
13.5 整体流程图
下面这张图总结了前后摄像头切换的完整流程。你可以看到,核心就是「关闭旧 → 打开新 → 复用 Surface → 恢复预览」这个闭环。
13.6 常见问题与避坑
- 切换时黑屏:最常见的原因是 Surface 被销毁了。检查
onSurfaceTextureDestroyed是否返回了 true。 - 画面闪烁:可能是新旧 Session 切换时,预览请求没有及时停止。建议在关闭旧 Session 前,先停止重复请求。
- 分辨率不匹配:前后摄像头的支持分辨率可能不同。切换后要重新选择最优尺寸,否则画面可能变形。
SurfaceView 才稳定。如果你遇到类似问题,可以试试换 SurfaceView。
13.7 小结
前后摄像头切换,说白了就是「关旧开新」四个字。但要做到无缝体验,你得注意三点:
- 先检测摄像头数量,别硬切。
- 复用 Surface,避免黑屏。
- 处理好方向校正,画面别歪。
嗯,掌握了这些,你的摄像头切换功能就能做到「润物细无声」了。