HarmonyOS技术精讲-Camera Kit(相机服务)第14篇:CameraPicker快速唤醒系统相机
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_MEDIA和ohos.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));
}
}
}
这段代码里需要注意的点:
-
getContext(this)获取的必须是UIAbilityContext,不能是UIContext。如果是在子组件里调用,需要从父组件传下来。 -
PickerConfig目前可以不传参数,但未来版本可能会增加配置项,比如最大时长、照片质量等。 -
返回的
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://可能无法正确加载。
解决方案:使用photoAccessHelper的requestFileUri方法获取可访问的文件路径:
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
takePicture和takeVideo都是异步操作,如果在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做持久化存储,且后续不再使用这张图片,建议及时调用photoAccessHelper的deleteAssets方法清理临时文件。系统不会自动清理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是否真的可访问,以及是否在正确的生命周期节点调用。官方文档对这个行为描述得比较简单,建议结合实际运行效果一起验证。不同设备上的行为可能存在差异,建议真机测试。
更多推荐

所有评论(0)