场景描述:

应用希望三方相机能实现类似系统相机的功能,即拍照后立即保存。但是由于权限控制,要么要申请ACL权限,要么要使用安全控件保存,这种方式用户体验非常不好,而分段式拍照接口提供了这种能力。

  • 无需申请ACL权限和使用安全控件保存,拍照后照片自动保存到相册。
  • 分段式拍照后的照片经过后处理(如加水印),然后保存到沙箱目录。

方案描述

  1. 通过XComponentController获取XComponent组件的surfaceId。
  2. 调用三方相机接口,执行相机初始化流程,将surfaceId传给预览输出流,实现相机预览功能。同时注册photoAssetAvailable回调,实现分段式拍照功能。
  3. 点击拍照,收到photoAssetAvailable回调后:
    1. 调用媒体库落盘接口saveCameraPhoto保存一阶段低质量图,二阶段真图就绪后媒体库会主动帮应用替换落盘图片(都无需任何权限)。
    2. 调用媒体库接口注册低质量图或高质量图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)

     

Logo

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

更多推荐