-
-
[原创]自定义界面扫码,满足应用个性化定制需求
-
发表于: 2024-9-9 11:34 2960
-
二维码识别技术已经成为我们日常生活中不可或缺的一部分,广泛应用于支付、交通、餐饮、生活服务以及智能家居等领域。它不仅是移动应用的重要流量入口,更是连接线上线下世界的桥梁。
不同的App在扫码界面的设计上各展其特色,从页面元素到交互方式,都体现了开发者对用户体验的重视。然而,标准化的扫码界面往往难以满足开发者对个性化定制的追求。例如,开发者可能希望调整扫码页面的标题、优化扫码框的尺寸与位置,甚至定制扫码框的颜色和动画效果。
HarmonyOS SDK 统一扫码服务(Scan Kit)提供了自定义界面扫码能力,开发者可以自行定义扫码的界面样式,让扫码界面更美观,和开发者的应用风格更加匹配。
自定义界面扫码能力提供扫码相机流控制接口,支持相机流的初始化、开启、暂停、释放功能;支持闪光灯的状态获取、开启、关闭;支持变焦比的获取和设置;支持对条形码、二维码等进行扫码识别,并获得码类型、码值、码位置信息、相机预览流(YUV)。该能力可用于单码和多码的扫描识别。
业务流程
功能演示
开发步骤
自定义界面扫码接口支持自定义UI界面,识别相机流中的条形码,二维码等,并返回码图的值、类型、码的位置信息(码图最小外接矩形左上角和右下角的坐标)以及相机预览流(YUV)。
以下示例为调用自定义界面扫码接口拉起相机流并返回扫码结果和相机预览流(YUV)。
1.在开发应用前,需要先申请相机相关权限,确保应用拥有访问相机的权限。在"module.json5"文件中配置相机权限,具体配置方式,请参见声明权限。
2.使用接口requestPermissionsFromUser去校验当前用户是否已授权。具体申请方式及校验方式,请参见向用户申请授权。
3.导入自定义界面扫码接口以及相关接口模块,导入方法如下。
1 2 3 4 5 6 | 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方式回调,调用自定义界面扫码接口拉起相机流并返回扫码结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 | @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方式回调的示例代码。
了解更多详情>>
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)