
HarmonyOS趣味相机实战第10篇闪光灯能力探测、模式校验与镜头切换失败回滚摘要相机页面上的“闪光灯开关”看起来只是一个布尔值真正接入 CameraKit 后却至少涉及四层状态用户想不想开启、当前 PhotoSession 是否存在、当前镜头有没有闪光灯、目标 FlashMode 是否受支持。前置镜头常常没有可用闪光灯切换镜头会重建会话预览尚未启动时用户也可能先点开关系统相机被占用或会话状态变化时setFlashMode()还可能抛出异常。本文继续基于D:/APP/1quweixiangjiHarmonyOS ArkTS 趣味相机项目复盘CameraPreviewService.setFlashEnabled()与Index.ets页面状态的完整闭环。当前代码已经实现session.hasFlash()、isFlashModeSupported()、setFlashMode()、异常捕获以及页面失败回滚本文进一步分析为什么镜头切换后要重新应用用户意图、为什么“没有会话”和“设备不支持”不能混成同一种错误以及如何补齐状态查询、真机矩阵和日志定位。工程背景与源码定位文件作用entry/src/main/ets/service/CameraPreviewService.etsPhotoSession 创建、闪光灯能力探测、模式校验与设置entry/src/main/ets/pages/Index.ets闪光灯按钮、页面状态、失败回滚及预览重启后的重应用entry/src/main/ets/service/CameraDeviceService.ets前后镜头发现与切换方向选择entry/src/main/ets/service/CameraPermissionService.ets相机授权决定是否能建立会话entry/src/main/module.json5ohos.permission.CAMERA权限声明entry/src/test/LocalUnit.test.ets当前已有相册快照测试可扩展闪光灯状态纯函数测试环境与版本边界项目当前值说明工程路径D:/APP/1quweixiangji本文按当前源码复盘应用版本1.0.2来自AppScope/app.json5bundleNamecom.fun.quweixiangji当前应用包名应用模型HarmonyOS Stage 模型EntryAbility加载单页相机工作台target SDK6.0.2(22)SDK 升级后需回归 PhotoSession 控制接口相机能力kit.CameraKitPhotoSession、FlashMode、预览和拍照权限ohos.permission.CAMERA仅在前台相机使用场景申请本地构建命令cd D:\APP\1quweixiangji$env:JAVA_HOMED:\Program Files\Huawei\DevEco Studio\jbr$env:Path$env:JAVA_HOME\bin;$env:PathD:\Program Files\Huawei\DevEco Studio\tools\hvigor\bin\hvigorw.bat--mode module-p moduleentrydefault-p productdefault assembleHap--no-daemon版本兼容与设备边界闪光灯能力必须以当前 CameraSession 和当前摄像头为准不能根据“手机通常有后置闪光灯”进行推断。工程当前使用FLASH_MODE_OPEN与FLASH_MODE_CLOSE并没有承诺自动闪光、常亮补光或前置屏幕补光。能力当前实现需要注意的边界是否有闪光灯session.hasFlash()前置、外接或特殊摄像头可能返回 false模式支持session.isFlashModeSupported()有闪光硬件不代表每种模式都支持设置模式session.setFlashMode()会话未运行、相机冲突或状态变化时可能失败切换镜头重建预览后重新应用新镜头必须重新探测能力页面开关flashEnabled表达用户意图不等同于硬件最终状态正式发版前应以目标 SDK 的 CameraKit API 参考和真机能力为准。模拟器不能替代闪光灯真机测试。一、页面状态只是用户意图Index.ets中维护StateflashEnabled:booleanfalse;按钮根据它显示文案Button(this.flashEnabled?闪光灯开:闪光灯关).onClick((){this.toggleFlash();})这个布尔值适合表达“用户当前希望闪光灯开启”但不能直接代表 CameraKit 已经成功切换模式。硬件状态还要经过 PhotoSession 校验。如果把flashEnabled true直接当作成功会出现下面的问题场景UI 可能显示实际硬件前置镜头无闪光已开启不支持预览尚未启动已开启尚无会话模式不支持已开启设置失败切换镜头后仍为已开启新会话尚未重应用所以页面需要接收服务层结果并决定是否回滚。二、页面先更新意图再根据结果回滚当前切换方法privatetoggleFlash():void{this.flashEnabled!this.flashEnabled;conststate:CameraPreviewStateCameraPreviewService.setFlashEnabled(this.flashEnabled);this.captureStatusTextstate.message;if(state.statuserror){this.flashEnabledfalse;}}流程可以拆成四步用户点击页面翻转期望值。服务层尝试把期望值应用到当前会话。页面展示服务层返回的明确文案。如果返回error页面把开关回滚为 false。这至少避免了“设备不支持但按钮仍显示开启”。不过当前回滚策略把所有错误都退到关闭如果以后支持更丰富状态可以用显式模型区分requested、applied与available。三、会话为空不是设备不支持服务入口先读取静态 PhotoSessionstaticsetFlashEnabled(enabled:boolean):CameraPreviewState{constsession:camera.PhotoSession|nullCameraPreviewService.session;if(sessionnull){return{status:idle,message:enabled?闪光灯会在相机启动后开启:闪光灯已关闭};}}这里返回idle而不是error非常关键。会话为空可能只是用户先设置闪光灯再授权相机。XComponent Surface 尚未创建。页面正在切换前后镜头。应用刚从后台返回预览正在重建。这些都不等于“当前设备不支持闪光灯”。页面可以保留用户意图等会话启动后再次应用。四、先用 hasFlash 判断硬件能力会话存在后第一步不是直接设置模式而是检查if(!session.hasFlash()){return{status:error,message:当前设备不支持闪光灯};}hasFlash()表达当前会话关联摄像头是否提供闪光能力。它比根据activeCameraPosition写死规则更可靠不能简单写成“前置永远不支持、后置永远支持”因为不同设备实现并不一致。建议页面未来把“不支持”视为能力状态而不是一次性错误。例如exportinterfaceFlashCapabilityState{available:boolean;enabled:boolean;message:string;}这样可以直接禁用按钮并显示原因减少用户反复点击。五、有闪光灯还要校验具体模式目标模式映射consttargetMode:camera.FlashModeenabled?camera.FlashMode.FLASH_MODE_OPEN:camera.FlashMode.FLASH_MODE_CLOSE;继续检查模式是否支持if(!session.isFlashModeSupported(targetMode)){return{status:error,message:enabled?当前相机不支持开启闪光灯:当前相机不支持关闭闪光灯};}hasFlash()和isFlashModeSupported()解决的是不同问题检查回答的问题hasFlash()当前会话是否具有闪光能力isFlashModeSupported(mode)当前会话是否支持目标模式跳过第二层校验直接调用setFlashMode()会把可预判的不支持状态变成运行时异常。六、最后才设置 FlashMode能力确认后执行session.setFlashMode(targetMode);return{status:running,message:enabled?闪光灯已开启:闪光灯已关闭};服务层返回的是页面能直接消费的CameraPreviewStateexportinterfaceCameraPreviewState{status:CameraPreviewStatus;message:string;}复用这个状态模型可以让页面不用依赖 CameraKit 的枚举和异常对象但也有一个边界idle、running、error原本描述预览状态现在同时承载闪光灯操作结果。项目变大后建议单独定义CameraControlResult避免状态语义混杂。七、异常必须转换为可恢复结果当前服务捕获设置异常}catch(error){hilog.warn(DOMAIN,TAG,set flash failed: %{public}s,JSON.stringify(error));return{status:error,message:闪光灯设置失败};}页面不会直接看到系统异常对象只收到稳定文案。常见异常来源包括异常来源处理方向PhotoSession 正在停止等新会话运行后重试相机被其他应用占用提示释放占用后重试镜头切换期间旧会话失效只向新会话重应用设备禁用相机提示检查系统安全设置SDK 或机型差异记录错误码并走关闭降级日志可以记录错误码和当前镜头但不要记录照片、用户水印或人体识别数据。八、镜头切换后为什么要重新应用相机切换会重新调用预览启动constresult:CameraPreviewStateawaitCameraPreviewService.startPreview(context,this.previewSurfaceId,this.activeCameraPosition);新 PhotoSession 运行后页面检查用户意图if(result.statusrunningthis.flashEnabled){CameraPreviewService.setFlashEnabled(true);}这是必要的因为 FlashMode 属于会话控制状态。旧会话释放后新会话不会自动继承设置。当前代码还有一个可改进点重应用结果没有写回页面。如果从后置开启闪光后切到不支持闪光的前置服务会返回 error但flashEnabled仍可能保持 true。建议改为if(result.statusrunningthis.flashEnabled){constflashState:CameraPreviewStateCameraPreviewService.setFlashEnabled(true);this.captureStatusTextflashState.message;if(flashState.statuserror){this.flashEnabledfalse;}}这样镜头切换和手工点击走同一套回滚规则。九、不要把预览常亮等同于拍照闪光工程使用FLASH_MODE_OPEN控制当前 PhotoSession 的闪光模式。产品文案应避免把它描述成所有设备都支持的“手电筒常亮”或“自动补光”。需要明确区分产品能力需要的验证拍照时开启闪光PhotoSession 对目标 FlashMode 的支持自动闪光对应自动模式是否支持常亮补光设备和会话是否允许持续模式前置屏幕补光ArkUI 亮屏方案不是摄像头闪光硬件文章和 UI 只承诺当前代码实际实现的开/关控制不虚构自动闪光或屏幕补光已经上线。十、建议抽出状态应用函数页面可以把回滚规则集中到一个方法privateapplyFlashPreference(enabled:boolean):void{constresult:CameraPreviewStateCameraPreviewService.setFlashEnabled(enabled);this.captureStatusTextresult.message;if(result.statuserror){this.flashEnabledfalse;return;}this.flashEnabledenabled;}手工点击privatetoggleFlash():void{this.applyFlashPreference(!this.flashEnabled);}预览重建if(result.statusrunningthis.flashEnabled){this.applyFlashPreference(true);}这样不再复制“写文案、检查 error、回滚 false”的逻辑。十一、状态机与降级策略状态条件页面行为pending会话为空但用户希望开启保留意图等待预览运行unsupportedhasFlash()为 false关闭并禁用按钮说明设备不支持modeUnsupported目标模式不支持回滚关闭避免继续调用enabled设置 OPEN 成功显示已开启disabled设置 CLOSE 成功显示已关闭failedsetFlashMode()异常回滚关闭允许用户重试前后镜头切换时重新进入能力探测不能沿用旧镜头的unsupported或enabled结论。十二、建议补充的测试服务依赖真实 PhotoSession不适合全部做普通单元测试但页面状态转换可以抽成纯函数exportfunctionnextFlashEnabled(requested:boolean,status:CameraPreviewStatus):boolean{returnstatuserror?false:requested;}测试矩阵requestedstatus期望truerunningtruetrueidletrue等待会话后重应用trueerrorfalsefalserunningfalse真机集成测试重点后置会话hasFlash()与模式支持结果。前置会话不支持时页面是否回滚。开启后切前置再切回后置的状态变化。预览未启动先点开启预览启动后能否应用。相机被占用或快速切后台时是否出现异常。十三、常见问题排查现象可能原因排查方式按钮显示开启但不闪页面状态没有经过服务结果确认检查toggleFlash()回滚前置镜头点击后报错当前会话没有闪光能力检查session.hasFlash()有闪光灯仍设置失败目标模式不受支持检查isFlashModeSupported()预览未启动提示错误把 session null 当成不支持应返回 pending/idle切镜头后设置丢失新会话没有重应用用户意图预览 running 后再次应用切到前置后按钮仍亮重应用结果没有回写页面统一走applyFlashPreference()快速切换时偶发异常旧会话正在释放使用会话代际忽略旧操作后台返回后无法开启新 PhotoSession 尚未运行等预览 running 再设置设置失败后无法重试页面没有回滚状态error 时恢复 false模拟器测试通过真机失败模拟器没有真实闪光能力必须做真机矩阵日志信息不足未记录镜头与目标模式记录 position、mode、错误码上架文案夸大宣称自动闪光或屏幕补光只描述已实现的开关能力十四、上线前验收清单页面闪光灯状态表达用户意图不直接等同于硬件成功状态。PhotoSession 为空时不调用控制接口。会话为空与设备不支持使用不同文案。设置前调用session.hasFlash()。设置前调用session.isFlashModeSupported(targetMode)。只在能力确认后调用session.setFlashMode()。设置异常转换为页面可恢复结果。手工点击失败时页面回滚为关闭。镜头切换后重新探测闪光能力。新会话重应用失败时页面也会回滚。后置、前置和仅单摄设备分别真机验证。预览未启动先点开关的 pending 路径验证。快速切镜头、切后台和相机占用路径验证。日志记录目标模式、镜头方向和错误码。日志不记录照片、水印正文或人体识别数据。UI 不承诺未实现的自动闪光、常亮补光或屏幕补光。SDK 升级后重新检查 PhotoSession 闪光控制接口。总结稳定的闪光灯开关不是flashEnabled !flashEnabled而是一条“用户意图 - 会话存在 - 硬件能力 - 模式支持 - 设置结果 - 页面回滚”的控制链。1quweixiangji已经通过hasFlash()、isFlashModeSupported()和异常捕获完成了核心防护并在预览重建后尝试重应用设置。进一步工程化时应把重应用结果同步回页面把会话为空视为等待状态把前后镜头的能力分别探测并用统一方法收口按钮点击和会话重建。这样即使设备没有闪光灯、前后镜头能力不一致或 PhotoSession 正在切换页面也不会显示错误状态更不会把不支持能力包装成成功。