HarmonyOS趣味相机实战第13篇:CameraPicker与CameraKit选型、系统拍照回退及架构边界
摘要
HarmonyOS 应用需要"拍一张照片"时,并不一定要自己管理 CameraInput、PhotoSession、XComponent Surface 和 PhotoOutput。若产品只需要调用系统拍摄界面并拿回结果,CameraPicker 能显著减少相机生命周期代码;若产品需要在应用内实时预览、倒计时、闪光灯、水印叠层、人体识别、前后镜头切换或双摄扩展,则必须使用 CameraKit 自定义会话。
本文继续基于 D:/APP/1quweixiangji 趣味相机项目,对比两条路线。当前项目已经深度依赖自定义预览,因此不能把 CameraKit 直接替换成 CameraPicker;更合理的方案是保留 CameraKit 主流程,并在相机冲突、设备兼容或用户希望使用系统拍摄体验时,提供明确的"使用系统相机拍摄"回退入口。
工程背景与源码定位
| 文件 | 当前 CameraKit 责任 | CameraPicker 接入点 |
|---|---|---|
entry/src/main/ets/service/CameraPreviewService.ets |
预览、PhotoSession、PhotoOutput、闪光灯和 PixelMap | 保持主流程,不与 Picker 混在同一会话类 |
entry/src/main/ets/pages/Index.ets |
XComponent 预览、倒计时、水印、拍照结果弹层 | 增加用户主动选择的系统相机按钮 |
entry/src/main/ets/service/PhotoAlbumService.ets |
创建和缓存 CapturedPhoto 元数据 | 接收 Picker 返回 URI 并转换统一来源模型 |
entry/src/main/ets/model/DecorationModels.ets |
照片、水印、来源和状态模型 | 增加 systemPicker 来源及可选 URI |
entry/src/main/module.json5 |
当前声明相机权限 | Picker 路径需按目标 SDK 文档复核权限边界 |
HarmonyOS SDK @ohos.multimedia.cameraPicker.d.ts |
系统 API 类型定义 | PickerProfile、PickerMediaType、PickerResult、pick() |
环境与版本边界
| 项目 | 当前值 | 说明 |
|---|---|---|
| 工程路径 | D:/APP/1quweixiangji |
当前实现使用 CameraKit 自定义预览 |
| 应用版本 | 1.0.2 |
来自 AppScope/app.json5 |
| target SDK | 6.0.2(22) |
本机 SDK 类型定义包含 CameraPicker API |
| 应用模型 | HarmonyOS Stage 模型 | 页面从 UIAbility 上下文调用能力 |
| CameraPicker 起始版本 | API 11 | 本机 SDK 类型定义标注 |
| 元服务支持标记 | API 12 | 本机 SDK 类型定义标注 atomicservice |
| 当前图像结果 | PixelMap + CapturedPhoto 元数据 | Picker 返回 resultUri,需要统一建模 |

本地 SDK 类型定义位置:
text
D:\Program Files\Huawei\DevEco Studio\sdk\default\openharmony\ets\api\@ohos.multimedia.cameraPicker.d.ts
D:\Program Files\Huawei\DevEco Studio\sdk\default\openharmony\ets\kits\@kit.CameraKit.d.ts
本文使用本机 SDK 中的真实签名,不把旧 Android Camera Engine 文档混入 HarmonyOS ArkTS API。
一、先看 CameraPicker 的真实类型
CameraKit 聚合导出:
ts
import camera from '@ohos.multimedia.camera';
import cameraPicker from '@ohos.multimedia.cameraPicker';
export { camera, cameraPicker };
应用侧可以统一从 Kit 导入:
ts
import { camera, cameraPicker } from '@kit.CameraKit';
PickerProfile 包含:
ts
class PickerProfile {
cameraPosition: camera.CameraPosition;
saveUri?: string;
videoDuration?: number;
}
媒体类型:
ts
enum PickerMediaType {
PHOTO = 'photo',
VIDEO = 'video'
}
结果:
ts
class PickerResult {
resultCode: number;
resultUri: string;
mediaType: PickerMediaType;
}
调用签名:
ts
function pick(
context: Context,
mediaTypes: Array<PickerMediaType>,
pickerProfile: PickerProfile
): Promise<PickerResult>;
这条 API 的核心是"启动系统相机选择器并返回 URI",而不是给应用一个可嵌入的 PreviewOutput。
二、CameraPicker 适合什么场景
CameraPicker 更适合:
- 表单中上传一张现场照片。
- 头像、证件或工单拍照。
- 应用不需要自定义实时预览。
- 不需要持续控制闪光灯、变焦和对焦。
- 不需要对每一帧做人体识别。
- 希望减少 CameraSession 生命周期和机型适配成本。
典型流程:
text
用户点击"系统相机拍摄"
-> 构造 PickerProfile
-> cameraPicker.pick()
-> 用户在系统界面拍摄或取消
-> 返回 PickerResult
-> 校验 resultCode / resultUri / mediaType
-> 读取或保存业务元数据
应用不需要自己创建 XComponent、CameraInput、PreviewOutput 和 PhotoSession。
三、CameraKit 适合什么场景
当前趣味相机必须保留 CameraKit,因为它已经实现:
| 产品能力 | 依赖的自定义能力 |
|---|---|
| 应用内实时取景 | XComponent Surface + PreviewOutput |
| 前后镜头切换 | CameraManager + CameraInput 重建 |
| 闪光灯按钮 | PhotoSession 能力探测与 setFlashMode |
| 3/5/10 秒倒计时 | 页面状态与拍照时序 |
| 三档拍照质量 | PhotoCaptureSetting + QualityLevel |
| 水印模板 | 拍照瞬间 WatermarkSnapshot |
| CoreVision 人体识别 | 预览帧或拍照 PixelMap 分析 |
| 结果弹层 | 页面持有真实 PixelMap |
| 双摄升级 | 并发能力探测和双 Surface |
CameraPicker 不能提供一个可由当前页面自由叠加水印和识别结果的实时预览 Surface,因此直接替换会让产品能力退化。
四、用决策表选型
| 需求 | CameraPicker | CameraKit |
|---|---|---|
| 只拍一张照片 | 推荐 | 可以但成本高 |
| 使用系统拍摄界面 | 推荐 | 不适用 |
| 应用内自定义预览 | 不适用 | 推荐 |
| 实时水印叠加 | 不适用 | 推荐 |
| 预览帧人体识别 | 不适用 | 推荐 |
| 自定义闪光灯/变焦 | 不适用 | 推荐 |
| 前后双摄同开 | 不适用 | CameraKit 能力探测 |
| 生命周期代码最少 | 推荐 | 需要完整资源治理 |
| 拿回结果 URI | 原生返回 | 需要自行写入文件或图库 |
| 拿回 PixelMap | 需要从 URI 解码 | PhotoOutput 可直接解码交付 |
一句话原则:只需要"结果"优先 Picker,需要"过程控制"使用 CameraKit。
五、CameraPicker 最小拍照封装
可以单独创建 SystemCameraPickerService.ets:
ts
import { camera, cameraPicker } from '@kit.CameraKit';
import { common } from '@kit.AbilityKit';
export interface SystemPhotoResult {
success: boolean;
cancelled: boolean;
uri: string;
message: string;
}
export class SystemCameraPickerService {
static async takePhoto(
context: common.UIAbilityContext,
position: camera.CameraPosition
): Promise<SystemPhotoResult> {
try {
const profile: cameraPicker.PickerProfile =
new cameraPicker.PickerProfile();
profile.cameraPosition = position;
const result: cameraPicker.PickerResult =
await cameraPicker.pick(
context,
[cameraPicker.PickerMediaType.PHOTO],
profile
);
if (result.resultUri.length === 0) {
return {
success: false,
cancelled: true,
uri: '',
message: '已取消系统相机拍摄'
};
}
return {
success: true,
cancelled: false,
uri: result.resultUri,
message: '系统相机拍摄完成'
};
} catch (error) {
return {
success: false,
cancelled: false,
uri: '',
message: '系统相机启动失败,请稍后重试'
};
}
}
}
正式实现时还应按目标 SDK 说明解释 resultCode,不要只靠 URI 判定取消。这里重点展示封装边界:页面只接收稳定业务结果,不直接散落 Picker API。
六、页面不能自动把用户带离当前相机
CameraKit 预览失败时,不建议无提示自动启动系统相机。系统 Picker 会切换到系统拍摄界面,属于明显导航行为,应由用户确认。
推荐页面状态:
ts
@State showSystemCameraFallback: boolean = false;
@State systemCameraBusy: boolean = false;
当 CameraKit 返回相机冲突或设备兼容问题时显示:
text
当前自定义相机无法启动
[重新启用相机] [使用系统相机拍摄]
用户点击第二个按钮后才调用 Picker。这样既保留当前页面,也让回退行为可预期。
七、调用 Picker 前先释放自定义会话
如果当前 CameraKit 会话仍然存在,直接启动系统相机可能造成资源竞争。页面入口应先停止预览:
ts
private async openSystemCamera(): Promise<void> {
if (this.systemCameraBusy) {
return;
}
this.systemCameraBusy = true;
try {
await CameraPreviewService.stopPreview();
this.previewStatus = 'idle';
const context: common.UIAbilityContext =
this.getUIContext().getHostContext() as common.UIAbilityContext;
const position: camera.CameraPosition =
this.activeCameraPosition === 'front' ?
camera.CameraPosition.CAMERA_POSITION_FRONT :
camera.CameraPosition.CAMERA_POSITION_BACK;
const result: SystemPhotoResult =
await SystemCameraPickerService.takePhoto(context, position);
await this.handleSystemPhotoResult(result);
} finally {
this.systemCameraBusy = false;
}
}
核心顺序:先释放 CameraKit,再进入系统 Picker,避免本应用仍占用摄像头。
八、Picker 返回后恢复自定义预览
无论用户拍摄、取消还是 Picker 异常,返回页面后都要重新判断:
ts
private async restoreCustomPreview(): Promise<void> {
if (!this.isCameraGranted() ||
!this.cameraAvailable ||
this.previewSurfaceId.length === 0) {
return;
}
await this.startCameraPreview();
}
恢复应放在统一出口:
ts
try {
// 打开 Picker 并处理结果
} finally {
this.systemCameraBusy = false;
await this.restoreCustomPreview();
}
如果页面已经退出或 Surface 已销毁,就不要重建预览。可结合 pageActive 和 previewGeneration 判断。
九、统一照片来源模型
当前模型:
ts
export type CaptureSource = 'real' | 'simulated';
接入 Picker 后建议扩展:
ts
export type CaptureSource =
'cameraKit' | 'systemPicker' | 'imported';
export interface CapturedPhoto {
id: string;
title: string;
createdAt: string;
captureSource: CaptureSource;
sourceUri?: string;
captureSummary: string;
status: 'preview' | 'saved';
watermark?: WatermarkSnapshot;
}
不要把 Picker 结果伪装成 CameraKit PixelMap 拍摄。来源字段能帮助后续决定:
- 是否需要从 URI 解码预览。
- 是否能复用拍照瞬间水印快照。
- 删除记录时是否应该删除外部文件。
- 导出文档时从 PixelMap 还是 URI 读取。
十、Picker 结果与水印快照的关系
当前 CameraKit 在快门时调用:
ts
this.watermarkSnapshot()
系统 Picker 的拍摄发生在外部界面,返回时页面的水印状态可能已经变化。推荐在启动 Picker 前保存快照:
ts
const pendingWatermark: WatermarkSnapshot =
this.watermarkSnapshot();
const result: SystemPhotoResult =
await SystemCameraPickerService.takePhoto(context, position);
创建照片记录时使用 pendingWatermark,而不是重新读取当前页面状态。这样来源照片与业务元数据属于同一次操作。
需要明确:系统拍摄结果本身未必已经烧录水印。若要生成真正带水印的图片,还要读取 URI、解码 PixelMap、应用层合成并保存新文件;只保存 WatermarkSnapshot 只是业务元数据。
十一、saveUri 的使用边界
PickerProfile 提供可选:
ts
saveUri?: string;
它允许调用方指定结果保存 URI,但 URI 必须来自合法、可写、符合目标 SDK 文件访问规则的来源。不要手工拼接任意文件路径作为 URI。
如果不需要固定保存位置,可以让 Picker 返回 resultUri,再由业务层决定是否只保存引用、复制到应用沙箱或生成新的带水印文件。正式实现前应按当前文件管理和媒体访问文档验证 URI 生命周期与访问权限。
十二、PHOTO 与 VIDEO 不要共用同一业务入口
CameraPicker 支持:
ts
cameraPicker.PickerMediaType.PHOTO
cameraPicker.PickerMediaType.VIDEO
虽然 pick() 接受媒体类型数组,但照片和视频的后续处理差异很大:
| 项目 | 照片 | 视频 |
|---|---|---|
| 结果处理 | 图片 URI/PixelMap | 视频 URI/时长/封面 |
| 权限评估 | 相机 | 带声音视频还涉及麦克风 |
| 业务模型 | CapturedPhoto | 应单独定义 CapturedVideo |
| 水印 | 单帧合成 | 视频编码或系统能力 |
| 资源成本 | 相对低 | 更高且需要时长限制 |
当前趣味相机只设计 PHOTO 路径,不应顺手把 VIDEO 加进同一个照片模型。
十三、CameraPicker 不能替代哪些能力
即使系统 Picker 更稳定,它也不能替代当前产品中的:
- 自定义取景框和九宫格。
- 实时水印预览。
- 倒计时遮罩与页面快门反馈。
- 自定义闪光灯状态和能力提示。
- 三档 PhotoCaptureSetting 质量偏好。
- CoreVision 预览帧采样。
- 智能贴纸目标跟随。
- 前后双摄画中画扩展。
所以架构上应是"双入口、统一结果",而不是用 Picker 替换 CameraPreviewService。
十四、推荐的双入口架构
text
拍摄页
|- 自定义拍摄
| -> CameraPreviewService
| -> CameraKit PhotoOutput
| -> PixelMap
|
|- 系统相机拍摄
-> SystemCameraPickerService
-> CameraPicker
-> resultUri
统一结果层
-> CapturedPhoto
-> PhotoAlbumService
-> PhotoDocumentService
统一的是业务结果,不统一底层资源对象。CameraKit 的 PixelMap 和 Picker 的 URI 应各自有清晰适配器。
十五、错误与取消状态
建议业务结果区分:
ts
export type SystemPickerStatus =
'success' | 'cancelled' | 'failed';
| 状态 | 页面行为 |
|---|---|
| success | 创建 systemPicker 来源照片,展示结果 |
| cancelled | 不显示错误,恢复自定义预览 |
| failed | 显示系统相机启动失败,并允许重试 |
用户取消是正常交互,不应该记录成错误日志或弹红色错误提示。
十六、权限与隐私边界
当前 CameraKit 主流程声明 ohos.permission.CAMERA 并进行运行时授权。CameraPicker 路径的调用权限、URI 访问和保存策略应按目标 SDK 的最新官方文档逐项确认,不要根据 CameraKit 的权限流程直接复制,也不要假设系统 Picker 返回的 URI 可以永久持有。
隐私说明至少覆盖:
- 用户主动选择系统相机拍摄。
- 应用接收系统 Picker 返回的结果 URI。
- 是否复制图片到应用沙箱。
- 是否生成带水印的新文件。
- 删除应用记录是否会影响原始系统照片。
十七、建议补充的测试
服务结果测试
| 输入 | 期望 |
|---|---|
| resultUri 有效且 mediaType=PHOTO | success |
| 用户取消 | cancelled,不创建照片 |
| pick 抛异常 | failed,允许重试 |
| mediaType 不是 PHOTO | 拒绝进入 CapturedPhoto |
页面集成测试
- 自定义预览运行时点击系统相机。
- 确认先释放 CameraKit 会话。
- 系统拍摄成功后创建 systemPicker 来源记录。
- 用户取消后不生成空记录。
- 返回页面后只重建一次自定义预览。
- 页面已退出时不重建预览。
- Picker 返回 URI 无法读取时给出明确提示。
- 水印快照使用启动 Picker 前的状态。
十八、常见问题排查
| 现象 | 可能原因 | 排查方式 |
|---|---|---|
| 打开系统相机时报占用 | 自定义 CameraKit 会话未释放 | Picker 前先 stopPreview |
| 取消后显示错误 | 没区分 cancelled 和 failed | 建立三态结果 |
| 返回后自定义预览黑屏 | 没有统一重建入口 | finally 中按闸门恢复 |
| 返回后启动两次预览 | Surface 回调和 Picker 返回同时触发 | 使用 previewGeneration/starting 锁 |
| Picker 照片没有水印 | 只保存了 WatermarkSnapshot | 需要 URI 解码后实际合成 |
| 水印内容与拍摄时不一致 | 返回后重新读取页面状态 | 启动 Picker 前保存快照 |
| 删除记录误删系统照片 | 没区分来源 URI 所有权 | CapturedPhoto 保存来源与删除策略 |
| resultUri 过期或不可读 | URI 生命周期处理不当 | 按文件访问文档复制或持久化 |
| 前置偏好没有生效 | PickerProfile cameraPosition 未设置 | 显式传入当前方向 |
| 视频误进照片流程 | PHOTO/VIDEO 共用模型 | 按 mediaType 分流 |
| 权限申请重复或错误 | 直接复制 CameraKit 权限逻辑 | 按 Picker 目标 SDK 文档复核 |
| 系统相机突然自动打开 | 失败后自动回退未征得用户操作 | 只由明确按钮启动 Picker |
十九、上线前验收清单
- 已按"只要结果"和"需要过程控制"完成选型。
- 当前趣味相机继续使用 CameraKit 主流程。
- CameraPicker 放在独立服务,不混入 PreviewService。
- Picker API 签名与本机目标 SDK 类型定义一致。
- 只在用户明确点击后启动系统相机。
- 启动 Picker 前释放 CameraKit 会话。
- Picker 返回后按页面和 Surface 状态恢复预览。
- success、cancelled、failed 三种结果分开处理。
- resultUri 为空或不可读时不创建照片记录。
- CapturedPhoto 区分 cameraKit、systemPicker 和 imported 来源。
- 启动 Picker 前保存水印快照。
- 不把 WatermarkSnapshot 描述成已经烧录到图片。
- PHOTO 与 VIDEO 使用不同业务模型。
- saveUri 使用合法 URI,不拼接任意路径。
- 删除记录不会误删不归应用所有的系统照片。
- 权限与 URI 生命周期按目标 SDK 官方文档复核。
- 自定义预览、系统拍摄成功、取消、异常和返回恢复完成真机测试。
- SDK 升级后重新核对 PickerProfile、PickerResult 和 pick() 签名。
总结
CameraPicker 和 CameraKit 不是新旧替代关系,而是两种不同产品边界:CameraPicker 负责把用户带到系统拍摄界面并返回结果 URI,CameraKit 负责应用内自定义预览和完整相机控制。1quweixiangji 的倒计时、水印、闪光灯、CoreVision 和双摄扩展都依赖 CameraKit,因此主流程不能迁移成 Picker。
更稳妥的架构是保留 CameraKit 自定义拍摄,同时提供用户可控的系统相机回退。进入 Picker 前释放自定义会话,返回后按页面状态重建预览;底层分别接收 PixelMap 和 resultUri,业务层统一转换成带来源信息的 CapturedPhoto。这样既保留趣味相机的差异化能力,也能在兼容性或相机冲突场景下给用户一条明确、可恢复的拍摄路径。