Flutter Mobile Scanner扫描条形码后会显示黑屏
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有时变得无响应
❓ 问题
- 这是
mobile_scanner的已知问题吗? - 导航时处理相机生命周期的正确方法是什么?
- 启用
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导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。