HarmonyOS趣味相机实战第10篇:闪光灯能力探测、模式校验与镜头切换失败回滚
HarmonyOS趣味相机实战第10篇:闪光灯能力探测、模式校验与镜头切换失败回滚
摘要
相机页面上的“闪光灯开关”看起来只是一个布尔值,真正接入 CameraKit 后却至少涉及四层状态:用户想不想开启、当前 PhotoSession 是否存在、当前镜头有没有闪光灯、目标 FlashMode 是否受支持。前置镜头常常没有可用闪光灯;切换镜头会重建会话;预览尚未启动时用户也可能先点开关;系统相机被占用或会话状态变化时,setFlashMode() 还可能抛出异常。
本文继续基于 D:/APP/1quweixiangji HarmonyOS ArkTS 趣味相机项目,复盘 CameraPreviewService.setFlashEnabled() 与 Index.ets 页面状态的完整闭环。当前代码已经实现 session.hasFlash()、isFlashModeSupported()、setFlashMode()、异常捕获以及页面失败回滚;本文进一步分析为什么镜头切换后要重新应用用户意图、为什么“没有会话”和“设备不支持”不能混成同一种错误,以及如何补齐状态查询、真机矩阵和日志定位。
工程背景与源码定位
| 文件 | 作用 |
|---|---|
entry/src/main/ets/service/CameraPreviewService.ets |
PhotoSession 创建、闪光灯能力探测、模式校验与设置 |
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.json5 |
ohos.permission.CAMERA 权限声明 |
entry/src/test/LocalUnit.test.ets |
当前已有相册快照测试,可扩展闪光灯状态纯函数测试 |
环境与版本边界
| 项目 | 当前值 | 说明 |
|---|---|---|
| 工程路径 | D:/APP/1quweixiangji |
本文按当前源码复盘 |
| 应用版本 | 1.0.2 |
来自 AppScope/app.json5 |
| bundleName | com.fun.quweixiangji |
当前应用包名 |
| 应用模型 | HarmonyOS Stage 模型 | EntryAbility 加载单页相机工作台 |
| target SDK | 6.0.2(22) |
SDK 升级后需回归 PhotoSession 控制接口 |
| 相机能力 | @kit.CameraKit |
PhotoSession、FlashMode、预览和拍照 |
| 权限 | ohos.permission.CAMERA |
仅在前台相机使用场景申请 |

本地构建命令:
cd D:\APP\1quweixiangji
$env:JAVA_HOME='D:\Program Files\Huawei\DevEco Studio\jbr'
$env:Path="$env:JAVA_HOME\bin;$env:Path"
& 'D:\Program Files\Huawei\DevEco Studio\tools\hvigor\bin\hvigorw.bat' --mode module -p module=entry@default -p product=default assembleHap --no-daemon
版本兼容与设备边界
闪光灯能力必须以当前 CameraSession 和当前摄像头为准,不能根据“手机通常有后置闪光灯”进行推断。工程当前使用 FLASH_MODE_OPEN 与 FLASH_MODE_CLOSE,并没有承诺自动闪光、常亮补光或前置屏幕补光。
| 能力 | 当前实现 | 需要注意的边界 |
|---|---|---|
| 是否有闪光灯 | session.hasFlash() |
前置、外接或特殊摄像头可能返回 false |
| 模式支持 | session.isFlashModeSupported() |
有闪光硬件不代表每种模式都支持 |
| 设置模式 | session.setFlashMode() |
会话未运行、相机冲突或状态变化时可能失败 |
| 切换镜头 | 重建预览后重新应用 | 新镜头必须重新探测能力 |
| 页面开关 | flashEnabled |
表达用户意图,不等同于硬件最终状态 |
正式发版前应以目标 SDK 的 CameraKit API 参考和真机能力为准。模拟器不能替代闪光灯真机测试。
一、页面状态只是用户意图
Index.ets 中维护:
@State flashEnabled: boolean = false;
按钮根据它显示文案:
Button(this.flashEnabled ? '闪光灯开' : '闪光灯关')
.onClick(() => {
this.toggleFlash();
})
这个布尔值适合表达“用户当前希望闪光灯开启”,但不能直接代表 CameraKit 已经成功切换模式。硬件状态还要经过 PhotoSession 校验。
如果把 flashEnabled = true 直接当作成功,会出现下面的问题:
| 场景 | UI 可能显示 | 实际硬件 |
|---|---|---|
| 前置镜头无闪光 | 已开启 | 不支持 |
| 预览尚未启动 | 已开启 | 尚无会话 |
| 模式不支持 | 已开启 | 设置失败 |
| 切换镜头后 | 仍为已开启 | 新会话尚未重应用 |
所以页面需要接收服务层结果并决定是否回滚。
二、页面先更新意图,再根据结果回滚
当前切换方法:
private toggleFlash(): void {
this.flashEnabled = !this.flashEnabled;
const state: CameraPreviewState =
CameraPreviewService.setFlashEnabled(this.flashEnabled);
this.captureStatusText = state.message;
if (state.status === 'error') {
this.flashEnabled = false;
}
}
流程可以拆成四步:
- 用户点击,页面翻转期望值。
- 服务层尝试把期望值应用到当前会话。
- 页面展示服务层返回的明确文案。
- 如果返回
error,页面把开关回滚为 false。
这至少避免了“设备不支持但按钮仍显示开启”。不过当前回滚策略把所有错误都退到关闭;如果以后支持更丰富状态,可以用显式模型区分 requested、applied 与 available。
三、会话为空不是设备不支持
服务入口先读取静态 PhotoSession:
static setFlashEnabled(enabled: boolean): CameraPreviewState {
const session: camera.PhotoSession | null = CameraPreviewService.session;
if (session === null) {
return {
status: 'idle',
message: enabled ? '闪光灯会在相机启动后开启' : '闪光灯已关闭'
};
}
}
这里返回 idle 而不是 error,非常关键。会话为空可能只是:
- 用户先设置闪光灯,再授权相机。
- XComponent Surface 尚未创建。
- 页面正在切换前后镜头。
- 应用刚从后台返回,预览正在重建。
这些都不等于“当前设备不支持闪光灯”。页面可以保留用户意图,等会话启动后再次应用。
四、先用 hasFlash 判断硬件能力
会话存在后,第一步不是直接设置模式,而是检查:
if (!session.hasFlash()) {
return {
status: 'error',
message: '当前设备不支持闪光灯'
};
}
hasFlash() 表达当前会话关联摄像头是否提供闪光能力。它比根据 activeCameraPosition 写死规则更可靠:不能简单写成“前置永远不支持、后置永远支持”,因为不同设备实现并不一致。
建议页面未来把“不支持”视为能力状态,而不是一次性错误。例如:
export interface FlashCapabilityState {
available: boolean;
enabled: boolean;
message: string;
}
这样可以直接禁用按钮并显示原因,减少用户反复点击。
五、有闪光灯还要校验具体模式
目标模式映射:
const targetMode: camera.FlashMode = enabled ?
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 ? '闪光灯已开启' : '闪光灯已关闭'
};
服务层返回的是页面能直接消费的 CameraPreviewState:
export interface CameraPreviewState {
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 或机型差异 | 记录错误码并走关闭降级 |
日志可以记录错误码和当前镜头,但不要记录照片、用户水印或人体识别数据。
八、镜头切换后为什么要重新应用
相机切换会重新调用预览启动:
const result: CameraPreviewState = await CameraPreviewService.startPreview(
context,
this.previewSurfaceId,
this.activeCameraPosition
);
新 PhotoSession 运行后,页面检查用户意图:
if (result.status === 'running' && this.flashEnabled) {
CameraPreviewService.setFlashEnabled(true);
}
这是必要的,因为 FlashMode 属于会话控制状态。旧会话释放后,新会话不会自动继承设置。
当前代码还有一个可改进点:重应用结果没有写回页面。如果从后置开启闪光后切到不支持闪光的前置,服务会返回 error,但 flashEnabled 仍可能保持 true。建议改为:
if (result.status === 'running' && this.flashEnabled) {
const flashState: CameraPreviewState =
CameraPreviewService.setFlashEnabled(true);
this.captureStatusText = flashState.message;
if (flashState.status === 'error') {
this.flashEnabled = false;
}
}
这样镜头切换和手工点击走同一套回滚规则。
九、不要把预览常亮等同于拍照闪光
工程使用 FLASH_MODE_OPEN 控制当前 PhotoSession 的闪光模式。产品文案应避免把它描述成所有设备都支持的“手电筒常亮”或“自动补光”。
需要明确区分:
| 产品能力 | 需要的验证 |
|---|---|
| 拍照时开启闪光 | PhotoSession 对目标 FlashMode 的支持 |
| 自动闪光 | 对应自动模式是否支持 |
| 常亮补光 | 设备和会话是否允许持续模式 |
| 前置屏幕补光 | ArkUI 亮屏方案,不是摄像头闪光硬件 |
文章和 UI 只承诺当前代码实际实现的开/关控制,不虚构自动闪光或屏幕补光已经上线。
十、建议抽出状态应用函数
页面可以把回滚规则集中到一个方法:
private applyFlashPreference(enabled: boolean): void {
const result: CameraPreviewState =
CameraPreviewService.setFlashEnabled(enabled);
this.captureStatusText = result.message;
if (result.status === 'error') {
this.flashEnabled = false;
return;
}
this.flashEnabled = enabled;
}
手工点击:
private toggleFlash(): void {
this.applyFlashPreference(!this.flashEnabled);
}
预览重建:
if (result.status === 'running' && this.flashEnabled) {
this.applyFlashPreference(true);
}
这样不再复制“写文案、检查 error、回滚 false”的逻辑。
十一、状态机与降级策略
| 状态 | 条件 | 页面行为 |
|---|---|---|
pending |
会话为空但用户希望开启 | 保留意图,等待预览运行 |
unsupported |
hasFlash() 为 false |
关闭并禁用按钮,说明设备不支持 |
modeUnsupported |
目标模式不支持 | 回滚关闭,避免继续调用 |
enabled |
设置 OPEN 成功 | 显示已开启 |
disabled |
设置 CLOSE 成功 | 显示已关闭 |
failed |
setFlashMode() 异常 |
回滚关闭,允许用户重试 |
前后镜头切换时重新进入能力探测,不能沿用旧镜头的 unsupported 或 enabled 结论。
十二、建议补充的测试
服务依赖真实 PhotoSession,不适合全部做普通单元测试,但页面状态转换可以抽成纯函数:
export function nextFlashEnabled(
requested: boolean,
status: CameraPreviewStatus
): boolean {
return status === 'error' ? false : requested;
}
测试矩阵:
| requested | status | 期望 |
|---|---|---|
| true | running | true |
| true | idle | true,等待会话后重应用 |
| true | error | false |
| false | running | false |
真机集成测试重点:
- 后置会话
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 正在切换,页面也不会显示错误状态,更不会把不支持能力包装成成功。
更多推荐


所有评论(0)