import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:go_router/go_router.dart';
import 'package:image_picker/image_picker.dart';
import 'package:oktoast/oktoast.dart';
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:permission_handler/permission_handler.dart';
import '../api/trace_utils.dart';
/// 公共页面 - 扫码页面
class ComScanPage extends StatefulWidget {
const ComScanPage({super.key});
@override
State<StatefulWidget> createState() => _ComScanPageState();
}
class _ComScanPageState extends State<ComScanPage> with SingleTickerProviderStateMixin {
MobileScannerController controller = MobileScannerController(
formats: [BarcodeFormat.all],
);
bool _isProcessing = false;
bool _hasPermission = false;
bool _permissionChecked = false;
//识别本地图片
final ImagePicker _imagePicker = ImagePicker();
// 新增动画控制器和扫描线位置动画
late AnimationController _scanLineController;
late Animation<double> _scanLineAnimation;
@override
void initState() {
super.initState();
controller = MobileScannerController(
autoStart: false, // 不自动启动
detectionSpeed: DetectionSpeed.normal,
facing: CameraFacing.back,
torchEnabled: false,
formats: [BarcodeFormat.all],
);
// 初始化动画控制器(持续2秒,循环播放)
_scanLineController = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat(reverse: false); // 重复动画
// 初始化扫描线位置动画(从0到1,映射到扫描框高度)
_scanLineAnimation = CurvedAnimation(
parent: _scanLineController,
curve: Curves.linear, // 线性移动
);
printLog("当前页面-相机扫描页面:ComScanPage", 'blue');
_checkCameraPermission();
}
// 检查相机权限
Future<void> _checkCameraPermission() async {
final status = await Permission.camera.status;
setState(() {
_hasPermission = status.isGranted;
_permissionChecked = true;
});
if (_hasPermission) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_startScanner();
});
} else {
_requestCameraPermission();
}
}
// 启动扫码器
Future<void> _startScanner() async {
try {
if (!controller.value.isRunning) {
await controller.start();
printLog("扫码器启动成功", 'green');
}
} catch (e) {
printLog("启动扫码器失败: $e", 'red');
Future.delayed(const Duration(milliseconds: 500), _startScanner);
}
}
// 请求相机权限
Future<void> _requestCameraPermission() async {
final status = await Permission.camera.request();
setState(() {
_hasPermission = status.isGranted;
});
if (_hasPermission) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_startScanner();
});
} else {
_showPermissionDialog();
}
}
// 处理扫描结果
Future<void> _handleBarcode(BarcodeCapture capture) async {
printLog("=== 扫描回调触发 ===", 'green');
printLog("扫描到二维码结果: ${capture.barcodes.length} 个", 'green');
if (_isProcessing || !mounted) return;
_isProcessing = true;
if (capture.barcodes.isEmpty) {
printLog("未识别到有效二维码", 'yellow');
_resumeScanning();
return;
}
final barcode = capture.barcodes.first;
final String codeString = barcode.rawValue ?? '';
printLog("扫描到二维码: $codeString", 'green');
try {
await controller.stop();
printLog("扫码器已暂停", 'blue');
_processBarcodeString(codeString);
} catch (e) {
printLog("处理二维码出错: $e", 'red');
showToast("处理二维码时出错");
_resumeScanning();
} finally {
if (mounted) {
Future.delayed(const Duration(seconds: 2), () {
setState(() {
_isProcessing = false;
});
});
}
}
}
// 恢复扫描
void _resumeScanning() {
if (mounted) {
Future.delayed(const Duration(seconds: 2), () {
if (mounted) {
setState(() {
_isProcessing = false;
});
if (!controller.value.isRunning) {
_startScanner();
}
}
});
}
}
// 处理二维码字符串
void _processBarcodeString(String codeString) {
if (codeString.contains('http://') || codeString.contains('https://')) {
printLog("识别为网页链接", 'blue');
_navigateToWebView(codeString);
} else if (codeString.contains('[jlm]')) {
if (codeString.contains('[bmd]')) {
final String bmdid = codeString.replaceAll('[jlm][bmd]', '');
printLog("识别为报名点二维码: $bmdid", 'blue');
_navigateToEnrollInfo(bmdid);
} else {
printLog("不支持的驾了么二维码类型", 'yellow');
showToast("暂不支持此类型的驾了么二维码");
_resumeScanning();
}
} else {
printLog("未识别二维码类型", 'yellow');
showToast("未识别信息");
_resumeScanning();
}
}
// 跳转到网页
void _navigateToWebView(String url) {
printLog("跳转至-Web页面:webView?url=$url", 'purple');
GoRouter.of(context).pushNamed("webView", queryParameters: {'url': url});
}
// 跳转到报名点详情
void _navigateToEnrollInfo(String bmdid) {
printLog("跳转至-报名点详情页面:enrollInfo?id=$bmdid", 'purple');
GoRouter.of(context).pushNamed("enrollInfo", queryParameters: {'id': bmdid});
}
// 新增:选择并识别本地图片二维码
Future<void> _pickImageAndScan() async {
try {
// 检查当前权限状态
PermissionStatus status;
// 根据平台使用不同的权限
if (Platform.isAndroid) {
// Android 10+ 使用 READ_MEDIA_IMAGES,旧版本使用 READ_EXTERNAL_STORAGE
if (await DeviceInfoPlugin().androidInfo.then((info) => info.version.sdkInt >= 33)) {
status = await Permission.photos.status;
} else {
status = await Permission.storage.status;
}
} else {
// iOS 使用照片权限
status = await Permission.photos.status;
}
// 如果权限已被永久拒绝,引导用户去设置
if (status.isPermanentlyDenied) {
_showPhotoPermissionDialog();
return;
}
// 如果权限被拒绝或未确定,请求权限
if (status.isDenied) {
PermissionStatus newStatus;
if (Platform.isAndroid && await DeviceInfoPlugin().androidInfo.then((info) => info.version.sdkInt >= 33)) {
newStatus = await Permission.photos.request();
} else if (Platform.isAndroid) {
newStatus = await Permission.storage.request();
} else {
newStatus = await Permission.photos.request();
}
if (!newStatus.isGranted) {
showToast("需要相册权限来选择图片");
return;
}
}
final XFile? image = await _imagePicker.pickImage(
source: ImageSource.gallery,
maxWidth: 1920,
maxHeight: 1080,
imageQuality: 80,
);
if (image != null) {
_scanImageQrCode(File(image.path));
}
} catch (e) {
printLog("选择图片失败: $e", 'red');
showToast("选择图片失败");
}
}
// 显示相册权限对话框
void _showPhotoPermissionDialog() {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: const Text("相册权限未开启"),
content: const Text("您未开启驾了么的相册访问权限,无法选择图片进行二维码识别。"),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text("暂不"),
),
TextButton(
onPressed: () {
openAppSettings().then((_) => Navigator.of(context).pop());
},
child: const Text("去设置"),
),
],
);
},
);
}
// 扫描图片中的二维码
Future<void> _scanImageQrCode(File imageFile) async {
// 使用局部变量跟踪对话框状态
bool isDialogOpen = true;
BuildContext? dialogContext;
showDialog(
context: context,
barrierDismissible: false,
builder: (context) {
dialogContext = context; // 保存对话框上下文
return PopScope(
canPop: false, // 禁用返回键关闭
child: const AlertDialog(
content: Row(
children: [
CircularProgressIndicator(),
SizedBox(width: 20),
Text('正在识别图片中的二维码...'),
],
),
),
);
},
);
try {
// 使用 MobileScanner 的图片识别功能
BarcodeCapture? result = await controller.analyzeImage(imageFile.path,formats:[BarcodeFormat.qrCode]);
// 安全关闭对话框
if (isDialogOpen && dialogContext != null && Navigator.canPop(dialogContext!)) {
Navigator.of(dialogContext!).pop();
isDialogOpen = false;
}
if (result != null && result.barcodes.isNotEmpty) {
final barcode = result.barcodes.first;
final String codeString = barcode.rawValue ?? '';
printLog("从图片中识别到二维码: $codeString", 'green');
_processBarcodeString(codeString);
} else {
if (mounted) showToast("未在图片中识别到二维码");
}
} catch (e) {
// 安全关闭对话框
if (isDialogOpen && dialogContext != null && Navigator.canPop(dialogContext!)) {
Navigator.of(dialogContext!).pop();
isDialogOpen = false;
}
printLog("识别图片二维码失败: $e", 'red');
if (mounted) showToast("识别图片二维码失败");
// 备用方案:尝试使用 BarcodeDetector
_tryAlternativeImageScan(imageFile);
}
}
// 备用图片识别方案
Future<void> _tryAlternativeImageScan(File imageFile) async {
bool isDialogOpen = true;
BuildContext? dialogContext;
try {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) {
dialogContext = context;
return PopScope(
canPop: false, // 禁用返回键关闭
child: const AlertDialog(
content: Row(
children: [
CircularProgressIndicator(),
SizedBox(width: 20),
Text('尝试备用识别方案...'),
],
),
),
);
},
);
// // 使用 BarcodeScanner 进行识别
// final pickedFile = await ImagePicker().pickImage(source: ImageSource.gallery);
// // 读取图片为字节
// final bytes = await File(imageFile.path).readAsBytes();
// 解析二维码
BarcodeCapture? result = await MobileScannerController(formats:[BarcodeFormat.qrCode],autoStart: false).analyzeImage(imageFile.path);
// 安全关闭对话框
if (isDialogOpen && dialogContext != null && Navigator.canPop(dialogContext!)) {
Navigator.of(dialogContext!).pop();
isDialogOpen = false;
}
if (result!.barcodes.isNotEmpty) {
final barcode = result.barcodes.first;
final String codeString = barcode.rawValue ?? '';
printLog("备用方案识别到二维码: $codeString", 'green');
_processBarcodeString(codeString);
} else {
showToast("无法识别图片中的二维码");
}
} catch (e) {
// 安全关闭对话框
if (isDialogOpen && dialogContext != null && Navigator.canPop(dialogContext!)) {
Navigator.of(dialogContext!).pop();
isDialogOpen = false;
}
printLog("备用识别方案也失败: $e", 'red');
showToast("二维码识别失败,请尝试其他图片");
}
}
@override
void dispose() {
_scanLineController.dispose(); // 释放动画资源
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.black,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.of(context).pop(),
),
title: const Text(
'扫一扫',
style: TextStyle(color: Colors.white),
),
),
body: _buildBody(),
);
}
Widget _buildBody() {
// 等待权限检查完成
if (!_permissionChecked) {
return _buildLoadingView('检查相机权限...');
}
// 权限被拒绝
if (!_hasPermission) {
return _buildPermissionDeniedView();
}
// 正常扫描界面
return Stack(
fit: StackFit.expand,
children: [
MobileScanner(
controller: controller,
fit: BoxFit.cover,
onDetect: _handleBarcode,
errorBuilder: (context, error) {
printLog("MobileScanner错误: ${error.errorCode}", 'red');
return _buildErrorView(error);
},
),
_buildScanOverlay(),
_buildHintText(),
_buildControlButtons(),
// 添加调试信息(开发时使用,发布时移除)
_buildDebugInfo(),
],
);
}
// 调试信息显示
Widget _buildDebugInfo() {
return Positioned(
top: 100,
left: 10,
child: Container(
padding: const EdgeInsets.all(8),
color: Colors.black54,
child: ValueListenableBuilder<MobileScannerState>(
valueListenable: controller,
builder: (context, state, child) {
return Text(
'状态: ${state.isRunning ? "运行中" : "停止"}\n'
'错误: ${state.error != null ? state.error?.errorCode : "无"}\n'
'处理中: $_isProcessing',
style: const TextStyle(color: Colors.white, fontSize: 12),
);
},
),
),
);
}
// 扫描框覆盖层
Widget _buildScanOverlay() {
// 使用动画构建器包裹扫描框
return AnimatedBuilder(
animation: _scanLineAnimation,
builder: (context, child) {
return CustomPaint(
painter: _ScannerOverlay(animationValue: _scanLineAnimation.value),
);
},
);
}
// 提示文字
Widget _buildHintText() {
return Positioned(
top: (MediaQuery.of(context).size.height / 3) * 2 + 20,
left: 0,
right: 0,
child: const Column(
children: [
Text(
'将二维码放入框内,即可自动扫描',
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w400,
),
textAlign: TextAlign.center,
),
SizedBox(height: 8),
Text(
'支持驾了么二维码和网页链接',
style: TextStyle(
color: Colors.white70,
fontSize: 12,
),
textAlign: TextAlign.center,
),
],
),
);
}
// 控制按钮
Widget _buildControlButtons() {
return Positioned(
bottom: 100,
right: 20,
child: Column(
children: [
// 相册按钮
IconButton(
icon: const Icon(Icons.photo_library, color: Colors.white, size: 32),
onPressed: _pickImageAndScan,
),
const SizedBox(height: 16),
// 手电筒按钮
ValueListenableBuilder(
valueListenable: controller,
builder: (context, state, child) {
return IconButton(
icon: Icon(
state.torchState == TorchState.on
? Icons.flash_on
: Icons.flash_off,
color: state.torchState == TorchState.on
? Colors.yellow
: Colors.white,
size: 32,
),
onPressed: () => controller.toggleTorch(),
);
},
),
const SizedBox(height: 16),
// 切换摄像头按钮
ValueListenableBuilder(
valueListenable: controller,
builder: (context, state, child) {
return IconButton(
icon: const Icon(Icons.cameraswitch, color: Colors.white, size: 32),
onPressed: () => controller.switchCamera(),
);
},
),
],
),
);
}
// 加载视图
Widget _buildLoadingView(String message) {
return Container(
color: Colors.black,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(color: Colors.white),
const SizedBox(height: 16),
Text(
message,
style: const TextStyle(color: Colors.white),
),
],
),
),
);
}
// 权限被拒绝视图
Widget _buildPermissionDeniedView() {
return Container(
color: Colors.black,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.camera_alt, size: 64, color: Colors.white54),
const SizedBox(height: 20),
const Text(
'需要相机权限',
style: TextStyle(color: Colors.white, fontSize: 18),
),
const SizedBox(height: 10),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 40),
child: Text(
'请允许应用访问相机以使用扫码功能',
style: TextStyle(color: Colors.white70, fontSize: 14),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: _requestCameraPermission,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
child: const Text('授权相机权限'),
),
const SizedBox(height: 16),
TextButton(
onPressed: () => openAppSettings(),
child: const Text(
'去设置中开启',
style: TextStyle(color: Colors.white70),
),
),
],
),
),
);
}
// 错误视图
Widget _buildErrorView(MobileScannerException error) {
String errorMessage;
switch (error.errorCode) {
case MobileScannerErrorCode.permissionDenied:
errorMessage = '相机权限被拒绝';
break;
case MobileScannerErrorCode.genericError:
errorMessage = '此设备不支持扫码功能';
break;
default:
errorMessage = '发生未知错误';
}
return Container(
color: Colors.black,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, color: Colors.white, size: 64),
const SizedBox(height: 16),
Text(
errorMessage,
style: const TextStyle(color: Colors.white, fontSize: 16),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
if (error.errorCode == MobileScannerErrorCode.permissionDenied) {
_requestCameraPermission();
} else {
controller.start();
}
},
child: const Text('重试'),
),
],
),
),
);
}
void _showPermissionDialog() {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: const Text("相机权限未开启"),
content: const Text("您未开启驾了么的相机服务,扫码功能需要相机权限才能正常使用。"),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text("暂不"),
),
TextButton(
onPressed: () {
openAppSettings();
Navigator.of(context).pop();
},
child: const Text("去设置"),
),
],
);
},
);
}
}
// 扫描框UI
class _ScannerOverlay extends CustomPainter {
final double animationValue; // 0.0 到 1.0 的动画值
_ScannerOverlay({this.animationValue = 0.0});
@override
void paint(Canvas canvas, Size size) {
final background = Paint()..color = Colors.black.withValues(alpha:0.6);
final border = Paint()
..color = Colors.green
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
// 计算扫描框位置和大小
final width = size.width * 0.7;
final height = width;
final left = (size.width - width) / 2;
final top = (size.height - height) / 3;
final scanWindow = Rect.fromLTWH(left, top, width, height);
// // 保存图层,确保 BlendMode.clear 只作用于当前半透明背景层
// canvas.saveLayer(Rect.fromLTWH(0, 0, size.width, size.height), Paint());
// // 绘制半透明背景(覆盖整个屏幕)
// canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), background);
// // 清除扫描窗口区域
// canvas.drawRect(scanWindow, Paint()..blendMode = BlendMode.clear);
// // 恢复图层
// canvas.restore();
// 绘制上方的半透明区域
canvas.drawRect(Rect.fromLTRB(0, 0, size.width, top), background);
// 绘制下方的半透明区域
canvas.drawRect(Rect.fromLTRB(0, top + height, size.width, size.height), background);
// 绘制左侧的半透明区域
canvas.drawRect(Rect.fromLTRB(0, top, left, top + height), background);
// 绘制右侧的半透明区域
canvas.drawRect(Rect.fromLTRB(left + width, top, size.width, top + height), background);
// 绘制扫描窗口边框
canvas.drawRect(scanWindow, border);
// 绘制四个角
final cornerLength = 20.0;
final corner = Paint()
..color = Colors.green
..style = PaintingStyle.stroke
..strokeWidth = 4.0
..strokeCap = StrokeCap.round;
// 左上角
canvas.drawLine(scanWindow.topLeft, scanWindow.topLeft + Offset(cornerLength, 0), corner);
canvas.drawLine(scanWindow.topLeft, scanWindow.topLeft + Offset(0, cornerLength), corner);
// 右上角
canvas.drawLine(scanWindow.topRight, scanWindow.topRight - Offset(cornerLength, 0), corner);
canvas.drawLine(scanWindow.topRight, scanWindow.topRight + Offset(0, cornerLength), corner);
// 左下角
canvas.drawLine(scanWindow.bottomLeft, scanWindow.bottomLeft + Offset(cornerLength, 0), corner);
canvas.drawLine(scanWindow.bottomLeft, scanWindow.bottomLeft - Offset(0, cornerLength), corner);
// 右下角
canvas.drawLine(scanWindow.bottomRight, scanWindow.bottomRight - Offset(cornerLength, 0), corner);
canvas.drawLine(scanWindow.bottomRight, scanWindow.bottomRight - Offset(0, cornerLength), corner);
// 绘制微信风格的扫描线
_drawWeChatScanLine(canvas, scanWindow);
// // 新增:绘制动态扫描线
// final scanLineY = top + (height * animationValue); // 计算扫描线Y坐标
// final scanLine = Rect.fromLTWH(
// left + 10, // 左右缩进10,避免贴边框
// scanLineY - scanLineHeight / 2, // 居中对齐
// width - 20, // 宽度减去缩进
// scanLineHeight,
// );
// final scanLinePaint = Paint()
// ..color = scanLineColor.withValues(alpha:0.8)
// ..style = PaintingStyle.fill;
// // 扫描线主体
// canvas.drawRect(scanLine, scanLinePaint);
//
// // 新增:扫描线渐变光晕(增强视觉效果)
// final gradient = LinearGradient(
// colors: [
// scanLineColor.withValues(alpha:0),
// scanLineColor.withValues(alpha:0.6),
// scanLineColor.withValues(alpha:0),
// ],
// stops: const [0, 0.5, 1],
// );
// final glowPaint = Paint()
// ..shader = gradient.createShader(scanLine)
// ..style = PaintingStyle.fill;
// canvas.drawRect(
// Rect.fromLTWH(
// scanLine.left,
// scanLine.top - 10, // 光晕高度比线条高
// scanLine.width,
// scanLine.height + 20,
// ),
// glowPaint,
// );
}
// 增强版微信风格扫描线
void _drawWeChatScanLine(Canvas canvas, Rect scanWindow) {
final lineY = scanWindow.top + scanWindow.height * animationValue;
// 微信风格的扫描线效果
_drawScanLineGlow(canvas, scanWindow, lineY);
_drawMainScanLine(canvas, scanWindow, lineY);
//_drawScanLineHead(canvas, scanWindow, lineY);
}
// 微信风格的扫描线效果
void _drawScanLineGlow(Canvas canvas, Rect scanWindow, double lineY) {
// 三层光晕效果
final gradients = [
LinearGradient(
colors: [
Colors.green.withValues(alpha: 0),
Colors.green.withValues(alpha: 0.1),
Colors.green.withValues(alpha: 0),
],
),
LinearGradient(
colors: [
Colors.green.withValues(alpha: 0.1),
Colors.green.withValues(alpha: 0.3),
Colors.green.withValues(alpha: 0.1),
],
),
LinearGradient(
colors: [
Colors.green.withValues(alpha: 0.2),
Colors.greenAccent.withValues(alpha: 0.5),
Colors.green.withValues(alpha: 0.2),
],
),
];
final glowHeights = [16.0, 12.0, 8.0];
for (int i = 0; i < gradients.length; i++) {
final glowRect = Rect.fromLTRB(
scanWindow.left,
lineY - glowHeights[i] / 2,
scanWindow.right,
lineY + glowHeights[i] / 2,
);
final glowPaint = Paint()
..shader = gradients[i].createShader(glowRect)
..style = PaintingStyle.fill;
canvas.drawRect(glowRect, glowPaint);
}
}
// 主扫描线
void _drawMainScanLine(Canvas canvas, Rect scanWindow, double lineY) {
// 主扫描线 - 微信风格的细亮线
final linePaint = Paint()
..color = Colors.greenAccent
..strokeWidth = 1
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
canvas.drawLine(
Offset(scanWindow.left + 10, lineY), // 稍微缩进,避免触及边框
Offset(scanWindow.right - 10, lineY),
linePaint,
);
}
// 扫描线头部
void _drawScanLineHead(Canvas canvas, Rect scanWindow, double lineY) {
// 微信风格的扫描线头部
final headPaint = Paint()
..color = Colors.greenAccent
..style = PaintingStyle.fill;
// 绘制三角形箭头
final path = Path();
final headSize = 8.0;
path.moveTo(scanWindow.left + scanWindow.width / 2, lineY - headSize);
path.lineTo(scanWindow.left + scanWindow.width / 2 - headSize / 2, lineY);
path.lineTo(scanWindow.left + scanWindow.width / 2 + headSize / 2, lineY);
path.close();
canvas.drawPath(path, headPaint);
// 绘制光点效果
final dotPaint = Paint()
..color = Colors.greenAccent
..style = PaintingStyle.fill;
// 在扫描线头部周围绘制光点
final positions = [
Offset(scanWindow.left + scanWindow.width / 4, lineY),
Offset(scanWindow.left + scanWindow.width * 3/4, lineY),
];
for (final position in positions) {
canvas.drawCircle(position, 1.5, dotPaint);
}
}
@override
bool shouldRepaint(covariant _ScannerOverlay oldDelegate) {
// 扫描线位置变化时重绘
return oldDelegate.animationValue != animationValue;
}
}
这个页面我不用 mobile_scanner 插件 我需要更更换为 flutter_scankit: ^2.0.6
最新发布