为开源鸿蒙 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.json5requestPermissions 中声明:

{
  "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/..."
]

十、运行效果截图验证

截图验证板块(请开发者运行后在此处插入截图)

  1. 扫码主页pages/ScanPage 运行截图,验证扫描线动画、闪光灯按钮、历史入口均正常显示
    在这里插入图片描述
  1. 扫描结果:点击扫码按钮后,底部弹出的结果面板,验证类型标签和操作按钮正确
    在这里插入图片描述
  1. 历史记录pages/ScanHistoryPage 运行截图,验证搜索、收藏过滤、统计数据正常
    在这里插入图片描述

结语

本文完整介绍了在一个真实的 Flutter for OpenHarmony 混合工程中,如何从权限配置、模型设计、持久化存储到 UI 组件,逐步构建一个功能完整的扫码识别模块。所有代码均经过 ArkTS 编译器验证,无类型错误。

扫码能力的核心价值在于将系统级相机能力与业务逻辑解耦:演示模式保证了在无硬件环境下的全链路开发调试,真实相机接入时只需替换 ScanService 中的数据源而不触及 UI 层。这种分层设计正是 Flutter 跨平台哲学在 OpenHarmony 上的自然延伸。

后续可进一步探索的方向包括:接入 @kit.CameraKit 实现真实扫码、在 Flutter 侧封装 scan 插件、以及支持相册图片扫码等。读者可在 AtomGit 仓库中获取完整工程代码进行实践。

感谢各位阅读!

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐