HarmonyOS技术精讲-Camera Kit(相机服务)第9篇:自动对焦与手动调焦

《HarmonyOS技术精讲-Camera Kit(相机服务)第9篇:自动对焦与手动调焦》

为啥对焦处理这么重要

开发相机应用时,很多人会遇到一个问题:官方示例拍照是清晰的,但放到实际项目里,画面总是模糊或者对焦不准。特别是当用户需要拍近距离物体(比如二维码)或者想手动控制焦点时,默认的自动对焦行为往往达不到预期。

HarmonyOS NEXT 的 Camera Kit 提供了对焦能力,但接口设计上有一些微妙之处。如果直接使用默认实现,容易出现对焦不稳定、回调不触发、或者切换模式后状态不同步的问题。这篇文章主要讲清楚三种对焦方式:自动对焦、触控对焦、手动调焦,以及对应的状态监听处理。

它解决什么问题

对焦本质上是在控制镜头组移动,让目标物体在传感器上形成清晰图像。不同的场景需要不同的对焦策略:

场景 推荐对焦模式 说明
日常拍照 FOCUS_MODE_AUTO 相机自动寻找对比度最高区域
拍二维码/文档 触控对焦 + FOCUS_MODE_AUTO 用户指定焦点区域
视频录制切换主体 FOCUS_MODE_CONTINUOUS_AUTO 持续追踪画面中的主体
显微镜/微距 手动调焦 自动对焦可能找不到清晰点

触控对焦和手动调焦是互补关系。触控对焦改变的是对焦"位置",手动调焦改变的是对焦"距离"。很多开发者会把这两者混为一谈,实际它们的 API 是完全独立的。

环境说明

text 复制代码
DevEco Studio 版本:DevEco Studio 5.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS API 12 及以上
目标设备:手机(P60/Mate60 系列等支持 Camera Kit 的机型)

核心实现

1. 基础对焦模式设置

先获取相机输入流,然后设置对焦模式。注意,这一步必须在 CameraSession 配置完成后、拍照流程开始前执行。

typescript 复制代码
import { camera } from '@kit.CameraKit';
import { common } from '@kit.AbilityKit';

// 假设已经获取到 cameraManager 和 sceneMode
async function setFocusMode(
  cameraManager: camera.CameraManager,
  surfaceId: string,
  mode: camera.FocusMode
): Promise<void> {
  // 获取支持的设备列表
  const cameraDevices: camera.CameraDevice[] = cameraManager.getSupportedCameras();
  if (cameraDevices.length === 0) {
    console.error('没有可用的相机设备');
    return;
  }

  // 创建相机输入
  const cameraInput: camera.CameraInput = cameraManager.createCameraInput(cameraDevices[0]);
  await cameraInput.open();

  // 创建会话(这里使用普通拍照会话)
  const session: camera.PhotoSession = cameraManager.createSession(
    camera.SceneMode.NORMAL_PHOTO
  ) as camera.PhotoSession;

  session.beginConfig();

  // 添加输入
  session.addInput(cameraInput);

  // 添加输出
  const photoOutput: camera.PhotoOutput = session.addPhotoOutput(surfaceId);

  // 提交配置
  await session.commitConfig();

  // 启动会话
  await session.start();

  // 设置对焦模式(关键是必须在 session 启动后设置)
  const cameraDeviceObj = cameraDevices[0];
  const isSupported = session.isFocusModeSupported(
    cameraDeviceObj,
    mode
  );

  if (isSupported) {
    session.setFocusMode(cameraDeviceObj, mode);
    console.info(`对焦模式设置为: ${mode}`);
  } else {
    console.warn(`设备不支持对焦模式: ${mode}`);
  }
  
  return;
}

这里有一个容易忽略的细节:setFocusMode 的调用时间必须在 session.start() 之后。有人会在 beginConfig/commitConfig 之间去设置,结果发现不生效。这是因为相机的对焦参数需要在流启动后才能动态调整。

2. 触控对焦(指定焦点区域)

触控对焦的本质是调用 setFocusPoint 接口,传入一个归一化的坐标点(范围 0-1)。应用层需要把屏幕触摸坐标映射成这个标准坐标系。

typescript 复制代码
@Entry
@Component
struct TouchFocusSample {
  @State focusPointX: number = 0.5;  // 默认中心
  @State focusPointY: number = 0.5;
  @State session: camera.PhotoSession | null = null;
  @State cameraDevice: camera.CameraDevice | null = null;

  build() {
    Column() {
      // 相机预览区域
      XComponent({
        id: 'xcomponentId',
        type: XComponentType.SURFACE,
        controller: new XComponentController()
      })
        .width('100%')
        .height(400)
        .onTouch((event: TouchEvent) => {
          if (event.type === TouchType.Down) {
            // 将触摸坐标转换为归一化坐标
            const rect = event.target.area;
            if (rect.width > 0 && rect.height > 0) {
              // 触摸点坐标在组件内的相对位置
              const relX = event.touches[0].x / rect.width;
              const relY = event.touches[0].y / rect.height;
              
              // 限制在 0-1 范围
              const normalizedX = Math.min(1, Math.max(0, relX));
              const normalizedY = Math.min(1, Math.max(0, relY));
              
              this.focusPointX = normalizedX;
              this.focusPointY = normalizedY;
              
              this.setFocusPoint(normalizedX, normalizedY);
            }
          }
        });
        
      // 显示当前焦点位置
      Text(`焦点位置: (${this.focusPointX.toFixed(2)}, ${this.focusPointY.toFixed(2)})`)
        .fontSize(16)
        .margin({ top: 20 });
    }
    .width('100%')
    .height('100%')
  }

  async setFocusPoint(x: number, y: number): Promise<void> {
    if (this.session === null || this.cameraDevice === null) {
      console.warn('会话或设备未初始化');
      return;
    }

    try {
      // 检查是否支持设置对焦点
      const supported = this.session.isFocusModeSupported(
        this.cameraDevice,
        camera.FocusMode.FOCUS_MODE_AUTO
      );

      if (!supported) {
        console.warn('不支持设置对焦点');
        return;
      }

      // 先把对焦模式设置为自动(触控对焦通常需要配合自动模式)
      this.session.setFocusMode(
        this.cameraDevice,
        camera.FocusMode.FOCUS_MODE_AUTO
      );

      // 设置对焦点
      const point: camera.Point = { x: x, y: y };
      this.session.setFocusPoint(this.cameraDevice, point);
      
      console.info(`对焦点设置为: (${x}, ${y})`);
    } catch (err) {
      console.error(`设置对焦点失败: ${JSON.stringify(err)}`);
    }
  }
}

代码逻辑不复杂,但有个细节需要注意:setFocusPoint 必须在自动对焦模式下才有效 。如果当前模式是手动对焦,调用 setFocusPoint 是无效的。这一点官方文档没有特别强调,实际开发中容易踩坑。

3. 手动调焦(滑动条控制)

手动调焦对应的是 focusDistance 参数,范围 0.0(最近)到 1.0(最远)。这个参数不和自动对焦冲突,但往往需要配合 FOCUS_MODE_MANUAL 模式使用。

typescript 复制代码
@Entry
@Component
struct ManualFocusSample {
  @State focusSliderValue: number = 0.5; // 初始在中间
  @State currentFocusDistance: number = 0.5;
  @State session: camera.PhotoSession | null = null;
  @State cameraDevice: camera.CameraDevice | null = null;

  build() {
    Column({ space: 20 }) {
      // 相机预览
      XComponent({
        id: 'previewXComponent',
        type: XComponentType.SURFACE,
        controller: new XComponentController()
      })
        .width('100%')
        .height(400);

      // 焦距滑动条
      Row() {
        Text('近')
          .fontSize(14);
        Slider({
          value: this.focusSliderValue,
          min: 0.0,
          max: 1.0,
          step: 0.01,
          style: SliderStyle.OutSet
        })
          .width('70%')
          .onChange((value: number) => {
            this.focusSliderValue = value;
            this.currentFocusDistance = value;
            this.setManualFocus(value);
          });
        Text('远')
          .fontSize(14);
      }
      .width('90%')
      .justifyContent(FlexAlign.Center);

      // 显示当前焦距值
      Text(`焦距值: ${this.currentFocusDistance.toFixed(2)}`)
        .fontSize(16);
    }
    .width('100%')
    .height('100%')
  }

  async setManualFocus(distance: number): Promise<void> {
    if (this.session === null || this.cameraDevice === null) {
      console.warn('会话或设备未初始化');
      return;
    }

    try {
      // 检查是否支持手动对焦模式
      const supported = this.session.isFocusModeSupported(
        this.cameraDevice,
        camera.FocusMode.FOCUS_MODE_MANUAL
      );

      if (!supported) {
        console.warn('设备不支持手动对焦');
        return;
      }

      // 切换到手动对焦模式
      this.session.setFocusMode(
        this.cameraDevice,
        camera.FocusMode.FOCUS_MODE_MANUAL
      );

      // 设置对焦距离(0.0最近,1.0最远)
      this.session.setFocusDistance(
        this.cameraDevice,
        distance
      );

      console.info(`手动对焦距离设置为: ${distance}`);
    } catch (err) {
      console.error(`设置手动对焦失败: ${JSON.stringify(err)}`);
    }
  }
}

这里有个实际开发中的问题:setFocusDistance 不是所有设备都支持 。华为的高端机型(如 P60 系列)支持,但有些中端机型或旧设备可能返回 false。所以一定要先做 isFocusModeSupported 检查。

4. 对焦状态监听

监听对焦状态是为了给用户反馈,比如屏幕上显示一个对焦框动画,或者拍照时等待对焦完成。

typescript 复制代码
// 在 session 启动后注册监听
function registerFocusCallback(session: camera.PhotoSession): void {
  // 对焦状态回调
  session.on('focusStateChange', (err: BusinessError, focusState: camera.FocusState) => {
    if (err) {
      console.error(`对焦状态回调错误: ${JSON.stringify(err)}`);
      return;
    }

    switch (focusState) {
      case camera.FocusState.FOCUS_STATE_SCAN:
        console.info('正在对焦中...');
        // 这里可以更新 UI,显示对焦框动画
        break;
      case camera.FocusState.FOCUS_STATE_FOCUSED:
        console.info('对焦成功');
        // 对焦完成,可以通知 UI 更新
        break;
      case camera.FocusState.FOCUS_STATE_UNFOCUSED:
        console.warn('对焦失败(未找到清晰点)');
        // 对焦失败时的处理
        break;
      default:
        console.info(`未知对焦状态: ${focusState}`);
    }
  });

  // 对焦点变化回调(触控对焦后触发)
  session.on('focusPointChange', (err: BusinessError, focusPoint: camera.Point) => {
    if (err) {
      console.error(`对焦点变化回调错误: ${JSON.stringify(err)}`);
      return;
    }
    console.info(`焦点位置变更为: (${focusPoint.x}, ${focusPoint.y})`);
    // 可以更新 UI 上的焦点指示器位置
  });
}

常见问题

问题 1:自动对焦模式不支持持续锁定

现象 :设置 FOCUS_MODE_AUTO 后,拍完一张照片焦点就乱了,画面变模糊。

原因FOCUS_MODE_AUTO 是单次自动对焦,每次拍照完成后会重新开始对焦锁定过程。如果需要连续对焦(比如视频场景),应该使用 FOCUS_MODE_CONTINUOUS_AUTO

解决方案 :区分使用场景。拍照用 FOCUS_MODE_AUTO,视频用 FOCUS_MODE_CONTINUOUS_AUTO。如果你发现在拍照后画面依然模糊,需要检查你是否在拍照后重新调用了 setFocusMode 或者 start()

问题 2:对焦回调频繁触发导致性能问题

现象 :注册 focusStateChange 回调后,日志疯狂输出,卡顿明显。

原因:在光线变化或画面移动时,相机会不断重新对焦,导致回调非常频繁。特别是连续自动对焦模式下,几乎每一帧都在检测和触发。

解决方案:增加防抖逻辑,不要每收到一次回调就更新 UI。

typescript 复制代码
// 使用节流控制
let lastFocusUpdateTime: number = 0;
const THROTTLE_INTERVAL = 200; // 200ms 内只更新一次

session.on('focusStateChange', (err, focusState) => {
  const now = Date.now();
  if (now - lastFocusUpdateTime < THROTTLE_INTERVAL) {
    return;  // 跳过这次回调
  }
  lastFocusUpdateTime = now;
  
  // 实际的更新逻辑
  updateFocusUI(focusState);
});

最佳实践

  1. 先检查能力,再调用 API 。每次设置对焦模式或对焦点之前,一定先调用对应的 is*Supported 方法。不同设备的对焦能力差异很大,直接调用可能导致 crash 或者静默失败。

  2. 对焦回调不要直接同步修改 UI 状态 。回调是在相机框架的线程中处理的,直接更新 @State 变量可能会触发跨线程的 UI 更新问题。建议使用 globalThis.postMessage 或者通过事件总线通知主线程。

  3. 触控对焦后自动恢复自动模式 。用户点击屏幕指定焦点后,通常期望相机继续保持对焦能力。建议在 setFocusPoint 之后继续保持在 FOCUS_MODE_AUTO 模式,而不是切换到 FOCUS_MODE_MANUAL

  4. 手动调焦的步长不宜过大Sliderstep 设置为 0.01 足够细粒度,过大的步长会让用户无法精确调节。

完整 Demo 入口

typescript 复制代码
@Entry
@Component
struct FocusDemo {
  @State session: camera.PhotoSession | null = null;
  @State cameraDevice: camera.CameraDevice | null = null;
  @State touchFocusX: number = 0.5;
  @State touchFocusY: number = 0.5;
  @State manualFocusValue: number = 0.5;
  @State focusState: string = '闲置';

  async aboutToAppear(): Promise<void> {
    // 获取相机管理器
    const context = getContext(this) as common.UIAbilityContext;
    const cameraManager = camera.getCameraManager(context);
    
    // 初始化相机
    await this.initCamera(cameraManager);
  }

  async initCamera(cameraManager: camera.CameraManager): Promise<void> {
    // ... 省略初始化逻辑,参考上面的代码段
    
    // 注册对焦回调
    this.registerFocusCallbacks();
  }

  registerFocusCallbacks(): void {
    if (this.session === null) return;
    // ... 省略回调注册逻辑
  }

  build() {
    Column({ space: 16 }) {
      // 预览区域(支持触控对焦)
      XComponent({ id: 'preview', type: XComponentType.SURFACE, controller: new XComponentController() })
        .width('100%')
        .height('60%')
        .onTouch((event: TouchEvent) => {
          if (event.type === TouchType.Down) {
            // 触控对焦逻辑
            this.handleTouchFocus(event);
          }
        });

      // 对焦状态
      Text(`对焦状态: ${this.focusState}`)
        .fontSize(18);

      // 手动调焦滑动条
      Row() {
        Text('近');
        Slider({ value: this.manualFocusValue, min: 0, max: 1, step: 0.01 })
          .width('70%')
          .onChange((val: number) => {
            this.manualFocusValue = val;
            this.setManualFocus(val);
          });
        Text('远');
      }
      .width('90%')
      .justifyContent(FlexAlign.Center);
    }
    .width('100%')
    .height('100%')
    .padding(10)
  }
}

FAQ

Q:为什么真机测试自动对焦正常,但模拟器上不触发回调?

A:模拟器不模拟相机硬件层,focusStateChange 回调在模拟器上不会触发。建议在真机上测试对焦相关功能。

Q:页面返回后重新进入,对焦设置失效怎么办?

A:页面销毁时 session 也会被释放,重新进入需要重新初始化相机。建议在 aboutToDisappear 中释放资源,aboutToAppear 中重新初始化。

Q:设置手动对焦后,界面还是会自动变清晰?

A:如果设备不支持 FOCUS_MODE_MANUAL,或者你只是在 setFocusDistance 但没有切换到手动模式,相机可能仍然在执行自动对焦。检查一下是否成功切换了模式。

Q:触控对焦后,对焦框动画怎么实现?

A:对焦框动画需要自己实现 UI 动画。可以用 animateTo 配合 @State 变量控制对焦框的位置和消失时间,相机 SDK 不提供对焦框的 UI 组件。

相关推荐
2501_919749031 小时前
华为鸿蒙免费口算闯关APP—小羊口算
华为·harmonyos
春卷同学2 小时前
HarmonyOS掌上记账APP开发实践第14篇:@Extend 扩展函数 — 告别重复样式代码
华为·harmonyos
蓝速科技2 小时前
蓝速科技鸿蒙广告机替代安卓实测:信创系统下的性能与边界评估
android·科技·harmonyos
2301_768103492 小时前
HarmonyOS趣味相机实战第12篇:CameraKit错误分层、相机占用恢复与用户可重试状态机
harmonyos·arkts·稳定性·错误处理·camerakit
最爱老式锅包肉4 小时前
HarmonyOS技术精讲-Camera Kit(相机服务)第8篇:曝光控制深入解析
数码相机·华为·harmonyos
Helen_cai5 小时前
OpenHarmony 路由封装 RouterUtil 页面跳转统一管理(API Version23+)
运维·服务器·nginx·华为·harmonyos
气概5 小时前
QT集成basler相机
开发语言·数码相机·qt
爸爸6196 小时前
鸿蒙应用开发实战【08】— AppStorage 全局状态与跨页面通信
ubuntu·华为·harmonyos·鸿蒙系统
ldsweet6 小时前
HarmonyOS技术精讲-剪贴板服务:进阶技巧与延迟粘贴
华为·harmonyos