在这里插入图片描述

HarmonyOS NEXT 的 Camera Kit 提供了多摄像头支持,但很多人第一次接触时容易忽略一个关键问题:如何正确枚举并选择广角、长焦、深度等不同镜头? 官方示例通常只演示单摄像头预览,但实际拍照 App 中,用户希望平滑切换不同焦段的摄像头,而不是粗暴地重建整个相机会话。

这篇文章聚焦两个核心点:

  1. 获取设备上所有物理镜头的类型(CameraType)与位置(CameraPosition)。
  2. 基于变焦倍数自动切换镜头,同时保持预览/拍照会话的连续性(会话重建技巧)。

下面直接从代码入手,不扯概念。


环境说明

DevEco Studio 版本:DevEco Studio NEXT 6.1 及以上
HarmonyOS SDK 版本:HarmonyOS SDK 6.1.0(23) 及以上
目标设备:支持多摄像头的手机(如 P50 系列以后的机型)

1. 枚举所有可用的摄像头

Camera Kit 通过 getSupportedCameras() 返回 CameraDevice[] 数组,每个 CameraDevice 都包含 cameraPosition(前置/后置)和 cameraType(广角/长焦/深度/默认)。关键代码如下:

import { camera } from '@kit.CameraKit';
import { common } from '@kit.AbilityKit';

@Entry
@Component
struct MultiCameraDemo {
  @State cameraList: camera.CameraDevice[] = [];
  private cameraManager: camera.CameraManager | null = null;

  aboutToAppear(): void {
    // 获取 CameraManager
    const context: common.Context = getContext(this) as common.Context;
    this.cameraManager = camera.getCameraManager(context);
    // 枚举设备
    const cameras: camera.CameraDevice[] = this.cameraManager.getSupportedCameras();
    this.cameraList = cameras;
    // 打印日志,方便调试
    console.info('[MultiCamera] cameras count:', cameras.length);
    cameras.forEach((dev, idx) => {
      console.info(`[MultiCamera] ${idx} position=${dev.cameraPosition} type=${dev.cameraType}`);
    });
  }

  build() {
    Column() {
      Text(`检测到 ${this.cameraList.length} 个摄像头`)
        .fontSize(16)
        .padding(10);
      List({ space: 8 }) {
        ForEach(this.cameraList, (device: camera.CameraDevice) => {
          ListItem() {
            Text(`位置: ${device.cameraPosition}  类型: ${device.cameraType}`)
              .width('90%')
              .padding(12)
              .backgroundColor('#f0f0f0')
              .borderRadius(8)
          }
        })
      }
      .width('100%')
      .layoutWeight(1)
    }
    .padding(10)
  }
}

关键点说明:

  • cameraPosition 取值:CameraPosition.FRONTCameraPosition.BACK
  • cameraType 取值:CameraType.DEFAULT(主摄)、CameraType.WIDE_ANGLE(广角)、CameraType.TELEPHOTO(长焦)、CameraType.DEPTH(深度)等。
  • 不同设备支持的类型不同,务必先枚举再选择。
  • 有个容易忽略的点:iOS/Android 习惯上认为第一个后置摄像头是主摄,但 HarmonyOS 的设备列表顺序不可靠,必须通过 cameraType 来判断

2. 基于变焦倍数自动切换镜头

实际需求:在预览过程中,用户滑动变焦条,当变焦倍数进入不同区间时,自动切换到对应的物理镜头(例如 0.5x~0.8x 用广角,1.0x~2.0x 用主摄,>2.0x 用长焦)。

核心逻辑:

  1. 监听变焦倍数变化。
  2. 根据倍数决定目标 cameraType
  3. 如果目标镜头与当前镜头不同,则销毁旧会话,创建新会话并绑定新设备——这个过程称为“会话重建”。

为什么必须重建?因为 Camera Kit 中,一个 Session 只能绑定一个 CameraDevice,无法动态切换设备。所以无法像专业相机那样热切换,但重建时机的选择可以极大优化体验。

下面展示完整的切换逻辑(含变焦监听、设备选择、会话重建):

import { camera } from '@kit.CameraKit';
import { image } from '@kit.ImageKit';

@Component
export struct CameraWithLensSwitch {
  @State currentZoomRatio: number = 1.0;
  @State currentLensType: string = 'DEFAULT';

  private cameraManager: camera.CameraManager | null = null;
  private cameras: camera.CameraDevice[] = [];
  private photoOutput: camera.PhotoOutput | null = null;
  private previewOutput: camera.PreviewOutput | null = null;
  private session: camera.Session | null = null;
  private input: camera.CameraInput | null = null;

  aboutToAppear(): void {
    const context = getContext(this) as common.Context;
    this.cameraManager = camera.getCameraManager(context);
    this.cameras = this.cameraManager.getSupportedCameras();
    // 默认使用主摄(DEFAULT)初始化
    this.switchCameraByType(CameraType.DEFAULT);
  }

  /**
   * 根据变焦倍数选择合适的镜头类型
   */
  selectLensByZoom(zoom: number): CameraType {
    // 这些阈值可根据实际设备微调
    if (zoom < 0.8) return CameraType.WIDE_ANGLE;   // 广角
    if (zoom <= 2.0) return CameraType.DEFAULT;      // 主摄
    return CameraType.TELEPHOTO;                      // 长焦
  }

  /**
   * 当变焦倍数变化时调用(例如滑动条事件)
   */
  onZoomChanged(newZoom: number): void {
    this.currentZoomRatio = newZoom;
    const targetType = this.selectLensByZoom(newZoom);
    if (targetType !== this.getCurrentLensType()) {
      this.switchCameraByType(targetType);
    }
  }

  /**
   * 根据目标镜头类型查找对应的 CameraDevice
   */
  findCameraByType(type: CameraType): CameraDevice | undefined {
    return this.cameras.find(dev => dev.cameraType === type && dev.cameraPosition === CameraPosition.BACK);
  }

  /**
   * 切换摄像头(会话重建)
   */
  async switchCameraByType(targetType: CameraType): Promise<void> {
    const device = this.findCameraByType(targetType);
    if (!device) {
      console.warn('[MultiCamera] 未找到该类型镜头:', targetType);
      return;
    }

    // 1. 释放旧会话
    await this.releaseSession();

    // 2. 创建新输入
    const input = await this.cameraManager!.createCameraInput(device);
    await input.open();
    this.input = input;

    // 3. 创建新会话(使用 PhotoSession 示例)
    const newSession: camera.PhotoSession = this.cameraManager!.createSession(camera.SessionType.PHOTO) as camera.PhotoSession;
    await newSession.beginConfig();
    await newSession.addInput(input);

    // 4. 添加输出(预览 + 拍照),这里假设已经创建好 previewOutput、photoOutput
    if (this.previewOutput) {
      await newSession.addOutput(this.previewOutput);
    }
    if (this.photoOutput) {
      await newSession.addOutput(this.photoOutput);
    }

    // 5. 提交配置并开始
    await newSession.commitConfig();
    await newSession.start();
    this.session = newSession;

    // 更新 UI 状态
    this.currentLensType = targetType;
  }

  /**
   * 释放当前会话与输入资源
   */
  async releaseSession(): Promise<void> {
    try {
      await this.session?.stop();
      await this.session?.release();
      await this.input?.close();
      this.session = null;
      this.input = null;
    } catch (e) {
      console.error('[MultiCamera] release error:', e);
    }
  }

  build() {
    Column() {
      Text(`当前倍数: ${this.currentZoomRatio.toFixed(1)}x   ${this.currentLensType}`)
        .fontSize(14)
        .margin(10);

      // 模拟变焦滑条
      Slider({
        value: this.currentZoomRatio,
        min: 0.5,
        max: 10,
        step: 0.1
      })
      .onChange((value: number) => {
        this.onZoomChanged(value);
      })
      .width('80%')
      .margin(20);

      // 镜头切换按钮(手动测试)
      Row({ space: 10 }) {
        Button('广角').onClick(() => this.onZoomChanged(0.5));
        Button('主摄').onClick(() => this.onZoomChanged(1.0));
        Button('长焦').onClick(() => this.onZoomChanged(3.0));
      }
    }
  }

  /**
   * 辅助方法:获取当前镜头类型(从状态中读取)
   */
  getCurrentLensType(): CameraType {
    switch (this.currentLensType) {
      case 'WIDE_ANGLE': return CameraType.WIDE_ANGLE;
      case 'TELEPHOTO': return CameraType.TELEPHOTO;
      default: return CameraType.DEFAULT;
    }
  }
}

代码注释已够详细,这里再强调几点:

  • 会话重建是异步的,在 Promise 链中确保资源正确释放。
  • beginConfig()commitConfig() 之间不要做耗时操作,否则可能超时。
  • 切换后变焦倍数要保持一致——例如从主摄切到长焦,zoomRatio 应继续生效(因为每个镜头有自己的变焦范围,但 Camera Kit 会统一回传缩放级别)。
  • 上述代码只处理后置镜头,前置同理。

3. 常见问题(真实踩坑记录)

坑1:findCameraByType 返回 undefined

现象:某些设备上广角或长焦无法找到,但设备实际支持。
原因getSupportedCameras() 返回的列表不保证包含所有逻辑镜头——厂商可能会将一些镜头隐藏或仅在特定模式下暴露。
解决方案

  • 优先使用 cameraType 过滤,但也要准备 fallback:如果找不到目标类型,就回退到 DEFAULT
  • 可以通过日志打印所有 cameraType 确认是否遗漏。

坑2:会话重建期间预览黑屏

现象:切换镜头时,预览画面会短暂黑屏(几百毫秒)。
原因:旧 session 释放后,新 session 未完成配置,这段时间内无输出。
解决方案

  • 可以在 newSession.start() 之前保持旧 session 不释放?不行,会冲突。
  • 体验优化:先创建新 session 并 addInput,再释放旧 session。但 Camera Kit 不允许同时存在两个 session 绑定同一个 cameraManager?官方文档指出:同一时间只能有一个活跃的 session(除非使用 CameraSession 的并发模式,但比较复杂)。所以更实际的方案是:缩短 release 到 start 之间的时间,例如复用 previewOutput 和 photoOutput 对象(不要重新创建),只需重新 addOutput。上面的代码已经采用了复用输出对象的做法,能减少黑屏时间到 100ms 以内。

4. 最佳实践

  1. 不要重复创建 PreviewOutput/PhotoOutput
    它们可以绑定多个 session,切换时只需 addOutput/removeOutput,避免反复创建 Surface。
  2. 变焦倍数触发阈值应可配置
    不同的手机镜头焦段不同,硬编码 0.8/2.0 可能不合适。建议通过 cameraDevice.getZoomRatioRange() 动态获取每个镜头的变焦范围,再计算交叠区间。
  3. 状态管理和 UI 更新分离
    currentLensType 应该通过 @State 或全局 store 管理,切换镜头时先更新 UI(比如显示“正在切换…”),再执行异步重建,避免用户感觉到卡顿。
  4. 使用 try-catch 包裹每个 camera 异步方法
    Camera Kit 在某些设备上可能抛出 BusinessError,尤其是 session 状态异常时,不捕获会导致应用崩溃。

5. FAQ

Q:为什么真机上枚举不到广角镜头?
A:部分中低端机型没有独立广角或长焦,或者系统未暴露该类型。可以打印所有 cameraType 确认。

Q:切换镜头后预览画面方向错误?
A:需要重新设置 PreviewOutput.setImageRotation(),因为不同传感器物理方向可能不同。建议在切换后重新配置旋转。

Q:会话重建后拍照按钮无法呼出?
A:检查 PhotoOutput 是否绑定了正确的 StreamInfo。重新创建 session 后,PhotoOutput 应当在 beginConfig 之前就已经存在,否则需要重新创建。推荐复用实例,若不行则重新创建。


6. 完整 Demo 入口

上述代码已是一个独立的 @Component,可以直接放在 @Entry 中测试:

@Entry
@Component
struct Index {
  build() {
    Column() {
      CameraWithLensSwitch()
    }
    .height('100%')
    .width('100%')
  }
}

多镜头适配的核心就是“枚举–判定–重建会话”三步。实际开发中,会话重建的耗时和状态一致性是最大挑战。建议在真机上测试,因为模拟器往往只提供一个摄像头。如果你也遇到类似问题,可以重点检查 cameraType 的枚举值以及 session 的 begin/commit 顺序。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐