背景
在聊天软件中,发送相册中视频和照片、用相机拍摄视频和图片发送是很常用的功能。在Android和iOS端,大部分应用都通过API方式定义UI来实现相册选择照片、视频,相机拍摄照片、视频,它们一般都支持以下功能:
相册选择:
支持单选或多选;
对图片支持是否原图选择;
对于视频支持选择视频的文件大小、视频时长等过滤;
支持点击图像放大预览
对于相机拍摄
支持点击拍照,长按录制视频;
视频录制支持最大最小录制时长限制;
拍摄或录制结束后支持预览。
对于鸿蒙应用要实现上述功能,系统也提供了对应API,要实现上述功能需要几个系统权限:
读取系统相册权限
麦克风权限
摄像头权限
HarmonyOS 权限系统介绍
与Android系统相比,HarmonyOS提供了更严谨的权限控制,这里不得不提HarmonyOS的应用权限管控策略。HarmonyOS 提供了一种允许应用访问系统资源(如:通讯录等)和系统能力(如:访问摄像头、麦克风等)的通用权限访问方式,来保护系统数据(包括用户个人数据)或功能,避免它们被不当或恶意使用,应用权限保护的对象可以分为数据和功能:
数据包括个人数据(如照片、通讯录、日历、位置等)、设备数据(如设备标识、相机、麦克风等)。
功能包括设备功能(如访问摄像头/麦克风、打电话、联网等)、应用功能(如弹出悬浮窗、创建快捷方式等)。
同时HarmonyOS 根据授权方式的不同,权限类型可分为system_grant(系统授权)和user_grant(用户授权):
system_grant(系统授权)指的是系统授权类型,在该类型的权限许可下,应用被允许访问的数据不会涉及到用户或设备的敏感信息,应用被允许执行的操作对系统或者其他应用产生的影响可控。如果在应用中申请了system_grant权限,那么系统会在用户安装应用时,自动把相应权限授予给应用。
user_grant(用户授权)指的是用户授权类型,在该类型的权限许可下,应用被允许访问的数据将会涉及到用户或设备的敏感信息,应用被允许执行的操作可能对系统或者其他应用产生严重的影响。该类型权限不仅需要在安装包中申请权限,还需要在应用动态运行时,通过发送弹窗的方式请求用户授权。在用户手动允许授权后,应用才会真正获取相应权限,从而成功访问操作目标对象。
例如,在应用权限列表中,麦克风和摄像头对应的权限都是属于用户授权权限,列表中给出了详细的权限使用理由。应用需要在应用商店的详情页面,向用户展示所申请的user_grant权限列表。
除了授权方式外还要了解另一个概念APL(Ability Privilege Level,元能力权限等级)等级。应用的等级可以分为以下三个等级,等级依次提高。
APL级别 | 说明 |
---|---|
normal | 默认情况下,应用的APL等级都为normal等级。 |
system_basic | 该等级的应用服务提供系统基础服务。 |
system_core | 该等级的应用服务提供操作系统核心能力。 应用APL等级不允许配置为system_core。 |
根据权限对于不同等级应用有不同的开放范围,权限类型对应分为以下三个等级,等级依次提高。
APL级别 | 说明 | 开放范围 |
---|---|---|
normal | 允许应用访问超出默认规则外的普通系统资源,如配置Wi-Fi信息、调用相机拍摄等。这些系统资源的开放(包括数据和功能)对用户隐私以及其他应用带来的风险低。 | APL等级为normal及以上的应用。 |
system_basic | 允许应用访问操作系统基础服务(系统提供或者预置的基础功能)相关的资源,如系统设置、身份认证等。这些系统资源的开放对用户隐私以及其他应用带来的风险较高。 | 1、APL等级为system_basic及以上的应用。2、部分权限对normal级别的应用受限开放,这部分权限在本指导中描述为“受限开放权限”。 |
system_core | 涉及开放操作系统核心资源的访问操作。这部分系统资源是系统最核心的底层服务,如果遭受破坏,操作系统将无法正常运行。 | 1、 APL等级为system_core的应用。2、 仅对系统应用开放。 |
了解了授权方式和权限等级后,我们再来聊聊这个设计背后的原因。 | ||
首先聊聊授权方式,其中系统授权类似于Android中的普通权限申请,在清单文件声明即可,对于用户授权,类似于Android中的动态权限,需要触发弹窗让用户感知后决定是否授权。这个涉及背后的逻辑很清晰,对于数据不会涉及到用户或设备的敏感信息(比如使用网络、使用蓝牙等),默认声明后授权不会对系统产生什么破坏,如果也强制用户感知授权后才可以使用,对于开发者来说增加工作量,对用户体验也不是很友好; | ||
其次,对于权限等级,HarmonyOS分成了三类,normal 级别的,即普通应用应用可以使用的权限,比如相机,麦克风等,只要说明使用的场景即可向用户申请;system_core级别的是只有系统应用可以使用,普通应用无法申请,如果普通应用使用可能会对系统造成不可修复的错误,比如类似Android的root权限,如果普通应用申请到后,删除了系统核心文件等,系统会遭到破坏;第三种是system_basic,主要对系统应用开发,对普通应用受限开放,什么是受限开放呢?比如说读取相册权限,如果授权给普通应用,用户赋予它这个权限后,它偷偷的将相册数据传送到应用服务端用户完全无法感知,但是对于一些应用,比如网盘、相册备份类应用,没有这个权限还无法工作,所以系统设计时考虑到这一点,对于特殊的应用向平台申请后,平台根据应用类型给特殊应用放开该权限。可能有人会疑惑,为什么读取相册的是受限,而相机、麦克风是normal权限,因为麦克风、相机无法在用户无感知时使用,而读取文件可以。 |
回到正题,选择图片视频需要system_basic 受限权限,麦克风相机也需要用户授权,为了减少授权导致的操作流程终端,HarmonyOS 提供了系统Picker,可以使用系统组件再不需要权限的情况下完成功能。 应用拉起系统Picker组件(文件选择器、照片选择器、联系人选择器等),由用户在Picker上选择对应的文件、照片、联系人等资源,应用即可获取到Picker的返回结果,系统Picker由系统独立进程实现,从系统Picker获取资源是一个跨进程调度过程。
为什么使用系统Picker获取资源不需要权限?因为从系统Picker获取资源需要用户操作,是用户可以感知的,所以不需要再申请权限。注意,此时应用获取到的读取资源的权限是临时的,受限的,只能临时受限访问对应的资源。我们从Picker里选择了1.jpeg,在我们应用进程可以操作这个文件,我们从系统相册中获取到另一个2.jpeg文件,在进程里直接访问是不可以的,而且对1.jpeg的访问也是有时效性的。
接下来介绍如何基于系统API,从相册选择照片和视频,以及从相机拍摄视频和照片。
从相册选择
HarmonyOS 提供了 photoAccessHelper.PhotoViewPicker() 获取系统相册中的图片,下面是使用PhotoViewPicker的示例:
import { BusinessError } from '@kit.BasicServicesKit';
async function example01() {
try {
let PhotoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
PhotoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
PhotoSelectOptions.maxSelectNumber = 5;
let photoPicker = new photoAccessHelper.PhotoViewPicker();
photoPicker.select(PhotoSelectOptions).then((PhotoSelectResult: photoAccessHelper.PhotoSelectResult) => {
console.info('PhotoViewPicker.select successfully, PhotoSelectResult uri: ' + JSON.stringify(PhotoSelectResult));
}).catch((err: BusinessError) => {
console.error(`PhotoViewPicker.select failed with err: ${err.code}, ${err.message}`);
});
} catch (error) {
let err: BusinessError = error as BusinessError;
console.error(`PhotoViewPicker failed with err: ${err.code}, ${err.message}`);
}
}
这里面涉及到两个对象PhotoSelectOptions和PhotoSelectResult,分别表示跳转到图片选择器的参数,和选择完返回的结果。 PhotoSelectOptions有一下三个参数:
名称 | 类型 | 必填 | 说明 |
---|---|---|---|
isEditSupported11+ | boolean | 否 | 是否支持编辑照片,true表示支持,false表示不支持,默认为true。 |
isOriginalSupported12+ | boolean | 否 | 是否显示选择原图按钮,true表示显示,false表示不显示,默认为true。 元服务API: 从API version 12开始,该接口支持在元服务中使用。 |
subWindowName12+ | string | 否 | 子窗窗口名称。 元服务API: 从API version 12开始,该接口支持在元服务中使用。 |
PhotoSelectOptions 继承自BaseSelectOptions,提供了以下配置:
名称 | 类型 | 必填 | 说明 |
---|---|---|---|
MIMEType10+ | PhotoViewMIMETypes | 否 | 可选择的媒体文件类型,若无此参数,则默认为图片和视频类型。 元服务API: 从API version 11开始,该接口支持在元服务中使用。 |
maxSelectNumber10+ | number | 否 | 选择媒体文件数量的最大值(最大可设置的值为500,若不设置则默认为50)。 元服务API: 从API version 11开始,该接口支持在元服务中使用。 |
isPhotoTakingSupported11+ | boolean | 否 | 是否支持拍照,true表示支持,false表示不支持,默认为true。 元服务API: 从API version 11开始,该接口支持在元服务中使用。 |
isSearchSupported11+ | boolean | 否 | 是否支持搜索,true表示支持,false表示不支持,默认为true。 元服务API: 从API version 11开始,该接口支持在元服务中使用。 |
recommendationOptions11+ | RecommendationOptions | 否 | 图片推荐相关配置参数。 元服务API: 从API version 11开始,该接口支持在元服务中使用。 |
preselectedUris11+ | Array | 否 | 预选择图片的uri数据。 元服务API: 从API version 11开始,该接口支持在元服务中使用。 |
isPreviewForSingleSelectionSupported12+ | boolean | 否 | 单选模式下是否需要进大图预览,true表示需要,false表示不需要,默认为true。 元服务API: 从API version 12开始,该接口支持在元服务中使用。 |
综合上述配置说明,通过PhotoViewPicker可以实现最开始我们提到的:
支持选择图片和视频
支持设置最多选择媒体数量
支持是否选择原图 不支持选择视频的最大最小长度配置。
PhotoSelectResult是返回图库选择后的结果集,结构如下:
名称 | 类型 | 可读 | 可写 | 说明 |
---|---|---|---|---|
photoUris | Array | 是 | 是 | 返回图库选择后的媒体文件的uri数组,此uri数组只能通过临时授权的方式调用photoAccessHelper.getAssets接口去使用,具体使用方式参见用户文件uri介绍中的媒体文件uri的使用方式。 |
isOriginalPhoto | boolean | 是 | 是 | 返回图库选择后的媒体文件是否为原图。 |
返回用户选中的媒体列表和是否选中原图。这里产生一个问题,如果既有选择视频又有选择图片,如何根据一个uri判断是视频还是图片? 这里有用到photoAccessHelper 来解析媒体信息,下面是一个方法封装:
public async uriGetAssets(uri:string): Promise<photoAccessHelper.PhotoAsset|undefined> {
try {
let context = getContext(this) as common.UIAbilityContext;
let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
let predicates: dataSharePredicates.DataSharePredicates = new dataSharePredicates.DataSharePredicates();
// 配置查询条件,使用PhotoViewPicker选择图片返回的uri进行查询
predicates.equalTo('uri', uri);
let fetchOption: photoAccessHelper.FetchOptions = {
fetchColumns: [photoAccessHelper.PhotoKeys.WIDTH, photoAccessHelper.PhotoKeys.HEIGHT, photoAccessHelper.PhotoKeys.TITLE, photoAccessHelper.PhotoKeys.DURATION],
predicates: predicates
};
let fetchResult: photoAccessHelper.FetchResult<photoAccessHelper.PhotoAsset> = await phAccessHelper.getAssets(fetchOption);
// 得到uri对应的PhotoAsset对象,读取文件的部分信息
const asset: photoAccessHelper.PhotoAsset = await fetchResult.getFirstObject();
Logg.i(TAG, 'asset displayName: ' + asset.displayName);
Logg.i(TAG, 'asset uri: '+asset.uri);
Logg.i(TAG, 'asset photoType: ' + asset.photoType);
Logg.i(TAG, 'asset width: ' + asset.get(photoAccessHelper.PhotoKeys.WIDTH));
Logg.i(TAG, 'asset height: ' + asset.get(photoAccessHelper.PhotoKeys.HEIGHT));
Logg.i(TAG, 'asset title: ' + asset.get(photoAccessHelper.PhotoKeys.TITLE));
return asset;
} catch (error){
Logg.e(TAG, 'uriGetAssets failed with err: ' + JSON.stringify(error));
}
return undefined;
}
FetchOptions用来配置要查询的媒体信息内容,比如长、宽等信息,其中photoAccessHelper.PhotoAsset的photoType表示媒体类型,是音频还是视频。
接下里我们再介绍另一个能力,如果是视频的话有时候想要获取视频封面,即Thumbnail,photoAccessHelper.PhotoAsset提供了查询封面的接口:getThumbnail(),返回的是一个pixelMap,如何将pixelMap转换成图像呢?在pixelMap文档里看到从pixelMap读取buffer的方法:
import { BusinessError } from '@kit.BasicServicesKit';
async function Demo() {
const readBuffer: ArrayBuffer = new ArrayBuffer(96); // 96为需要创建的像素buffer大小,取值为:height * width *4
if (pixelMap != undefined) {
pixelMap.readPixelsToBuffer(readBuffer, (error: BusinessError, res: void) => {
if(error) {
console.error(`Failed to read image pixel data. code is ${error.code}, message is ${error.message}`);// 不符合条件则进入
return;
} else {
console.info('Succeeded in reading image pixel data.'); //符合条件则进入
}
})
}
}
上面ArrayBuffer需要指定大小,将前面获取到的宽高相乘再乘以4即可。将获取到的readBuffer写入文件发现图片不是正常的图片,这里又需要用到image.createImagePacker():
let imagePath:string|undefined = undefined;
let pixelMap = await matedata.getThumbnail();
Logg.i(this.TAG, 'getThumbnail successful ' + JSON.stringify(pixelMap));
let cacheDir = getContext().cacheDir;
imagePath = `${cacheDir}/thumbnail${Date.now()}.jpg`;
let dstFile = fs.openSync(imagePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
let packOpts: image.PackingOption = { format: "image/jpeg", quality: 98 };
await image.createImagePacker().packToFile(pixelMap, dstFile.fd, packOpts)
fs.close(dstFile);
Logg.i(this.TAG, "copy file success");
需用用createImagePacker将pixelMap写入到目标文件,并且指定图片格式和质量。
从相机拍摄
上面实现了从相册获取照片,接下来实现使用摄像机拍照和拍视频功能。 有两种方式可以实现打开系统相机进行拍摄。
Want
下面是示例代码:
invokeCamera(callback: (uri: string) => void) {
const context = getContext(this) as common.UIAbilityContext;
const want: Want = {
action: 'ohos.want.action.imageCapture',
parameters: {
"callBundleName": context.abilityInfo.bundleName,
}
};
const result: (error: BusinessError, data: common.AbilityResult) => void =
(error: BusinessError, data: common.AbilityResult) => {
if (error && error.code !== 0) {
console.log(`${TAG_CAMERA_ERROR} ${JSON.stringify(error.message)}`)
return;
}
// 获取相机拍照后返回的图片地址
const resultUri: string = data.want?.parameters?.resourceUri as string;
if (callback && resultUri) {
callback(resultUri);
}
}
context.startAbilityForResult(want, result);
}
通过want意图对象打开相机界面,参考 如何调用系统拍照并获取图片,这种方式无法设置录制视频最大长度等。
cameraPicker
相机选择器使用示例:
import { cameraPicker as picker } from '@kit.CameraKit';
import { camera } from '@kit.CameraKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
let mContext = getContext(this) as common.Context;
async function demo() {
try {
let pickerProfile: picker.PickerProfile = {
cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK
};
let pickerResult: picker.PickerResult = await picker.pick(mContext,
[picker.PickerMediaType.PHOTO, picker.PickerMediaType.VIDEO], pickerProfile);
console.log("the pick pickerResult is:" + JSON.stringify(pickerResult));
} catch (error) {
let err = error as BusinessError;
console.error(`the pick call failed. error code: ${err.code}`);
}
}
示例中mediaTypes数组指定了媒体格式,可以包含视频和图片;PickerProfile指定了摄像头是前置还是后置,主要有一下属性:
名称 | 类型 | 必填 | 说明 |
---|---|---|---|
cameraPosition | camera.CameraPosition | 是 | 相机的位置。 |
saveUri | string | 否 | 保存配置信息的uri。 |
videoDuration | number | 否 | 录制的最大时长。 |
PickerResult是相机选择器的处理结果,有一下属性: |
名称 | 类型 | 必填 | 说明 |
---|---|---|---|
resultCode | number | 是 | 处理的结果,成功返回0,失败返回-1。 |
resultUri | string | 是 | 返回的uri地址。若saveUri为空,resultUri为公共媒体路径。若saveUri不为空且具备写权限,resultUri与saveUri相同。若saveUri不为空且不具备写权限,则无法获取到resultUri。 |
mediaType | PickerMediaType | 是 | 返回的媒体类型。 |
根据返回结果中mediaType来表示是图片还是视频,按照不同媒体类型处理即可。 |
总结
本文介绍了HarmonyOS Next中图片视频选择、图片视频拍摄的能力,重点分析了无需权限即可实现的photoAccessHelper和cameraPicker组件。
作者:轻口味
链接:https://juejin.cn/post/7419185467708735515
关注我获取更多知识或者投稿