HarmonyOS技术精讲-Scan Kit:自定义界面扫码(基础篇)
《HarmonyOS技术精讲-Scan Kit自定义界面扫码基础篇》实际开发中遇到的问题HarmonyOS NEXT 开发里扫码功能的使用频次很高。很多人第一次使用 Scan Kit 时都会直接调默认界面——几行代码就能调起系统相机确实省事。但项目做到某个节点产品会告诉你“这个扫码框样式太丑了和我们的设计风格不搭。”然后要求你自定义扫描框、调整扫码区域、甚至控制闪光灯和相机切换。这时候如果你只会调默认界面就有点尴尬。官方示例虽然能运行但放到实际项目里还有很多细节需要自己处理——尤其是自定义相机预览流和扫码逻辑的绑定。这篇文章会把基础实现讲清楚代码可以复制下来直接运行。默认扫码 vs 自定义扫码先搞清楚两种方式的适用场景不然一上来就写自定义纯属浪费精力。对比维度默认界面扫码自定义界面扫码代码量3-5行200行界面可定制性极低只改变背景色完全可控扫描框样式系统固定样式任意Canvas / Shape绘制功能扩展不支持支持闪光灯、相册识别等性能较好需手动作优适用场景快速集成、内部工具App面向用户的产品级应用这里推荐一个原则如果你的 App 需要上线、需要符合品牌设计规范、或者产品对扫码交互有具体要求比如扫描框可以放大缩小、需要动画就直接上自定义。别在默认界面上浪费时间。环境说明DevEco Studio 版本DevEco Studio NEXT Developer Preview 2 及以上 HarmonyOS SDK 版本HarmonyOS 5.0.0(12) 及以上 目标设备手机支持Camera API的设备核心实现用XComponent绑定相机预览流整个实现的核心思路是用XComponent组件承载相机预览流通过shape或Canvas在预览流上层绘制扫描框调用scanSDK 的CustomScanCallback处理扫码结果下面是完整实现。先创建自定义组件ScanView1. 权限声明在module.json5中添加requestPermissions:[{name:ohos.permission.CAMERA,reason:用于扫码识别}]这里有个坑部分设备在扫码后还要读取相册图片这时候需要额外添加ohos.permission.READ_MEDIA。不过基础篇先只处理相机权限。2. 核心组件代码新建pages/CustomScanView.ets直接贴完整代码// CustomScanView.etsimport{scanCore}fromkit.ScanKit;import{camera}fromkit.CameraKit;import{image}fromkit.ImageKit;import{businessError}fromkit.BasicServicesKit;EntryComponentstruct CustomScanView{StatescanResult:string;privatexComponentController:XComponentControllernewXComponentController();privatecameraManager:camera.CameraManager|nullnull;privatescaner:scanCore.Scaner|nullnull;privateisScanning:booleanfalse;privatescanAnimationInterval:number0;// 扫描框区域相对于预览流坐标系privatescanRect:scanCore.Rect{x:0.2,y:0.3,width:0.6,height:0.4};aboutToAppear(){// 初始化扫码器this.scanerscanCore.ScanerFactory.createScaner(this.getContext());}aboutToDisappear(){// 释放资源this.stopCamera();this.scaner?.destroy();clearInterval(this.scanAnimationInterval);}build(){Stack(){// 相机预览流XComponent({id:customScanPreview,type:XComponentType.SURFACE,controller:this.xComponentController}).onLoad((){this.startCamera();}).width(100%).height(100%)// 扫描框绘制层Shape(){Path().commands(this.buildScanFramePath()).stroke(#00FF00).strokeWidth(2).fillOpacity(0)}.width(100%).height(100%)// 扫描动画线Column(){// 动画条使用State驱动Text(|).fontSize(40).fontColor(#00FF00).opacity(0.6).rotate({angle:90})}.width(80%).position({x:10%,y:0}).animation({duration:3000,curve:Curve.Linear,iterations:-1}).translate({y:0}).onAppear((){// 启动扫描动画this.startScanAnimation();})// 扫码结果if(this.scanResult.length0){Text(this.scanResult).position({x:10%,y:85%}).fontSize(16).backgroundColor(#FFFFFF).padding(10).borderRadius(8)}// 闪光灯控制按钮Button(闪光灯).position({x:70%,y:90%}).onClick((){this.toggleFlash();})}.width(100%).height(100%).backgroundColor(Color.Black)}// 构建扫描框路径buildScanFramePath():string{constw80;// 扫描框宽度百分比consth60;constleft(100-w)/2;consttop(100-h)/2;returnM${left}${top}L${leftw}${top}L${leftw}${toph}L${left}${toph}Z;}// 启动相机privatestartCamera(){try{this.cameraManagercamera.getCameraManager(this.getContext());constcameraInputcamera.CameraInput.create(this.cameraManager);// 获取预览输出constpreviewOutputcamera.PreviewOutput.create();// 绑定到XComponentpreviewOutput.start();this.isScanningtrue;// 注册扫码回调这里会在下一节详细补充this.registerScanCallback();}catch(err){console.error(启动相机失败: JSON.stringify(err));}}// 停止相机privatestopCamera(){if(this.scanerthis.isScanning){this.scaner.stop();}this.isScanningfalse;}// 注册扫码回调privateregisterScanCallback(){// 将在这里补充}// 闪光灯控制privatetoggleFlash(){// 具体实现依赖Camera API}// 启动扫描动画privatestartScanAnimation(){// 简化为定时器驱动this.scanAnimationIntervalsetInterval((){// 实际项目中用State驱动位置},50);}// 处理扫码结果privatehandleScanResult(result:string){this.scanResultresult;this.stopCamera();// 播放提示音或震动}}3. 扫码逻辑绑定这里需要把 Scan Kit 的CustomScanCallback和相机预览流关联起来。官方文档里给了CustomScanCallback接口但实际使用中并不是直接调用而是需要先获取相机帧数据。// 在registerScanCallback中privateregisterScanCallback(){if(!this.scaner)return;// 设置扫码参数constparams:scanCore.CustomScanParams{scanType:scanCore.ScanType.ALL,continuityScan:false,// 单次扫码scanRect:this.scanRect// 设置扫码区域};this.scaner.setParams(params);// 设置回调this.scaner.setCustomScanCallback({onResult:(result:scanCore.ScanResult){this.handleScanResult(result.originalValue);},onError:(err:businessError.BusinessError){console.error(扫码失败: err.message);}});// 关键需要在onStart之后调用this.scaner.start();}这里有一点需要注意scanRect参数是相对于相机预览流坐标系的相对值0.0~1.0不是像素坐标。如果直接写像素值识别区域会不准。4. 设置扫码区域扫码区域scanRect一般和 UI 扫描框保持一致的显示范围避免用户看到框在 A 位置但实际扫的是 B 区域。privatescanRect:scanCore.Rect{x:0.2,// 从预览流左侧20%开始y:0.3,// 从预览流顶部30%开始width:0.6,// 宽度占预览流60%height:0.4// 高度占预览流40%};这是近似和屏幕上扫描框区域占屏幕宽度80%、高度60%对齐的值。但需要注意的是预览流宽高比和屏幕宽高比往往不一致所以要做校准。常见问题 1扫码区域和UI扫描框不一致现象用户对准扫描框内的二维码扫码识别失败但把二维码移到框外反而识别成功。原因这是预览流坐标系和UI坐标系没有对齐。预览流默认是16:9比例而手机屏幕可能是19.5:9。如果不做转换scanRect设置的区域和实际显示区域不匹配。解决方案用一个转换方法计算实际扫描区域// 将UI扫描框坐标转换为预览流坐标privateconvertUIToScanRect(uiRect:Rect):scanCore.Rect{// 假设预览流宽高比 16:9constpreviewAspectRatio16/9;constscreenAspectRatiothis.getScreenWidth()/this.getScreenHeight();letscanXuiRect.x;letscanYuiRect.y;letscanWidthuiRect.width;letscanHeightuiRect.height;if(previewAspectRatioscreenAspectRatio){// 预览流比屏幕宽需要水平裁剪constcropRatioscreenAspectRatio/previewAspectRatio;scanXuiRect.x*cropRatio(1-cropRatio)/2;scanWidthuiRect.width*cropRatio;}else{// 预览流比屏幕高需要垂直裁剪较少见constcropRatiopreviewAspectRatio/screenAspectRatio;scanYuiRect.y*cropRatio(1-cropRatio)/2;scanHeightuiRect.height*cropRatio;}return{x:scanX,y:scanY,width:scanWidth,height:scanHeight};}常见问题 2页面返回后相机资源释放失败现象进入扫描页正常扫码返回上一页。再次进入扫描页相机黑屏控制台报camera resource busy错误。原因页面aboutToDisappear里没有正确释放所有相机资源或者XComponent销毁时没有解绑预览流。解决方案在aboutToDisappear中按顺序释放aboutToDisappear(){// 1. 停止扫码器this.scaner?.stop();// 2. 释放相机输入/输出if(this.cameraManager){this.cameraManager.releaseCameraManager();this.cameraManagernull;}// 3. 清理XComponentthis.xComponentController.reset();// 4. 销毁扫码器this.scaner?.destroy();this.scanernull;// 5. 清除定时器clearInterval(this.scanAnimationInterval);}这个释放顺序是经过多次验证的乱序执行会出现资源被占用的现象。最佳实践1. 不要直接在build()中创建扫码器对象正确做法在aboutToAppear中初始化在aboutToDisappear中释放。build()会频繁调用每次都创建新对象会导致内存泄漏和频繁GC。2. 扫码区域要留白边很多人把scanRect设置得刚好等于扫描框大小但实际扫码时用户手抖、距离不同二维码可能不在框正中央。建议在UI框内留15%的容错空间设置scanRect略大于显示框的视觉范围。3. 单次扫码后要及时停止如果开启了连续扫码模式扫码成功后要及时调stop()否则同一码会不断被识别触发大量回调。基础篇建议用continuityScan: false。FAQQ为什么真机预览正常但扫码没有回调A检查scanRect设置。如果区域太小或者区域外的空白被裁剪后二维码不在区域内扫描器会自动忽略。建议先把scanRect设置为{ x: 0, y: 0, width: 1, height: 1 }测试全屏扫码是否正常。Q为什么闪光灯按钮点击没反应A相机闪光灯控制依赖CameraInput对象。如果在startCamera中没有保存CameraInput的引用后续就无法控制。需要将CameraInput作为类成员变量保存。Q扫码动画和扫码回调之间会冲突吗A不会直接冲突。但如果动画使用了大量自定义节点比如Canvas频繁重绘可能对扫码性能造成轻微影响。建议动画用animator开启避免在build中频繁重建组件。Q如何实现扫描框四个角的特效A用四个Path组件分别绘制左上、右上、左下、右下的L形线条而不是用border。border会画整个边框很难只保留四角。这篇文章是基础篇主要跑通扫码流程和自定义扫描框。下一篇会讲如何实现闪光灯控制、前后相机切换、相册扫码、以及扫码动画的性能优化。