Flutter Mobile Scanner扫描条形码后会显示黑屏

移动开发 2026-07-10

Flutter Mobile Scanner扫描条码后显示黑屏

我在Flutter中使用 mobile_scanner 包来扫描条形码。扫描器在检测条形码方面工作正常,但 扫描后,屏幕变成黑屏,且没有正确恢复。

🔍 我想要做的事

  • 打开一个条码扫描屏幕
  • 扫描一个条码
  • 使用 Navigator.pop() 返回扫描到的值

❗ 问题

成功扫描后:

  • 相机预览变为 黑屏
  • 有时屏幕会卡死
  • 返回到扫描器时,相机无法正确启动

📦 软件包

mobile_scanner: ^7.2.0

🧩 我的实现

下面是我的扫描逻辑:

// lib/screens/business_pos/barcode_scanner_screen.dart
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:salesapp/constants/colors.dart';

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

  @override
  State<BarcodeScannerScreen> createState() => _BarcodeScannerScreenState();
}

class _BarcodeScannerScreenState extends State<BarcodeScannerScreen>
    with SingleTickerProviderStateMixin {
  final MobileScannerController _controller = MobileScannerController(
    detectionSpeed: DetectionSpeed.noDuplicates,
    facing: CameraFacing.back,
    torchEnabled: false,
    autoStart: true,
  );

  bool _torchOn = false;
  bool _detected = false;
  bool _isProcessing = false; // HARD LOCK to prevent multiple detections

  // Scan line animation
  late AnimationController _scanLineController;
  late Animation<double> _scanLineAnim;

  @override
  void initState() {
    super.initState();
    _scanLineController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 1800),
    )..repeat(reverse: true);
    _scanLineAnim = CurvedAnimation(
      parent: _scanLineController,
      curve: Curves.easeInOut,
    );
  }

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

void _onDetect(BarcodeCapture capture) async {
  if (_isProcessing || !mounted) return;

  final value = capture.barcodes.firstOrNull?.rawValue;
  if (value == null) return;

  _isProcessing = true; // 🔒 HARD LOCK

  try {
    await _controller.stop();
  } catch (e) {
    debugPrint('Camera stop error: $e');
  }

  if (!mounted) return;

  // small delay prevents navigator lock
  await Future.delayed(const Duration(milliseconds: 150));

  if (!mounted) return;

  Navigator.of(context).pop(value);
}

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Stack(
        children: [
          // ── Camera feed ──
          MobileScanner(
            controller: _controller,
            onDetect: _onDetect,
            errorBuilder: (context, error, child) {
              return Container(
                color: Colors.black,
                child: Center(
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      const Icon(
                        Icons.error_outline_rounded,
                        color: Colors.white,
                        size: 48,
                      ),
                      const SizedBox(height: 16),
                      Text(
                        _cameraErrorMessage(error),
                        style: GoogleFonts.manrope(
                          color: Colors.white,
                          fontSize: 14,
                        ),
                        textAlign: TextAlign.center,
                      ),
                      const SizedBox(height: 20),
                      GestureDetector(
                        onTap: () async {
                          await _controller.stop();
                          await Future.delayed(
                            const Duration(milliseconds: 300),
                          );
                          await _controller.start();
                        },
                        child: Container(
                          padding: const EdgeInsets.symmetric(
                            horizontal: 20,
                            vertical: 10,
                          ),
                          decoration: BoxDecoration(
                            color: CBaseColor,
                            borderRadius: BorderRadius.circular(12),
                          ),
                          child: Text(
                            'Retry',
                            style: GoogleFonts.manrope(
                              color: Colors.white,
                              fontWeight: FontWeight.w700,
                            ),
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              );
            },
          ),

          // ── Dark overlay with cutout ──
          _buildScanOverlay(),

          // ── Top bar ──
          Positioned(
            top: MediaQuery.of(context).padding.top + 12,
            left: 0,
            right: 0,
            child: _buildTopBar(),
          ),

          // ── Bottom controls ──
          Positioned(
            bottom: MediaQuery.of(context).padding.bottom + 24,
            left: 0,
            right: 0,
            child: _buildBottomControls(),
          ),

          // ── Scan line animation inside the cutout ──
          _buildScanLine(),
        ],
      ),
    );
  }

  String _cameraErrorMessage(MobileScannerException error) {
    switch (error.errorCode) {
      case MobileScannerErrorCode.permissionDenied:
        return 'Camera permission denied.\nPlease enable it in Settings.';
      case MobileScannerErrorCode.unsupported:
        return 'Camera not supported on this device.';
      default:
        return 'Camera error. Please try again.';
    }
  }

  // ── Overlay with transparent square cutout ──
  Widget _buildScanOverlay() {
    return LayoutBuilder(
      builder: (context, constraints) {
        final size = constraints.maxWidth * 0.65;
        final top = (constraints.maxHeight - size) / 2 - 40;
        final left = (constraints.maxWidth - size) / 2;

        return Stack(
          children: [
            // Top dark
            Positioned(
              top: 0,
              left: 0,
              right: 0,
              child: Container(height: top, color: Colors.black54),
            ),
            // Bottom dark
            Positioned(
              top: top + size,
              left: 0,
              right: 0,
              bottom: 0,
              child: Container(color: Colors.black54),
            ),
            // Left dark
            Positioned(
              top: top,
              left: 0,
              width: left,
              height: size,
              child: Container(color: Colors.black54),
            ),
            // Right dark
            Positioned(
              top: top,
              right: 0,
              width: constraints.maxWidth - left - size,
              height: size,
              child: Container(color: Colors.black54),
            ),

            // Corner brackets
            Positioned(
              top: top,
              left: left,
              width: size,
              height: size,
              child: _buildCornerBrackets(size),
            ),
          ],
        );
      },
    );
  }

  Widget _buildCornerBrackets(double size) {
    const thickness = 3.0;
    const length = 28.0;
    const radius = 8.0;

    return Stack(
      children: [
        // Top-left
        Positioned(
          top: 0,
          left: 0,
          child: Container(
            width: length,
            height: thickness,
            decoration: BoxDecoration(
              color: CBaseColor,
              borderRadius: const BorderRadius.only(
                topLeft: Radius.circular(radius),
              ),
            ),
          ),
        ),
        Positioned(
          top: 0,
          left: 0,
          child: Container(
            width: thickness,
            height: length,
            decoration: BoxDecoration(
              color: CBaseColor,
              borderRadius: const BorderRadius.only(
                topLeft: Radius.circular(radius),
              ),
            ),
          ),
        ),
        // Top-right
        Positioned(
          top: 0,
          right: 0,
          child: Container(
            width: length,
            height: thickness,
            decoration: BoxDecoration(
              color: CBaseColor,
              borderRadius: const BorderRadius.only(
                topRight: Radius.circular(radius),
              ),
            ),
          ),
        ),
        Positioned(
          top: 0,
          right: 0,
          child: Container(
            width: thickness,
            height: length,
            decoration: BoxDecoration(
              color: CBaseColor,
              borderRadius: const BorderRadius.only(
                topRight: Radius.circular(radius),
              ),
            ),
          ),
        ),
        // Bottom-left
        Positioned(
          bottom: 0,
          left: 0,
          child: Container(
            width: length,
            height: thickness,
            decoration: BoxDecoration(
              color: CBaseColor,
              borderRadius: const BorderRadius.only(
                bottomLeft: Radius.circular(radius),
              ),
            ),
          ),
        ),
        Positioned(
          bottom: 0,
          left: 0,
          child: Container(
            width: thickness,
            height: length,
            decoration: BoxDecoration(
              color: CBaseColor,
              borderRadius: const BorderRadius.only(
                bottomLeft: Radius.circular(radius),
              ),
            ),
          ),
        ),
        // Bottom-right
        Positioned(
          bottom: 0,
          right: 0,
          child: Container(
            width: length,
            height: thickness,
            decoration: BoxDecoration(
              color: CBaseColor,
              borderRadius: const BorderRadius.only(
                bottomRight: Radius.circular(radius),
              ),
            ),
          ),
        ),
        Positioned(
          bottom: 0,
          right: 0,
          child: Container(
            width: thickness,
            height: length,
            decoration: BoxDecoration(
              color: CBaseColor,
              borderRadius: const BorderRadius.only(
                bottomRight: Radius.circular(radius),
              ),
            ),
          ),
        ),
      ],
    );
  }

  // ── Animated scan line ──
Widget _buildScanLine() {
  return LayoutBuilder(
    builder: (context, constraints) {
      final size = constraints.maxWidth * 0.65;
      final top = (constraints.maxHeight - size) / 2 - 40;
      final left = (constraints.maxWidth - size) / 2;

      return Stack( // ✅ ADD THIS
        children: [
          AnimatedBuilder(
            animation: _scanLineAnim,
            builder: (_, __) {
              final lineY = top + _scanLineAnim.value * size;

              return Positioned(
                top: lineY,
                left: left + 8,
                right: constraints.maxWidth - left - size + 8,
                child: Container(
                  height: 2,
                  decoration: BoxDecoration(
                    gradient: LinearGradient(
                      colors: [
                        Colors.transparent,
                        CBaseColor.withOpacity(0.8),
                        CBaseColor,
                        CBaseColor.withOpacity(0.8),
                        Colors.transparent,
                      ],
                    ),
                  ),
                ),
              );
            },
          ),
        ],
      );
    },
  );
}

  Widget _buildTopBar() {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16),
      child: Row(
        children: [
          // Back
          GestureDetector(
            onTap: () => Navigator.pop(context),
            child: Container(
              width: 42,
              height: 42,
              decoration: BoxDecoration(
                color: Colors.black.withOpacity(0.5),
                borderRadius: BorderRadius.circular(12),
                border: Border.all(color: Colors.white.withOpacity(0.15)),
              ),
              child: const Icon(
                Icons.arrow_back_ios_new_rounded,
                color: Colors.white,
                size: 18,
              ),
            ),
          ),
          const Spacer(),
          Column(
            children: [
              Text(
                'Scan Barcode',
                style: GoogleFonts.manrope(
                  fontSize: 16,
                  fontWeight: FontWeight.w700,
                  color: Colors.white,
                ),
              ),
              Text(
                'Point camera at product barcode',
                style: GoogleFonts.manrope(fontSize: 11, color: Colors.white60),
              ),
            ],
          ),
          const Spacer(),
          // Torch
          GestureDetector(
            onTap: () {
              _controller.toggleTorch();
              setState(() => _torchOn = !_torchOn);
            },
            child: AnimatedContainer(
              duration: const Duration(milliseconds: 200),
              width: 42,
              height: 42,
              decoration: BoxDecoration(
                color: _torchOn
                    ? Colors.amber.withOpacity(0.3)
                    : Colors.black.withOpacity(0.5),
                borderRadius: BorderRadius.circular(12),
                border: Border.all(
                  color: _torchOn
                      ? Colors.amber.withOpacity(0.6)
                      : Colors.white.withOpacity(0.15),
                ),
              ),
              child: Icon(
                _torchOn
                    ? Icons.flashlight_on_rounded
                    : Icons.flashlight_off_rounded,
                color: _torchOn ? Colors.amber : Colors.white,
                size: 20,
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildBottomControls() {
    return Column(
      children: [
        Text(
          'Align barcode within the frame',
          style: GoogleFonts.manrope(fontSize: 13, color: Colors.white70),
        ),
        const SizedBox(height: 20),
        // Manual entry button
        GestureDetector(
          onTap: () async {
            final result = await showDialog<String>(
              context: context,
              builder: (_) => _ManualEntryDialog(),
            );
            if (result != null && result.isNotEmpty && mounted) {
              Navigator.pop(context, result);
            }
          },
          child: Container(
            padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
            decoration: BoxDecoration(
              color: Colors.white.withOpacity(0.15),
              borderRadius: BorderRadius.circular(24),
              border: Border.all(color: Colors.white.withOpacity(0.3)),
            ),
            child: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                const Icon(
                  Icons.keyboard_rounded,
                  color: Colors.white,
                  size: 18,
                ),
                const SizedBox(width: 8),
                Text(
                  'Enter manually',
                  style: GoogleFonts.manrope(
                    fontSize: 13,
                    fontWeight: FontWeight.w600,
                    color: Colors.white,
                  ),
                ),
              ],
            ),
          ),
        ),
      ],
    );
  }
}

// ── Manual barcode entry dialog ──
class _ManualEntryDialog extends StatelessWidget {
  final _ctrl = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
      title: Text(
        'Enter Barcode',
        style: GoogleFonts.manrope(
          fontWeight: FontWeight.w700,
          color: const Color(0xFF1A2E1A),
        ),
      ),
      content: TextField(
        controller: _ctrl,
        autofocus: true,
        keyboardType: TextInputType.number,
        decoration: InputDecoration(
          hintText: 'e.g. 8901234560001',
          hintStyle: GoogleFonts.manrope(color: Colors.grey.shade400),
          border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
          focusedBorder: OutlineInputBorder(
            borderRadius: BorderRadius.circular(10),
            borderSide: BorderSide(color: CBaseColor),
          ),
        ),
        onSubmitted: (_) => Navigator.pop(context, _ctrl.text.trim()),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: Text('Cancel', style: GoogleFonts.manrope(color: Colors.grey)),
        ),
        ElevatedButton(
          onPressed: () => Navigator.pop(context, _ctrl.text.trim()),
          style: ElevatedButton.styleFrom(
            backgroundColor: CBaseColor,
            elevation: 0,
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(10),
            ),
          ),
          child: Text('Add', style: GoogleFonts.manrope(color: Colors.white)),
        ),
      ],
    );
  }
}

🧪 我已经尝试过的

  • 在导航前调用 _controller.stop()
  • 在重新启动相机前添加延迟
  • 使用 autoStart: true,并且手动调用 start()
  • 使用 errorBuilder 处理错误
  • 使用带重试按钮重新启动扫描器

以上都未能修复黑屏问题。


📱 预期行为

  • 扫描后相机应干净地停止
  • 屏幕切换时不应出现任何黑屏闪烁
  • 重新打开扫描器时,相机应正常工作

⚠️ 实际行为

  • 扫描后出现黑屏
  • 相机预览有时无法重启
  • UI有时变得无响应

❓ 问题

  1. 这是 mobile_scanner 的已知问题吗?
  2. 导航时处理相机生命周期的正确方法是什么?
  3. 启用 autoStart 时,是否应该避免手动调用 start()

🙏 任何帮助都将不胜感激!

解决方案

你代码的问题在于你没有正确接收返回值,或者你弹到了一个空页面。

我已经测试了你的代码并做了一些修改。我添加了一个首页并放置一个按钮。当按下按钮时,它会打开你的条码页。因此,从条码页弹出后,应用会返回到首页(它能找到一个可以返回的页面!)

以下是核心修改点:

1- 增加一个首页。我会把代码附在下面。

2- 使用 Navigator.push 将返回值赋给一个变量,以便在 Text() 小部件中查看。

3- 首页应为 StatefulWidget 类型,并且在弹出后应使用 SetState() 来更新页面。

关于 home_page.dart 的代码

import 'package:flutter/material.dart';
import 'package:just_test/scanner.dart';

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

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {

  String? returnValue;

  void buttonOnPressed() async {
    returnValue = await Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => BarcodeScannerScreen()),
            );

    setState(() {}); // to refresh the screen and display the value
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
            onPressed: () => buttonOnPressed(),
            child: Text('Go to Scanner Page'),
          ),
          SizedBox(height: 40,),
          Text('Value: $returnValue'),

          ]
        ),
      ),

    );
  }
}
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章