Flutter 实现多终端同时支持实时摄像头扫描和从相册选择图片识别二维码
Flutter实现全平台二维码扫描方案摘要: 本文介绍了一种基于Flutter的跨平台二维码扫描解决方案,可同时支持实时摄像头扫描和相册图片识别。该方案使用camera插件控制摄像头获取实时画面,配合image_picker插件实现图片选择功能,并采用纯Dart实现的zxing_lib进行二维码解码,确保在Android/iOS/Windows/macOS/Linux/Web/鸿蒙等全平台兼容。文
目录
Flutter 实现多终端同时支持实时摄像头扫描和从相册选择图片识别二维码
要实现同时支持实时摄像头扫描和从相册选择图片识别二维码,并且覆盖 Android/iOS/Windows/macOS/Linux/Web/鸿蒙 全平台,最稳妥的方案是:
- 实时扫描:使用
camera插件获取相机帧,配合纯 Dart 二维码解码库zxing_lib进行识别。 - 图片选择:使用
image_picker获取图片文件,同样用zxing_lib解码。
这套方案完全基于 Dart 实现,不依赖平台原生扫码库,因此可以在所有 Flutter 支持的平台(包括 Web 和鸿蒙)上运行,只是个别插件在部分平台可能需要额外配置(如 Web 的 camera 支持尚在实验阶段,但可用)。
1. 添加依赖
dependencies:
flutter:
sdk: flutter
camera: ^0.10.6 # 相机控制(支持 Android/iOS/Windows/macOS/Linux/Web)
image_picker: ^1.0.7 # 图片选择(支持全平台,Web 需额外导入 image_picker_web)
zxing_lib: ^1.0.0 # 纯 Dart 二维码解码
image: ^4.1.3 # 图片处理(用于像素转换)
2. 权限配置
Android
在 android/app/src/main/AndroidManifest.xml 中添加:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
iOS
在 ios/Runner/Info.plist 中添加:
<key>NSCameraUsageDescription</key>
<string>需要相机权限以扫描二维码</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>需要相册权限以选择二维码图片</string>
Web
Web 平台无需显式权限声明,但 camera 插件在 Web 上需要用户通过浏览器授权摄像头访问,image_picker 通过文件选择对话框工作。
3. 核心工具函数:二维码解码器
首先封装一个通用的解码函数,既能处理相机帧(CameraImage),也能处理图片文件(XFile),内部调用 zxing_lib 进行识别。
import 'dart:typed_data';
import 'package:zxing_lib/zxing.dart';
import 'package:image/image.dart' as img;
/// 从 RGB 像素数据中解码二维码
Future<String?> decodeQrFromRgb({
required int width,
required int height,
required List<int> rgbBytes, // 顺序:R,G,B,R,G,B,...
}) async {
try {
final luminanceSource = RGBLuminanceSource(width, height, rgbBytes);
final binaryBitmap = BinaryBitmap(HybridBinarizer(luminanceSource));
final reader = Reader();
final result = reader.decode(binaryBitmap);
return result.text;
} catch (e) {
return null; // 解码失败或未发现二维码
}
}
/// 从图片文件解码二维码
Future<String?> decodeQrFromImageFile(String path) async {
final fileBytes = await XFile(path).readAsBytes();
final img.Image? image = img.decodeImage(fileBytes);
if (image == null) return null;
// 提取 RGB 数据(忽略 Alpha 通道)
final rgb = <int>[];
for (int y = 0; y < image.height; y++) {
for (int x = 0; x < image.width; x++) {
final pixel = image.getPixel(x, y); // 0xAARRGGBB
rgb.add((pixel >> 16) & 0xFF); // R
rgb.add((pixel >> 8) & 0xFF); // G
rgb.add(pixel & 0xFF); // B
}
}
return decodeQrFromRgb(
width: image.width,
height: image.height,
rgbBytes: rgb,
);
}
/// 从相机帧(CameraImage)解码二维码
Future<String?> decodeQrFromCameraImage(CameraImage image) async {
// 将 CameraImage 转换为 RGB 数据(需根据格式处理)
// 这里以最常见的 YUV_420_888 格式为例,转换为 RGB
final rgb = await convertYUV420ToRGB(image);
if (rgb == null) return null;
return decodeQrFromRgb(
width: image.width,
height: image.height,
rgbBytes: rgb,
);
}
/// YUV_420_888 转 RGB (简易实现,生产环境建议使用优化版本)
Future<List<int>?> convertYUV420ToRGB(CameraImage image) async {
// 参考:https://github.com/flutter/flutter/issues/30290
// 此处给出一个基本实现(效率较低,可改用 compute 或 native 方式)
// 完整代码较长,建议直接使用已有工具类,或参考:
// https://github.com/flutter-ml/google_ml_kit_flutter/blob/master/packages/google_ml_kit/lib/src/vision_utils.dart
// 为节省篇幅,假设已实现并返回 List<int> rgbBytes
}
提示:
convertYUV420ToRGB的完整实现较复杂,建议直接使用社区已验证的代码,例如 camera 示例中的转换函数 或 image 库中的convertYuv420ToImage。如果对性能要求不高,可简单将每个 YUV 帧转换为 PNG 再解码,但会严重影响实时性。
4. 实时扫描页面
创建一个 LiveScanPage,使用 camera 插件显示预览并持续解码。
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
class LiveScanPage extends StatefulWidget {
_LiveScanPageState createState() => _LiveScanPageState();
}
class _LiveScanPageState extends State<LiveScanPage> {
CameraController? _controller;
bool _isScanning = true;
String? _result;
void initState() {
super.initState();
_initCamera();
}
Future<void> _initCamera() async {
final cameras = await availableCameras();
if (cameras.isEmpty) return;
_controller = CameraController(cameras[0], ResolutionPreset.medium);
await _controller!.initialize();
_controller!.startImageStream(_processCameraImage);
setState(() {});
}
void _processCameraImage(CameraImage image) async {
if (!_isScanning) return;
final code = await decodeQrFromCameraImage(image);
if (code != null) {
_isScanning = false;
setState(() => _result = code);
// 显示结果对话框
_showResultDialog(code);
}
}
void _showResultDialog(String code) {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text('扫描结果'),
content: Text(code),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
setState(() {
_isScanning = true;
_result = null;
});
},
child: Text('继续扫描'),
),
],
),
);
}
Widget build(BuildContext context) {
if (_controller == null || !_controller!.value.isInitialized) {
return Scaffold(body: Center(child: CircularProgressIndicator()));
}
return Scaffold(
appBar: AppBar(title: Text('实时扫描')),
body: Stack(
children: [
CameraPreview(_controller!),
if (_result != null)
Positioned(
bottom: 40,
left: 20,
right: 20,
child: Container(
padding: EdgeInsets.all(12),
color: Colors.black54,
child: Text(
'结果: $_result',
style: TextStyle(color: Colors.white),
textAlign: TextAlign.center,
),
),
),
],
),
);
}
void dispose() {
_controller?.dispose();
super.dispose();
}
}
5. 图片选择页面(或对话框)
创建一个简单的图片选择功能,使用 image_picker 选取图片并解码。
Future<void> _pickImage(ImageSource source) async {
final picker = ImagePicker();
final XFile? image = await picker.pickImage(source: source);
if (image == null) return;
// 显示加载中
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => Center(child: CircularProgressIndicator()),
);
final code = await decodeQrFromImageFile(image.path);
Navigator.pop(context); // 关闭加载
showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text('识别结果'),
content: Text(code ?? '未识别到二维码'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('确定'),
),
],
),
);
}
6. 整合到主页面
提供一个主页,让用户选择“实时扫描”或“从相册选择”。
class HomePage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('二维码扫描')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => LiveScanPage()),
);
},
child: Text('实时扫描'),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () => _pickImage(ImageSource.gallery),
child: Text('从相册选择'),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () => _pickImage(ImageSource.camera),
child: Text('拍照识别'),
),
],
),
),
);
}
}
7. 跨平台注意事项
- Web 平台:
camera插件在 Web 上需要使用camera_web,但camera包本身已经包含了 Web 实现(需确保pubspec.lock正确)。另外,Web 上无法直接使用dart:isolate,但我们的解码函数未使用 isolate,因此可以正常工作。如果担心性能,可在 Web 上降低帧率或图像分辨率。 - 鸿蒙:目前 Flutter 对鸿蒙的支持主要通过 OpenHarmony 适配层,
camera和image_picker可能尚未正式适配,但纯 Dart 部分(zxing_lib)可以运行。建议关注鸿蒙官方插件进展,或使用平台通道调用鸿蒙原生扫码能力。 - 性能优化:
- 实时扫描时,可每隔几帧处理一次(例如每 5 帧),避免过度消耗 CPU。
- 在
_processCameraImage中,将图像缩小后再解码(例如缩放至 640x480),提高解码速度。 - 使用
compute将解码任务放到后台 isolate,但需要注意CameraImage无法跨 isolate 传递,需要先转换为普通数据。简化起见,当前示例在 UI 线程进行,对于低分辨率帧通常可接受。
8. 完整代码示例仓库
这里给出核心代码片段,完整的可运行项目可以参考 GitHub 上的示例(如有需要可自行搜索 “flutter qr scanner camera image_picker zxing_lib”)。关键点都已覆盖,你可以根据实际需求调整 UI 和解码参数。
这样,你就拥有了一个全平台兼容的二维码扫描应用,同时支持实时摄像头和图片识别。
更多推荐




所有评论(0)