HarmonyOS技术精讲-Camera Kit(相机服务)第14篇:CameraPicker快速唤醒系统相机

在这里插入图片描述

为什么需要CameraPicker

在HarmonyOS NEXT开发中,相机功能几乎是每个涉及媒体处理的应用的标配。但直接使用Camera Kit开发相机应用,需要申请ohos.permission.CAMERA权限,这个权限属于用户敏感权限,申请流程长、用户拒绝率高。

更麻烦的是,很多场景下开发者根本不需要自定义取景界面、不需要控制曝光和对焦参数,只是想让用户拍张照片或者录段视频,然后把文件拿回来用。这种情况下自己搭一套相机界面,属于明显的过度开发。

CameraPicker就是来解决这个问题的。它是系统级相机快捷入口,调用后会直接拉起系统原生的相机界面,用户拍完或录完后自动返回,开发者拿到的是photoAccessHelper的URI。最关键的一点是:不需要申请相机权限

这个设计思路在iOS的UIImagePickerController和Android的Intent.ACTION_IMAGE_CAPTURE上已经验证过很多年,HarmonyOS NEXT在API 12开始也提供了类似能力。

和直接开发Camera Kit的对比

维度 直接开发Camera Kit 使用CameraPicker
权限要求 需申请CAMERA、MIC等权限 无需额外权限
开发工作量 高,需自定义UI、状态管理 低,仅需调用接口
功能可定制性 完全可控 受限于系统相机
返回数据类型 Surface/Buffer/Image 文件URI
适配工作量 需处理多设备适配 系统相机已统一

选择CameraPicker的场景很明确:你只需要结果,不需要过程。用户头像、OCR扫描、证件照、快捷分享这些场景都适合。


环境要求

DevEco Studio版本:DevEco Studio 6.1.0及以上
HarmonyOS SDK版本:HarmonyOS 6.1.0(23)及以上
目标设备:手机(支持相机功能的设备)

注意:模拟器上调用CameraPicker通常会直接返回失败或崩溃,这个需要在真机上测试。


核心实现

前置准备

虽然CameraPicker不需要相机权限,但仍然需要申请ohos.permission.READ_MEDIAohos.permission.WRITE_MEDIA权限。这个很多人会忽略。原因是:拍摄完成后,你需要把照片或视频存到媒体库,写媒体库文件需要媒体库权限。

module.json5中配置:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.READ_MEDIA",
        "reason": "用于保存拍摄的照片和视频"
      },
      {
        "name": "ohos.permission.WRITE_MEDIA",
        "reason": "用于保存拍摄的照片和视频"
      }
    ]
  }
}

拍照功能实现

CameraPicker的核心API是@ohos.multimedia.cameraPicker模块中的takePicture方法。

下面这个组件包含了拍照入口和结果展示:

// CameraPickerPage.ets
import { cameraPicker } from '@ohos.multimedia.cameraPicker';
import { photoAccessHelper } from '@ohos.multimedia.mediaLibrary';
import { common } from '@kit.AbilityKit';

@Entry
@Component
struct CameraPickerPage {
  @State selectedImageUri: string = '';

  build() {
    Column() {
      // 拍照入口按钮
      Button('打开相机拍照')
        .width('100%')
        .height(48)
        .margin({ bottom: 12 })
        .onClick(() => {
          this.takePhoto();
        })

      // 如果选择了图片,展示缩略图
      if (this.selectedImageUri) {
        Image(this.selectedImageUri)
          .width('100%')
          .height(300)
          .objectFit(ImageFit.Cover)
          .margin({ top: 12 })
      }
    }
    .padding(16)
    .width('100%')
    .height('100%')
  }

  private async takePhoto(): Promise<void> {
    try {
      // 获取上下文,用于拉起系统相机
      let context = getContext(this) as common.UIAbilityContext;
      
      // 创建Picker的配置
      let pickerConfig: cameraPicker.PickerConfig = {
        // 不传任何额外配置,使用默认参数
      };

      // 调用拍照
      let result = await cameraPicker.takePicture(context, pickerConfig);
      
      if (result && result.uri) {
        this.selectedImageUri = result.uri;
        console.info('CameraPicker拍照成功,URI:', result.uri);
      } else {
        console.warn('CameraPicker拍照返回结果异常');
      }
    } catch (error) {
      console.error('CameraPicker拍照失败:', JSON.stringify(error));
    }
  }
}

这段代码里需要注意的点:

  1. getContext(this)获取的必须是UIAbilityContext,不能是UIContext。如果是在子组件里调用,需要从父组件传下来。

  2. PickerConfig目前可以不传参数,但未来版本可能会增加配置项,比如最大时长、照片质量等。

  3. 返回的result.uri是一个photoAccessHelper的URI,格式类似content://media/external/images/media/xxx,可以直接传给Image组件显示。

录像功能实现

录像对应的是takeVideo方法,代码结构类似:

// CameraPickerVideoPage.ets
import { cameraPicker } from '@ohos.multimedia.cameraPicker';
import { photoAccessHelper } from '@ohos.multimedia.mediaLibrary';
import { common } from '@kit.AbilityKit';

@Entry
@Component
struct CameraPickerVideoPage {
  @State selectedVideoUri: string = '';

  build() {
    Column() {
      Button('打开相机录像')
        .width('100%')
        .height(48)
        .margin({ bottom: 12 })
        .onClick(() => {
          this.takeVideo();
        })

      if (this.selectedVideoUri) {
        // 视频用Video组件播放
        Video({
          src: this.selectedVideoUri,
          currentProgressRate: 1.0,
          previewUri: ''
        })
        .width('100%')
        .height(300)
        .controls(true)
        .margin({ top: 12 })
      }
    }
    .padding(16)
    .width('100%')
    .height('100%')
  }

  private async takeVideo(): Promise<void> {
    try {
      let context = getContext(this) as common.UIAbilityContext;
      
      let pickerConfig: cameraPicker.PickerConfig = {};

      let result = await cameraPicker.takeVideo(context, pickerConfig);
      
      if (result && result.uri) {
        this.selectedVideoUri = result.uri;
        console.info('CameraPicker录像成功,URI:', result.uri);
      } else {
        console.warn('CameraPicker录像返回结果异常');
      }
    } catch (error) {
      console.error('CameraPicker录像失败:', JSON.stringify(error));
    }
  }
}

录像返回的URI也是photoAccessHelper格式,可以直接给Video组件使用。

使用拿到的URI

拿到URI后,通常需要做两件事:读取文件内容、保存到相册。读取数据需要配合photoAccessHelper

import { photoAccessHelper } from '@ohos.multimedia.mediaLibrary';

async function readPhotoFromUri(uri: string): Promise<ArrayBuffer | null> {
  try {
    let helper = photoAccessHelper.getPhotoAccessHelper(getContext(this));
    let fetchOp = {
      uri: uri
    };
    let fetchResult = await helper.getAssets(fetchOp);
    
    if (fetchResult && fetchResult.length > 0) {
      let asset = fetchResult[0];
      let fd = await asset.open('r');
      // 读取fd,具体代码略
      return null;
    }
    
    return null;
  } catch (error) {
    console.error('读取照片失败:', JSON.stringify(error));
    return null;
  }
}

这里有个坑:getAssets的查询条件如果传uri,必须是完整的content://路径,不能只传文件名。


常见问题

问题1:系统相机关闭后回调不生效

现象:调用takePicture后,系统相机被拉起来,但按确认键后页面没有回到原应用,或者回调没有被触发。

原因:这不是CameraPicker的问题,而是takePicture是异步方法,如果调用它的页面在相机打开期间被销毁了(比如内存不足被回收),回调不会再执行。

解决方案:不要在aboutToAppear或组件的构造函数中直接调用takePicture,因为此时上下文可能还不稳定。建议使用按钮手动触发,且触发前确保当前页面处于Foreground状态。

问题2:真机上返回的URI在Image组件中不显示

现象:拍照成功后,把URI赋值给Image组件的src属性,但图片不显示。

原因:URI是photoAccessHelper格式,Image组件虽然支持这种格式,但需要先通过photoAccessHelper的API获取到文件描述符,然后通过file://协议访问。直接传content://可能无法正确加载。

解决方案:使用photoAccessHelperrequestFileUri方法获取可访问的文件路径:

import { fileIo } from '@kit.CoreFileKit';

async function getAccessibleUri(context: common.UIAbilityContext, contentUri: string): Promise<string> {
  let helper = photoAccessHelper.getPhotoAccessHelper(context);
  // 通过contentUri获取文件信息
  let uri = await helper.getFileUriByContentUri(contentUri);
  return uri;  // 返回file://开头的路径
}

拿到file://路径后再传给Image组件,基本不会有问题。


最佳实践

1. 不要在build中直接调用takePicture

takePicturetakeVideo都是异步操作,如果在build方法中直接调用,会导致组件在渲染期间触发异步任务,容易出现状态不一致问题。建议始终通过按钮或用户交互事件来触发。

2. 做好取消返回的处理

用户可能打开相机后直接返回,此时takePicture会抛出一个BusinessError,错误码通常是401(操作取消)。需要区分取消和真正的异常:

try {
  let result = await cameraPicker.takePicture(context, {});
} catch (error) {
  if (error.code === 401) {
    console.info('用户取消了拍照');
    // 不弹Toast打扰用户
  } else {
    console.error('拍照遇到错误:', error.message);
    // 弹出错误提示
  }
}

3. 图片保存后及时释放资源

如果不对URI做持久化存储,且后续不再使用这张图片,建议及时调用photoAccessHelperdeleteAssets方法清理临时文件。系统不会自动清理CameraPicker产生的临时文件,长时间累积会占用存储空间。

async function deleteTempFile(uri: string): Promise<void> {
  let helper = photoAccessHelper.getPhotoAccessHelper(getContext(this));
  await helper.deleteAssets([uri]);
  console.info('临时文件已删除');
}

完整Demo入口

// Index.ets
import { router } from '@kit.AbilityKit';

@Entry
@Component
struct Index {
  build() {
    Column({ space: 16 }) {
      Button('CameraPicker拍照Demo')
        .width('100%')
        .height(48)
        .onClick(() => {
          router.pushUrl({
            url: 'pages/CameraPickerPage'
          });
        })

      Button('CameraPicker录像Demo')
        .width('100%')
        .height(48)
        .onClick(() => {
          router.pushUrl({
            url: 'pages/CameraPickerVideoPage'
          });
        })
    }
    .padding(16)
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

FAQ

Q:为什么真机正常,模拟器不生效?

A:模拟器没有实际相机硬件,CameraPicker在模拟器上会直接抛异常。这属于正常行为,不是代码问题。必须在真机上测试。

Q:为什么第一次授权成功,第二次失败?

A:检查一下权限申请逻辑。CameraPicker不需要相机权限,但需要媒体库权限。如果用户拒绝了媒体库权限,第二次调用时权限检查会失败。建议在调用前用abilityAccessCtrl检查权限状态,授权失败时引导用户去设置页手动开启。

Q:返回的URI是临时的还是永久的?

A:返回的URI指向系统相册中实际保存的文件,是永久有效的。但需要注意:如果用户后续手动删除了这张照片,URI对应的文件就不存在了。所以建议如果有长期使用的需求,把文件复制到应用沙箱目录。

Q:CameraPicker支持设置照片质量或分辨率吗?

A:当前版本(API 12)的PickerConfig不支持传这些参数,系统相机会使用默认参数。如果需要精细控制,建议走Camera Kit的全套流程。CameraPicker的设计定位就是快速且简单的方案。


如果你也遇到类似问题,重点检查一下takePicture返回的URI是否真的可访问,以及是否在正确的生命周期节点调用。官方文档对这个行为描述得比较简单,建议结合实际运行效果一起验证。不同设备上的行为可能存在差异,建议真机测试。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐