第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_FACINGFOCAL_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。

切换时的平滑过渡

切换摄像头最怕什么?画面闪一下、黑一下、或者卡顿。我建议你这样做:

  1. 预创建CaptureSession:在切换前,先为目标摄像头创建好CaptureSession,不要等用户点了才创建。
  2. 使用相同的Surface:尽量复用同一个SurfaceView或TextureView,避免重新创建。
  3. 关闭旧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.createCaptureSessionSessionConfiguration了。

说白了,就是在一个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);

不过说实话,多摄同时工作的功耗和发热问题很严重。我建议只在必要场景下使用,比如拍照时,不要一直开着。

知识体系总览

下面这张图,是我自己总结的多摄像头开发知识体系。你可以把它当作一个检查清单,看看自己掌握了哪些。

多摄像头开发 硬件层 物理摄像头ID 焦距/传感器尺寸 对焦距离/光圈 系统层 逻辑摄像头封装 CameraManager API CaptureSession管理 应用层 超广角/长焦/微距切换 平滑过渡策略 多摄同时工作 核心:物理摄像头是基础,逻辑摄像头是封装 切换时注意预创建Session + 复用Surface

避坑总结

最后,把我这些年踩过的坑列一下,你遇到了可以直接对照:

  • 不要假设所有摄像头都支持相同分辨率。超广角可能只支持到1080p,而主摄支持4K。切换时记得重新配置。
  • 逻辑摄像头不等于所有物理摄像头都能同时用。有些逻辑摄像头只是用来做平滑切换的,不支持多摄同时输出。
  • 微距摄像头的最近对焦距离一定要实测。有些厂商标称2cm,实际只能到5cm。
  • 切换摄像头时,预览画面可能会闪一下。这是正常的,但你可以通过动画或过渡效果来掩盖。

多摄像头开发,说白了就是跟硬件打交道。每个厂商、每款设备的实现都不一样。我的建议是:先摸清设备的能力,再写代码。不要一上来就想着做多摄融合,先把单摄切换做好,再逐步加功能。

个人经验:我一般会在应用的「开发者选项」里加一个摄像头信息页面,把所有物理摄像头和逻辑摄像头的参数都列出来。调试的时候特别方便,也能快速发现厂商的「特殊实现」。

好了,这一章的内容就到这里。多摄像头开发的核心就是理解物理和逻辑的关系,然后做好切换策略。代码量不大,但细节很多。多写、多测、多踩坑,慢慢就熟练了。


公众号:蓝海资料掘金营,微信deep3321