##鸿蒙开发能力 ##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(); }) } }