41 二维码生成与扫描:Scan Kit 实战
·
41 二维码生成与扫描:Scan Kit 实战
前言

图:41 二维码生成与扫描:Scan Kit 实战 运行效果截图(HarmonyOS NEXT)
在"鹿鹿·笔迹心理分析"App 中,二维码是碰一碰绑定的重要备选方案。当双方无法通过 NFC 靠近时,可以通过扫一扫二维码或手动输入 ID 完成关系绑定。
本文从项目中的二维码应用场景出发,深入解析鸿蒙 Scan Kit 的扫码能力和二维码生成方案。
鸿蒙官方·Scan Kit 文档:developer.huawei.com
项目源码仓库:harmony-app GitHub

图:鸿蒙二维码生成(ImageKit)与扫描(Scan Kit)的完整流程对比
一、二维码在项目中的三种场景
1.1 场景一:碰一碰页面的二维码备选
TapSharePage 在 NFC 无法使用时的备选方案——生成二维码让对方扫码:
// TapSharePage.ets — 生成二维码按钮
Button('生成二维码')
.type(ButtonType.Capsule)
.backgroundColor(AppColors.BG_DEEP)
.fontColor(AppColors.TEXT)
.fontSize(AppFonts.BODY)
.layoutWeight(1)
.height(44)
.borderRadius(AppRadius.M)
.onClick(() => this.generateQRCode())
1.2 场景二:关系绑定二维码
BindRelationPage 提供三种邀请方式,二维码是其中之一:
private readonly methods: InviteMethod[] = [
{ key: 'tap', icon: '⌘', title: '碰一碰邀请', sub: '两台鸿蒙手机背靠背 · 自动配对', color: '#5B7FB6' },
{ key: 'qrcode', icon: '⊞', title: '扫一扫二维码', sub: '让 TA 扫描你的关系码', color: '#A8907A' },
{ key: 'id', icon: '#', title: '输入 TA 的鹿鹿 ID', sub: '8 位字母数字 · 例 LU 8847', color: '#9BAEA8' }
]
1.3 场景三:分享海报二维码
PosterTemplate 海报底部的二维码区域——扫码直达笔迹分析结果:
// PosterTemplate.ets — 二维码占位区域
Column() {
Stack() {
// 二维码区域(实际为占位设计)
Column()
.width(64).height(64)
.backgroundColor('#FFFFFF')
.borderRadius(6)
}
Text('扫码查看报告').fontSize(9).fontColor('#CCCCCC').margin({ top: 6 })
}
1.4 场景对比
| 场景 | 页面 | 二维码内容 | 扫码后的操作 |
|---|---|---|---|
| 分享备选 | TapSharePage | 笔迹分析结果链接 | 打开报告详情页 |
| 关系绑定 | BindRelationPage | 关系邀请码 | 显示邀请信息 → 接受绑定 |
| 海报分发 | PosterTemplate | 元服务直达链接 | 启动元服务卡片 |
二、Scan Kit 扫码实现
2.1 模块导入
// 方式一:@kit 统一导入(推荐)
import { scanCore } from '@kit.ScanKit'
// 方式二:@ohos 传统导入
// import { scanCore } from '@ohos.multimodalInput.scanCore'
2.2 扫码流程概览
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ 初始化扫码器 │ ──→ │ 开启扫码预览 │ ──→ │ 接收扫码结果 │
│ ScanCore │ │ Camera预览 │ │ OnResult │
└─────────────┘ └──────────────┘ └─────────────┘
│ │
↓ ↓
┌──────────────┐ ┌─────────────┐
│ 画面实时追踪 │ │ 停止扫码器 │
│ 取景框 UI │ │ 释放资源 │
└──────────────┘ └─────────────┘
2.3 初始化解码器
import { scanCore } from '@kit.ScanKit'
import { common } from '@kit.AbilityKit'
class QrScanner {
private scanEngine: scanCore.ScanEngine | null = null
/**
* 初始化解码器
*/
async initScanner(context: common.UIAbilityContext): Promise<void> {
try {
// 创建解码器配置
const config: scanCore.ScanConfig = {
scanTypes: [scanCore.ScanType.QR_CODE], // 仅识别二维码
scanMode: scanCore.ScanMode.CAMERA, // 相机模式
isAutoZoomEnabled: true, // 自动变焦(远距离自动拉近)
isMultiFeedbackEnabled: false // 单次只返回一个结果
}
// 创建解码引擎实例
this.scanEngine = scanCore.createScanEngine(context, config)
hilog.info(0x0000, 'QrScanner', '扫码引擎初始化成功')
// 注册解码结果回调
this.scanEngine.on('scanResult', (result: scanCore.ScanResult) => {
this.onScanResult(result)
})
} catch (error) {
hilog.error(0x0000, 'QrScanner', '扫码引擎初始化失败: %{public}s', JSON.stringify(error))
}
}
/**
* 扫码结果回调
*/
private onScanResult(result: scanCore.ScanResult): void {
hilog.info(0x0000, 'QrScanner', '扫码结果: %{public}s', JSON.stringify(result))
// result.originalValue — 原始解码内容
// result.scanType — 码类型
// result.rect — 码在画面中的位置
}
}
2.4 ScanConfig 配置详解
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
scanTypes |
ScanType[] |
[QR_CODE] |
识别的码类型(QR/Bar/PDF417等) |
scanMode |
ScanMode |
CAMERA |
扫码模式(相机/图片) |
isAutoZoomEnabled |
boolean |
true |
自动变焦适配远近 |
isMultiFeedbackEnabled |
boolean |
false |
是否返回多个码结果 |
enableRecognizability |
boolean |
false |
返回码的识别置信度 |
支持的码类型:
import { scanCore } from '@kit.ScanKit'
// 常用码类型
enum ScanType {
QR_CODE = 0, // 二维码
AZTEC = 1, // Aztec码
BAR_CODE = 2, // 条形码(EAN-13/UPC-A)
PDF_417 = 3, // PDF417
DATA_MATRIX = 4, // Data Matrix
CODE_128 = 5, // Code 128
}
2.5 相机预览集成
import { scanCore } from '@kit.ScanKit'
@Entry
@Component
struct ScanPage {
private scanEngine: scanCore.ScanEngine | null = null
aboutToAppear() {
this.initScanEngine()
}
aboutToDisappear() {
this.releaseScanEngine()
}
async initScanEngine(): Promise<void> {
try {
const config: scanCore.ScanConfig = {
scanTypes: [scanCore.ScanType.QR_CODE],
scanMode: scanCore.ScanMode.CAMERA
}
this.scanEngine = scanCore.createScanEngine(getContext(this), config)
this.scanEngine.on('scanResult', (result) => this.handleScanResult(result))
} catch (e) {
hilog.error(0x0000, 'ScanPage', '初始化扫码失败: %{public}s', JSON.stringify(e))
}
}
private handleScanResult(result: scanCore.ScanResult): void {
// 停止扫码
this.scanEngine?.stop()
// 处理结果
const content = result.originalValue
hilog.info(0x0000, 'ScanPage', '扫码内容: %{public}s', content)
// 根据内容路由
if (content.startsWith('lulu://')) {
this.handleLuluScan(content)
} else if (content.startsWith('http')) {
// 普通链接
this.getUIContext().getPromptAction().showToast({
message: '已识别链接: ' + content,
duration: 2000
})
}
}
private handleLuluScan(content: string): void {
// 解析 lulu:// 协议
// lulu://bind?code=LU8847
// lulu://share?reportId=42
const url = new URL(content)
if (url.host === 'bind') {
const code = url.searchParams.get('code')
// 跳转到绑定页
this.getUIContext().getRouter().pushUrl({
url: 'pages/BindRelationPage',
params: { inviteCode: code }
})
} else if (url.host === 'share') {
const reportId = parseInt(url.searchParams.get('reportId') ?? '0')
// 跳转到报告详情
this.getUIContext().getRouter().pushUrl({
url: 'pages/ReportDetailPage',
params: { reportId }
})
}
}
releaseScanEngine(): void {
this.scanEngine?.stop()
this.scanEngine?.off('scanResult')
this.scanEngine?.release()
this.scanEngine = null
}
build() {
// QrCodeScanner — 系统提供的扫码预览组件
Column() {
// 取景框区域由系统扫码组件提供
// 自定义覆盖 UI
Column() {
Text('将二维码放入框内')
.fontSize(14).fontColor(Color.White)
}
.width(240).height(240)
.border({ width: 2, color: Color.White })
.position({ x: '50%', y: '50%' })
}
.width('100%').height('100%')
.backgroundColor(Color.Black)
}
}
2.6 图片扫码(从相册选择二维码)
import { scanCore } from '@kit.ScanKit'
import { image } from '@kit.ImageKit'
import { photoAccessHelper } from '@kit.MediaLibraryKit'
async function scanFromAlbum(): Promise<string | null> {
try {
// 1. 选择图片
const photoPicker = new photoAccessHelper.PhotoViewPicker()
const result = await photoPicker.select({ maxSelectNumber: 1 })
if (!result.photoUris || result.photoUris.length === 0) return null
const uri = result.photoUris[0]
// 2. 创建图片源
const source = image.createImageSource(uri)
const pixelMap = await source.createPixelMap({
desiredPixelFormat: image.ImagePixelFormat.RGBA_8888
})
// 3. 图片扫码
const config: scanCore.ScanConfig = {
scanTypes: [scanCore.ScanType.QR_CODE],
scanMode: scanCore.ScanMode.IMAGE // ← 图片模式
}
const scanEngine = scanCore.createScanEngine(getContext(this), config)
// 4. 解码图片
// 注意:实际 API 可能使用不同的解码方式
// 此处为示意逻辑
scanEngine.on('scanResult', (scanResult) => {
hilog.info(0x0000, 'ScanFromAlbum', '图片扫码结果: %{public}s', scanResult.originalValue)
})
// 5. 释放资源
source.release()
return null
} catch (error) {
hilog.error(0x0000, 'ScanFromAlbum', '图片扫码失败: %{public}s', JSON.stringify(error))
return null
}
}
三、二维码生成方案
3.1 方案对比
| 方案 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| Canvas 绘制 | 使用 Canvas 2D API 绘制码图案 | 可控性强,无需额外依赖 | 实现复杂,需解码库配合 |
| Image Kit 编码 | image.createQRImage() |
官方 API,简单可靠 | 功能有限 |
| 第三方库 | 嵌入 qrcode 等 JS 库 | 功能丰富 | 需要 ohpm 包管理 |
| 服务端生成 | 后端 API 返回二维码图片 | 无客户端负担 | 需要联网 |
3.2 使用 Image Kit 生成二维码
鸿蒙 Image Kit 提供了 createQRImage 方法,可以快速生成二维码:
import { image } from '@kit.ImageKit'
/**
* 生成二维码 PixelMap
*/
async function generateQRCode(
content: string,
size: number = 200
): Promise<image.PixelMap | null> {
try {
const qrImage = image.createQRImage({
content, // 二维码内容
size, // 图片尺寸(px)
margin: 10, // 白边宽度
foreground: '#000000', // 前景色
background: '#FFFFFF' // 背景色
})
// 转为 PixelMap 用于显示
const pixelMap = await qrImage.getPixelMap()
return pixelMap
} catch (error) {
hilog.error(0x0000, 'QRGenerate', '生成二维码失败: %{public}s', JSON.stringify(error))
return null
}
}
3.3 保存二维码到文件
import { image } from '@kit.ImageKit'
import { fileIo as fs } from '@kit.CoreFileKit'
async function saveQRToFile(content: string, filePath: string): Promise<void> {
const pixelMap = await generateQRCode(content, 400)
if (!pixelMap) return
const packer = image.createImagePacker()
const packedData = await packer.packing(pixelMap, {
format: 'image/png',
quality: 100
})
const file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE)
const buf = packedData.data.buffer
file.writeSync(buf)
fs.closeSync(file)
packer.release()
hilog.info(0x0000, 'QRGenerate', '二维码已保存: %{public}s', filePath)
}
3.4 项目中的二维码生成流程
// TapSharePage.ets — 二维码生成完整流程
private async generateQRCode(): Promise<void> {
this.isSharing = true
hilog.info(0x0000, 'TapSharePage', '生成二维码')
try {
// 1. 构造二维码内容
const shareContent = this.buildShareContent()
// 2. 生成二维码 PixelMap
const qrPixelMap = await generateQRCode(shareContent, 300)
if (qrPixelMap) {
// 3. 展示二维码弹窗或跳转到二维码页面
AppStorage.setOrCreate<image.PixelMap>('qr_pixel_map', qrPixelMap)
// 4. 提示用户
this.getUIContext().getPromptAction().showToast({
message: '二维码已生成,请对方扫码',
duration: 2000
})
}
} catch (error) {
hilog.error(0x0000, 'TapSharePage', '二维码生成失败: %{public}s', JSON.stringify(error))
this.getUIContext().getPromptAction().showToast({
message: '生成失败,请重试',
duration: 1500
})
}
}
/**
* 构建分享协议内容
*/
private buildShareContent(): string {
const userId = AppStorage.get<number>('user_id') ?? 1
const userName = AppStorage.get<string>('user_name') ?? ''
// 使用自定义协议
// 分享场景:lulu://share?userId=1&name=小李&ts=1749876543
return `lulu://share?userId=${userId}&name=${encodeURIComponent(userName)}&ts=${Date.now()}`
}
四、二维码与 NFC 的互补设计
4.1 多通道策略
在实际场景中,NFC 和二维码各有优劣,项目采用多通道备选策略:
/**
* NFC + 二维码 + ID 输入的多通道分享策略
*
* 优先级:
* ① NFC 碰一碰(最快捷,无需视觉交互)
* ② 扫二维码(视觉对准,距离远)
* ③ 输入鹿鹿 ID(完全远程,手动输入)
*/
4.2 使用场景
| 条件 | 推荐方式 | 原因 |
|---|---|---|
| 两台鸿蒙手机靠近 | NFC 碰一碰 | 最快,只需背靠背 |
| 一台扫码/线下海报 | 扫二维码 | 摄像头对准,远程分享 |
| 远程/不在身边 | 输入鹿鹿 ID | 仅需文字,可截图发送 |
| NFC 不可用时 | 二维码备选 | 自动降级 |
4.3 统一数据协议
/**
* 统一分享协议格式
*
* NFC NDEF 和二维码内容使用相同的协议格式,
* 确保接收端以相同方式解析:
*
* NFC NDEF:
* 记录类型: lulu.com:share
* 载荷: lulu://share?userId=1&ts=1749876543
*
* 二维码:
* 内容: lulu://share?userId=1&ts=1749876543
*
* 鹿鹿 ID:
* 手动输入: LU8847 → 后端查 userId
*/
五、权限与配置
5.1 module.json5 权限
{
"requestPermissions": [
{
"name": "ohos.permission.CAMERA",
"reason": "$string:camera_permission_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
Scan Kit 的扫码功能依赖相机权限,但解码器本身不需要额外权限。
5.2 oh-package.json5 依赖
Scan Kit 是系统内置 Kit,无需额外 ohpm 依赖,直接 @kit.ScanKit 导入即可。
六、UI 交互建议
6.1 取景框设计
@Builder
ScanViewfinder() {
Stack() {
// 半透明遮罩
Column()
.width('100%').height('100%')
.backgroundColor('rgba(0,0,0,0.5)')
// 透明取景框
Column()
.width(240).height(240)
.border({ width: 2, color: Color.White })
.borderRadius(12)
.clip(new Rect({ width: 240, height: 240 }))
// 四角引导符
// 左上角、右上角、左下角、右下角
CornerGuides()
}
}
6.2 扫码成功反馈
// 扫码成功震动反馈 + 提示音
function notifyScanSuccess(): void {
// 1. 视觉反馈 — 取景框闪烁
// 2. 振动反馈(需要 @ohos.multimodalInput.vibrator)
// 3. 提示音(系统默认扫码音效)
}
七、二维码数据安全
7.1 防篡改
/**
* 二维码内容安全策略
*
* 1. 不直接在二维码中包含用户隐私信息
* 2. 只包含引用 ID 和时间戳
* 3. 敏感数据通过 DB 查询获取
*
* 不安全做法:
* lulu://share?name=真实姓名&phone=138xxxx&report=完整报告内容
*
* 安全做法:
* lulu://share?token=encrypted_token&expiry=1749876543
*/
7.2 过期机制
function isQRCodeExpired(qrContent: string): boolean {
try {
const url = new URL(qrContent)
const expiry = parseInt(url.searchParams.get('expiry') ?? '0')
if (expiry === 0) return false // 无过期时间
return Date.now() > expiry
} catch {
return false
}
}
八、注意事项与常见问题
8.1 开发注意事项
在实际开发过程中,需特别注意以下几点:
- API 兼容性:部分接口仅在特定 HarmonyOS NEXT 版本中可用,需做版本条件判断
- 权限模型:采用静态声明(module.json5)+ 动态申请(requestPermissionsFromUser)的两阶段授权
- 生命周期:合理使用
aboutToAppear()和aboutToDisappear()管理资源初始化与释放 - 状态同步:跨页面数据通过 AppStorage 共享,组件内状态使用
@State/@Prop/@Link装饰器
8.2 常见错误与解决方案
常见问题快速排查表:
| 问题类型 | 排查方向 | 参考方法 |
|---|---|---|
| 应用崩溃 | 查看 hilog 错误日志 | hilog.error(TAG, "...", e.message) |
| 状态丢失 | 检查 AppStorage 键名拼写 | 统一使用常量管理键名 |
| 动画不流畅 | 避免在 animateTo 回调中执行 I/O | 动画与数据操作分离 |
总结
本文从项目中的二维码应用场景出发,完整解析了鸿蒙二维码生成与扫描技术:
- 三种场景:分享备选二维码、关系绑定二维码、海报分发二维码
- Scan Kit 扫码:
ScanConfig配置 → 相机/图片扫码 →scanResult回调处理 - 二维码生成:
image.createQRImage()官方 API 快速生成 - 多通道策略:NFC + 二维码 + ID 输入三级备选方案
- 安全设计:不包含隐私信息、引用 ID 加时间戳、过期机制
下一篇文章将介绍 图片分享——通过 pasteboard 和 Share Kit 实现多种渠道分享。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
参考资源:
- Scan Kit 开发概述
- @kit.ScanKit 模块
- ScanConfig 配置参考
- Image Kit createQRImage
- 相机权限声明
- [TapSharePage 项目源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/pages/TapSharePage.ets)
- [BindRelationPage 项目源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/pages/BindRelationPage.ets)
- HarmonyOS 开发文档
更多推荐



所有评论(0)