HarmonyOS NEXT ScanKit 自定义扫码完全指南
在 HarmonyOS NEXT 应用开发中,扫码功能几乎是各类 App 的标配——支付、登录、信息录入,处处都需要它。鸿蒙 ScanKit 提供了从“一行代码搞掂”到“完全自己画 UI”的完整能力栈。本文将聚焦自定义界面扫码,深入拆解业务流程、核心 API 调用逻辑、UI 自定义技巧和常见坑点,帮助你在应用中实现风格统一、体验流畅的扫码功能。
一、ScanKit 能力分层:明确“自定义扫码”的定位
在动手之前,先搞清楚 ScanKit 的能力分层,这会直接影响你的技术选型:
| 层级 | 能力 | 适用场景 | 权限要求 |
|---|---|---|---|
| 系统级扫码直达 | 系统相机/控制中心扫码直接跳转 App 指定页面 | 希望从系统入口直达 App 服务 | 无需应用内申请相机权限 |
| 默认界面扫码 | 调起系统统一扫码界面,返回结果 | 通用扫码场景,对 UI 无定制需求 | 系统预授权,无需应用申请相机权限 |
| 自定义界面扫码 | 开发者完全自建扫码 UI,控制相机流 | 需要 UI 与 App 风格统一、加入交互特效、多码选择等深度定制 | 需在 module.json5 中声明相机权限并动态申请 |
| 图片解析/码图生成 | 从相册图片识别二维码,或将文本生成二维码 | “识别图中二维码”、离线生成码图 | 存储权限、无相机权限要求 |
本文聚焦第三层——自定义界面扫码,适合需要完全掌控扫码界面的场景。
二、自定义扫码的核心业务流程
自定义扫码的本质是:开发者自己提供相机预览界面,通过 ScanKit 的customScan模块将相机流数据喂给识码引擎,引擎回调返回识别结果。
整体流程如下:
text
用户进入扫码页 ↓ 动态申请相机权限(ohos.permission.CAMERA) ↓ 初始化 XComponent(渲染相机预览流) ↓ 调用 customScan.start() 启动识别引擎,传入 Surface ID ↓ 引擎持续分析帧数据,通过 Callback 返回识别结果 ↓ 收到结果 → 业务处理 → 暂停/释放相机资源
关键点在于:相机预览由 XComponent 组件负责,扫码引擎由customScan模块控制,两者通过 Surface ID 关联。
三、前置准备:权限与依赖
3.1 声明相机权限
在entry/src/main/module.json5中添加权限声明:
json
{ "module": { "requestPermissions": [ { "name": "ohos.permission.CAMERA", "reason": "$string:permission_camera_reason", "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } } ] } }同时需要在resources/base/element/string.json中配置权限使用说明(否则应用市场审核会打回)。
3.2 动态申请权限
ScanKit 自定义扫码不会像默认扫码那样预授权,必须由应用主动申请:
ts
import { abilityAccessCtrl, common } from '@kit.AbilityKit'; import { BusinessError } from '@kit.BasicServicesKit'; async function requestCameraPermission(context: common.UIAbilityContext): Promise<boolean> { const atManager = abilityAccessCtrl.createAtManager(); const grantStatus = await atManager.requestPermissionsFromUser(context, ['ohos.permission.CAMERA']); return grantStatus.authResults[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED; }3.3 导入所需模块
ts
import { customScan, scanBarcode, scanCore } from '@kit.ScanKit'; import { BusinessError } from '@kit.BasicServicesKit'; import { hilog } from '@kit.PerformanceAnalysisKit';四、自定义扫码开发实战
4.1 XComponent:相机预览的“画布”
自定义扫码的 UI 核心是XComponent组件,它负责承载相机预览流。需要指定type = 'surface',并获取其surfaceId传给扫码引擎。
ts
@Component struct CustomScanPage { @State surfaceId: string = ''; private xComponentController: XComponentController = new XComponentController(); build() { Stack() { // 相机预览区域 XComponent({ id: 'scan_xcomponent', type: XComponentType.SURFACE, controller: this.xComponentController }) .onLoad((context) => { this.surfaceId = context.getSurfaceId(); // Surface 加载完成后,启动扫码引擎 this.startCustomScan(); }) .width('100%') .height('100%') // 在此之上叠加自定义 UI 元素(扫描框、提示文字、闪光灯按钮等) this.buildCustomUI(); } } }4.2 启动自定义扫码引擎
在获取到surfaceId后,调用customScan.start将相机流与识码引擎绑定:
ts
import { customScan, scanBarcode, scanCore } from '@kit.ScanKit'; import { BusinessError } from '@kit.BasicServicesKit'; function startCustomScan(surfaceId: string): void { const viewControl: customScan.ViewControl = { surfaceId: surfaceId, // 从 XComponent 获取 width: 1080, // 预览宽度,需与 XComponent 实际尺寸匹配 height: 1920 // 预览高度 }; try { customScan.start(viewControl, (err: BusinessError, data: Array<scanBarcode.ScanResult>) => { if (err) { hilog.error(0x0001, 'CustomScan', `扫码启动失败: ${err.code}, ${err.message}`); return; } if (data && data.length > 0) { // 处理扫码结果 const result = data[0]; hilog.info(0x0001, 'CustomScan', `识别结果: ${result.originalValue}, 类型: ${result.scanType}`); // 跳转到结果页或执行业务逻辑 handleScanResult(result); } }); } catch (error) { const e = error as BusinessError; hilog.error(0x0001, 'CustomScan', `customScan.start 异常: ${e.message}`); } }4.3 控制扫码引擎的生命周期
自定义扫码需要手动管理引擎状态,避免资源泄露:
ts
// 暂停识别(如用户切换到后台) customScan.pause(); // 恢复识别 customScan.resume(); // 重新扫码(当识别到的码不在扫描框内时,可调用此方法继续扫描) customScan.rescan(); // 完全释放资源(页面销毁时必须调用) customScan.release();
典型调用时机:
aboutToDisappear()中调用release()扫码成功后调用
pause(),待用户确认后再resume()或release()应用切后台时调用
pause(),切回前台时resume()
五、自定义 UI 的核心技巧
5.1 绘制扫码框与识别区域限定
很多场景要求“只识别扫描框内的码”。customScan返回的结果中包含scanCodeRect(码图最小外接矩形坐标),开发者需要自行判断该矩形是否位于扫描框内。
ts
// 假设扫码框位于屏幕中央,尺寸 800x800 px const scanBox = { left: (cameraWidth - 800) / 2, top: (cameraHeight - 800) / 2, right: (cameraWidth + 800) / 2, bottom: (cameraHeight + 800) / 2 }; // 在 customScan.start 的 Callback 中判断 if (data && data.length > 0) { const rect = data[0].scanCodeRect; if (rect) { const isInBox = rect.left >= scanBox.left && rect.top >= scanBox.top && rect.right <= scanBox.right && rect.bottom <= scanBox.bottom; if (isInBox) { // 码在框内,处理结果 handleResult(data[0]); } else { // 码不在框内,继续扫描 customScan.rescan(); } } }注意:
scanCodeRect的坐标系与XComponent的尺寸单位需保持一致(px 或 vp),否则判断会失准。
5.2 闪光灯控制
自定义扫码需要自己实现闪光灯按钮。通过customScan模块获取和设置闪光灯状态:
ts
// 获取当前闪光灯状态 const isOn = customScan.isFlashlightOn(); // 开启/关闭闪光灯 customScan.setFlashlight(true); // true=开,false=关
也可以监听环境光传感器,在暗光条件下自动显示闪光灯按钮:
ts
// 通过 customScan.isSensorLight 判断是否暗光 if (customScan.isSensorLight) { // 显示闪光灯按钮 }5.3 调整闪光灯按钮位置
从官方示例代码中可以看出,闪光灯按钮的位置可通过margin自由调整:
ts
// 原示例中将闪光灯放在右下角 FlashLight({ isLargeScreen: false }) .margin({ bottom: -100, left: 350 }) .visibility((this.scanService.isFlashlight || this.scanService.isSensorLight) && this.scanLayout.isFlashShow ? Visibility.Visible : Visibility.Hidden)5.4 变焦控制
扫码引擎支持动态调整变焦比,适合远距离扫码场景:
ts
// 获取当前变焦比 const zoom = customScan.getZoom(); // 设置变焦比(通常为 1.0 ~ 3.0 之间) customScan.setZoom(2.0);
六、从相册选图识别
自定义扫码场景中,往往需要支持“从相册选择二维码图片”作为补充。此时使用scanBarcode.decode接口:
ts
import { scanBarcode, scanCore } from '@kit.ScanKit'; import { image } from '@kit.ImageKit'; async function decodeFromAlbum(pixelMap: image.PixelMap): Promise<string> { const options: scanBarcode.ScanOptions = { scanTypes: [scanCore.ScanType.ALL] // 支持所有码类型 }; try { const results = await scanBarcode.decode(pixelMap, options); if (results && results.length > 0) { return results[0].originalValue; } return ''; } catch (error) { const e = error as BusinessError; hilog.error(0x0001, 'Decode', `图片识别失败: ${e.message}`); return ''; } }需要配合photoAccessHelper选择图片,将选中的图片转换为PixelMap后传入。
七、扫码直达:系统级无缝跳转
虽然本文聚焦自定义扫码,但“扫码直达”是 ScanKit 6.1 引入的重要能力,值得了解。配置后可让用户通过系统相机或控制中心扫码,直接跳转到 App 指定页面。
配置步骤:
在
module.json5的skills中声明uris:
json
{ "skills": [{ "actions": ["action.view"], "uris": [{ "scheme": "https", "host": "www.yourapp.com", "path": "/product" }] }] }在
EntryAbility的onCreate和onNewWant中解析want.uri,提取参数并跳转对应页面。
这样用户扫到https://www.yourapp.com/product?id=12345的二维码时,系统会直接拉起你的 App 并跳转到商品详情页。
八、常见问题与避坑指南
Q1:自定义扫码时相机无法打开?
检查三点:
module.json5是否声明了ohos.permission.CAMERA运行时是否动态申请了权限并得到用户授权
XComponent的surfaceId是否在onLoad回调中正确获取并传入customScan.start
Q2:扫码结果一直返回空或识别率低?
检查
scanTypes配置是否正确(如只识 QR_CODE 却去扫条形码)调整
customScan.start中的width/height参数,使其与实际预览尺寸匹配适当调整对焦距离,或调用
setZoom优化画面清晰度
Q3:如何实现“只识别扫描框内的码”?
参考第五章 5.1 节的坐标判断逻辑,通过scanCodeRect与自定义扫描框的位置比对,不在框内的码调用customScan.rescan()忽略。
Q4:页面销毁后仍然收到扫码回调?
务必在aboutToDisappear()中调用customScan.release()释放引擎资源,避免内存泄漏和回调残留。
九、总结
HarmonyOS NEXT 的 ScanKit 通过customScan模块为开发者提供了完整的自定义扫码能力。核心思路是:
用 XComponent 渲染相机预览,获取
surfaceId调用
customScan.start绑定引擎,在 Callback 中处理识别结果通过
scanCodeRect实现“框内识别”,精准控制扫码范围手动管理引擎生命周期(pause/resume/release),优化资源使用
叠加自定义 UI 元素,实现与 App 风格一致的扫码界面
