鸿蒙(API 12 Beta3版)【自定义界面扫码】

基本概念

自定义界面扫码能力提供了相机流控制接口,可根据自身需求自定义扫码界面,适用于对扫码界面有定制化需求的应用开发。

说明

通过自定义页面扫码可以实现应用内的扫码功能,为了应用更好的体验,推荐同时[接入“扫码直达”服务],应用可以同时支持系统扫码入口(控制中心扫一扫)和应用内扫码两种方式跳转到指定服务页面。

场景介绍

自定义界面扫码能力提供扫码相机流控制接口,支持相机流的初始化、开启、暂停、释放、重新扫码功能;支持闪光灯的状态获取、开启、关闭;支持变焦比的获取和设置;支持设置相机焦点和连续自动对焦;支持对条形码、二维码、MULTIFUNCTIONAL CODE进行扫码识别(具体类型参见[ScanType]),并获得码类型、码值、码位置信息、相机预览流(YUV)。该能力可用于单码和多码的扫描识别。

开发者集成自定义界面扫码能力可以自行定义扫码的界面样式,请按照业务流程完成扫码接口调用实现实时扫码功能。建议开发者基于[Sample Code]做个性化修改。

说明

YUV(相机预览流图像数据)适合于扫码和识物的综合识别场景,开发者需要自己控制相机流,普通扫码场景无需关注。

约束与限制

  • 需要请求相机的使用权限。
  • 需要开发者自行实现扫码的人机交互界面。例如:多码场景需要暂停相机流由用户选择一个码图进行识别。

业务流程

1

  1. 发起请求: 用户向开发者的应用发起扫码请求,应用拉起已定义好的扫码界面。

  2. 申请授权: 应用需要向用户申请相机权限授权。若未同意授权,则无法使用此功能。

  3. 启动自定义界面扫码: 在扫码前必须调用init接口初始化自定义扫码界面,加载资源。相机流初始化结束后,调用start接口开始扫码。

  4. 自定义界面扫码相机操作: 可以配置自定义界面扫码相机操作参数,调整相应功能,包括闪光灯、变焦、焦距、暂停、重启扫码等。例如:

    • 根据当前码图位置,比如当前码图太远或太近时,调用getZoom获取变焦比,setZoom接口设置变焦比,调整焦距以便于用户扫码。
    • 根据当前扫码的光线条件或根据on(‘lightingFlash’)监听闪光灯开启时机,通过getFlashLightStatus接口先获取闪光灯状态,再调用openFlashLight/closeFlashLight接口控制闪光灯开启或关闭,以便于用户进行扫码。
    • 调用setFocusPoint设置对焦位置,resetFocus恢复默认对焦模式,以便于用户进行扫码。
    • 在应用处于前后台或其他特殊场景需要中断/重新进行扫码时,可调用stop或start接口来控制相机流达到暂停或重新扫码的目的。
  5. 自定义界面扫码: Scan Kit API在扫码完成后会返回扫码结果。同时根据开发者的需要,Scan Kit API会返回每帧相机预览流数据。如需不重启相机并重新触发一次扫码,可以在start接口的Callback异步回调中,调用rescan接口。完成扫码后,需调用release接口进行释放扫码资源的操作。

  6. 获取结果: 解析码值结果跳转应用服务页。

接口说明

自定义界面扫码提供init、start、stop、release、getFlashLightStatus、openFlashLight、closeFlashLight、setZoom、getZoom、setFocusPoint、resetFocus、rescan、on(‘lightingFlash’)、off(‘lightingFlash’)接口,其中部分接口返回值有两种返回形式:Callback和Promise回调。Callback和Promise回调函数只是返回值方式不一样,功能相同。

接口名描述
[init](options?: scanBarcode.[ScanOptions]): void初始化自定义界面扫码,加载资源。无返回结果。
[start](viewControl: [ViewControl]): Promise<Array<scanBarcode.[ScanResult]>>启动扫码相机流。使用Promise异步回调获取扫码结果。
stop: Promise暂停扫码相机流。使用Promise异步回调返回执行结果。
release: Promise释放扫码相机流。使用Promise异步回调返回执行结果。
[start](viewControl: ViewControl, callback: AsyncCallback<Array<scanBarcode.ScanResult>>, frameCallback?: AsyncCallback<[ScanFrame]>): void启动扫码相机流。使用Callback异步回调返回扫码结果以及YUV图像数据。
getFlashLightStatus: boolean获取闪光灯状态。返回结果为布尔值,true为打开状态,false为关闭状态。
openFlashLight: void开启闪光灯。无返回结果。
closeFlashLight: void关闭闪光灯。无返回结果。
[setZoom](zoomValue : number): void设置变焦比。无返回结果。
getZoom: number获取当前的变焦比。
[setFocusPoint](point: scanBarcode.[Point]): void设置相机焦点。
resetFocus: void设置连续自动对焦模式。
rescan: void触发一次重新扫码。仅对start接口Callback异步回调有效,Promise异步回调无效。
[stop](callback: AsyncCallback): void暂停扫码相机流。使用Callback异步回调返回执行结果。
[release](callback: AsyncCallback): void释放扫码相机流。使用Callback异步回调返回执行结果。
[on](type: ‘lightingFlash’, callback: AsyncCallback): void注册闪光灯打开时机回调,使用Callback异步回调返回闪光灯打开时机。
[off](type: ‘lightingFlash’, callback?: AsyncCallback): void注销闪光灯打开时机回调,使用Callback异步回调返回注销结果。

开发步骤

自定义界面扫码接口支持自定义UI界面,识别相机流中的条形码,二维码以及MULTIFUNCTIONAL CODE,并返回码图的值、类型、码的位置信息(码图最小外接矩形左上角和右下角的坐标)以及相机预览流(YUV)。

以下示例为调用自定义界面扫码接口拉起相机流并返回扫码结果和相机预览流(YUV)。

  1. 在开发应用前,需要先申请相机相关权限,确保应用拥有访问相机的权限。在“module.json5”文件中配置相机权限,具体配置方式

    权限名说明授权方式
    ohos.permission.CAMERA允许应用使用相机扫码。user_grant
  2. 使用接口[requestPermissionsFromUser]请求用户授权。具体申请方式及校验方式。

  3. 导入自定义界面扫码接口以及相关接口模块,导入方法如下。

import { scanCore, scanBarcode, customScan } from '@kit.ScanKit';
// 导入功能涉及的权限申请、回调接口
import { router, promptAction, display } from '@kit.ArkUI';
import { AsyncCallback, BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { common, abilityAccessCtrl } from '@kit.AbilityKit';
  1. 遵循[业务流程]完成自定义界面扫码功能。

    说明

    1. 在设置start接口的viewControl参数时,width和height与[XComponent]的宽高值相同,start接口会根据XComponent的宽高比例从相机的分辨率选择最优分辨率,如果比例与相机的分辨率比例相差过大会返回内部错误。当前支持的分辨率比例为16:9、4:3、1:1。竖屏场景下,XComponent的高度需要大于宽度,且高宽比在支持的分辨率比例中。横屏场景下,XComponent的宽度需要大于高度,且宽高比在支持的分辨率比例中。
    2. XComponent的宽高需根据使用场景计算适配。例如:在开发设备为折叠屏时,需按照折叠屏的展开态和折叠态分别计算XComponent的宽高,start接口会根据XComponent的宽高适配对应的相机分辨率。设备屏幕宽高可通过[display.getDefaultDisplaySync]方法获取(获取的为px单位,需要通过[px2vp]。
    • 通过Promise方式回调,调用自定义界面扫码接口拉起相机流并返回扫码结果。
@Entry
@Component
struct CustomScanPage {
  @State userGrant: boolean = false // 是否已申请相机权限
  @State surfaceId: string = '' // xComponent组件生成id
  @State isShowBack: boolean = false // 是否已经返回扫码结果
  @State isFlashLightEnable: boolean = false // 是否开启了闪光灯
  @State isSensorLight: boolean = false // 记录当前环境亮暗状态
  @State cameraHeight: number = 640 // 设置预览流高度,默认单位:vp
  @State cameraWidth: number = 360 // 设置预览流宽度,默认单位:vp
  @State cameraOffsetX: number = 0 // 设置预览流x轴方向偏移量,默认单位:vp
  @State cameraOffsetY: number = 0 // 设置预览流y轴方向偏移量,默认单位:vp
  @State zoomValue: number = 1 // 预览流缩放比例
  @State setZoomValue: number = 1 // 已设置的预览流缩放比例
  @State scaleValue: number = 1 // 屏幕缩放比
  @State pinchValue: number = 1 // 双指缩放比例
  @State displayHeight: number = 0 // 屏幕高度,单位vp
  @State displayWidth: number = 0 // 屏幕宽度,单位vp
  @State scanResult: Array<scanBarcode.ScanResult> = [] // 扫码结果
  private mXComponentController: XComponentController = new XComponentController()
  private TAG: string = '[customScanPage]'

  async onPageShow() {
    // 自定义启动第一步,用户申请权限
    await this.requestCameraPermission();
    // 多码扫码识别,enableMultiMode: true 单码扫码识别enableMultiMode: false
    let options: scanBarcode.ScanOptions = {
      scanTypes: [scanCore.ScanType.ALL],
      enableMultiMode: true,
      enableAlbum: true
    }
    // 自定义启动第二步:设置预览流布局尺寸
    this.setDisplay();
    // 自定义启动第三步,初始化接口
    customScan.init(options);
  }

  async onPageHide() {
    // 页面消失或隐藏时,停止并释放相机流
    this.userGrant = false;
    this.isFlashLightEnable = false;
    this.isSensorLight = false;
    try {
      customScan.off('lightingFlash');
    } catch (error) {
      hilog.error(0x0001, this.TAG, `Failed to off lightingFlash. Code: ${error.code}, message: ${error.message}`);
    }
    await customScan.stop();
    // 自定义相机流释放接口
    customScan.release().then(() => {
      hilog.info(0x0001, this.TAG, 'Succeeded in releasing customScan by promise.');
    }).catch((error: BusinessError) => {
      hilog.error(0x0001, this.TAG,
        `Failed to release customScan by promise. Code: ${error.code}, message: ${error.message}`);
    })
  }

  // 用户申请权限
  async reqPermissionsFromUser(): Promise<number[]> {
    hilog.info(0x0001, this.TAG, 'reqPermissionsFromUser start');
    let context = getContext() as common.UIAbilityContext;
    let atManager = abilityAccessCtrl.createAtManager();
    let grantStatus = await atManager.requestPermissionsFromUser(context, ['ohos.permission.CAMERA']);
    return grantStatus.authResults;
  }

  // 用户申请相机权限
  async requestCameraPermission() {
    let grantStatus = await this.reqPermissionsFromUser();
    for (let i = 0; i < grantStatus.length; i++) {
      if (grantStatus[i] === 0) {
        // 用户授权,可以继续访问目标操作
        hilog.info(0x0001, this.TAG, 'Succeeded in getting permissions.');
        this.userGrant = true;
      }
    }
  }

  // 竖屏时获取屏幕尺寸,设置预览流全屏示例
  setDisplay() {
    // 默认竖屏
    let displayClass = display.getDefaultDisplaySync();
    this.displayHeight = px2vp(displayClass.height);
    this.displayWidth = px2vp(displayClass.width);
    let maxLen: number = Math.max(this.displayWidth, this.displayHeight);
    let minLen: number = Math.min(this.displayWidth, this.displayHeight);
    const RATIO: number = 16 / 9;
    this.cameraHeight = maxLen;
    this.cameraWidth = maxLen / RATIO;
    this.cameraOffsetX = (minLen - this.cameraWidth) / 2;
  }

  // toast显示扫码结果
  async showScanResult(result: scanBarcode.ScanResult) {
    // 使用toast显示出扫码结果
    promptAction.showToast({
      message: JSON.stringify(result),
      duration: 5000
    });
  }
  initCamera() {
    this.isShowBack = false;
    this.scanResult = [];
    let viewControl: customScan.ViewControl = {
      width: this.cameraWidth,
      height: this.cameraHeight,
      surfaceId: this.surfaceId
    };
    // 自定义启动第四步,请求扫码接口,通过Promise方式回调
    customScan.start(viewControl)
      .then(async (result: Array<scanBarcode.ScanResult>) => {
        hilog.info(0x0001, this.TAG, `result: ${JSON.stringify(result)}`);
        if (result.length) {
          // 解析码值结果跳转应用服务页
          this.scanResult = result;
          this.isShowBack = true;
          // 获取到扫描结果后暂停相机流
          customScan.stop();
        }
      });
  }

  // 自定义扫码界面的顶部返回按钮和扫码提示
  @Builder
  TopTool() {
    Column() {
      Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) {
        Text('返回')
          .onClick(async () => {
            router.back();
          })
      }.padding({ left: 24, right: 24, top: 40 })

      Column() {
        Text('扫描二维码/条形码')
        Text('对准二维码/条形码,即可自动扫描')
      }.margin({ left: 24, right: 24, top: 24 })
    }
    .height(146)
    .width('100%')
  }

  build() {
    Stack() {
      if (this.userGrant) {
        Column() {
          XComponent({
            id: 'componentId',
            type: XComponentType.SURFACE,
            controller: this.mXComponentController
          })
            .onLoad(async () => {
              hilog.info(0x0001, this.TAG, 'Succeeded in loading, onLoad is called.');
              // 获取XComponent组件的surfaceId
              this.surfaceId = this.mXComponentController.getXComponentSurfaceId();
              hilog.info(0x0001, this.TAG, `Succeeded in getting surfaceId: ${this.surfaceId}`);
              this.initCamera();
              // 闪光灯监听接口
              customScan.on('lightingFlash', (error, isLightingFlash) => {
                if (error) {
                  hilog.error(0x0001, this.TAG,
                    `Failed to on lightingFlash. Code: ${error.code}, message: ${error.message}`);
                  return;
                }
                if (isLightingFlash) {
                  this.isFlashLightEnable = true;
                } else {
                  if (!customScan.getFlashLightStatus()) {
                    this.isFlashLightEnable = false;
                  }
                }
                this.isSensorLight = isLightingFlash;
              });
            })  
            .width(this.cameraWidth)
            .height(this.cameraHeight)
            .position({ x: this.cameraOffsetX, y: this.cameraOffsetY })
        }
        .height('100%')
        .width('100%')
      }

      Column() {
        this.TopTool()
        Column() {
        }
        .layoutWeight(1)
        .width('100%')

        Column() {
          Row() {
            // 闪光灯按钮,启动相机流后才能使用
            Button('FlashLight')
              .onClick(() => {
                // 根据当前闪光灯状态,选择打开或关闭闪关灯
                if (customScan.getFlashLightStatus()) {
                  customScan.closeFlashLight();
                  setTimeout(() => {
                    this.isFlashLightEnable = this.isSensorLight;
                  }, 200);
                } else {
                  customScan.openFlashLight();
                }
              })
              .visibility((this.userGrant && this.isFlashLightEnable) ? Visibility.Visible : Visibility.None)

            // 扫码成功后,点击按钮后重新扫码
            Button('Scan')
              .onClick(() => {
                // 点击按钮重启相机流,重新扫码
                this.initCamera();
              })
              .visibility(this.isShowBack ? Visibility.Visible : Visibility.None)
          }

          Row() {
            // 预览流设置缩放比例
            Button('缩放比例,当前比例:' + this.setZoomValue)
              .onClick(() => {
                // 设置相机缩放比例
                if (!this.isShowBack) {
                  if (!this.zoomValue || this.zoomValue === this.setZoomValue) {
                    this.setZoomValue = customScan.getZoom();
                  } else {
                    this.zoomValue = this.zoomValue;
                    customScan.setZoom(this.zoomValue);
                    setTimeout(() => {
                      if (!this.isShowBack) {
                        this.setZoomValue = customScan.getZoom();
                      }
                    }, 1000);
                  }
                }
              })
          }
          .margin({ top: 10, bottom: 10 })

          Row() {
            // 输入要设置的预览流缩放比例
            TextInput({ placeholder: '输入缩放倍数' })
              .type(InputType.Number)
              .borderWidth(1)
              .backgroundColor(Color.White)
              .onChange(value => {
                this.zoomValue = Number(value);
              })
          }
        }
        .width('50%')
        .height(180)
      }

      // 单码、多码扫描后,显示码图蓝点位置。点击toast码图信息
      ForEach(this.scanResult, (item: scanBarcode.ScanResult, index: number) => {
        if (item.scanCodeRect) {
          Image($rawfile('scan_selected2.svg'))
            .width(40)
            .height(40)
            .markAnchor({ x: 20, y: 20 })
            .position({
              x: (item.scanCodeRect.left + item?.scanCodeRect?.right) / 2 + this.cameraOffsetX,
              y: (item.scanCodeRect.top + item?.scanCodeRect?.bottom) / 2 + this.cameraOffsetY
            })
            .onClick(() => {
              this.showScanResult(item);
            })
        }
      })
    }
    // 建议相机流设置为全屏
    .width('100%')
    .height('100%')
    .onClick((event: ClickEvent) => {
      // 是否已扫描到结果
      if (this.isShowBack) {
        return;
      }
      // 点击屏幕位置,获取点击位置(x,y),设置相机焦点
      let x1 = vp2px(event.displayY) / (this.displayHeight + 0.0);
      let y1 = 1.0 - (vp2px(event.displayX) / (this.displayWidth + 0.0));
      customScan.setFocusPoint({ x: x1, y: y1 });
      hilog.info(0x0001, this.TAG, `Succeeded in setting focusPoint x1: ${x1}, y1: ${y1}`);
      // 设置连续自动对焦模式
      setTimeout(() => {
        customScan.resetFocus();
      }, 200);
    }).gesture(PinchGesture({ fingers: 2 })
      .onActionStart((event: GestureEvent) => {
        hilog.info(0x0001, this.TAG, 'Pinch start');
      })
      .onActionUpdate((event: GestureEvent) => {
        if (event) {
          this.scaleValue = event.scale;
        }
      })
      .onActionEnd((event: GestureEvent) => {
        // 是否已扫描到结果
        if (this.isShowBack) {
          return;
        }
        // 获取双指缩放比例,设置变焦比
        try {
          let zoom = customScan.getZoom();
          this.pinchValue = this.scaleValue * zoom;
          customScan.setZoom(this.pinchValue);
          hilog.info(0x0001, this.TAG, 'Pinch end');
        } catch (error) {
          hilog.error(0x0001, this.TAG, `Failed to setZoom. Code: ${error.code}, message: ${error.message}`);
        }
      }))
  }
}
  • 通过Callback方式回调,调用自定义界面扫码接口拉起相机流并返回扫码结果和相机预览流(YUV)。
import { bundleManager, PermissionRequestResult, Permissions } from '@kit.AbilityKit';

const TAG = '[YUV CPSample]';
let context = getContext(this) as common.UIAbilityContext;

// 用户申请权限
export class PermissionsUtil {
  public static async checkAccessToken(permission: Permissions): Promise<abilityAccessCtrl.GrantStatus> {
    let atManager = abilityAccessCtrl.createAtManager();
    let grantStatus: abilityAccessCtrl.GrantStatus = -1;
    // 获取应用程序的accessTokenID
    let tokenId: number = 0;
    let bundleInfo: bundleManager.BundleInfo =
      await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
    let appInfo: bundleManager.ApplicationInfo = bundleInfo.appInfo;
    tokenId = appInfo.accessTokenId;
    // 校验应用是否被授予权限
    grantStatus = await atManager.checkAccessToken(tokenId, permission);
    return grantStatus;
  }

  // 申请相机权限
  public static async reqPermissionsFromUser(): Promise<number[]> {
    hilog.info(0x0001, TAG, 'Succeeded in getting permissions by promise.')
    let atManager = abilityAccessCtrl.createAtManager();
    let grantStatus: PermissionRequestResult = { permissions: [], authResults: [] }
    grantStatus = await atManager.requestPermissionsFromUser(context, ['ohos.permission.CAMERA']);
    return grantStatus.authResults;
  }
}

@Extend(Column)
function mainStyle() {
  .width('100%')
  .height('100%')
  .padding({
    top: 40
  })
  .justifyContent(FlexAlign.Center)
}

@Entry
@Component
struct YUVScan {
  @State userGrant: boolean = false // 是否已申请相机权限
  @State surfaceId: string = '' // xComponent组件生成id
  @State cameraHeight: number = 640 // 设置预览流高度,默认单位:vp
  @State cameraWidth: number = 360 // 设置预览流宽度,默认单位:vp
  @State cameraOffsetX: number = 0 // 设置预览流x轴方向偏移量,默认单位:vp
  @State cameraOffsetY: number = 0 // 设置预览流y轴方向偏移量,默认单位:vp
  @State zoomValue: number = 1 // 预览流缩放比例
  @State setZoomValue: number = 1 // 已设置的预览流缩放比例
  @State isReleaseCamera: boolean = false // 是否已释放相机流
  @State scanWidth: number = 384 // xComponent宽度,默认设置384,单位vp
  @State scanHeight: number = 682 // xComponent高度,默认设置682,单位vp
  @State scanBottom: number = 220
  @State scanOffsetX: number = 0 // xComponent位置x轴偏移量,单位vp
  @State scanOffsetY: number = 0 // xComponent位置y轴偏移量,单位vp
  @State scanCodeRect: Array<scanBarcode.ScanCodeRect> = [] // 扫码结果码图位置
  @State scanFlag: boolean = false // 是否已经扫码到结果
  @State scanFrameResult: string = ''
  @State scaleValue: number = 1 // 屏幕缩放比
  @State pinchValue: number = 1 // 双指缩放比例
  @State displayHeight: number = 0 // 屏幕高度,单位vp
  @State displayWidth: number = 0 // 屏幕宽度,单位vp
  private mXComponentController: XComponentController = new XComponentController()
  private viewControl: customScan.ViewControl = { width: 1920, height: 1080, surfaceId: this.surfaceId }
  options: scanBarcode.ScanOptions = {
    // 扫码类型,可选参数
    scanTypes: [scanCore.ScanType.ALL],
    // 是否开启多码识别,可选参数
    enableMultiMode: true,
    // 是否开启相册扫码,可选参数
    enableAlbum: true,
  }
  // 返回自定义扫描结果的回调
  private callback: AsyncCallback<scanBarcode.ScanResult[]> =
    async (error: BusinessError, result: scanBarcode.ScanResult[]) => {
      if (error && error.code) {
        hilog.error(0x0001, TAG,
          `Failed to get ScanResult by callback. Code: ${error.code}, message: ${error.message}`);
        return;
      }
      // 解析码值结果跳转应用服务页
      hilog.info(0x0001, TAG, `Succeeded in getting ScanResult by callback, result: ${JSON.stringify(result)}`);
    }
  // 返回相机帧的回调
  private frameCallback: AsyncCallback<customScan.ScanFrame> =
    async (error: BusinessError, frameResult: customScan.ScanFrame) => {
      if (error) {
        hilog.error(0x0001, TAG, `Failed to get ScanFrame by callback. Code: ${error.code}, message: ${error.message}`);
        return;
      }
      // byteBuffer相机YUV图像数组
      hilog.info(0x0001, TAG,
        `Succeeded in getting ScanFrame.byteBuffer.byteLength: ${frameResult.byteBuffer.byteLength}`)
      hilog.info(0x0001, TAG, `Succeeded in getting ScanFrame.width: ${frameResult.width}`)
      hilog.info(0x0001, TAG, `Succeeded in getting ScanFrame.height: ${frameResult.height}`)
      this.scanFrameResult = JSON.stringify(frameResult.scanCodeRects);
      let newWidth = frameResult.height;
      if (frameResult && frameResult.scanCodeRects && frameResult.scanCodeRects.length > 0 && !this.scanFlag) {
        if (frameResult.scanCodeRects[0]) {
          this.stopCamera();
          this.scanCodeRect = [];
          this.scanFlag = true;
          // 码图位置信息转换
          this.changeToXComponent(frameResult);
        } else {
          this.scanFlag = false;
        }
      }
    }

  // frameCallback横向码图位置信息转换为预览流xComponent对应码图位置信息
  changeToXComponent(frameResult: customScan.ScanFrame) {
    if (frameResult && frameResult.scanCodeRects) {
      let frameHeight = frameResult.height;
      let ratio = this.scanWidth / frameHeight;
      frameResult.scanCodeRects.forEach((item) => {
        this.scanCodeRect.push({
          left: this.toFixedNumber((frameHeight - item.bottom) * ratio),
          top: this.toFixedNumber(item.left * ratio),
          right: this.toFixedNumber((frameHeight - item.top) * ratio),
          bottom: this.toFixedNumber(item.right * ratio)
        });
      });
      this.scanFrameResult = JSON.stringify(this.scanCodeRect);
    }
  }

  toFixedNumber(no: number): number {
    return Number((no).toFixed(1));
  }

  async onPageShow() {
    // 自定义启动第一步,用户申请权限
    const permissions: Array<Permissions> = ['ohos.permission.CAMERA'];
    // 自定义启动第二步:设置预览流布局尺寸
    this.setDisplay();
    let grantStatus = await PermissionsUtil.checkAccessToken(permissions[0]);
    if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
      // 已经授权,可以继续访问目标操作
      this.userGrant = true;
      if (this.surfaceId) {
        // 自定义启动第三步,初始化接口
        this.initCamera();
      }
    } else {
      // 申请相机权限
      this.requestCameraPermission();
    }
  }

  async onPageHide() {
    this.releaseCamera();
  }

  // 用户申请权限
  async requestCameraPermission() {
    let grantStatus = await PermissionsUtil.reqPermissionsFromUser()
    let length: number = grantStatus.length;
    for (let i = 0; i < length; i++) {
      if (grantStatus[i] === 0) {
        // 用户授权,可以继续访问目标操作
        this.userGrant = true;
      } else {
        // 用户拒绝授权,提示用户必须授权才能访问当前页面的功能,并引导用户到系统设置中打开相应的权限
        this.userGrant = false;
      }
    }
  }

  // 竖屏时获取屏幕尺寸,设置预览流全屏示例
  setDisplay() {
    // 以手机为例计算宽高
    let displayClass = display.getDefaultDisplaySync();
    this.displayHeight = px2vp(displayClass.height);
    this.displayWidth = px2vp(displayClass.width);
    if (displayClass !== null) {
      this.scanWidth = px2vp(displayClass.width);
      this.scanHeight = Math.round(this.scanWidth * this.viewControl.width / this.viewControl.height);
      this.scanBottom = Math.max(220, px2vp(displayClass.height) - this.scanHeight);
      this.scanOffsetX = 0;
      this.scanOffsetY = 0;
    }
  }

  // 初始化相机流
  async initCamera() {
    this.isReleaseCamera = false;
    customScan.init(this.options);
    hilog.info(0x0001, TAG, 'Succeeded in initing customScan with options.');
    this.scanCodeRect = [];
    this.scanFlag = false;
    // 自定义启动第四步,请求扫码接口
    customScan.start(this.viewControl, this.callback, this.frameCallback);
  }

  // 暂停相机流
  async stopCamera() {
    if (!this.isReleaseCamera) {
      customScan.stop();
    }
  }

  // 释放相机流
  async releaseCamera() {
    if (!this.isReleaseCamera) {
      await this.stopCamera();
      await customScan.release();
      this.isReleaseCamera = true;
    }
  }

  build() {
    Stack() {
      // 相机预览流XComponent
      if (this.userGrant) {
        Column() {
          XComponent({
            id: 'componentId',
            type: XComponentType.SURFACE,
            controller: this.mXComponentController
          })
            .onLoad(() => {
              hilog.info(0x0001, TAG, 'Succeeded in loading, onLoad is called.');
              this.surfaceId = this.mXComponentController.getXComponentSurfaceId();
              hilog.info(0x0001, TAG, `Succeeded in getting surfaceId is ${this.surfaceId}`);
              this.viewControl = { width: this.scanWidth, height: this.scanHeight, surfaceId: this.surfaceId };
              // 启动相机进行扫码
              this.initCamera();
            })
            .height(this.scanHeight)
            .width(this.scanWidth)
            .position({ x: 0, y: 0 })
        }
        .height('100%')
        .width('100%')
        .position({ x: this.scanOffsetX, y: this.scanOffsetY })
      }

      Column() {
        Column() {
        }
        .layoutWeight(1)
        .width('100%')

        Column() {

          Row() {
            // 闪光灯按钮,启动相机流后才能使用
            Button('FlashLight')
              .onClick(() => {
                // 根据当前闪光灯状态,选择打开或关闭闪关灯
                if (customScan.getFlashLightStatus()) {
                  customScan.closeFlashLight();
                } else {
                  customScan.openFlashLight();
                }
              })
              .visibility(this.scanFlag ? Visibility.None : Visibility.Visible)
          }

          Row() {
            // 预览流设置缩放比例
            Button('缩放比例,当前比例:' + this.setZoomValue)
              .width(200)
              .alignSelf(ItemAlign.Center)
              .onClick(() => {
                // 设置相机缩放比例
                if (!this.scanFlag) {
                  if (!this.zoomValue || this.zoomValue === this.setZoomValue) {
                    this.setZoomValue = customScan.getZoom();
                  } else {
                    this.zoomValue = this.zoomValue;
                    customScan.setZoom(this.zoomValue);
                    setTimeout(() => {
                      if (!this.scanFlag) {
                        this.setZoomValue = customScan.getZoom();
                      }
                    }, 1000);
                  }
                }
              })
          }
          .margin({ top: 10, bottom: 10 })
          .visibility(this.scanFlag ? Visibility.None : Visibility.Visible)

          Row() {
            // 输入要设置的预览流缩放比例
            TextInput({ placeholder: '输入缩放倍数' })
              .width(200)
              .type(InputType.Number)
              .borderWidth(1)
              .backgroundColor(Color.White)
              .onChange(value => {
                this.zoomValue = Number(value);
              })
          }
          .visibility(this.scanFlag ? Visibility.None : Visibility.Visible)

          Text(this.scanFlag ? '继续扫码' : '扫码中')
            .height(30)
            .fontSize(16)
            .fontColor(Color.White)
            .onClick(() => {
              if (this.scanFlag) {
                this.scanFrameResult = '';
                this.initCamera();
              }
            })
          Text('扫码结果:' + this.scanFrameResult).fontColor(Color.White).fontSize(12)
        }
        .width('100%')
        .height(this.scanBottom)
        .backgroundColor(Color.Black)
      }
      .mainStyle()

      Image($rawfile('scan_back.svg'))
        .width(20)
        .height(20)
        .position({
          x: 40,
          y: 40
        })
        .onClick(() => {
          router.back();
        })

      // 实时扫码码图中心点位置
      if (this.scanFlag && this.scanCodeRect.length > 0) {
        ForEach(this.scanCodeRect, (item: scanBarcode.ScanCodeRect, index: number) => {
          Image($rawfile('scan_selected2.svg'))
            .width(40)
            .height(40)
            .markAnchor({ x: 20, y: 20 })
            .position({
              x: (item.left + item.right) / 2 + this.scanOffsetX,
              y: (item.top + item.bottom) / 2 + this.scanOffsetY
            })
        })
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor(this.userGrant ? Color.Transparent : Color.Black)
    .onClick((event: ClickEvent) => {
      // 是否已扫描到结果
      if (this.scanFlag) {
        return;
      }
      // 点击屏幕位置,获取点击位置(x,y),设置相机焦点
      let x1 = vp2px(event.displayY) / (this.displayHeight + 0.0);
      let y1 = 1.0 - (vp2px(event.displayX) / (this.displayWidth + 0.0));
      customScan.setFocusPoint({ x: x1, y: y1 });
      hilog.info(0x0001, TAG, `Succeeded in setting focusPoint x1: ${x1}, y1: ${y1}`);
      setTimeout(() => {
        customScan.resetFocus();

      }, 200);
    })
    .gesture(PinchGesture({ fingers: 2 })
      .onActionStart((event: GestureEvent) => {
        hilog.info(0x0001, TAG, 'Pinch start');
      })
      .onActionUpdate((event: GestureEvent) => {
        if (event) {
          this.scaleValue = event.scale;
        }
      })
      .onActionEnd((event: GestureEvent) => {
        // 是否已扫描到结果
        if (this.scanFlag) {
          return;
        }
        // 获取双指缩放比例,设置变焦比
        try {
          let zoom = customScan.getZoom();
          this.pinchValue = this.scaleValue * zoom;
          customScan.setZoom(this.pinchValue);
          hilog.info(0x0001, TAG, 'Pinch end');
        } catch (error) {
          hilog.error(0x0001, TAG, `Failed to setZoom. Code: ${error.code}, message: ${error.message}`);
        }
      }))
  }
}
  1. 通过scanCodeRect数据可确定码图中心点的位置,使用说明如下。

    • scanCodeRect的四个点坐标如下,可根据坐标点绘制码图外围矩形框。

      • 左上角(x, y):(left, top)
      • 右上角(x, y):(right, top)
      • 左下角(x, y):(left, bottom)
      • 右下角(x, y):(right, bottom)
    • 由于码图中心点坐标需和xComponent的坐标保持一致,如果xComponent的x轴和y轴存在偏移,则码图位置需做相应的偏移。例如:x轴偏移量为:scanOffsetX;y轴偏移量为:scanOffsetY,中心点坐标最终转换为:

      • x = (left + right) / 2 + scanOffsetX
      • y = (top + bottom) / 2 + scanOffsetY

模拟器开发

暂不支持模拟器使用,调用会返回错误信息“Emulator is not supported.”

最后呢

很多开发朋友不知道需要学习那些鸿蒙技术?鸿蒙开发岗位需要掌握那些核心技术点?为此鸿蒙的开发学习必须要系统性的进行。

而网上有关鸿蒙的开发资料非常的少,假如你想学好鸿蒙的应用开发与系统底层开发。你可以参考这份资料,少走很多弯路,节省没必要的麻烦。由两位前阿里高级研发工程师联合打造的《鸿蒙NEXT星河版OpenHarmony开发文档》里面内容包含了(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、音频、视频、WebGL、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、Harmony南向开发、鸿蒙项目实战等等)鸿蒙(Harmony NEXT)技术知识点

如果你是一名Android、Java、前端等等开发人员,想要转入鸿蒙方向发展。可以直接领取这份资料辅助你的学习。下面是鸿蒙开发的学习路线图。

在这里插入图片描述

针对鸿蒙成长路线打造的鸿蒙学习文档。话不多说,我们直接看详细鸿蒙(OpenHarmony )手册(共计1236页)与鸿蒙(OpenHarmony )开发入门视频,帮助大家在技术的道路上更进一步。

  • 《鸿蒙 (OpenHarmony)开发学习视频》
  • 《鸿蒙生态应用开发V2.0白皮书》
  • 《鸿蒙 (OpenHarmony)开发基础到实战手册》
  • OpenHarmony北向、南向开发环境搭建
  • 《鸿蒙开发基础》
  • 《鸿蒙开发进阶》
  • 《鸿蒙开发实战》

在这里插入图片描述

总结

鸿蒙—作为国家主力推送的国产操作系统。部分的高校已经取消了安卓课程,从而开设鸿蒙课程;企业纷纷跟进启动了鸿蒙研发。

并且鸿蒙是完全具备无与伦比的机遇和潜力的;预计到年底将有 5,000 款的应用完成原生鸿蒙开发,未来将会支持 50 万款的应用。那么这么多的应用需要开发,也就意味着需要有更多的鸿蒙人才。鸿蒙开发工程师也将会迎来爆发式的增长,学习鸿蒙势在必行! 自↓↓↓拿
1

  • 22
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值