【maaath】 为开源鸿蒙 Flutter 跨平台工程集成扫码识别能力
在移动应用开发中,扫码识别是最常见的功能之一,涵盖二维码、条形码、Data Matrix 等多种格式。传统 native 开发需要针对不同平台编写平台特定代码,而借助跨平台技术栈,开发者可以在 ArkTS 原生项目中以极低的接入成本复用一个 Flutter 引擎,统一处理业务逻辑与 UI 渲染。本文以一个真实的 OpenHarmony demo 工程oh_demo11二维码/条形码扫描闪光灯控制扫
为开源鸿蒙 Flutter 跨平台工程集成扫码识别能力
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
作者:maaath
前言
在移动应用开发中,扫码识别是最常见的功能之一,涵盖二维码、条形码、Data Matrix 等多种格式。传统 native 开发需要针对不同平台编写平台特定代码,而借助 Flutter for OpenHarmony 跨平台技术栈,开发者可以在 ArkTS 原生项目中以极低的接入成本复用一个 Flutter 引擎,统一处理业务逻辑与 UI 渲染。
本文以一个真实的 OpenHarmony demo 工程 oh_demo11 为例,详细讲解如何在该项目中集成完整的扫码识别能力,包括三个核心功能:二维码/条形码扫描、闪光灯控制、扫描历史记录管理。全文代码均来自经过 ArkTS 编译验证的实际工程,无虚构内容。
一、技术背景:Flutter 与 OpenHarmony 的融合
oh_demo11 项目采用 ArkTS + Flutter 混合架构:
App (EntryAbility)
└── FlutterAbility (Flutter 引擎)
└── GeneratedPluginRegistrant (Flutter 插件注册)
└── 业务代码 (ArkTS Pages / Services)
EntryAbility 是应用的主 Ability,继承自 FlutterAbility,在 onWindowStageCreate 中加载 Flutter 页面。这种架构的优势在于:Flutter 擅长快速构建复杂 UI 与业务页面,ArkTS 则负责系统级能力(权限、传感器、媒体)的调用。扫码功能恰好横跨这两层——相机与权限属于系统级能力,扫描逻辑与历史管理属于业务层。
本文的扫码方案采用演示模式(Demo Mode):在真实设备上,相机 API 需要完整的摄像头硬件和 HAL 支持;在模拟器或开发环境中,我们使用预设的模拟扫描数据完成全链路的 UI 和业务逻辑验证。一旦需要对接真实相机,只需在 ScanService 中补充 @kit.CameraKit 的调用即可,接口层完全不需要改动。
二、项目结构与能力规划
扫码功能涉及以下 8 个新文件(均位于 entry/src/main/ets/ 下):
| 路径 | 说明 |
|---|---|
model/ScanModels.ets |
数据模型:扫描类型枚举、结果类型枚举、历史记录接口 |
utils/ScanConstants.ets |
常量配置:颜色、动画时长、最大历史条数 |
service/ScanHistoryManager.ets |
持久化管理:历史记录的增删改查,基于 @ohos.data.preferences |
service/ScanService.ets |
扫描服务:闪光灯状态、演示扫描触发、内容类型推断 |
components/ScanResultCard.ets |
结果卡片组件:扫描结果展示 + 复制/分享操作 |
components/ScanHistoryListItem.ets |
历史列表项组件:@ObjectLink + @Observed 响应式更新 |
pages/ScanPage.ets |
扫码主页:取景框动画、闪光灯开关、历史入口 |
pages/ScanHistoryPage.ets |
历史记录页:搜索过滤、收藏管理、清空确认 |
三、权限声明与多语言配置
扫码依赖相机权限,需在 module.json5 的 requestPermissions 中声明:
{
"name": "ohos.permission.CAMERA",
"reason": "$string:permission_camera_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
理由字符串需要分别在中英文资源配置文件中添加:
// resources/zh_CN/element/string.json
{ "name": "permission_camera_reason", "value": "需要使用相机进行二维码/条形码扫描" }
// resources/en_US/element/string.json
{ "name": "permission_camera_reason", "value": "Need to use camera for QR/barcode scanning" }
完整的权限申请由 PermissionManager 在运行时触发——这也是该 demo 工程中统一封装权限管理的好处:扫码页不需要自行处理 abilityAccessCtrl 的细节。
四、数据模型设计
ScanModels.ets 定义了三个核心枚举和两个接口。枚举采用字符串字面量形式,便于序列化:
export enum ScanType {
QR_CODE = 'QR_CODE',
BAR_CODE = 'BAR_CODE',
DATA_MATRIX = 'DATA_MATRIX',
AZTEC = 'AZTEC',
PDF_417 = 'PDF_417',
}
export enum ScanResultType {
URL = 'URL',
TEXT = 'TEXT',
PHONE = 'PHONE',
EMAIL = 'EMAIL',
SMS = 'SMS',
WIFI = 'WIFI',
CONTACT = 'CONTACT',
GEO = 'GEO',
CALENDAR = 'CALENDAR',
PRODUCT = 'PRODUCT',
UNKNOWN = 'UNKNOWN',
}
export interface ScanHistoryItem {
id: string;
content: string;
scanType: ScanType;
resultType: ScanResultType;
scanTime: number;
isFavorite: boolean;
note?: string;
}
ScanResult 类负责从原始扫描内容推断出结果类型——这是提升用户体验的关键细节:扫描到 URL 时自动识别为"链接",扫描到纯数字时识别为"商品条码",扫描到 WIFI: 前缀时识别为"Wi-Fi 配置"。无需用户手动分类,扫描结果直接展示对应的操作建议。
五、持久化历史记录服务
ScanHistoryManager 采用单例模式,通过 @ohos.data.preferences 实现轻量级持久化。设计要点如下:
private historyCache: ScanHistoryItem[] = [];
private cacheLoaded: boolean = false;
历史数据以 JSON 字符串存储在 scan_history_prefs 中。为避免 JSON 反序列化后的类型丢失(ArkTS 严格模式下不允许直接 cast),我们使用类型守卫函数逐条校验:
function isHistoryItem(raw: Record<string, Object>): boolean {
return typeof raw['id'] === 'string' &&
typeof raw['content'] === 'string' &&
typeof raw['scanTime'] === 'number' &&
typeof raw['isFavorite'] === 'boolean';
}
function toHistoryItem(raw: Record<string, Object>): ScanHistoryItem {
return {
id: raw['id'] as string,
content: raw['content'] as string,
scanType: raw['scanType'] as ScanType,
resultType: raw['resultType'] as ScanResultType,
scanTime: raw['scanTime'] as number,
isFavorite: raw['isFavorite'] as boolean,
note: raw['note'] as string | undefined,
};
}
这样做有双重好处:既满足了 ArkTS 严格模式的类型安全要求,又保证了即使磁盘数据损坏也不会导致整个缓存崩溃。addItem 方法还会自动去重——如果扫描到一条之前记录过的内容,会将其移至列表顶部,确保历史记录始终按最近扫描时间排序。
六、扫描服务与闪光灯控制
ScanService 是整个功能的核心服务类,负责闪光灯状态管理和演示扫描的触发:
export class ScanService {
private static instance: ScanService | null = null;
private flashEnabled: boolean = false;
private demoMode: boolean = true;
async toggleFlash(): Promise<boolean> {
this.flashEnabled = !this.flashEnabled;
return this.flashEnabled;
}
triggerDemoScan(): ScanResult {
const index = Math.floor(Math.random() * this.DEMO_ITEMS.length);
const selected: DemoScanItem = this.DEMO_ITEMS[index];
const result = new ScanResult(selected.content, selected.type);
const historyItem = result.toHistoryItem();
this.historyManager.addItem(historyItem);
if (this.scanCallback !== null) {
this.scanCallback(result);
}
return result;
}
}
DEMO_ITEMS 中预置了 6 种典型扫描内容,涵盖 URL、纯数字条形码、邮箱、电话和 Wi-Fi 配置。切换至真实相机时,只需在 initialize() 中完成 @kit.CameraKit 的会话建立,并在 triggerDemoScan() 中替换为图像帧回调即可,UI 层完全不受影响。
七、UI 组件:响应式列表与结果卡片
历史列表使用 @ObjectLink + @Observed 模式实现高效的细粒度更新:
@Observed
export class ScanHistoryItemWrap {
model: ScanHistoryItemModel = new ScanHistoryItemModel();
constructor(data?: ScanHistoryItem) {
if (data !== undefined) {
this.model.init(data);
}
}
}
@Component
export struct ScanHistoryItemView {
@ObjectLink model: ScanHistoryItemModel;
onTap?: () => void;
onDelete?: () => void;
onFavorite?: () => void;
}
在 ScanHistoryPage 中,每次删除或切换收藏状态时,不需要重新构建整个列表,只需修改对应 wrap.model 的字段即可触发 UI 更新:
async handleFavorite(id: string): Promise<void> {
const newState: boolean = await this.historyManager.toggleFavorite(id);
const item: ScanHistoryItemWrap | undefined = this.state.historyItems.find(
(w: ScanHistoryItemWrap) => w.model.id === id
);
if (item !== undefined) {
item.model.isFavorite = newState;
}
this.applyFilter();
}
八、扫码页面动画设计
良好的扫码体验离不开精心设计的交互动画。ScanPage 实现了三个层次的动画:
1. 入场动画:pageOpacity 从 0 渐变为 1,headerTranslateY 从 -20 回归 0,配合 Curve.FastOutSlowIn 曲线营造自然的出现效果。
2. 扫描线动画:通过 setInterval 每 25ms 更新一次 scanLinePos(0-99),驱动一个带渐变色的横线在取景框内循环移动:
startScanLineAnimation(): void {
this.stopScanLineAnimation();
this.state.scanLinePos = 0;
this.scanLineTimerId = setInterval(() => {
this.state.scanLinePos = (this.state.scanLinePos + 1) % 100;
}, 25) as number;
}
3. 扫描脉冲动画:点击扫码按钮时,取景框产生一次快速放大再收缩的脉冲效果(200ms),给予用户明确的操作反馈:
startScanPulse(): void {
this.state.scanPulseScale = 0.8;
this.state.scanPulseOpacity = 0.6;
animateTo({
duration: 200,
curve: Curve.EaseOut,
iterations: 1,
playMode: PlayMode.Normal
}, () => {
this.state.scanPulseScale = 1.0;
this.state.scanPulseOpacity = 0;
});
}
九、导航集成与页面注册
扫码功能需要在主页面 Index.ets 中添加入口按钮,遵循工程既有的动画按钮模式:
// 状态变量(与已有按钮保持一致的动画声明风格)
@State navButton10Opacity: number = 0;
@State navButton10TranslateY: number = 20;
@State button10Scale: number = 1.0;
页面路由在 main_pages.json 中注册:
"src": [
"pages/Index",
"pages/ScanPage",
"pages/ScanHistoryPage",
"pages/..."
]
十、运行效果截图验证
截图验证板块(请开发者运行后在此处插入截图)
- 扫码主页:
pages/ScanPage运行截图,验证扫描线动画、闪光灯按钮、历史入口均正常显示
- 扫描结果:点击扫码按钮后,底部弹出的结果面板,验证类型标签和操作按钮正确
- 历史记录:
pages/ScanHistoryPage运行截图,验证搜索、收藏过滤、统计数据正常
结语
本文完整介绍了在一个真实的 Flutter for OpenHarmony 混合工程中,如何从权限配置、模型设计、持久化存储到 UI 组件,逐步构建一个功能完整的扫码识别模块。所有代码均经过 ArkTS 编译器验证,无类型错误。
扫码能力的核心价值在于将系统级相机能力与业务逻辑解耦:演示模式保证了在无硬件环境下的全链路开发调试,真实相机接入时只需替换 ScanService 中的数据源而不触及 UI 层。这种分层设计正是 Flutter 跨平台哲学在 OpenHarmony 上的自然延伸。
后续可进一步探索的方向包括:接入 @kit.CameraKit 实现真实扫码、在 Flutter 侧封装 scan 插件、以及支持相册图片扫码等。读者可在 AtomGit 仓库中获取完整工程代码进行实践。
感谢各位阅读!
更多推荐







所有评论(0)