《HarmonyOS技术精讲-Camera Kit(相机服务)第9篇:自动对焦与手动调焦》
为啥对焦处理这么重要
开发相机应用时,很多人会遇到一个问题:官方示例拍照是清晰的,但放到实际项目里,画面总是模糊或者对焦不准。特别是当用户需要拍近距离物体(比如二维码)或者想手动控制焦点时,默认的自动对焦行为往往达不到预期。
HarmonyOS NEXT 的 Camera Kit 提供了对焦能力,但接口设计上有一些微妙之处。如果直接使用默认实现,容易出现对焦不稳定、回调不触发、或者切换模式后状态不同步的问题。这篇文章主要讲清楚三种对焦方式:自动对焦、触控对焦、手动调焦,以及对应的状态监听处理。
它解决什么问题
对焦本质上是在控制镜头组移动,让目标物体在传感器上形成清晰图像。不同的场景需要不同的对焦策略:
| 场景 | 推荐对焦模式 | 说明 |
|---|---|---|
| 日常拍照 | FOCUS_MODE_AUTO | 相机自动寻找对比度最高区域 |
| 拍二维码/文档 | 触控对焦 +FOCUS_MODE_AUTO | 用户指定焦点区域 |
| 视频录制切换主体 | FOCUS_MODE_CONTINUOUS_AUTO | 持续追踪画面中的主体 |
| 显微镜/微距 | 手动调焦 | 自动对焦可能找不到清晰点 |
触控对焦和手动调焦是互补关系。触控对焦改变的是对焦“位置”,手动调焦改变的是对焦“距离”。很多开发者会把这两者混为一谈,实际它们的 API 是完全独立的。
环境说明
DevEco Studio 版本:DevEco Studio 5.1.0 及以上 HarmonyOS SDK 版本:HarmonyOS API 12 及以上 目标设备:手机(P60/Mate60 系列等支持 Camera Kit 的机型)核心实现
1. 基础对焦模式设置
先获取相机输入流,然后设置对焦模式。注意,这一步必须在CameraSession配置完成后、拍照流程开始前执行。
import{camera}from'@kit.CameraKit';import{common}from'@kit.AbilityKit';// 假设已经获取到 cameraManager 和 sceneModeasyncfunctionsetFocusMode(cameraManager:camera.CameraManager,surfaceId:string,mode:camera.FocusMode):Promise<void>{// 获取支持的设备列表constcameraDevices:camera.CameraDevice[]=cameraManager.getSupportedCameras();if(cameraDevices.length===0){console.error('没有可用的相机设备');return;}// 创建相机输入constcameraInput:camera.CameraInput=cameraManager.createCameraInput(cameraDevices[0]);awaitcameraInput.open();// 创建会话(这里使用普通拍照会话)constsession:camera.PhotoSession=cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO)ascamera.PhotoSession;session.beginConfig();// 添加输入session.addInput(cameraInput);// 添加输出constphotoOutput:camera.PhotoOutput=session.addPhotoOutput(surfaceId);// 提交配置awaitsession.commitConfig();// 启动会话awaitsession.start();// 设置对焦模式(关键是必须在 session 启动后设置)constcameraDeviceObj=cameraDevices[0];constisSupported=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)。应用层需要把屏幕触摸坐标映射成这个标准坐标系。
@Entry@Componentstruct TouchFocusSample{@StatefocusPointX:number=0.5;// 默认中心@StatefocusPointY:number=0.5;@Statesession:camera.PhotoSession|null=null;@StatecameraDevice:camera.CameraDevice|null=null;build(){Column(){// 相机预览区域XComponent({id:'xcomponentId',type:XComponentType.SURFACE,controller:newXComponentController()}).width('100%').height(400).onTouch((event:TouchEvent)=>{if(event.type===TouchType.Down){// 将触摸坐标转换为归一化坐标constrect=event.target.area;if(rect.width>0&&rect.height>0){// 触摸点坐标在组件内的相对位置constrelX=event.touches[0].x/rect.width;constrelY=event.touches[0].y/rect.height;// 限制在 0-1 范围constnormalizedX=Math.min(1,Math.max(0,relX));constnormalizedY=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%')}asyncsetFocusPoint(x:number,y:number):Promise<void>{if(this.session===null||this.cameraDevice===null){console.warn('会话或设备未初始化');return;}try{// 检查是否支持设置对焦点constsupported=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);// 设置对焦点constpoint: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模式使用。
@Entry@Componentstruct ManualFocusSample{@StatefocusSliderValue:number=0.5;// 初始在中间@StatecurrentFocusDistance:number=0.5;@Statesession:camera.PhotoSession|null=null;@StatecameraDevice:camera.CameraDevice|null=null;build(){Column({space:20}){// 相机预览XComponent({id:'previewXComponent',type:XComponentType.SURFACE,controller:newXComponentController()}).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%')}asyncsetManualFocus(distance:number):Promise<void>{if(this.session===null||this.cameraDevice===null){console.warn('会话或设备未初始化');return;}try{// 检查是否支持手动对焦模式constsupported=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. 对焦状态监听
监听对焦状态是为了给用户反馈,比如屏幕上显示一个对焦框动画,或者拍照时等待对焦完成。
// 在 session 启动后注册监听functionregisterFocusCallback(session:camera.PhotoSession):void{// 对焦状态回调session.on('focusStateChange',(err:BusinessError,focusState:camera.FocusState)=>{if(err){console.error(`对焦状态回调错误:${JSON.stringify(err)}`);return;}switch(focusState){casecamera.FocusState.FOCUS_STATE_SCAN:console.info('正在对焦中...');// 这里可以更新 UI,显示对焦框动画break;casecamera.FocusState.FOCUS_STATE_FOCUSED:console.info('对焦成功');// 对焦完成,可以通知 UI 更新break;casecamera.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。
// 使用节流控制letlastFocusUpdateTime:number=0;constTHROTTLE_INTERVAL=200;// 200ms 内只更新一次session.on('focusStateChange',(err,focusState)=>{constnow=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 入口
@Entry@Componentstruct FocusDemo{@Statesession:camera.PhotoSession|null=null;@StatecameraDevice:camera.CameraDevice|null=null;@StatetouchFocusX:number=0.5;@StatetouchFocusY:number=0.5;@StatemanualFocusValue:number=0.5;@StatefocusState:string='闲置';asyncaboutToAppear():Promise<void>{// 获取相机管理器constcontext=getContext(this)ascommon.UIAbilityContext;constcameraManager=camera.getCameraManager(context);// 初始化相机awaitthis.initCamera(cameraManager);}asyncinitCamera(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:newXComponentController()}).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 组件。