【听见课堂 HarmonyOS NEXT 实战系列 31】CameraPicker 拍照接入:从权限边界到 resultUri
【听见课堂 HarmonyOS NEXT 实战系列 31】CameraPicker 拍照接入:从权限边界到 resultUri
课堂扫描看起来只是“点一下拍照,再拿到一张图”,真正落地时却要处理 UIAbilityContext、系统相机返回值、用户取消、空 URI、设备能力差异,以及项目自身的权限策略。把这些分支全部塞进 ArkUI 页面,后续 OCR、相册导入和错误恢复就会越来越难维护。
听见课堂把系统拍照入口封装在 ScanCaptureService.capturePhoto() 中,页面只负责展示处理状态并把成功 URI 交给 OCR。本文按当前源码拆解这条链路,同时说明项目实现与当前官方 CameraPicker 最小权限建议之间的差异。

一、为什么首版选择系统 CameraPicker
扫描板书的首版目标是获得一张由用户主动确认的照片,不是自建取景、曝光、对焦和录像系统。CameraPicker 直接拉起系统相机,应用无需维护复杂 Camera Kit 会话,取消和确认也沿用系统交互。
这减少了首版代码量,也把相机预览生命周期留给系统。当前华为官方 FAQ 同样把 CameraPicker 描述为系统提供交互页面、由用户主动完成拍摄与确认的入口。
二、页面只传 UIAbilityContext
页面先从 UI 上下文取得宿主 Context:
const hostContext: Context | undefined = this.getUIContext().getHostContext();
if (hostContext === undefined) {
throw new Error('UIAbility context is unavailable.');
}
const context: common.UIAbilityContext = hostContext as common.UIAbilityContext;
const uri: string = await this.scanService.capturePhoto(context);
系统 Picker 需要在 UIAbility 场景中拉起界面。页面不拼接文件路径,也不直接解析相机返回对象,只提供必要上下文。
三、PickerProfile 用对象字面量初始化
项目当前写法是:
const profile: cameraPicker.PickerProfile = {
cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK
};
这不是纯风格选择。项目曾在真机遇到对 new PickerProfile() 返回对象赋值时的只读属性异常,随后改为对象字面量。对于跨 SDK 版本的配置对象,应优先按当前 API 类型一次性构造,并以真实设备输出校验。
四、媒体类型只请求 PHOTO
result = await cameraPicker.pick(
context,
[cameraPicker.PickerMediaType.PHOTO],
profile
);
板书 OCR 不需要视频,因此媒体类型只包含 PHOTO。能力范围越小,返回处理越清晰,也避免把录像时长、音频权限和大文件管理引入扫描首版。
五、异常和业务失败必须分开
cameraPicker.pick() 抛异常,代表系统调用阶段没有正常完成;返回对象但 resultCode !== 0,则是有结构化返回但业务结果失败。项目分别处理:
try {
result = await cameraPicker.pick(context, mediaTypes, profile);
} catch (error) {
throw new Error('camera picker exception: ' + this.formatError(error));
}
if (result.resultCode !== 0) {
throw new Error('camera picker failed, resultCode=' + result.resultCode);
}
两类错误若统一写成“拍照失败”,日志和用户恢复路径都会失去依据。
六、resultUri 要先去空格再判断
项目读取并归一化 URI:
const uri: string = result.resultUri.trim();
if (uri.length === 0) {
throw new Error('camera picker returned empty uri.');
}
return uri;
空字符串不是成功结果。若继续把它交给 ImageSource,错误会被推迟到解码阶段,页面只能看到与真实原因无关的 OCR 失败。
七、日志不打印完整图片 URI
当前日志只记录 resultCode、媒体类型和是否存在 URI:
hilog.info(DOMAIN, TAG,
'CameraPicker resultCode=%{public}d mediaType=%{public}s hasUri=%{public}s',
result.resultCode, result.mediaType, uri.length > 0 ? 'true' : 'false');
图片 URI 可能包含用户文件位置或媒体标识。诊断只需知道是否拿到 URI,不应默认把完整地址写入公开日志。
八、用户取消不能写入课堂记录
页面把空 URI 视为取消:
if (uri.length === 0) {
this.scanStatus = '已取消拍照,未读取任何图片';
return;
}
取消不是 OCR 失败,更不是一条空扫描记录。状态文案明确“未读取任何图片”,也不会触发 Repository 写入。
九、项目当前权限策略需要如实说明
ScanCaptureService 当前在调用 Picker 前检查并请求 ohos.permission.CAMERA,module.json5 也声明该权限。这是项目现状,文章不能删去这条事实。
但当前华为 CameraPicker FAQ 说明:系统相机交互由用户主动完成时,应用开发者可以不申请相机权限。因此这里应区分“项目当前实现”与“平台当前最小要求”。是否移除项目权限门禁,需要结合目标 API、既有真机回归和上架隐私说明单独评估,不能只改文章不改代码。
十、为什么不直接把 resultUri 存入数据库
图片 URI 在当前流程中只用于临时预览和本机 OCR。保存时写入的是复核后的文字与来源,页面完成后清空 scanImageUri。
长期保存外部选择器 URI 还要处理访问期限、原图删除、迁移和隐私说明。首版没有这些需求,就不应把临时句柄当作永久业务数据。
十一、系统相机失败后的三条恢复路
P05 页面保留:
- 再次尝试拍照;
- 从系统图库选择一张已有图片;
- 使用本地示例继续体验 OCR 校对和保存闭环。
第三条只能标为“本地示例”,不能冒充真实拍照或真实 OCR 成功。

十二、页面用 isScanProcessing 防重复点击
if (this.isScanProcessing) {
return;
}
this.isScanProcessing = true;
try {
// 打开系统相机并处理结果
} finally {
this.isScanProcessing = false;
}
系统 UI 拉起期间重复点击可能创建并发流程。幂等门禁同时用于按钮 enabled,让视觉状态和业务状态一致。
十三、错误文本要映射成恢复动作
页面通过 scanFailureMessage() 识别权限、取消、resultCode 和普通异常,再追加“检查权限、从图库选择或使用本地示例”等动作。用户需要的不是堆栈,而是下一步。
底层仍保留序列化后的 code/name/message,用于区分设备、系统能力和业务错误。
十四、当前验证证据到哪一级
项目契约脚本、HAP 构建、模拟器安装和 P05 降级路径曾通过;模拟器没有可用相机,真实拍照成功、返回 URI、真图预览和 OCR 仍不能由这组证据证明。后续真机 HDC 又曾被 USB 安全通道阻塞。
因此本文能确认“链路已实现并可构建、失败可恢复”,不能写成“所有真机拍照已验证通过”。
十五、验收清单
至少应覆盖:后置相机正常拍照、用户取消、权限策略、系统相机不可用、非零 resultCode、空 URI、连续点击、页面切后台、图片解码失败,以及完成复核后数据库不保留临时 URI。
还要在目标 SDK 上复核 CameraPicker 的权限最小化方案,确保 manifest、运行时行为和隐私声明一致。
十六、总结
CameraPicker 接入的核心不是一行 pick(),而是把系统 UI、返回协议、临时资源和恢复路径组织成可验证流程。听见课堂把相机调用收口在 Service 中,页面只消费 URI 和状态;真正需要继续补齐的是目标真机成功证据与当前权限策略复核。
下一篇继续讨论:即使项目当前相机权限门禁被拒绝,扫描功能为什么仍不能整体失效。
参考:华为 CameraPicker 常见问题、项目 ScanCaptureService.ets、P05/P06 扫描 OCR 复验报告。
更多推荐


所有评论(0)