在这里插入图片描述

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

🎯 欢迎来到 Flutter for OpenHarmony 社区!本文将深入讲解 Flutter 中 qr_code_scanner 二维码扫描组件的使用方法,带你全面掌握在应用中实现二维码扫描和生成的功能。


一、qr_code_scanner 组件概述

二维码已经成为现代生活中不可或缺的一部分。从移动支付、扫码登录,到商品追溯、信息获取,二维码的应用场景无处不在。作为一种信息密度高、识别速度快、成本低廉的数据载体,二维码在移动互联网时代发挥着重要作用。

在 Flutter for OpenHarmony 应用开发中,qr_code_scanner 是一个非常实用的二维码扫描插件。它基于原生平台的相机和图像识别能力,提供了高性能、高识别率的二维码扫描功能。开发者可以通过简单的 API 调用,快速在应用中集成二维码扫描能力,无需深入了解底层的图像处理和识别算法。

📋 qr_code_scanner 组件特点

特点 说明
跨平台支持 支持 Android、iOS、Web、OpenHarmony
多码制支持 支持 QR Code、EAN-13、EAN-8、Code-128 等多种码制
实时扫描 支持摄像头实时预览和扫描
闪光灯控制 支持开启/关闭闪光灯
前后摄像头 支持切换前后摄像头
扫描框定制 支持自定义扫描框样式和位置
高性能 基于原生平台 API,扫描速度快、识别率高

二维码的发展历程

二维码最早由日本 Denso Wave 公司于 1994 年发明,最初用于汽车零部件管理。QR 是 “Quick Response”(快速响应)的缩写,体现了二维码快速读取的特点。与传统的一维条码相比,二维码具有以下优势:

信息容量大:二维码可以存储数千个字符,而一维条码通常只能存储几十个字符。这使得二维码可以存储网址、名片、WiFi 配置等复杂信息。

容错能力强:二维码采用 Reed-Solomon 纠错算法,即使部分区域被遮挡或损坏,仍然可以被正确识别。根据纠错级别的不同,可以容忍 7% 到 30% 的损坏。

识别方向自由:二维码有三个定位图案,扫描设备可以从任意角度识别二维码,无需对准特定方向。

支持多种数据类型:二维码可以存储数字、字母、汉字、二进制数据等多种类型的信息。

二维码的应用场景

在移动应用开发中,二维码扫描功能有着广泛的应用:

移动支付:支付宝、微信支付等支付应用通过扫描商家二维码完成支付,已经成为中国主流的支付方式。

扫码登录:许多网站和应用支持通过扫描二维码登录,无需输入账号密码,既方便又安全。

信息获取:扫描商品条码获取价格和评价信息,扫描名片二维码快速添加联系人。

活动签到:会议、展览等活动通过扫描二维码完成签到,提高效率。

WiFi 配置:扫描 WiFi 二维码自动连接网络,无需手动输入密码。

应用下载:扫描二维码直接下载应用,简化分发流程。

💡 使用场景:扫码支付、扫码登录、商品条码识别、名片识别、活动签到、信息获取等。


二、OpenHarmony 平台适配说明

2.1 兼容性信息

本项目基于 qr_code_scanner@0.7.0 开发,适配 Flutter 3.27.5-ohos-1.0.4。OpenHarmony 平台的二维码扫描功能基于系统相机和图像识别能力实现,具有与 Android 平台相当的性能和识别率。

2.2 支持的码制

二维码扫描插件不仅支持 QR Code 二维码,还支持多种一维条码格式。了解这些码制的特点,可以帮助你在不同场景下选择合适的码制:

码制 说明 OpenHarmony 支持 应用场景
QR Code 二维码 ✅ yes 网址、名片、支付
EAN-13 商品条码 ✅ yes 商品零售
EAN-8 商品条码 ✅ yes 小型商品
Code-128 工业条码 ✅ yes 物流、仓储
Code-39 工业条码 ✅ yes 工业制造
Code-93 工业条码 ✅ yes 物流管理
Codabar 库德巴码 ✅ yes 图书馆、血库
Data Matrix 数据矩阵码 ✅ yes 电子元器件标识
ITF 交叉二五码 ✅ yes 包装箱标识
PDF-417 PDF417码 ✅ yes 身份证、票据
Aztec 阿兹特克码 ✅ yes 交通票据

2.3 各码制详解

QR Code(二维码)

QR Code 是最常用的二维码格式,由黑白方块组成的正方形图案。它支持数字、字母、汉字和二进制数据,最高可存储约 7000 个字符。QR Code 有四个纠错级别(L、M、Q、H),纠错级别越高,容错能力越强,但数据容量越小。

EAN-13

EAN-13 是国际通用的商品条码,由 13 位数字组成。前 2-3 位是国家代码,接着是厂商代码和商品代码,最后一位是校验码。几乎所有零售商品都使用 EAN-13 条码。

Code-128

Code-128 是一种高密度条码,可以表示全部 128 个 ASCII 字符。它广泛应用于物流、仓储和制造业,常用于运输标签和库存管理。

Data Matrix

Data Matrix 是一种二维条码,由黑白方块组成的正方形或矩形图案。它体积小、信息密度高,常用于电子元器件的标识和追溯。

2.4 OpenHarmony 相机权限

二维码扫描需要使用设备的相机,因此在 OpenHarmony 平台上需要申请相机权限。与 Android 平台不同,OpenHarmony 的权限管理更加严格,需要在配置文件中声明权限并说明用途。


三、项目配置与安装

3.1 添加依赖配置

首先,需要在你的 Flutter 项目的 pubspec.yaml 文件中添加 qr_code_scanner 依赖。

打开项目根目录下的 pubspec.yaml 文件,找到 dependencies 部分,添加以下配置:

dependencies:
  flutter:
    sdk: flutter

  # 添加 qr_code_scanner 依赖(OpenHarmony 适配版本)
  qr_code_scanner_ohos:
    git:
      url: https://atomgit.com/openharmony-sig/fluttertpc_qr_code_scanner
      path: ohos
  # 添加图片保存到相册的依赖
  image_gallery_saver:
    git:
      url: https://atomgit.com/openharmony-sig/flutter_image_gallery_saver
  # 添加从图片识别二维码的依赖
  recognition_qrcode:
    git:
      url: https://atomgit.com/openharmony-sig/fluttertpc_recognition_qrcode

配置说明:

  • 使用 git 方式引用开源鸿蒙适配的 fluttertpc_qr_code_scanner 仓库
  • url:指定 AtomGit 托管的仓库地址
  • 本项目基于 qr_code_scanner@0.7.0 开发,适配 Flutter 3.27.5-ohos-1.0.4

⚠️ 重要:对于 OpenHarmony 平台,必须使用 git 方式引用适配版本,不能直接使用 pub.dev 的版本号。pub.dev 上的官方版本可能尚未支持 OpenHarmony 平台,或者存在兼容性问题。使用适配版本可以确保插件在 OpenHarmony 平台上正常工作。

3.2 下载依赖

配置完成后,需要在项目根目录执行以下命令下载依赖:

flutter pub get

执行成功后,你会看到类似以下的输出:

Running "flutter pub get" in my_cross_platform_app...
Resolving dependencies...
Got dependencies!

这个命令会从 AtomGit 仓库下载 qr_code_scanner 插件及其依赖项。下载完成后,插件会被缓存到本地,后续构建时会自动链接到项目中。

3.3 权限配置

二维码扫描需要相机权限,在 OpenHarmony 平台上需要配置:

ohos/entry/src/main/module.json5:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.CAMERA",
        "reason": "$string:camera_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

配置字段说明:

  • name:权限名称,必须是 OpenHarmony 系统定义的标准权限名 ohos.permission.CAMERA
  • reason:权限说明,引用字符串资源,会在权限请求对话框中显示
  • usedScene:使用场景配置
    • abilities:使用该权限的 Ability 名称列表
    • when:使用时机,inuse 表示使用时,always 表示始终

ohos/entry/src/main/resources/base/element/string.json:

{
  "string": [
    {
      "name": "camera_reason",
      "value": "用于扫描二维码和条形码"
    }
  ]
}

💡 提示reason 字段的内容会在用户首次请求权限时显示在系统权限对话框中。请务必编写清晰、准确的权限说明,帮助用户理解为什么应用需要相机权限。这可以提高用户授权的可能性。


四、qr_code_scanner 基础用法

4.1 导入库

在使用 qr_code_scanner_ohos 之前,需要先导入库:

import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:qr_code_scanner_ohos/qr_code_scanner_ohos.dart';

导入后,你就可以使用 qr_code_scanner_ohos 提供的所有类和方法了。主要的类包括:

  • QRView:扫描视图组件,用于显示相机预览和扫描界面
  • QRViewController:扫描控制器,用于控制扫描行为
  • Barcode:扫描结果,包含扫描到的内容
  • QrScannerOverlayShape:扫描框样式配置

4.2 基本扫描器

创建一个基本的二维码扫描器非常简单。你需要使用 QRView 组件,并在 onQRViewCreated 回调中获取控制器:

class QRViewExample extends StatefulWidget {
  const QRViewExample({super.key});

  
  State<QRViewExample> createState() => _QRViewExampleState();
}

class _QRViewExampleState extends State<QRViewExample> {
  final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
  QRViewController? controller;
  String result = '';

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: <Widget>[
          Expanded(
            flex: 5,
            child: QRView(
              key: qrKey,
              onQRViewCreated: _onQRViewCreated,
            ),
          ),
          Expanded(
            flex: 1,
            child: Center(
              child: Text('扫描结果: $result'),
            ),
          )
        ],
      ),
    );
  }

  void _onQRViewCreated(QRViewController controller) {
    this.controller = controller;
    controller.scannedDataStream.listen((scanData) {
      setState(() {
        result = scanData.code ?? '';
      });
    });
  }

  
  void dispose() {
    controller?.dispose();
    super.dispose();
  }
}

代码解析:

  1. GlobalKey:用于标识 QRView 组件,在某些操作中需要用到
  2. QRViewController:扫描控制器,通过它可以控制扫描行为、获取扫描结果
  3. scannedDataStream:扫描结果流,每当扫描到二维码时会发出事件
  4. dispose:页面销毁时释放控制器资源,避免内存泄漏

4.3 扫描框定制

默认的扫描界面是全屏相机预览,没有扫描框提示。为了提供更好的用户体验,通常会添加一个扫描框,引导用户将二维码对准扫描区域。

QRView(
  key: qrKey,
  onQRViewCreated: _onQRViewCreated,
  overlay: QrScannerOverlayShape(
    borderColor: Colors.red,
    borderRadius: 10,
    borderLength: 30,
    borderWidth: 10,
    cutOutSize: 250,
    overlayColor: Colors.black.withOpacity(0.5),
  ),
)

扫描框参数说明:

  • borderColor:扫描框边框颜色,通常使用品牌色或醒目的颜色
  • borderRadius:扫描框圆角半径,0 为直角
  • borderLength:扫描框四个角的长度
  • borderWidth:扫描框边框宽度
  • cutOutSize:扫描框大小,即透明区域的尺寸
  • overlayColor:遮罩颜色,通常使用半透明黑色

4.4 扫描框设计原则

一个好的扫描框设计应该遵循以下原则:

视觉引导:扫描框应该清晰可见,让用户一眼就能看到需要将二维码放在哪里。

大小适中:扫描框不宜过大或过小。过大可能导致用户难以对准,过小可能导致识别困难。一般建议 250-300dp。

颜色对比:扫描框颜色应与背景形成对比,同时不要过于刺眼。常用的颜色有绿色、蓝色、红色等。

动画效果:添加扫描线动画可以增强用户体验,让用户知道正在扫描中。


五、常用 API 详解

5.1 QRViewController - 扫描控制器

QRViewController 是二维码扫描的核心控制器,提供了丰富的控制方法:

闪光灯控制

// 开启/关闭闪光灯
Future<void> toggleFlash()

// 检查闪光灯状态
Future<bool?> get flashStatus

闪光灯在光线不足的环境下非常有用。调用 toggleFlash() 可以切换闪光灯状态,调用 flashStatus 可以获取当前闪光灯是否开启。

摄像头切换

// 翻转摄像头
Future<void> flipCamera()

// 获取当前摄像头
Future<CameraFacing?> get cameraFacing

默认使用后置摄像头扫描二维码。如果需要扫描身份证等场景,可以切换到前置摄像头。

扫描控制

// 暂停扫描
Future<void> pauseCamera()

// 恢复扫描
Future<void> resumeCamera()

在某些场景下,可能需要暂停扫描。例如,扫描到二维码后显示结果页面时,可以暂停扫描,避免重复识别。

系统相机信息

// 获取系统相机信息
Future<List<CameraInfo>?> get cameraInfo

获取设备上可用的相机列表,可以用于判断设备是否有前后摄像头。

5.2 BarcodeFormat - 码制类型

BarcodeFormat 枚举定义了所有支持的码制类型。你可以通过 formatsAllowed 参数限制只扫描特定类型的码:

BarcodeFormat.qrCode       // QR Code 二维码
BarcodeFormat.ean13        // EAN-13 商品条码
BarcodeFormat.ean8         // EAN-8 商品条码
BarcodeFormat.code128      // Code-128 工业条码
BarcodeFormat.code39       // Code-39 工业条码
BarcodeFormat.code93       // Code-93 工业条码
BarcodeFormat.codabar      // Codabar 库德巴码
BarcodeFormat.dataMatrix   // Data Matrix 数据矩阵码
BarcodeFormat.itf          // ITF 交叉二五码
BarcodeFormat.pdf417       // PDF-417
BarcodeFormat.aztec        // Aztec 阿兹特克码

限制码制类型

QRView(
  key: qrKey,
  onQRViewCreated: _onQRViewCreated,
  formatsAllowed: [BarcodeFormat.qrCode],  // 只扫描二维码
)

限制码制类型可以提高扫描效率,减少误识别。例如,如果你的应用只需要扫描二维码,可以只允许 BarcodeFormat.qrCode

5.3 Barcode - 扫描结果

Barcode 类包含了扫描结果的所有信息:

class Barcode {
  final String? code;           // 扫描内容
  final BarcodeFormat format;   // 码制类型
  final String? rawBytes;       // 原始字节数据
}

code:扫描到的内容,是最常用的字段。对于二维码,通常是网址、文本或 JSON 数据;对于条形码,是数字序列。

format:码制类型,可以用于判断扫描到的是二维码还是条形码。

rawBytes:原始字节数据,用于需要处理二进制数据的场景。

5.4 QrScannerOverlayShape - 扫描框配置

QrScannerOverlayShape 提供了丰富的扫描框样式配置选项:

属性 类型 默认值 说明
borderColor Color Colors.red 边框颜色
borderRadius double 0 边框圆角
borderLength double 40 边框角长度
borderWidth double 4 边框宽度
cutOutSize double 250 扫描框大小
cutOutBottomOffset double 0 扫描框底部偏移
overlayColor Color Colors.black54 遮罩颜色
overlayBorder double 0 遮罩边框

5.5 CameraFacing - 摄像头方向

enum CameraFacing {
  back,   // 后置摄像头
  front,  // 前置摄像头
}

大多数二维码扫描场景使用后置摄像头,因为后置摄像头通常有更好的对焦能力和更高的分辨率。但在某些特殊场景下,如扫描身份证、人脸识别等,可能需要使用前置摄像头。


六、实际应用场景

6.1 基础扫描页面

下面是一个功能完整的基础扫描页面,包含闪光灯控制、扫描结果展示、复制和打开链接等功能:

class BasicScannerPage extends StatefulWidget {
  const BasicScannerPage({super.key});

  
  State<BasicScannerPage> createState() => _BasicScannerPageState();
}

class _BasicScannerPageState extends State<BasicScannerPage> {
  final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
  QRViewController? controller;
  String scanResult = '';
  bool flashOn = false;

  
  void reassemble() {
    super.reassemble();
    controller?.pauseCamera();
    controller?.resumeCamera();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('扫码'),
        actions: [
          IconButton(
            icon: Icon(flashOn ? Icons.flash_on : Icons.flash_off),
            onPressed: _toggleFlash,
          ),
        ],
      ),
      body: Stack(
        children: [
          QRView(
            key: qrKey,
            onQRViewCreated: _onQRViewCreated,
            overlay: QrScannerOverlayShape(
              borderColor: const Color(0xFF6366F1),
              borderRadius: 12,
              borderLength: 30,
              borderWidth: 4,
              cutOutSize: 250,
              overlayColor: Colors.black.withOpacity(0.7),
            ),
          ),
          if (scanResult.isNotEmpty)
            Positioned(
              bottom: 100,
              left: 20,
              right: 20,
              child: _buildResultCard(),
            ),
        ],
      ),
    );
  }

  Widget _buildResultCard() {
    return Card(
      color: Colors.white,
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Row(
              children: [
                Icon(Icons.check_circle, color: Colors.green),
                SizedBox(width: 8),
                Text('扫描成功', style: TextStyle(fontWeight: FontWeight.bold)),
              ],
            ),
            const SizedBox(height: 8),
            Text(
              scanResult,
              style: const TextStyle(fontSize: 14),
              maxLines: 3,
              overflow: TextOverflow.ellipsis,
            ),
            const SizedBox(height: 12),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                TextButton.icon(
                  onPressed: _copyResult,
                  icon: const Icon(Icons.copy),
                  label: const Text('复制'),
                ),
                TextButton.icon(
                  onPressed: _openUrl,
                  icon: const Icon(Icons.open_in_new),
                  label: const Text('打开'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  void _onQRViewCreated(QRViewController controller) {
    this.controller = controller;
    controller.scannedDataStream.listen((scanData) {
      if (scanData.code != null && scanData.code != scanResult) {
        setState(() => scanResult = scanData.code!);
        HapticFeedback.mediumImpact();
      }
    });
  }

  Future<void> _toggleFlash() async {
    await controller?.toggleFlash();
    final status = await controller?.flashStatus;
    setState(() => flashOn = status ?? false);
  }

  void _copyResult() {
    Clipboard.setData(ClipboardData(text: scanResult));
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('已复制到剪贴板')),
    );
  }

  void _openUrl() {
    if (scanResult.startsWith('http')) {
      launchUrl(Uri.parse(scanResult));
    } else {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('不是有效的网址')),
      );
    }
  }

  
  void dispose() {
    controller?.dispose();
    super.dispose();
  }
}

代码解析:

这个示例展示了二维码扫描的完整流程:

  1. 扫描界面:使用 QRView 组件显示相机预览和扫描框
  2. 扫描结果处理:监听 scannedDataStream 获取扫描结果
  3. 震动反馈:扫描成功时触发震动,提供触觉反馈
  4. 结果展示:在扫描界面底部显示扫描结果卡片
  5. 功能操作:支持复制结果和打开链接

6.2 多功能扫描器

下面是一个功能更丰富的扫描器,支持闪光灯控制、摄像头切换、码制过滤等功能:

class AdvancedScannerPage extends StatefulWidget {
  const AdvancedScannerPage({super.key});

  
  State<AdvancedScannerPage> createState() => _AdvancedScannerPageState();
}

class _AdvancedScannerPageState extends State<AdvancedScannerPage> {
  final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
  QRViewController? controller;
  String scanResult = '';
  bool flashOn = false;
  bool isFrontCamera = false;
  List<BarcodeFormat> allowedFormats = [BarcodeFormat.qrCode];

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('多功能扫码'),
        actions: [
          PopupMenuButton<String>(
            onSelected: _handleMenuSelection,
            itemBuilder: (context) => [
              const PopupMenuItem(value: 'qr', child: Text('仅二维码')),
              const PopupMenuItem(value: 'all', child: Text('所有码制')),
              const PopupMenuItem(value: 'barcode', child: Text('仅条形码')),
            ],
          ),
        ],
      ),
      body: Stack(
        children: [
          QRView(
            key: qrKey,
            onQRViewCreated: _onQRViewCreated,
            formatsAllowed: allowedFormats,
            overlay: QrScannerOverlayShape(
              borderColor: const Color(0xFF6366F1),
              borderRadius: 16,
              borderLength: 40,
              borderWidth: 5,
              cutOutSize: 280,
              overlayColor: Colors.black.withOpacity(0.75),
            ),
          ),
          Positioned(
            bottom: 0,
            left: 0,
            right: 0,
            child: _buildControlPanel(),
          ),
          if (scanResult.isNotEmpty)
            Positioned(
              top: 60,
              left: 16,
              right: 16,
              child: _buildResultBanner(),
            ),
        ],
      ),
    );
  }

  Widget _buildControlPanel() {
    return Container(
      padding: const EdgeInsets.all(20),
      decoration: BoxDecoration(
        color: Colors.black.withOpacity(0.8),
        borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
      ),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          _buildControlButton(
            icon: flashOn ? Icons.flash_on : Icons.flash_off,
            label: flashOn ? '关闭闪光' : '开启闪光',
            onPressed: _toggleFlash,
          ),
          _buildControlButton(
            icon: Icons.cameraswitch,
            label: '切换摄像头',
            onPressed: _flipCamera,
          ),
          _buildControlButton(
            icon: Icons.image,
            label: '从相册选择',
            onPressed: _pickFromGallery,
          ),
        ],
      ),
    );
  }

  Widget _buildControlButton({
    required IconData icon,
    required String label,
    required VoidCallback onPressed,
  }) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Container(
          width: 56,
          height: 56,
          decoration: BoxDecoration(
            color: Colors.white.withOpacity(0.1),
            borderRadius: BorderRadius.circular(16),
          ),
          child: IconButton(
            icon: Icon(icon, color: Colors.white, size: 28),
            onPressed: onPressed,
          ),
        ),
        const SizedBox(height: 8),
        Text(label, style: const TextStyle(color: Colors.white70, fontSize: 12)),
      ],
    );
  }

  Widget _buildResultBanner() {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.green,
        borderRadius: BorderRadius.circular(12),
      ),
      child: Row(
        children: [
          const Icon(Icons.check_circle, color: Colors.white),
          const SizedBox(width: 12),
          Expanded(
            child: Text(
              scanResult,
              style: const TextStyle(color: Colors.white),
              maxLines: 2,
              overflow: TextOverflow.ellipsis,
            ),
          ),
          IconButton(
            icon: const Icon(Icons.close, color: Colors.white),
            onPressed: () => setState(() => scanResult = ''),
          ),
        ],
      ),
    );
  }

  void _onQRViewCreated(QRViewController controller) {
    this.controller = controller;
    controller.scannedDataStream.listen((scanData) {
      if (scanData.code != null && scanData.code != scanResult) {
        setState(() => scanResult = scanData.code!);
      }
    });
  }

  Future<void> _toggleFlash() async {
    await controller?.toggleFlash();
    final status = await controller?.flashStatus;
    setState(() => flashOn = status ?? false);
  }

  Future<void> _flipCamera() async {
    await controller?.flipCamera();
    final facing = await controller?.cameraFacing;
    setState(() => isFrontCamera = facing == CameraFacing.front);
  }

  Future<void> _pickFromGallery() async {
    final picker = ImagePicker();
    final image = await picker.pickImage(source: ImageSource.gallery);
    if (image != null) {
      _scanImageForQRCode(File(image.path));
    }
  }

  Future<void> _scanImageForQRCode(File imageFile) async {
    try {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('正在识别二维码...')),
      );

      final recognitionResult = await RecognitionManager.recognition(imageFile.path);

      if (mounted) {
        ScaffoldMessenger.of(context).hideCurrentSnackBar();
      }

      String codeStr = recognitionResult.code.toString();
      bool isSuccess = codeStr == "success" || codeStr == "0";
      bool hasValue = recognitionResult.value != null && recognitionResult.value.toString().isNotEmpty;

      if (isSuccess && hasValue) {
        HapticFeedback.mediumImpact();
        setState(() {
          scanResult = recognitionResult.value.toString();
        });
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('识别成功')),
          );
        }
      } else {
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('未识别到二维码')),
          );
        }
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).hideCurrentSnackBar();
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('识别失败: $e')),
        );
      }
    }
  }

  void _handleMenuSelection(String value) {
    setState(() {
      switch (value) {
        case 'qr':
          allowedFormats = [BarcodeFormat.qrCode];
          break;
        case 'all':
          allowedFormats = [];
          break;
        case 'barcode':
          allowedFormats = [
            BarcodeFormat.ean13,
            BarcodeFormat.ean8,
            BarcodeFormat.code128,
            BarcodeFormat.code39,
          ];
          break;
      }
    });
  }

  
  void dispose() {
    controller?.dispose();
    super.dispose();
  }
}

功能说明:

这个多功能扫描器提供了以下功能:

  1. 码制过滤:可以选择只扫描二维码、条形码或所有码制
  2. 闪光灯控制:在光线不足时开启闪光灯
  3. 摄像头切换:切换前后摄像头
  4. 相册选择:从相册选择图片识别二维码(需要额外实现)

6.3 完整示例:二维码扫描应用

下面是一个功能完整的二维码扫描应用示例,包含扫描、历史记录、结果处理等功能:

import 'dart:io';
import 'dart:typed_data';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:qr_code_scanner_ohos/qr_code_scanner_ohos.dart';
import 'package:image_picker/image_picker.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:http/http.dart' as http;
import 'package:recognition_qrcode/recognition_qrcode.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'QR Scanner 示例',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF6366F1)),
        useMaterial3: true,
      ),
      home: const ScannerDemoPage(),
    );
  }
}

class ScannerDemoPage extends StatefulWidget {
  const ScannerDemoPage({super.key});

  
  State<ScannerDemoPage> createState() => _ScannerDemoPageState();
}

class _ScannerDemoPageState extends State<ScannerDemoPage> {
  final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
  QRViewController? controller;
  String scanResult = '';
  bool flashOn = false;
  bool isScanning = true;
  final List<ScanHistory> _history = [];

  
  void reassemble() {
    super.reassemble();
    if (isScanning) {
      controller?.resumeCamera();
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          _buildScanner(),
          _buildTopBar(),
          _buildBottomPanel(),
        ],
      ),
    );
  }

  Widget _buildScanner() {
    return QRView(
      key: qrKey,
      onQRViewCreated: _onQRViewCreated,
      overlay: QrScannerOverlayShape(
        borderColor: const Color(0xFF6366F1),
        borderRadius: 16,
        borderLength: 40,
        borderWidth: 4,
        cutOutSize: 260,
        cutOutBottomOffset: 80,
        overlayColor: Colors.black.withOpacity(0.7),
      ),
    );
  }

  Widget _buildTopBar() {
    return Positioned(
      top: 0,
      left: 0,
      right: 0,
      child: Container(
        padding: EdgeInsets.only(
          top: MediaQuery.of(context).padding.top + 8,
          left: 8,
          right: 8,
          bottom: 16,
        ),
        decoration: BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
            colors: [Colors.black.withOpacity(0.6), Colors.transparent],
          ),
        ),
        child: Row(
          children: [
            IconButton(
              icon: const Icon(Icons.arrow_back, color: Colors.white),
              onPressed: () => Navigator.pop(context),
            ),
            const Expanded(
              child: Text(
                '扫描二维码',
                style: TextStyle(
                  color: Colors.white,
                  fontSize: 18,
                  fontWeight: FontWeight.w600,
                ),
              ),
            ),
            IconButton(
              icon: Icon(
                flashOn ? Icons.flash_on : Icons.flash_off,
                color: Colors.white,
              ),
              onPressed: _toggleFlash,
            ),
            IconButton(
              icon: const Icon(Icons.history, color: Colors.white),
              onPressed: _showHistory,
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildBottomPanel() {
    return Positioned(
      bottom: 0,
      left: 0,
      right: 0,
      child: Container(
        padding: EdgeInsets.fromLTRB(
          20,
          20,
          20,
          MediaQuery.of(context).padding.bottom + 20,
        ),
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
          boxShadow: [
            BoxShadow(
              color: Colors.black.withOpacity(0.1),
              blurRadius: 20,
              offset: const Offset(0, -5),
            ),
          ],
        ),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            if (scanResult.isNotEmpty) _buildResultCard(),
            const SizedBox(height: 16),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                _buildActionButton(
                  icon: Icons.photo_library,
                  label: '相册',
                  onPressed: _pickFromGallery,
                ),
                _buildActionButton(
                  icon: Icons.link,
                  label: '网络图片',
                  onPressed: _showNetworkQRCode,
                ),
                _buildActionButton(
                  icon: Icons.qr_code,
                  label: '我的二维码',
                  onPressed: _showMyQRCode,
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildResultCard() {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        gradient: const LinearGradient(
          colors: [Color(0xFF6366F1), Color(0xFF8B5CF6)],
        ),
        borderRadius: BorderRadius.circular(16),
      ),
      child: Column(
        children: [
          Row(
            children: [
              Container(
                padding: const EdgeInsets.all(8),
                decoration: BoxDecoration(
                  color: Colors.white.withOpacity(0.2),
                  borderRadius: BorderRadius.circular(8),
                ),
                child: const Icon(Icons.check_circle, color: Colors.white),
              ),
              const SizedBox(width: 12),
              const Expanded(
                child: Text(
                  '扫描成功',
                  style: TextStyle(
                    color: Colors.white,
                    fontWeight: FontWeight.w600,
                    fontSize: 16,
                  ),
                ),
              ),
              IconButton(
                icon: const Icon(Icons.close, color: Colors.white),
                onPressed: () => setState(() => scanResult = ''),
              ),
            ],
          ),
          const SizedBox(height: 12),
          Container(
            width: double.infinity,
            padding: const EdgeInsets.all(12),
            decoration: BoxDecoration(
              color: Colors.white.withOpacity(0.2),
              borderRadius: BorderRadius.circular(8),
            ),
            child: Text(
              scanResult,
              style: const TextStyle(color: Colors.white),
              maxLines: 3,
              overflow: TextOverflow.ellipsis,
            ),
          ),
          const SizedBox(height: 12),
          Row(
            children: [
              Expanded(
                child: OutlinedButton.icon(
                  onPressed: _copyResult,
                  icon: const Icon(Icons.copy, size: 18),
                  label: const Text('复制'),
                  style: OutlinedButton.styleFrom(
                    foregroundColor: Colors.white,
                    side: const BorderSide(color: Colors.white54),
                  ),
                ),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: ElevatedButton.icon(
                  onPressed: _handleResult,
                  icon: const Icon(Icons.open_in_new, size: 18),
                  label: const Text('打开'),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.white,
                    foregroundColor: const Color(0xFF6366F1),
                  ),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }

  Widget _buildActionButton({
    required IconData icon,
    required String label,
    required VoidCallback onPressed,
  }) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Container(
          width: 56,
          height: 56,
          decoration: BoxDecoration(
            color: const Color(0xFF6366F1).withOpacity(0.1),
            borderRadius: BorderRadius.circular(16),
          ),
          child: IconButton(
            icon: Icon(icon, color: const Color(0xFF6366F1)),
            onPressed: onPressed,
          ),
        ),
        const SizedBox(height: 8),
        Text(label, style: const TextStyle(fontSize: 12)),
      ],
    );
  }

  void _onQRViewCreated(QRViewController controller) {
    this.controller = controller;
    controller.scannedDataStream.listen((scanData) {
      if (scanData.code != null && scanData.code != scanResult) {
        HapticFeedback.mediumImpact();
        setState(() {
          scanResult = scanData.code!;
          _history.insert(0, ScanHistory(
            content: scanResult,
            time: DateTime.now(),
          ));
          if (_history.length > 20) _history.removeLast();
        });
      }
    });
  }

  Future<void> _toggleFlash() async {
    await controller?.toggleFlash();
    setState(() => flashOn = !flashOn);
  }

  void _copyResult() {
    Clipboard.setData(ClipboardData(text: scanResult));
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('已复制到剪贴板')),
    );
  }

  void _handleResult() {
    if (scanResult.startsWith('http://') || scanResult.startsWith('https://')) {
      _showOpenUrlDialog();
    } else if (scanResult.startsWith('WIFI:')) {
      _handleWifiResult();
    } else {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('无法识别的内容类型')),
      );
    }
  }

  void _showOpenUrlDialog() {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('打开链接'),
        content: Text(scanResult),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('取消'),
          ),
          ElevatedButton(
            onPressed: () {
              Navigator.pop(context);
              Clipboard.setData(ClipboardData(text: scanResult));
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('链接已复制,请在浏览器中打开')),
              );
            },
            child: const Text('复制链接'),
          ),
        ],
      ),
    );
  }

  void _handleWifiResult() {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('检测到 WiFi 配置信息,已复制到剪贴板')),
    );
    Clipboard.setData(ClipboardData(text: scanResult));
  }

  Future<void> _pickFromGallery() async {
    final picker = ImagePicker();
    final image = await picker.pickImage(source: ImageSource.gallery);
    if (image != null) {
      _scanImageForQRCode(File(image.path));
    }
  }

  Future<void> _scanImageForQRCode(File imageFile) async {
    try {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('正在识别二维码...')),
      );

      final result = await RecognitionManager.recognition(imageFile.path);

      if (mounted) {
        ScaffoldMessenger.of(context).hideCurrentSnackBar();
      }

      String codeStr = result.code.toString();
      bool isSuccess = codeStr == "success" || codeStr == "0";
      bool hasValue = result.value != null && result.value.toString().isNotEmpty;

      if (isSuccess && hasValue) {
        HapticFeedback.mediumImpact();
        setState(() {
          scanResult = result.value.toString();
          _history.insert(0, ScanHistory(
            content: scanResult,
            time: DateTime.now(),
          ));
          if (_history.length > 20) _history.removeLast();
        });
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('识别成功')),
          );
        }
      } else {
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('未识别到二维码')),
          );
        }
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).hideCurrentSnackBar();
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('识别失败: $e')),
        );
      }
    }
  }

  void _showNetworkQRCode() {
    showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      backgroundColor: Colors.transparent,
      builder: (context) => Container(
        height: 550,
        decoration: const BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
        ),
        child: Column(
          children: [
            Container(
              width: 40,
              height: 4,
              margin: const EdgeInsets.only(top: 12),
              decoration: BoxDecoration(
                color: Colors.grey.shade300,
                borderRadius: BorderRadius.circular(2),
              ),
            ),
            const Padding(
              padding: EdgeInsets.all(20),
              child: Text(
                '网络二维码图片',
                style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
              ),
            ),
            const Padding(
              padding: EdgeInsets.symmetric(horizontal: 20),
              child: Text(
                '点击保存按钮可将二维码保存到相册,然后用相机扫描',
                style: TextStyle(color: Colors.grey),
                textAlign: TextAlign.center,
              ),
            ),
            const SizedBox(height: 16),
            Expanded(
              child: ListView(
                padding: const EdgeInsets.symmetric(horizontal: 20),
                children: [
                  _buildNetworkQRItem(
                    'Flutter 官网',
                    'https://flutter.dev',
                    'https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=https://flutter.dev',
                  ),
                  _buildNetworkQRItem(
                    'OpenHarmony 官网',
                    'https://www.openharmony.cn',
                    'https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=https://www.openharmony.cn',
                  ),
                  _buildNetworkQRItem(
                    '测试文本',
                    'Hello OpenHarmony!',
                    'https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=Hello%20OpenHarmony!',
                  ),
                  _buildNetworkQRItem(
                    'WiFi 配置',
                    'WIFI:T:WPA;S:TestWiFi;P:12345678;;',
                    'https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=WIFI:T:WPA;S:TestWiFi;P:12345678;;',
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildNetworkQRItem(String title, String content, String qrUrl) {
    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Row(
          children: [
            ClipRRect(
              borderRadius: BorderRadius.circular(8),
              child: Image.network(
                qrUrl,
                width: 80,
                height: 80,
                fit: BoxFit.cover,
                errorBuilder: (context, error, stackTrace) => Container(
                  width: 80,
                  height: 80,
                  color: Colors.grey.shade200,
                  child: const Icon(Icons.qr_code, size: 40, color: Colors.grey),
                ),
              ),
            ),
            const SizedBox(width: 12),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    title,
                    style: const TextStyle(
                      fontWeight: FontWeight.bold,
                      fontSize: 16,
                    ),
                  ),
                  const SizedBox(height: 4),
                  Text(
                    content,
                    style: TextStyle(
                      color: Colors.grey.shade600,
                      fontSize: 12,
                    ),
                    maxLines: 2,
                    overflow: TextOverflow.ellipsis,
                  ),
                ],
              ),
            ),
            Column(
              children: [
                IconButton(
                  icon: const Icon(Icons.save_alt, color: Color(0xFF6366F1)),
                  tooltip: '保存到相册',
                  onPressed: () => _saveQRToGallery(qrUrl, title),
                ),
                IconButton(
                  icon: const Icon(Icons.copy, color: Colors.grey),
                  tooltip: '复制内容',
                  onPressed: () {
                    Clipboard.setData(ClipboardData(text: content));
                    ScaffoldMessenger.of(context).showSnackBar(
                      SnackBar(content: Text('已复制: $content')),
                    );
                  },
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Future<void> _saveQRToGallery(String qrUrl, String title) async {
    try {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('正在保存...')),
      );

      final response = await http.get(Uri.parse(qrUrl));
      final Uint8List bytes = response.bodyBytes;

      final result = await ImageGallerySaver.saveImage(
        bytes,
        quality: 100,
        name: 'qr_${title.replaceAll(' ', '_')}_${DateTime.now().millisecondsSinceEpoch}',
      );

      if (result['isSuccess'] == true) {
        if (mounted) {
          ScaffoldMessenger.of(context).hideCurrentSnackBar();
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text('已保存到相册: $title')),
          );
        }
      } else {
        if (mounted) {
          ScaffoldMessenger.of(context).hideCurrentSnackBar();
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('保存失败,请检查相册权限')),
          );
        }
      }
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).hideCurrentSnackBar();
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('保存失败: $e')),
        );
      }
    }
  }

  void _showMyQRCode() {
    showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      backgroundColor: Colors.transparent,
      builder: (context) => Container(
        height: 400,
        decoration: const BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
        ),
        child: Column(
          children: [
            Container(
              width: 40,
              height: 4,
              margin: const EdgeInsets.only(top: 12),
              decoration: BoxDecoration(
                color: Colors.grey.shade300,
                borderRadius: BorderRadius.circular(2),
              ),
            ),
            const Padding(
              padding: EdgeInsets.all(20),
              child: Text(
                '我的二维码',
                style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
              ),
            ),
            const Expanded(
              child: Center(
                child: Text('二维码生成功能需要额外库支持'),
              ),
            ),
          ],
        ),
      ),
    );
  }

  void _showHistory() {
    showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      backgroundColor: Colors.transparent,
      builder: (context) => Container(
        height: MediaQuery.of(context).size.height * 0.6,
        decoration: const BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
        ),
        child: Column(
          children: [
            Container(
              width: 40,
              height: 4,
              margin: const EdgeInsets.only(top: 12),
              decoration: BoxDecoration(
                color: Colors.grey.shade300,
                borderRadius: BorderRadius.circular(2),
              ),
            ),
            const Padding(
              padding: EdgeInsets.all(20),
              child: Text(
                '扫描历史',
                style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
              ),
            ),
            const Divider(),
            Expanded(
              child: _history.isEmpty
                  ? const Center(child: Text('暂无扫描记录'))
                  : ListView.builder(
                      itemCount: _history.length,
                      itemBuilder: (context, index) {
                        final item = _history[index];
                        return ListTile(
                          leading: const CircleAvatar(
                            child: Icon(Icons.qr_code),
                          ),
                          title: Text(
                            item.content,
                            maxLines: 1,
                            overflow: TextOverflow.ellipsis,
                          ),
                          subtitle: Text(
                            '${item.time.year}-${item.time.month.toString().padLeft(2, '0')}-${item.time.day.toString().padLeft(2, '0')} ${item.time.hour.toString().padLeft(2, '0')}:${item.time.minute.toString().padLeft(2, '0')}',
                          ),
                          onTap: () {
                            Clipboard.setData(ClipboardData(text: item.content));
                            ScaffoldMessenger.of(context).showSnackBar(
                              const SnackBar(content: Text('已复制')),
                            );
                          },
                        );
                      },
                    ),
            ),
          ],
        ),
      ),
    );
  }

  
  void dispose() {
    controller?.dispose();
    super.dispose();
  }
}

class ScanHistory {
  final String content;
  final DateTime time;

  ScanHistory({required this.content, required this.time});
}


七、从图片识别二维码(上方代码已包含)

除了使用相机实时扫描二维码外,有时我们需要从已有的图片中识别二维码。例如,用户从相册选择图片、从网络下载二维码图片等场景。

7.1 recognition_qrcode 插件

recognition_qrcode 是一款基于 Google ML Kit 框架开发的 Flutter 二维码识别插件,专门为鸿蒙系统进行了适配优化。它提供了简单易用的 API 接口,能够快速识别图片中的二维码、条形码等多种码制。

主要功能特点:

  • 多码制支持:基于 Google ML Kit 框架,支持识别二维码、条形码等多种码制
  • 多输入方式:支持通过本地文件路径、网络 URL 和 base64 数据进行识别
  • 多码识别:能够识别图片中包含的多个二维码或条形码
  • 灵活配置:支持自定义识别界面的图标、文字大小和内容
  • 鸿蒙适配:专门为鸿蒙系统进行了适配,提供了完整的鸿蒙系统支持
  • 简单易用:提供简洁的 API 接口,方便开发者快速集成

7.2 添加依赖

pubspec.yaml 文件中添加 recognition_qrcode 依赖:

dependencies:
  # 添加从图片识别二维码的依赖
  recognition_qrcode:
    git:
      url: https://atomgit.com/openharmony-sig/fluttertpc_recognition_qrcode

7.3 基本用法

使用 RecognitionManager.recognition() 方法识别图片中的二维码:

import 'package:recognition_qrcode/recognition_qrcode.dart';

// 识别本地图片中的二维码
final result = await RecognitionManager.recognition('/path/to/image.jpg');

// 识别网络图片中的二维码
final result = await RecognitionManager.recognition('https://example.com/qrcode.png');

// 处理识别结果
String codeStr = result.code.toString();
bool isSuccess = codeStr == "success" || codeStr == "0";
bool hasValue = result.value != null && result.value.toString().isNotEmpty;

if (isSuccess && hasValue) {
  print("识别成功: ${result.value}");
} else {
  print("识别失败: ${result.code}");
}

RecognitionResult 对象说明:

  • code:识别结果状态码,"success""0" 表示识别成功
  • value:识别出的二维码内容

⚠️ 注意code 字段可能是字符串 "0" 或整数 0,建议使用 result.code.toString() 进行比较。

7.4 从相册选择图片识别

结合 image_picker 插件,可以实现从相册选择图片并识别二维码的功能:

import 'dart:io';
import 'package:image_picker/image_picker.dart';
import 'package:recognition_qrcode/recognition_qrcode.dart';

Future<void> _pickFromGallery() async {
  final picker = ImagePicker();
  final image = await picker.pickImage(source: ImageSource.gallery);
  if (image != null) {
    _scanImageForQRCode(File(image.path));
  }
}

Future<void> _scanImageForQRCode(File imageFile) async {
  try {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('正在识别二维码...')),
    );

    final result = await RecognitionManager.recognition(imageFile.path);

    if (mounted) {
      ScaffoldMessenger.of(context).hideCurrentSnackBar();
    }

    String codeStr = result.code.toString();
    bool isSuccess = codeStr == "success" || codeStr == "0";
    bool hasValue = result.value != null && result.value.toString().isNotEmpty;

    if (isSuccess && hasValue) {
      HapticFeedback.mediumImpact();
      setState(() {
        scanResult = result.value.toString();
      });
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('识别成功')),
        );
      }
    } else {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('未识别到二维码')),
        );
      }
    }
  } catch (e) {
    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('识别失败: $e')),
      );
    }
  }
}

7.5 网络二维码图片

有时我们需要提供一些测试用的二维码图片,或者让用户保存二维码到相册后用相机扫描。可以使用在线二维码生成服务生成二维码图片。

使用示例:

import 'package:http/http.dart' as http;
import 'dart:typed_data';

// 生成二维码图片 URL
final qrUrl = 'https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=Hello%20OpenHarmony';

// 显示二维码图片
Image.network(qrUrl)

7.6 保存二维码到相册

使用 image_gallery_saver 插件可以将网络二维码图片保存到相册:

import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:http/http.dart' as http;

Future<void> _saveQRToGallery(String qrUrl, String title) async {
  try {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('正在保存...')),
    );

    // 下载二维码图片
    final response = await http.get(Uri.parse(qrUrl));
    final Uint8List bytes = response.bodyBytes;

    // 保存到相册
    final result = await ImageGallerySaver.saveImage(
      bytes,
      quality: 100,
      name: 'qr_${title.replaceAll(' ', '_')}_${DateTime.now().millisecondsSinceEpoch}',
    );

    if (result['isSuccess'] == true) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('已保存到相册: $title')),
        );
      }
    } else {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('保存失败,请检查相册权限')),
        );
      }
    }
  } catch (e) {
    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('保存失败: $e')),
      );
    }
  }
}

注意事项:

  • 保存图片到相册需要 ohos.permission.WRITE_IMAGEVIDEO 权限
  • 该权限是 system_basic 级别,普通应用无法申请
  • 如果无法申请该权限,可以提示用户使用系统截图功能保存二维码

八、常见问题与解决方案

8.1 扫描无反应

问题描述:打开扫描页面后无法扫描二维码,相机显示正常但扫描不到结果。

可能原因

  1. 相机权限未授予或被拒绝
  2. 二维码不在支持的码制范围内
  3. 二维码模糊或光线不足
  4. 扫描框配置问题

解决方案

  1. 检查相机权限是否已授予
  2. 确保 module.json5 中声明了相机权限
  3. 检查 formatsAllowed 配置是否正确
  4. 尝试调整光线或让用户更靠近二维码

8.2 扫描框显示异常

问题描述:扫描框位置或大小不正确,影响用户体验。

解决方案

overlay: QrScannerOverlayShape(
  cutOutSize: MediaQuery.of(context).size.width * 0.7,  // 根据屏幕宽度调整
  cutOutBottomOffset: 50,  // 调整底部偏移
)

根据设备屏幕尺寸动态调整扫描框大小,确保在不同设备上都有良好的显示效果。

8.3 Android 热重载后相机黑屏

问题描述:在开发过程中进行热重载后,相机显示黑屏。

原因分析:热重载不会重新初始化相机,导致相机状态异常。

解决方案


void reassemble() {
  super.reassemble();
  controller?.pauseCamera();
  controller?.resumeCamera();
}

reassemble 方法中重新初始化相机,确保热重载后相机正常工作。

8.4 识别率低

问题描述:二维码识别率低,需要多次尝试才能成功扫描。

解决方案

  1. 确保二维码清晰、光线充足
  2. 调整扫描框大小,确保二维码完全在框内
  3. 尝试开启闪光灯辅助照明
  4. 引导用户保持手机稳定,避免抖动

九、最佳实践

9.1 权限检查

在打开扫描页面前,应该先检查相机权限:

Future<bool> checkCameraPermission() async {
  final status = await Permission.camera.status;
  if (status.isGranted) return true;

  final result = await Permission.camera.request();
  if (result.isPermanentlyDenied) {
    await openAppSettings();
    return false;
  }
  return result.isGranted;
}

9.2 扫描结果处理

根据扫描内容的类型,提供相应的处理方式:

void handleScanResult(String result) {
  if (result.startsWith('http://') || result.startsWith('https://')) {
    _openUrl(result);
  } else if (result.startsWith('WIFI:')) {
    _handleWifiConfig(result);
  } else if (RegExp(r'^\d+$').hasMatch(result)) {
    _handleNumber(result);
  } else {
    _handleText(result);
  }
}

9.3 生命周期管理

正确管理扫描器的生命周期,避免资源泄漏:


void initState() {
  super.initState();
  // 初始化
}


void dispose() {
  controller?.dispose();  // 释放资源
  super.dispose();
}


void reassemble() {
  super.reassemble();
  // 热重载时重新初始化相机
  controller?.pauseCamera();
  controller?.resumeCamera();
}

9.4 用户体验优化

震动反馈:扫描成功时触发震动,提供即时反馈。

声音提示:扫描成功时播放提示音。

动画效果:添加扫描线动画,增强视觉效果。

引导提示:在首次使用时显示操作引导。


十、总结

本文详细介绍了 Flutter for OpenHarmony 中 qr_code_scanner 二维码扫描插件的使用方法,包括:

  1. 基础概念:二维码的发展历程、应用场景、码制类型
  2. 项目配置:依赖添加、权限配置、平台差异
  3. 核心 API:扫描控制器、码制类型、扫描结果、扫描框配置
  4. 实际应用:基础扫描、多功能扫描、完整扫描应用
  5. 从图片识别:使用 recognition_qrcode 插件从相册图片识别二维码
  6. 网络二维码:提供测试用二维码图片,支持保存到相册
  7. 最佳实践:权限检查、结果处理、生命周期管理、用户体验优化

二维码扫描是移动应用中非常常见的功能。通过本文的学习,你应该能够在 Flutter for OpenHarmony 应用中熟练使用二维码扫描功能,包括相机实时扫描和从图片识别二维码,为用户提供便捷的扫码体验。


📌 提示:本文基于 Flutter 3.27.5-ohos-1.0.4 和 qr_code_scanner@0.7.0 编写,不同版本可能略有差异。

Logo

昇腾计算产业是基于昇腾系列(HUAWEI Ascend)处理器和基础软件构建的全栈 AI计算基础设施、行业应用及服务,https://devpress.csdn.net/organization/setting/general/146749包括昇腾系列处理器、系列硬件、CANN、AI计算框架、应用使能、开发工具链、管理运维工具、行业应用及服务等全产业链

更多推荐