##鸿蒙开发能力 ##HarmonyOS SDK应用服务##鸿蒙金融类应用 (金融理财#
一、前言
鸿蒙官方提供了ScanKit来实现自定义扫码的功能诉求。但是对于扫码业务的讲解缺失,所以这篇文章主要是通过扫码业务路程,串连官方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; }
4. 配置初始化扫码相机
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(); }) } }