往期鸿蒙5.0全套实战文章必看:(文中附带全栈鸿蒙5.0学习资料)
场景描述:
应用希望三方相机能实现类似系统相机的功能,即拍照后立即保存。但是由于权限控制,要么要申请ACL权限,要么要使用安全控件保存,这种方式用户体验非常不好,而分段式拍照接口提供了这种能力。
- 无需申请ACL权限和使用安全控件保存,拍照后照片自动保存到相册。
- 分段式拍照后的照片经过后处理(如加水印),然后保存到沙箱目录。
方案描述
1、通过XComponentController获取XComponent组件的surfaceId。
2、调用三方相机接口,执行相机初始化流程,将surfaceId传给预览输出流,实现相机预览功能。同时注册photoAssetAvailable回调,实现分段式拍照功能。
3、点击拍照,收到photoAssetAvailable回调后:
a. 调用媒体库落盘接口saveCameraPhoto保存一阶段低质量图,二阶段真图就绪后媒体库会主动帮应用替换落盘图片(都无需任何权限)。
b. 调用媒体库接口注册低质量图或高质量图buffer回调,实现预览和加水印保存沙箱功能。备注:为了直观展示分段式拍照一阶段和二阶段的效果图,增加了拍照后的预览功能(3.2中的预览),将两个阶段获取到的PixelMap通过Iamge组件显示出来。
场景实现
-
无需申请ACL权限和使用安全控件保存,拍照后照片自动保存到相册
效果图:
核心代码
1、通过XComponentController获取XComponent组件的surfaceId。
XComponent({
type: XComponentType.SURFACE,
controller: this.mXComponentController,
})
.onLoad(async () => {
Logger.info(TAG, 'onLoad is called');
this.surfaceId = this.mXComponentController.getXComponentSurfaceId();
GlobalContext.get().setObject('cameraDeviceIndex', this.defaultCameraDeviceIndex);
GlobalContext.get().setObject('xComponentSurfaceId', this.surfaceId);
Logger.info(TAG, `onLoad surfaceId: ${this.surfaceId}`);
await CameraService.initCamera(this.surfaceId, this.defaultCameraDeviceIndex);
})
2、调用三方相机接口,执行相机初始化流程,将surfaceId传给预览输出流,实现相机预览功能。
async onPageShow(): Promise<void> {
Logger.info(TAG, 'onPageShow');
this.isOpenEditPage = false;
if (this.surfaceId !== '' && !this.isOpenEditPage) {
await CameraService.initCamera(this.surfaceId, GlobalContext.get().getT<number>('cameraDeviceIndex'));
}
}
async initCamera(surfaceId: string, cameraDeviceIndex: number): Promise<void> {
Logger.debug(TAG, `initCamera cameraDeviceIndex: ${cameraDeviceIndex}`);
this.photoMode = AppStorage.get('photoMode');
if (!this.photoMode) {
return;
}
try {
await this.releaseCamera();
// Get Camera Manager Instance
this.cameraManager = this.getCameraManagerFn();
if (this.cameraManager === undefined) {
Logger.error(TAG, 'cameraManager is undefined');
return;
}
// Gets the camera device object that supports the specified
this.cameras = this.getSupportedCamerasFn(this.cameraManager);
if (this.cameras.length < 1 || this.cameras.length < cameraDeviceIndex + 1) {
return;
}
this.curCameraDevice = this.cameras[cameraDeviceIndex];
let isSupported = this.isSupportedSceneMode(this.cameraManager, this.curCameraDevice);
if (!isSupported) {
Logger.error(TAG, 'The current scene mode is not supported.');
return;
}
let cameraOutputCapability =
this.cameraManager.getSupportedOutputCapability(this.curCameraDevice, this.curSceneMode);
let previewProfile = this.getPreviewProfile(cameraOutputCapability);
if (previewProfile === undefined) {
Logger.error(TAG, 'The resolution of the current preview stream is not supported.');
return;
}
this.previewProfileObj = previewProfile;
// Creates the previewOutput output object
this.previewOutput = this.createPreviewOutputFn(this.cameraManager, this.previewProfileObj, surfaceId);
if (this.previewOutput === undefined) {
Logger.error(TAG, 'Failed to create the preview stream.');
return;
}
// Listening for preview events
this.previewOutputCallBack(this.previewOutput);
let photoProfile = this.getPhotoProfile(cameraOutputCapability);
if (photoProfile === undefined) {
Logger.error(TAG, 'The resolution of the current photo stream is not supported.');
return;
}
this.photoProfileObj = photoProfile;
// Creates a photoOutPut output object
this.photoOutput = this.createPhotoOutputFn(this.cameraManager, this.photoProfileObj);
if (this.photoOutput === undefined) {
Logger.error(TAG, 'Failed to create the photo stream.');
return;
}
// Creates a cameraInput output object
this.cameraInput = this.createCameraInputFn(this.cameraManager, this.curCameraDevice);
if (this.cameraInput === undefined) {
Logger.error(TAG, 'Failed to create the camera input.');
return;
}
// Turn on the camera
let isOpenSuccess = await this.cameraInputOpenFn(this.cameraInput);
if (!isOpenSuccess) {
Logger.error(TAG, 'Failed to open the camera.');
return;
}
// Camera status callback
this.onCameraStatusChange(this.cameraManager);
// Listens to CameraInput error events
this.onCameraInputChange(this.cameraInput, this.curCameraDevice);
// Session Process
await this.sessionFlowFn(this.cameraManager, this.cameraInput, this.previewOutput, this.photoOutput);
} catch (error) {
let err = error as BusinessError;
Logger.error(TAG, `initCamera fail: ${JSON.stringify(err)}`);
}
}
3、注册photoAssetAvailable回调,实现分段式拍照功能。
// 注册photoAssetAvailable回调
photoOutput.on('photoAssetAvailable', (err: BusinessError, photoAsset: photoAccessHelper.PhotoAsset) => {
Logger.info(TAG, 'photoAssetAvailable begin');
if (err) {
Logger.error(TAG, `photoAssetAvailable err:${err.code}`);
return;
}
this.handlePhotoAssetCb(photoAsset);
});
setSavePictureCallback(callback: (photoAsset: photoAccessHelper.PhotoAsset | image.PixelMap) => void): void {
this.handlePhotoAssetCb = callback;
}
// ModeComponent.ets中实现handlePhotoAssetCb回调方法,在收到回调后跳转到EditPage.ets页面进行逻辑处理。
changePageState(): void {
if (this.isOpenEditPage) {
this.onJumpClick();
}
}
aboutToAppear(): void {
Logger.info(TAG, 'aboutToAppear');
CameraService.setSavePictureCallback(this.handleSavePicture);
}
handleSavePicture = (photoAsset: photoAccessHelper.PhotoAsset | image.PixelMap): void => {
Logger.info(TAG, 'handleSavePicture');
this.setImageInfo(photoAsset);
AppStorage.set<boolean>('isOpenEditPage', true);
Logger.info(TAG, 'setImageInfo end');
}
4、点击拍照,收到photoAssetAvailable回调后,调用媒体库落盘接口saveCameraPhoto保存一阶段低质量图。
// EditPage.ets中进行回调后的具体逻辑处理
requestImage(requestImageParams: RequestImageParams): void {
if (requestImageParams.photoAsset) {
// 1. 调用媒体库落盘接口保存一阶段低质量图,二阶段真图就绪后媒体库会主动帮应用替换落盘图片
mediaLibSavePhoto(requestImageParams.photoAsset);
}
}
aboutToAppear() {
Logger.info(TAG, 'aboutToAppear begin');
if (this.photoMode === Constants.SUBSECTION_MODE) {
let curPhotoAsset = GlobalContext.get().getT<photoAccessHelper.PhotoAsset>('photoAsset');
this.photoUri = curPhotoAsset.uri;
let requestImageParams: RequestImageParams = {
context: getContext(),
photoAsset: curPhotoAsset,
callback: this.photoBufferCallback
};
this.requestImage(requestImageParams);
Logger.info(TAG, `aboutToAppear photoUri: ${this.photoUri}`);
} else if (this.photoMode === Constants.SINGLE_STAGE_MODE) {
this.curPixelMap = GlobalContext.get().getT<image.PixelMap>('photoAsset');
}
}
落盘接口saveCameraPhoto(媒体库提供的系统函数)保存一阶段低质量图,二阶段真图就绪后媒体库会主动帮应用替换落盘图片。
async function mediaLibSavePhoto(photoAsset: photoAccessHelper.PhotoAsset): Promise<void> {
try {
let assetChangeRequest: photoAccessHelper.MediaAssetChangeRequest =
new photoAccessHelper.MediaAssetChangeRequest(photoAsset)
assetChangeRequest.saveCameraPhoto()
await photoAccessHelper.getPhotoAccessHelper(context).applyChanges(assetChangeRequest)
console.info('apply saveCameraPhoto successfully')
} catch (err) {
console.error(`apply saveCameraPhoto failed with error: ${err.code}, ${err.message}`)
}
}
-
分段式拍照后的照片经过后处理(如加水印),然后保存到沙箱目录
效果图:
核心代码
1、调用媒体库接口注册低质量图或高质量(通过设置DeliveryMode枚举值实现)图buffer回调。DeliveryMode枚举中有:
FAST_MODE:一次回调,速度快,直接返回一阶段低质量图。
HIGH_QUALITY_MODE:一次回调,速度慢,返回高质量资源图。
BALANCE_MODE:两次回调,先快速返回一阶段低质量图,第二次会收到高质量图的回调。
if (requestImageParams.photoAsset) {
// 2. 调用媒体库接口注册低质量图或高质量图buffer回调,自定义处理
mediaLibRequestBuffer(requestImageParams);
}
async function mediaLibRequestBuffer(requestImageParams: RequestImageParams) {
try {
class MediaDataHandler implements photoAccessHelper.MediaAssetDataHandler<ArrayBuffer> {
onDataPrepared(data: ArrayBuffer, map: Map<string, string>): void {
Logger.info(TAG, 'onDataPrepared map' + JSON.stringify(map));
requestImageParams.callback(data);
Logger.info(TAG, 'onDataPrepared end');
}
};
let requestOptions: photoAccessHelper.RequestOptions = {
deliveryMode: photoAccessHelper.DeliveryMode.BALANCE_MODE,
};
const handler = new MediaDataHandler();
photoAccessHelper.MediaAssetManager.requestImageData(requestImageParams.context, requestImageParams.photoAsset,
requestOptions, handler);
} catch (error) {
Logger.error(TAG, `Failed in requestImage, error code: ${error.code}`);
}
}
自定义回调处理
photoBufferCallback: (arrayBuffer: ArrayBuffer) => void = (arrayBuffer: ArrayBuffer) => {
Logger.info(TAG, 'photoBufferCallback is called');
let imageSource = image.createImageSource(arrayBuffer);
saveWatermarkPhoto(imageSource, context).then(pixelMap => {
this.curPixelMap = pixelMap
})
};
2、实现加水印和保存沙箱功能,注意这里只能保存到沙箱,要保存到图库是需要权限的。
export async function saveWatermarkPhoto(imageSource: image.ImageSource, context: Context) {
const imagePixelMap = await imageSource2PixelMap(imageSource);
const addedWatermarkPixelMap: image.PixelMap = addWatermark(imagePixelMap);
await saveToFile(addedWatermarkPixelMap!, context);
// 拍照后预览页面不带水印显示
//return imagePixelMap.pixelMap;
// 拍照后预览页面带水印显示
return addedWatermarkPixelMap;
}
加水印功能
export function addWatermark(
imagePixelMap: ImagePixelMap,
text: string = 'watermark',
drawWatermark?: (OffscreenContext: OffscreenCanvasRenderingContext2D) => void): image.PixelMap {
const height = px2vp(imagePixelMap.height);
const width = px2vp(imagePixelMap.width);
const offScreenCanvas = new OffscreenCanvas(width, height);
const offScreenContext = offScreenCanvas.getContext('2d');
offScreenContext.drawImage(imagePixelMap.pixelMap, 0, 0, width, height);
if (drawWatermark) {
drawWatermark(offScreenContext);
} else {
const imageScale = width / px2vp(display.getDefaultDisplaySync().width);
offScreenContext.textAlign = 'center';
offScreenContext.fillStyle = '#A2FF0000';
offScreenContext.font = 50 * imageScale + 'vp';
const padding = 5 * imageScale;
offScreenContext.setTransform(1, -0.3, 0, 1, -170, -200);
offScreenContext.fillText(text, width - padding, height - padding);
}
return offScreenContext.getPixelMap(0, 0, width, height);
}
保存沙箱功能
export async function saveToFile(pixelMap: image.PixelMap, context: Context): Promise<void> {
const path: string = context.cacheDir + "/pixel_map.jpg";
let file = fs.openSync(path, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
const imagePackerApi = image.createImagePacker();
let packOpts: image.PackingOption = { format: "image/jpeg", quality: 100 };
imagePackerApi.packToFile(pixelMap, file.fd, packOpts).then(() => {
// 直接打包进文件
}).catch((error: BusinessError) => {
console.error('Failed to pack the image. And the error is: ' + error);
})
}
3、实现拍照后预览功能,pixelMap从上面的自定义回调处理中获取,然后通过Image组件显示。
Column() {
Image(this.curPixelMap)
.objectFit(ImageFit.Cover)
.width(Constants.FULL_PERCENT)
.height(Constants.EIGHTY_PERCENT)
}
.width(Constants.FULL_PERCENT)
.margin({ top: 68 })
.layoutWeight(this.textLayoutWeight)