HarmonyOS趣味相机实战第13篇:CameraPicker与CameraKit选型、系统拍照回退及架构边界

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 类型定义 PickerProfilePickerMediaTypePickerResultpick()

环境与版本边界

项目 当前值 说明
工程路径 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 更稳定,它也不能替代当前产品中的:

  1. 自定义取景框和九宫格。
  2. 实时水印预览。
  3. 倒计时遮罩与页面快门反馈。
  4. 自定义闪光灯状态和能力提示。
  5. 三档 PhotoCaptureSetting 质量偏好。
  6. CoreVision 预览帧采样。
  7. 智能贴纸目标跟随。
  8. 前后双摄画中画扩展。

所以架构上应是"双入口、统一结果",而不是用 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

页面集成测试

  1. 自定义预览运行时点击系统相机。
  2. 确认先释放 CameraKit 会话。
  3. 系统拍摄成功后创建 systemPicker 来源记录。
  4. 用户取消后不生成空记录。
  5. 返回页面后只重建一次自定义预览。
  6. 页面已退出时不重建预览。
  7. Picker 返回 URI 无法读取时给出明确提示。
  8. 水印快照使用启动 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。这样既保留趣味相机的差异化能力,也能在兼容性或相机冲突场景下给用户一条明确、可恢复的拍摄路径。

相关推荐
2301_768103497 小时前
HarmonyOS趣味相机实战第14篇:装饰素材强类型建模、锚点语义与套装组合校验
harmonyos·arkts·数据建模·arkui·相机贴纸
不羁的木木7 小时前
HarmonyOS技术精讲-Connectivity Kit:Wi-Fi进阶能力——热点、P2P与Aware
asp.net·harmonyos·p2p
Helen_cai7 小时前
HarmonyOS ArkTS 实战:实现一个校园宿舍管理与查寝应用
华为·harmonyos
listening7777 小时前
HarmonyOS ArkTS 实战:实现一个校园打印复印预约应用
华为·harmonyos
绝世番茄8 小时前
鸿蒙原生 ArkTS 布局之道:Grid 替代多层嵌套布局深度解析
华为·harmonyos·鸿蒙
●VON8 小时前
鸿蒙 PC Markdown 编辑器可靠性设计:未保存内容的崩溃恢复闭环
华为·中间件·编辑器·harmonyos·鸿蒙
listening7778 小时前
《从算法到落地:基于HarmonyOS API 23的端侧图像推理实战》
算法·华为·harmonyos
●VON9 小时前
鸿蒙 PC Markdown 编辑器导出架构:安全 HTML 与系统 PDF
华为·架构·编辑器·harmonyos·鸿蒙
不羁的木木9 小时前
HarmonyOS技术精讲-Connectivity Kit(短距通信服务):初识统一短距通信框架
华为·wpf·harmonyos