Scan Kit 默认界面扫码:5分钟集成
为什么需要默认界面扫码?
HarmonyOS 开发里,集成扫码能力是个高频需求。很多人第一次接触时,会尝试自己用 Camera Kit 写一个扫码界面,结果发现要处理相机预览、焦距控制、码识别、取景框绘制、闪光灯切换……一整套下来至少要多花两三天,而且稳定性还得自己扛。
Scan Kit 提供的默认界面模式,就是来解决这个问题的——你只需要调一个 API,系统就把完整的扫码界面展示给你。用户扫到码后,结果通过回调返回。整个过程不超过 5 行核心代码。
这个方案适合的场景非常明确:
- 快速验证:原型阶段,或者功能优先级不高,先跑通再说
- 功能完整即可:不需要定制 UI 样式,比如扫描框颜色、提示文案,用系统默认的够用
- 码制简单:常规的 QR 码、条形码,不需要支持过于冷门的码制
如果你需要自定义扫描框样式、手动控制相机对焦、或集成扫码结果到某个复杂的业务流程中,那就需要走自定义界面模式。但那是另一篇文章的内容。
下表可以快速判断该选哪个模式:
| 评估维度 | 默认界面模式 | 自定义界面模式 |
|---|---|---|
| 集成速度 | 5-10分钟 | 2-3天 |
| UI定制能力 | 极低 | 完全控制 |
| 相机控制 | 系统接管 | 开发者管理 |
| 码制扩展 | 基础码制 | 全面支持 |
| 稳定性 | 系统保障 | 依赖开发者代码质量 |
环境说明
DevEco Studio 版本:DevEco Studio 6.1.0 及以上 HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上 目标设备:手机(含华为机型)核心实现
1. 添加依赖并申请权限
打开entry/oh-package.json5,添加 Scan Kit 依赖:
{ "dependencies": { "@kit.ScanKit": "file:./oh_modules/@kit/ScanKit" } }然后在module.json5中声明相机权限和扫码服务:
{ "module": { "requestPermissions": [ { "name": "ohos.permission.CAMERA", "reason": "$string:permission_reason_camera", "usedScene": { "abilities": ["EntryAbility"] } } ] } }注意:相机权限属于敏感权限,运行时系统会自动弹窗提醒用户授权。不需要手动调用requestPermissionsFromUser,但reason字段必须填写,否则上架应用时会审核失败。
2. 编写一个完整的扫码页面
创建一个新的页面文件ScanPage.ets,集成 Scan Kit 的默认扫码界面:
// ScanPage.etsimport{scan}from'@kit.ScanKit';import{BusinessError}from'@kit.BasicServicesKit';@Entry@Componentstruct ScanPage{// 用于接收扫码结果的回调@StatescanResult:string='';// 控制扫码界面的显示与隐藏privateisBack:boolean=false;build(){Column(){// 默认扫码界面的调用方式Scan().startScan({scanOptions:{scanType:[scan.ScanType.QR_CODE,scan.ScanType.BARCODE],// 支持QR码和条形码enableMultiMode:false,// 关闭连续扫码,扫到一个码就返回hintText:'将二维码/条码放入框内'// 提示文字},onResult:(result:scan.ScanResult)=>{// 扫描结果回调this.scanResult=result.originalValue;// 处理结果后可以退出扫码界面this.isBack=true;},onError:(error:BusinessError)=>{console.error('Scan error: '+JSON.stringify(error));}}).width('100%').height('100%');// 显示扫描结果if(this.scanResult!==''){Text('扫描结果: '+this.scanResult).fontSize(16).margin({top:20}).padding(10);}// 返回按钮Button('返回').onClick(()=>{this.isBack=true;}).margin({top:20});}.width('100%').height('100%').padding(10);}}关键点解释:
scan.ScanType.QR_CODE和scan.ScanType.BARCODE是常用的两种码制,可以按需添加,例如AZTEC、DATA_MATRIX等enableMultiMode: false:关闭连续扫码模式。如果设置为true,用户扫完一个码后界面不会退出,可以继续扫码,适合批量录入场景hintText:扫描框下方的提示文字,可以用来说明扫码用途onResult回调中的result.originalValue就是解码后的字符串内容
但是,这段代码在实际运行中有一个比较隐蔽的问题,后面踩坑部分会详细分析。
3. 从主页导航到扫码页
在主页面Index.ets中,添加一个跳转到扫码页的按钮:
// Index.etsimport{router}from'@kit.ArkUI';@Entry@Componentstruct Index{build(){Column(){Button('打开扫码').width('80%').height(48).margin({top:200}).onClick(()=>{router.pushUrl({url:'pages/ScanPage'});});}.width('100%').height('100%');}}到这里,一个完整的扫码功能就集成完了。但别急着测试,下面这两个坑很可能会让你卡住。
常见问题 1:扫码界面无法正常的生命周期
现象:扫码页面正常弹出,也能看到系统相机预览,但扫码框不显示,或者相机启动后马上闪退。更常见的是,扫到码后「返回」按钮失效,页面无法正常回退。
原因:这个问题的本质是Scan()组件的生命周期与@State变量之间的同步问题。@State isBack虽然看起来控制着是否「返回」,但Scan()组件并不是一个普通的 UI 组件,它内部持有了相机预览、解码引擎等系统级资源。当isBack被设置为true时,系统并不会自动释放Scan()实例,导致相机资源还在占用,页面看起来卡在扫码界面。
解决方案:使用条件渲染 + 生命周期回调来彻底销毁Scan()实例。
// ScanPage.ets (修正版)import{scan}from'@kit.ScanKit';import{BusinessError}from'@kit.BasicServicesKit';@Entry@Componentstruct ScanPage{@StatescanResult:string='';@StateshowScanner:boolean=true;// 控制扫码组件的显示与销毁build(){Column(){if(this.showScanner){Scan().startScan({scanOptions:{scanType:[scan.ScanType.QR_CODE,scan.ScanType.BARCODE],enableMultiMode:false,hintText:'将二维码/条码放入框内'},onResult:(result:scan.ScanResult)=>{this.scanResult=result.originalValue;// 关闭扫码组件this.showScanner=false;},onError:(error:BusinessError)=>{console.error('Scan error: '+JSON.stringify(error));}}).width('100%').height('100%');}// 扫描结果和返回按钮if(this.scanResult!==''){Text('扫描结果: '+this.scanResult).fontSize(16).margin({top:20}).padding(10);Button('返回').onClick(()=>{// 使用系统路由返回this.showScanner=false;router.back();}).margin({top:20});}}.width('100%').height('100%').padding(10);}}关键改进点:用if (this.showScanner)包裹Scan(),当设置this.showScanner = false时,条件渲染会从组件树中移除Scan(),系统再释放相机资源。这时候再去调用router.back()才能正常返回。
常见问题 2:连续扫码模式下的结果混乱
现象:设置enableMultiMode: true后,每次扫码都会触发onResult回调,但如果用户连续快速扫码,scanResult状态会被后一次结果覆盖,导致前面扫到的码丢失。
原因:@State是单向数据流,不能直接作为累积容器用。连续扫码时,回调是异步的,多次this.scanResult = result.originalValue会相互覆盖,最终只保留最后一个值。
解决方案:使用@State数组来存储扫描结果。
// ScanPage.ets (连续扫码版本)@Entry@Componentstruct ScanMultiPage{@StatescanResults:string[]=[];// 使用数组存储多个结果@StateshowScanner:boolean=true;build(){Column(){if(this.showScanner){Scan().startScan({scanOptions:{scanType:[scan.ScanType.QR_CODE,scan.ScanType.BARCODE],enableMultiMode:true,// 开启连续扫码hintText:'支持连续扫码'},onResult:(result:scan.ScanResult)=>{// 追加到数组末尾this.scanResults.push(result.originalValue);},onError:(error:BusinessError)=>{console.error('Scan error: '+JSON.stringify(error));}}).width('100%').height('100%');}// 展示扫码历史if(this.scanResults.length>0){List(){ForEach(this.scanResults,(item:string)=>{ListItem(){Text(item).fontSize(14).padding({left:10,right:10,top:5,bottom:5});}},(item:string)=>item);}.width('100%').height(200).margin({top:20});Button('返回').onClick(()=>{this.showScanner=false;router.back();}).margin({top:20});}}.width('100%').height('100%').padding(10);}}注意:ForEach的键值生成函数直接使用item本身作为 key,因为字符串是唯一的。如果扫描结果可能重复,建议使用index作为 key。
最佳实践
默认界面模式下不要调整
Scan()组件的布局参数,比如margin、padding、scale等。Scan()内部有自己的相机预览区域,外部布局变化会影响预览区域的裁剪,导致相机黑边或显示不全。只需要设置width和height为全屏即可。扫码结果拿到后不要立即做复杂操作,比如网络请求、数据库写入、弹窗。
onResult回调是在子线程中执行的,直接在这个回调里修改 UI 状态虽然 ArkUI 能自动同步到主线程,但如果处理逻辑超过 10ms,会造成 UI 卡顿。建议只做状态赋值(如设置this.scanResult),真正业务处理放在组件渲染后的onPageShow或aboutToAppear里,或者用setTimeout延迟一帧处理。真机测试优先,不要在模拟器上调试扫码功能。模拟器没有真实的摄像头硬件,
Scan()组件在模拟器上会直接报错返回onError。这不是代码的问题,是开发环境的限制。很多人在模拟器上跑通了权限逻辑,但扫码一直报错,其实换个真机就正常了。
FAQ
Q:扫码界面出现了,但是相机预览是黑的,没有画面?
A:最常见的原因是权限申请时机不对。Scan()组件会在创建时立即尝试打开相机,如果那时系统权限弹窗还没被用户授权,相机就启动失败了。建议在进入扫码页前先通过security.privacyManager检查相机权限状态,如果没有授权,先引导用户去设置中开启。另一个常见原因是Scan()背景区域太小或父容器设置了overlay遮盖。
Q:为什么扫 QR 码正常,扫条形码就不识别?
A:ScanOptions.scanType中需要显式添加BARCODE类型,默认只包含QR_CODE。如果只扫 QR 码,可以保持不变;如果需要扫条形码,一定要像示例代码那样同时添加scan.ScanType.BARCODE。另外条形码对排版有要求,尽量让条码水平放入扫描框中央区域。
Q:连续扫码模式下,为什么扫了第一个之后就回调不触发了?
A:enableMultiMode: true开启后,Scan()组件内部会在每次扫码后重置解码状态,但这个重置过程需要 500ms-1s 左右。如果用户在这段时间内连续扫码,第二次扫码可能不会被识别。这是系统解码引擎的缓存机制,无法通过配置缩短。实际项目中建议在每次扫码回调后,播放一个短提示音或震动,给用户一个视觉/触觉反馈,告知可以扫下一个了。