You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Flutter中实现带固定跟踪框的QR码扫描器

Flutter 实现跟随QR码的固定识别框方案

当然有开发者实现过这类功能,下面给你几个实用的实现思路和代码参考:

一、推荐依赖包

优先选择支持返回QR码边界坐标的扫描库,这类库能直接提供QR码在相机预览中的位置信息,是实现跟随框的核心:

  • mobile_scanner:Flutter社区维护的高性能扫描库,支持Android/iOS,能返回条码的boundingBox(边界矩形),API简洁易上手。
  • qr_code_scanner:老牌QR扫描库,同样支持获取QR码的位置参数,文档成熟稳定。

这里以mobile_scanner为例演示,先在pubspec.yaml添加依赖:

dependencies:
  mobile_scanner: ^5.1.0

二、核心实现步骤

  1. 初始化扫描控制器,监听条码扫描回调,实时获取QR码的边界坐标
  2. 在相机预览层上方叠加自定义组件,根据返回的坐标动态更新框的位置和大小
  3. 处理相机预览与屏幕坐标系的转换,确保框的位置与实际QR码完全匹配

三、代码示例

import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';

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

  @override
  State<QrScannerWithOverlay> createState() => _QrScannerWithOverlayState();
}

class _QrScannerWithOverlayState extends State<QrScannerWithOverlay> {
  final MobileScannerController _controller = MobileScannerController();
  Rect? _qrBoundingBox; // 存储QR码的边界矩形
  Size? _previewSize; // 相机预览尺寸

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          // 相机预览层
          MobileScanner(
            controller: _controller,
            onScannerStarted: (size) {
              setState(() => _previewSize = size);
            },
            onDetect: (capture) {
              final barcodes = capture.barcodes;
              if (barcodes.isNotEmpty) {
                setState(() => _qrBoundingBox = barcodes.first.boundingBox);
              } else {
                setState(() => _qrBoundingBox = null);
              }
            },
          ),
          // 跟随QR码的识别框
          if (_qrBoundingBox != null && _previewSize != null)
            AnimatedPositioned(
              duration: const Duration(milliseconds: 100),
              left: _convertX(_qrBoundingBox!.left),
              top: _convertY(_qrBoundingBox!.top),
              width: _convertWidth(_qrBoundingBox!.width),
              height: _convertHeight(_qrBoundingBox!.height),
              child: Container(
                decoration: BoxDecoration(
                  border: Border.all(color: Colors.greenAccent, width: 3),
                  borderRadius: BorderRadius.circular(8),
                ),
                // 顶部标签,模拟Google Lens的样式
                child: const Align(
                  alignment: Alignment.topCenter,
                  child: Padding(
                    padding: EdgeInsets.only(top: 4),
                    child: Text(
                      "QR码",
                      style: TextStyle(
                        color: Colors.greenAccent,
                        backgroundColor: Colors.black54,
                        fontSize: 12,
                      ),
                    ),
                  ),
                ),
              ),
            ),
        ],
      ),
    );
  }

  // 相机预览X坐标转屏幕X坐标
  double _convertX(double x) {
    final screenWidth = MediaQuery.of(context).size.width;
    return x * (screenWidth / _previewSize!.width);
  }

  // 相机预览Y坐标转屏幕Y坐标
  double _convertY(double y) {
    final screenHeight = MediaQuery.of(context).size.height;
    return y * (screenHeight / _previewSize!.height);
  }

  // 转换宽度
  double _convertWidth(double width) {
    final screenWidth = MediaQuery.of(context).size.width;
    return width * (screenWidth / _previewSize!.width);
  }

  // 转换高度
  double _convertHeight(double height) {
    final screenHeight = MediaQuery.of(context).size.height;
    return height * (screenHeight / _previewSize!.height);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

四、优化建议

  • 平滑过渡:用AnimatedPositioned替代普通Positioned,让框的移动更流畅,避免生硬跳变
  • 多码支持:如果需要同时识别多个QR码,可遍历barcodes列表,绘制多个识别框
  • 样式定制:可以给框添加阴影、闪烁动画,或者替换成自定义图标/标签样式
  • 权限处理:记得在Android的AndroidManifest.xml和iOS的Info.plist中配置相机权限申请

内容的提问来源于stack exchange,提问作者NewbieMF

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 06:00:16