##鸿蒙开发能力 ##HarmonyOS SDK应用服务##鸿蒙金融类应用 (金融理财# 一、前言 鸿蒙官方提供了ScanKit来实现自定义扫码的功能诉求。但是对于扫码业务的讲解缺失,所以这篇文章主要是通过扫码业务路程,串连官方Kit的接口。让大家能更深刻的理解自定义扫码业务。 官方Scan Kit接口说明 (1)鸿蒙提供的ScanKit具备以下五种能力:
- 扫码直达
- 自定义扫码,图像识码 (自定义扫码需要这两种能力组合在一起,所以我分类在一起)
- 码图生成
- 系统提供的默认界面扫码
(2)业内市面上的自定义扫码界面,主要由以下几个部分功能点构成:
- 扫码(单,多)【鸿蒙最多支持四个二维码的识别】
- 解析图片二维码
- 扫码动画
- 扫码振动和音效
- 无网络监测与提示
- 多码暂停选中点的绘制
- 扫码结果根据类型分开处理(应用内部处理,外部H5处理)【这个不做展开】
- 焦距控制(放大缩小) 二、功能设计思路:
首先我们需要绘制整体UI界面布局,常规分为相机流容器view,动画表现view,按钮控制区view。 1.创建相机视频流容器 在ScanKit中相机流通过XComponent组件作为相机流的容器。 @Builder ScanKitView(){ XComponent({ id: 'componentId', type: XComponentType.SURFACE, controller: this.mXComponentController }) .onLoad(async () => { // 视频流开始加载回调 }) .width(this.cameraWidth) // cameraWidth cameraHeight 参见步骤二 .height(this.cameraHeight)
} 2.需要测算XComponent呈现的相机流宽高 // 竖屏时获取屏幕尺寸,设置预览流全屏示例 setDisplay() { // 折叠屏无 or 折叠 if(display.getFoldStatus() == display.FoldStatus.FOLD_STATUS_UNKNOWN || display.getFoldStatus() == display.FoldStatus.FOLD_STATUS_FOLDED){ // 默认竖屏 let displayClass = display.getDefaultDisplaySync(); this.displayHeight = px2vp(displayClass.height); this.displayWidth = px2vp(displayClass.width);
}else{
  // 折叠屏展开 or 半展开
  let displayClass = display.getDefaultDisplaySync();
  let tempHeight = px2vp(displayClass.height);
  let tempWidth = px2vp(displayClass.width);
  console.info("debugDisplay", 'tempHeight: ' + tempHeight + " tempWidth: " + tempWidth);
  this.displayHeight = tempHeight + px2vp(8);
  this.displayWidth = ( tempWidth - px2vp(64) ) / 2;
}
console.info("debugDisplay", 'final displayHeight: ' + this.displayHeight + " displayWidth: " + this.displayWidth);
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;} 3.使用相机,需要用户同意申请的Camera权限 module.json5配置 "requestPermissions": [ { "name" : "ohos.permission.CAMERA", "reason": "$string:app_name", "usedScene": { "abilities": [ "EntryAbility" ], "when":"inuse" } } ] 需要注意时序,每次显示自定义扫码界面,都需要检查权限。所有建议放在onPageshow系统周期内。 async onPageShow() { await this.requestCameraPermission();
}
/**
- 用户申请相机权限
- /
async requestCameraPermission() {
let grantStatus = await this.reqPermissionsFromUser();
for (let i = 0; i < grantStatus.length; i++) {
  if (grantStatus[i] === 0) {
 } } } /**// 用户授权,可以继续访问目标操作 console.log(this.TAG, "Succeeded in getting permissions."); this.userGrant = true;
- 用户申请权限
- @returns
- / async reqPermissionsFromUser(): Promise<number[]> { let context = getContext() as common.UIAbilityContext; let atManager = abilityAccessCtrl.createAtManager(); let grantStatus = await atManager.requestPermissionsFromUser(context, ['ohos.permission.CAMERA']); return grantStatus.authResults; }
- 配置初始化扫码相机 import { customScan } from '@kit.ScanKit' - private setScanConfig(){ // 多码扫码识别,enableMultiMode: true 单码扫码识别enableMultiMode: false let options: scanBarcode.ScanOptions = { scanTypes: [scanCore.ScanType.ALL], enableMultiMode: true, enableAlbum: true } } // 初始化接口 customScan.init(options); 
5.开启相机 此处需要注意时序,开启相机需要在权限检查后,配置初始化了相机,并且在XComponent相机视频流容器加载回调后进行。(如果需要配置闪光灯的处理,可在此处一同处理)【完整代码示例,参见章节三】 @Builder ScanKitView(){ XComponent({ id: 'componentId', type: XComponentType.SURFACE, controller: this.mXComponentController }) .onLoad(async () => {
    // 获取XComponent组件的surfaceId
    this.surfaceId = this.mXComponentController.getXComponentSurfaceId();
    console.info(this.TAG, "Succeeded in getting surfaceId: " + this.surfaceId);
    this.startCamera();
    this.setFlashLighting();
  })
  .width(this.cameraWidth)
  .height(this.cameraHeight)
  .position({ x: this.cameraOffsetX, y: this.cameraOffsetY })}
/**
- 启动相机 
- / private startCamera() { this.isShowBack = false; this.scanResult = []; let viewControl: customScan.ViewControl = { width: this.cameraWidth, height: this.cameraHeight, surfaceId : this.surfaceId }; // 自定义启动第四步,请求扫码接口,通过Promise方式回调 try { customScan.start(viewControl) - .then(async (result: Array<scanBarcode.ScanResult>) => { console.error(this.TAG, 'result: ' + JSON.stringify(result)); if (result.length) { // 解析码值结果跳转应用服务页 this.scanResult = result; this.isShowBack = true; // 获取到扫描结果后暂停相机流 try { customScan.stop().then(() => { console.info(this.TAG, 'Succeeded in stopping scan by promise '); }).catch((error: BusinessError) => { console.error(this.TAG, 'Failed to stop scan by promise err: ' + JSON.stringify(error)); }); } catch (error) { console.error(this.TAG, 'customScan.stop err: ' + JSON.stringify(error)); } } }).catch((error: BusinessError) => { console.error(this.TAG, 'customScan.start err: ' + JSON.stringify(error));- }); } catch (err) { console.error(this.TAG, 'customScan.start err: ' + JSON.stringify(err)); } } 完成以上步骤后,就可以使用自定义扫码功能,进行二维码和条码的识别了。 三、示例源码: 
ScanPage.ets 兼容折叠屏,Navigation。 import { customScan, scanBarcode, scanCore } from '@kit.ScanKit' import { hilog } from '@kit.PerformanceAnalysisKit' import { BusinessError } from '@kit.BasicServicesKit' import { abilityAccessCtrl, common } from '@kit.AbilityKit' import { display, promptAction, router } from '@kit.ArkUI'
@Builder export function ScanPageBuilder(name: string, param: object){ if(isLog(name, param)){ ScanPage() } }
function isLog(name: string, param: object){ console.log("ScanPageBuilder", " ScanPageBuilder init name: " + name); return true; }
@Entry @Component export struct ScanPage { private TAG: string = '[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 = 480 // 设置预览流高度,默认单位:vp @State cameraWidth: number = 300 // 设置预览流宽度,默认单位: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()
async onPageShow() { // 自定义启动第一步,用户申请权限 await this.requestCameraPermission(); // 自定义启动第二步:设置预览流布局尺寸 this.setDisplay(); // 自定义启动第三步,配置初始化接口 this.setScanConfig(); }
private setScanConfig(){ // 多码扫码识别,enableMultiMode: true 单码扫码识别enableMultiMode: false let options: scanBarcode.ScanOptions = { scanTypes: [scanCore.ScanType.ALL], enableMultiMode: true, enableAlbum: true } 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});
    })
  }
/**
- 用户申请权限 
- @returns 
- / 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) { - // 用户授权,可以继续访问目标操作 console.log(this.TAG, "Succeeded in getting permissions."); this.userGrant = true;- } } } - // 竖屏时获取屏幕尺寸,设置预览流全屏示例 setDisplay() { // 折叠屏无 or 折叠 if(display.getFoldStatus() == display.FoldStatus.FOLD_STATUS_UNKNOWN || display.getFoldStatus() == display.FoldStatus.FOLD_STATUS_FOLDED){ // 默认竖屏 let displayClass = display.getDefaultDisplaySync(); this.displayHeight = px2vp(displayClass.height); this.displayWidth = px2vp(displayClass.width); - }else{ // 折叠屏展开 or 半展开 let displayClass = display.getDefaultDisplaySync(); let tempHeight = px2vp(displayClass.height); let tempWidth = px2vp(displayClass.width); console.info("debugDisplay", 'tempHeight: ' + tempHeight + " tempWidth: " + tempWidth); this.displayHeight = tempHeight + px2vp(8); this.displayWidth = ( tempWidth - px2vp(64) ) / 2; - } console.info("debugDisplay", 'final displayHeight: ' + this.displayHeight + " displayWidth: " + this.displayWidth); - 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 }); } - /** 
- 启动相机 
- / private startCamera() { this.isShowBack = false; this.scanResult = []; let viewControl: customScan.ViewControl = { width: this.cameraWidth, height: this.cameraHeight, surfaceId : this.surfaceId }; // 自定义启动第四步,请求扫码接口,通过Promise方式回调 try { customScan.start(viewControl) - .then(async (result: Array<scanBarcode.ScanResult>) => { console.error(this.TAG, 'result: ' + JSON.stringify(result)); if (result.length) { // 解析码值结果跳转应用服务页 this.scanResult = result; this.isShowBack = true; // 获取到扫描结果后暂停相机流 try { customScan.stop().then(() => { console.info(this.TAG, 'Succeeded in stopping scan by promise '); }).catch((error: BusinessError) => { console.error(this.TAG, 'Failed to stop scan by promise err: ' + JSON.stringify(error)); }); } catch (error) { console.error(this.TAG, 'customScan.stop err: ' + JSON.stringify(error)); } } }).catch((error: BusinessError) => { console.error(this.TAG, 'customScan.start err: ' + JSON.stringify(error));- }); } catch (err) { console.error(this.TAG, 'customScan.start err: ' + JSON.stringify(err)); } } - /** 
- 注册闪光灯监听接口 
- / private setFlashLighting(){ customScan.on('lightingFlash', (error, isLightingFlash) => { if (error) { - console.info(this.TAG, "customScan lightingFlash error: " + JSON.stringify(error)); return;- } if (isLightingFlash) { - this.isFlashLightEnable = true;- } else { - if (!customScan?.getFlashLightStatus()) { this.isFlashLightEnable = false; }- } this.isSensorLight = isLightingFlash; }); } - // 自定义扫码界面的顶部返回按钮和扫码提示 @Builder TopTool() { Column() { Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) { - Text('返回') .onClick(async () => { // router.back(); this.mNavContext?.pathStack.removeByName("ScanPage"); })- }.padding({ left: 24, right: 24, top: 40 }) - Column() { - Text('扫描二维码/条形码') Text('对准二维码/条形码,即可自动扫描')- }.margin({ left: 24, right: 24, top: 24 }) } .height(146) .width('100%') } - @Builder ScanKitView(){ XComponent({ id: 'componentId', type: XComponentType.SURFACE, controller: this.mXComponentController }) .onLoad(async () => { - // 获取XComponent组件的surfaceId this.surfaceId = this.mXComponentController.getXComponentSurfaceId(); console.info(this.TAG, "Succeeded in getting surfaceId: " + this.surfaceId); this.startCamera(); this.setFlashLighting();- }) .width(this.cameraWidth) .height(this.cameraHeight) .position({ x: this.cameraOffsetX, y: this.cameraOffsetY }) } - @Builder ScanView(){ Stack() { - Column() { - if (this.userGrant) { this.ScanKitView() }- } .height('100%') .width('100%') .backgroundColor(Color.Red) - 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('ReScan') .onClick(() => { try { customScan.rescan(); } catch (error) { console.error(this.TAG, 'customScan.rescan err: ' + JSON.stringify(error)); } // 点击按钮重启相机流,重新扫码 this.startCamera(); }) .visibility(this.isShowBack ? Visibility.Visible : Visibility.None) // 跳转下个页面 Button('点击跳转界面') .onClick(() => { router.pushUrl({ url: "pages/Index1", }) }) } 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($r("app.media.icon_select_dian")) .width(20) .height(20) .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}`); }- })) } - private mNavContext: NavDestinationContext | null = null; - build() { NavDestination(){ this.ScanView() } .width("100%") .height("100%") .hideTitleBar(true) .onReady((navContext: NavDestinationContext)=>{ this.mNavContext = navContext; }) .onShown(()=>{ this.onPageShow(); }) .onHidden(()=>{ this.onPageHide(); }) } } 

 
  
  
 
 
 

 
  
 