基础入门 Flutter for OpenHarmony:qr_code_scanner 二维码扫描详解
二维码已经成为现代生活中不可或缺的一部分。从移动支付、扫码登录,到商品追溯、信息获取,二维码的应用场景无处不在。作为一种信息密度高、识别速度快、成本低廉的数据载体,二维码在移动互联网时代发挥着重要作用。在 Flutter for OpenHarmony 应用开发中, 是一个非常实用的二维码扫描插件。它基于原生平台的相机和图像识别能力,提供了高性能、高识别率的二维码扫描功能。开发者可以通过简单的 A

欢迎加入开源鸿蒙跨平台社区: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();
}
}
代码解析:
- GlobalKey:用于标识 QRView 组件,在某些操作中需要用到
- QRViewController:扫描控制器,通过它可以控制扫描行为、获取扫描结果
- scannedDataStream:扫描结果流,每当扫描到二维码时会发出事件
- 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();
}
}
代码解析:
这个示例展示了二维码扫描的完整流程:
- 扫描界面:使用 QRView 组件显示相机预览和扫描框
- 扫描结果处理:监听 scannedDataStream 获取扫描结果
- 震动反馈:扫描成功时触发震动,提供触觉反馈
- 结果展示:在扫描界面底部显示扫描结果卡片
- 功能操作:支持复制结果和打开链接
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();
}
}
功能说明:
这个多功能扫描器提供了以下功能:
- 码制过滤:可以选择只扫描二维码、条形码或所有码制
- 闪光灯控制:在光线不足时开启闪光灯
- 摄像头切换:切换前后摄像头
- 相册选择:从相册选择图片识别二维码(需要额外实现)
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 扫描无反应
问题描述:打开扫描页面后无法扫描二维码,相机显示正常但扫描不到结果。
可能原因:
- 相机权限未授予或被拒绝
- 二维码不在支持的码制范围内
- 二维码模糊或光线不足
- 扫描框配置问题
解决方案:
- 检查相机权限是否已授予
- 确保
module.json5中声明了相机权限 - 检查
formatsAllowed配置是否正确 - 尝试调整光线或让用户更靠近二维码
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 识别率低
问题描述:二维码识别率低,需要多次尝试才能成功扫描。
解决方案:
- 确保二维码清晰、光线充足
- 调整扫描框大小,确保二维码完全在框内
- 尝试开启闪光灯辅助照明
- 引导用户保持手机稳定,避免抖动
九、最佳实践
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 二维码扫描插件的使用方法,包括:
- 基础概念:二维码的发展历程、应用场景、码制类型
- 项目配置:依赖添加、权限配置、平台差异
- 核心 API:扫描控制器、码制类型、扫描结果、扫描框配置
- 实际应用:基础扫描、多功能扫描、完整扫描应用
- 从图片识别:使用 recognition_qrcode 插件从相册图片识别二维码
- 网络二维码:提供测试用二维码图片,支持保存到相册
- 最佳实践:权限检查、结果处理、生命周期管理、用户体验优化
二维码扫描是移动应用中非常常见的功能。通过本文的学习,你应该能够在 Flutter for OpenHarmony 应用中熟练使用二维码扫描功能,包括相机实时扫描和从图片识别二维码,为用户提供便捷的扫码体验。
📌 提示:本文基于 Flutter 3.27.5-ohos-1.0.4 和 qr_code_scanner@0.7.0 编写,不同版本可能略有差异。
昇腾计算产业是基于昇腾系列(HUAWEI Ascend)处理器和基础软件构建的全栈 AI计算基础设施、行业应用及服务,https://devpress.csdn.net/organization/setting/general/146749包括昇腾系列处理器、系列硬件、CANN、AI计算框架、应用使能、开发工具链、管理运维工具、行业应用及服务等全产业链
更多推荐


所有评论(0)