第25章 多摄像头开发:双摄/三摄切换、逻辑摄像头与物理摄像头、超广角/长焦/微距切换
多摄像头,现在已经是手机的标配了。你想想看,从当年的单摄到双摄,再到现在的三摄、四摄甚至更多,摄像头数量越来越多。但作为开发者,我们面对的不只是「多了一个镜头」这么简单。
我个人习惯把多摄像头开发分成三个层次来理解:硬件层(物理摄像头)、系统层(逻辑摄像头)、应用层(切换策略)。今天我们就一层层把它说透。
物理摄像头 vs 逻辑摄像头
先搞清楚这两个概念,不然后面代码写起来会晕。
物理摄像头,就是手机背后那个实实在在的镜头模组。比如一颗IMX586主摄、一颗OV08A10超广角。每个物理摄像头都有自己的ID,在Android系统里用CameraId来标识。
逻辑摄像头,是Android 9之后引入的概念。说白了,就是把多个物理摄像头「打包」成一个虚拟摄像头,对外统一暴露。比如「后置主摄」这个逻辑摄像头,可能包含了广角主摄和长焦两颗物理摄像头。
为什么要这么设计?
嗯,这里要注意。逻辑摄像头的存在,是为了让系统能自动做平滑切换。比如你从1倍变焦拉到2倍,系统会悄悄从主摄切换到长焦,而你的应用代码几乎不用改。这就是逻辑摄像头的价值。
核心概念速查表
| 概念 | 说明 | 举例 |
|---|---|---|
| 物理摄像头 | 真实的硬件模组 | CameraId: 0 (主摄), 1 (广角), 2 (长焦) |
| 逻辑摄像头 | 多个物理摄像头的组合 | CameraId: 3 (后置逻辑摄像头) |
| 超广角 | 焦距更短,视野更宽 | 等效13mm-18mm |
| 长焦 | 焦距更长,能拍远 | 等效50mm-120mm |
| 微距 | 最近对焦距离极短 | 通常2cm-10cm |
如何获取摄像头列表
在代码里,我们通过CameraManager来获取所有摄像头信息。我建议你先把物理摄像头和逻辑摄像头都列出来,看看你的设备上到底有哪些。
// 获取CameraManager
CameraManager cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
// 获取所有摄像头ID
String[] cameraIdList = cameraManager.getCameraIdList();
for (String cameraId : cameraIdList) {
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
// 判断是逻辑摄像头还是物理摄像头
Integer logicalType = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
boolean isLogical = logicalType != null && Arrays.asList(logicalType).contains(
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA);
// 如果是逻辑摄像头,获取它包含的物理摄像头
if (isLogical) {
Set<String> physicalCameraIds = characteristics.get(
CameraCharacteristics.LOGICAL_MULTI_CAMERA_PHYSICAL_IDS);
Log.d("MultiCamera", "逻辑摄像头: " + cameraId + " 包含: " + physicalCameraIds);
} else {
Log.d("MultiCamera", "物理摄像头: " + cameraId);
}
}
这段代码跑一遍,你就能看到自己手机上的摄像头拓扑结构了。我在项目中遇到过一台设备,逻辑摄像头ID是3,里面包了ID 0和ID 1两个物理摄像头。当时排查了半天才发现,原来系统把广角和主摄打包了。
超广角/长焦/微距的切换策略
切换摄像头,说白了就是关闭当前CameraCaptureSession,打开另一个物理摄像头。但这里有个坑——你不能直接说「我要用长焦」,你得先知道长焦对应的物理摄像头ID是什么。
我个人习惯的做法是:先通过CameraCharacteristics获取每个摄像头的LENS_FACING和FOCAL_LENGTH,然后建立一张映射表。
小技巧:焦距(FOCAL_LENGTH)的单位是毫米,但不同摄像头的传感器尺寸不同,所以等效焦距才是我们关心的。不过对于切换来说,直接用物理焦距做比较也够用了——长焦的物理焦距一定比广角大。
// 获取摄像头的焦距信息
float[] focalLengths = characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS);
if (focalLengths != null && focalLengths.length > 0) {
float focalLength = focalLengths[0];
Log.d("CameraInfo", "摄像头 " + cameraId + " 焦距: " + focalLength + "mm");
// 根据焦距判断类型
if (focalLength < 3.0f) {
// 超广角
} else if (focalLength < 6.0f) {
// 主摄
} else {
// 长焦
}
}
当然,这只是个粗略判断。实际项目中,不同厂商的焦距值差异很大。我记得有一次,某款手机的超广角焦距是2.2mm,但另一款是2.8mm。所以阈值要根据具体设备微调。
微距摄像头的特殊处理
微距摄像头比较特殊。它通常是一个独立的物理摄像头,但有些厂商把它和超广角共用——通过切换对焦距离来实现微距模式。
怎么判断?看LENS_INFO_MINIMUM_FOCUS_DISTANCE。如果这个值非常小(比如小于2cm),那基本就是微距摄像头了。
避坑指南:我曾经在微距摄像头上栽过跟头。当时以为所有微距摄像头都支持自动对焦,结果某款低端机的微距是固定焦距的,只能拍特定距离。所以一定要检查LENS_INFO_FOCUS_DISTANCE_CALIBRATION,确认是否支持AF。
切换时的平滑过渡
切换摄像头最怕什么?画面闪一下、黑一下、或者卡顿。我建议你这样做:
- 预创建CaptureSession:在切换前,先为目标摄像头创建好CaptureSession,不要等用户点了才创建。
- 使用相同的Surface:尽量复用同一个SurfaceView或TextureView,避免重新创建。
- 关闭旧Session后立即打开新Session:中间不要留空档。
// 切换摄像头的核心逻辑
private void switchCamera(String newCameraId) {
// 1. 关闭当前会话
if (mCaptureSession != null) {
mCaptureSession.close();
mCaptureSession = null;
}
// 2. 打开新摄像头
CameraDevice.StateCallback callback = new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice camera) {
mCameraDevice = camera;
// 3. 创建新的CaptureSession
createCaptureSession();
}
@Override
public void onDisconnected(@NonNull CameraDevice camera) {
camera.close();
}
@Override
public void onError(@NonNull CameraDevice camera, int error) {
camera.close();
}
};
cameraManager.openCamera(newCameraId, callback, null);
}
嗯,这里要注意。上面的代码是简化版。实际项目中,你还需要处理onConfigured回调、设置预览请求、处理异常情况。但核心思路就是:先关后开,无缝衔接。
多摄像头同时工作
有些场景需要同时使用多个摄像头,比如双摄虚化、多摄融合。这时候就要用到CameraDevice.createCaptureSession的SessionConfiguration了。
说白了,就是在一个CaptureSession里绑定多个物理摄像头的Surface。系统会帮你做同步。
// 同时打开主摄和长焦
List<OutputConfiguration> outputConfigs = new ArrayList<>();
outputConfigs.add(new OutputConfiguration(physicalCameraId1, surface1));
outputConfigs.add(new OutputConfiguration(physicalCameraId2, surface2));
SessionConfiguration sessionConfig = new SessionConfiguration(
SessionConfiguration.SESSION_REGULAR,
outputConfigs,
executor,
callback);
cameraDevice.createCaptureSession(sessionConfig);
不过说实话,多摄同时工作的功耗和发热问题很严重。我建议只在必要场景下使用,比如拍照时,不要一直开着。
知识体系总览
下面这张图,是我自己总结的多摄像头开发知识体系。你可以把它当作一个检查清单,看看自己掌握了哪些。
避坑总结
最后,把我这些年踩过的坑列一下,你遇到了可以直接对照:
- 不要假设所有摄像头都支持相同分辨率。超广角可能只支持到1080p,而主摄支持4K。切换时记得重新配置。
- 逻辑摄像头不等于所有物理摄像头都能同时用。有些逻辑摄像头只是用来做平滑切换的,不支持多摄同时输出。
- 微距摄像头的最近对焦距离一定要实测。有些厂商标称2cm,实际只能到5cm。
- 切换摄像头时,预览画面可能会闪一下。这是正常的,但你可以通过动画或过渡效果来掩盖。
多摄像头开发,说白了就是跟硬件打交道。每个厂商、每款设备的实现都不一样。我的建议是:先摸清设备的能力,再写代码。不要一上来就想着做多摄融合,先把单摄切换做好,再逐步加功能。
个人经验:我一般会在应用的「开发者选项」里加一个摄像头信息页面,把所有物理摄像头和逻辑摄像头的参数都列出来。调试的时候特别方便,也能快速发现厂商的「特殊实现」。
好了,这一章的内容就到这里。多摄像头开发的核心就是理解物理和逻辑的关系,然后做好切换策略。代码量不大,但细节很多。多写、多测、多踩坑,慢慢就熟练了。