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 适配层,cameraimage_picker 可能尚未正式适配,但纯 Dart 部分(zxing_lib)可以运行。建议关注鸿蒙官方插件进展,或使用平台通道调用鸿蒙原生扫码能力。
  • 性能优化
    • 实时扫描时,可每隔几帧处理一次(例如每 5 帧),避免过度消耗 CPU。
    • _processCameraImage 中,将图像缩小后再解码(例如缩放至 640x480),提高解码速度。
    • 使用 compute 将解码任务放到后台 isolate,但需要注意 CameraImage 无法跨 isolate 传递,需要先转换为普通数据。简化起见,当前示例在 UI 线程进行,对于低分辨率帧通常可接受。

8. 完整代码示例仓库

这里给出核心代码片段,完整的可运行项目可以参考 GitHub 上的示例(如有需要可自行搜索 “flutter qr scanner camera image_picker zxing_lib”)。关键点都已覆盖,你可以根据实际需求调整 UI 和解码参数。

这样,你就拥有了一个全平台兼容的二维码扫描应用,同时支持实时摄像头和图片识别。

Logo

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

更多推荐