二维码识别技术已经成为我们日常生活中不可或缺的一部分,广泛应用于支付、交通、餐饮、生活服务以及智能家居等领域。它不仅是移动应用的重要流量入口,更是连接线上线下世界的桥梁。

不同的App在扫码界面的设计上各展其特色,从页面元素到交互方式,都体现了开发者对用户体验的重视。然而,标准化的扫码界面往往难以满足开发者对个性化定制的追求。例如,开发者可能希望调整扫码页面的标题、优化扫码框的尺寸与位置,甚至定制扫码框的颜色和动画效果。

HarmonyOS SDK 统一扫码服务(Scan Kit)提供了自定义界面扫码能力,开发者可以自行定义扫码的界面样式,让扫码界面更美观,和开发者的应用风格更加匹配。

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

业务流程

功能演示

开发步骤

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

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

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

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';

4.遵循业务流程完成自定义界面扫码功能。

通过Promise方式回调,调用自定义界面扫码接口拉起相机流并返回扫码结果。

@Entry
@Component
struct CustomScanPage {
  @State userGrant: boolean = false
  @State surfaceId: string = ''
  @State isShowBack: boolean = false
  @State isFlashLightEnable: boolean = false
  @State isSensorLight:boolean = false
  // 设置预览流高度,默认单位:vp
  @State cameraHeight: number = 640
  // 设置预览流宽度,默认单位:vp
  @State cameraWidth: number = 360
  @State cameraOffsetX: number = 0
  @State cameraOffsetY: number = 0
  @State zoomValue: number = 1
  @State setZoomValue: number = 1
  @State scaleValue: number = 1
  @State pinchValue: number = 1
  @State displayHeight: number = 0
  @State displayWidth: number = 0
  private mXComponentController: XComponentController = new XComponentController()
  private TAG: string = '[customScanPage]'

  async showScanResult(result: Array<scanBarcode.ScanResult>) {
    if (result.length > 0) {
      // 获取到扫描结果后暂停相机流
      customScan.stop().then(() => {
        hilog.info(0x0001, this.TAG, 'Succeeded in stopping customScan by promise!');
      }).catch((error: BusinessError) => {
        hilog.error(0x0001, this.TAG,
          `Failed to stop customScan by promise. Code: ${error.code}, message: ${error.message}`);
      })

      // 使用toast显示出扫码结果
      promptAction.showToast({
        message: JSON.stringify(result),
        duration: 5000
      });
      this.isShowBack = true; 
    }
  }

  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;
  }

  async onPageShow() {
    await this.requestCameraPermission();
    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}`);
    })
  }

  // 自定义扫码界面的顶部返回按钮和扫码提示
  @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: '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}`);
              let viewControl: customScan.ViewControl = {
                width: this.cameraWidth,
                height: this.cameraHeight,
                surfaceId: this.surfaceId
              };
              // 启动相机进行扫码
              // 通过Promise方式回调
              customScan.start(viewControl)
                .then(async (result: Array<scanBarcode.ScanResult>) => {
                  // 处理扫码结果
                  this.showScanResult(result);
                });
              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;
             });
            })// XComponent宽、高,默认单位vp,支持px、lpx、vp
            .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(() => {
                // 点击按钮重启相机流,重新扫码
                customScan.start({ width: 1920, height: 1080, surfaceId: this.surfaceId })
                  .then(async (result: Array<scanBarcode.ScanResult>) => {
                    // 处理扫码结果
                    this.showScanResult(result);
                  })
                this.isShowBack = false;
              })
              .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)
      }
    }
    // 建议相机流设置为全屏
    .width('100%')
    .height('100%')
    .onClick((event: ClickEvent) => {
      if (this.isShowBack) {
        return;
      }
      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) => {
        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),具体可以参考Callback方式回调的示例代码

了解更多详情>>

访问统一扫码服务联盟官网

获取自定义界面扫码服务开发指导文档

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐