《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);
});
最佳实践
-
先检查能力,再调用 API 。每次设置对焦模式或对焦点之前,一定先调用对应的
is*Supported方法。不同设备的对焦能力差异很大,直接调用可能导致 crash 或者静默失败。 -
对焦回调不要直接同步修改 UI 状态 。回调是在相机框架的线程中处理的,直接更新 @State 变量可能会触发跨线程的 UI 更新问题。建议使用
globalThis.postMessage或者通过事件总线通知主线程。 -
触控对焦后自动恢复自动模式 。用户点击屏幕指定焦点后,通常期望相机继续保持对焦能力。建议在
setFocusPoint之后继续保持在FOCUS_MODE_AUTO模式,而不是切换到FOCUS_MODE_MANUAL。 -
手动调焦的步长不宜过大 。
Slider的step设置为 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 组件。