import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:lottis_birthday_escaperoom_app/scanned_barcode_label.dart'; import 'package:lottis_birthday_escaperoom_app/scanner_error_widget.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; class BarcodeScannerWithScanWindow extends StatefulWidget { const BarcodeScannerWithScanWindow({super.key}); @override State createState() => _BarcodeScannerWithScanWindowState(); } class _BarcodeScannerWithScanWindowState extends State { final MobileScannerController controller = MobileScannerController(); Widget _buildBarcodeOverlay() { return ValueListenableBuilder( valueListenable: controller, builder: (context, value, child) { // Not ready. if (!value.isInitialized || !value.isRunning || value.error != null) { return const SizedBox(); } return StreamBuilder( stream: controller.barcodes, builder: (context, snapshot) { final BarcodeCapture? barcodeCapture = snapshot.data; // No barcode. if (barcodeCapture == null || barcodeCapture.barcodes.isEmpty) { return const SizedBox(); } final scannedBarcode = barcodeCapture.barcodes.first; // No barcode corners, or size, or no camera preview size. if (scannedBarcode.corners.isEmpty || value.size.isEmpty || barcodeCapture.size.isEmpty) { return const SizedBox(); } return CustomPaint( painter: BarcodeOverlay( barcodeCorners: scannedBarcode.corners, barcodeSize: barcodeCapture.size, boxFit: BoxFit.contain, cameraPreviewSize: value.size, ), ); }, ); }, ); } Widget _buildScanWindow(Rect scanWindowRect) { return ValueListenableBuilder( valueListenable: controller, builder: (context, value, child) { // Not ready. if (!value.isInitialized || !value.isRunning || value.error != null || value.size.isEmpty) { return const SizedBox(); } return CustomPaint( painter: ScannerOverlay(scanWindowRect), ); }, ); } @override Widget build(BuildContext context) { final scanWindow = Rect.fromCenter( center: MediaQuery.sizeOf(context).center(Offset.zero), width: 200, height: 200, ); return Scaffold( appBar: AppBar(title: const Text('With Scan window')), backgroundColor: Colors.black, body: Stack( fit: StackFit.expand, children: [ MobileScanner( fit: BoxFit.contain, scanWindow: scanWindow, controller: controller, errorBuilder: (context, error, child) { return ScannerErrorWidget(error: error); }, ), _buildBarcodeOverlay(), _buildScanWindow(scanWindow), Align( alignment: Alignment.bottomCenter, child: Container( alignment: Alignment.center, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), height: 100, color: Colors.black.withOpacity(0.4), child: ScannedBarcodeLabel(barcodes: controller.barcodes), ), ), ], ), ); } @override Future dispose() async { super.dispose(); await controller.dispose(); } } class ScannerOverlay extends CustomPainter { ScannerOverlay(this.scanWindow); final Rect scanWindow; @override void paint(Canvas canvas, Size size) { // TODO: use `Offset.zero & size` instead of Rect.largest // we need to pass the size to the custom paint widget final backgroundPath = Path()..addRect(Rect.largest); final cutoutPath = Path()..addRect(scanWindow); final backgroundPaint = Paint() ..color = Colors.black.withOpacity(0.5) ..style = PaintingStyle.fill ..blendMode = BlendMode.dstOut; final backgroundWithCutout = Path.combine( PathOperation.difference, backgroundPath, cutoutPath, ); canvas.drawPath(backgroundWithCutout, backgroundPaint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) { return false; } } class BarcodeOverlay extends CustomPainter { BarcodeOverlay({ required this.barcodeCorners, required this.barcodeSize, required this.boxFit, required this.cameraPreviewSize, }); final List barcodeCorners; final Size barcodeSize; final BoxFit boxFit; final Size cameraPreviewSize; @override void paint(Canvas canvas, Size size) { if (barcodeCorners.isEmpty || barcodeSize.isEmpty || cameraPreviewSize.isEmpty) { return; } final adjustedSize = applyBoxFit(boxFit, cameraPreviewSize, size); double verticalPadding = size.height - adjustedSize.destination.height; double horizontalPadding = size.width - adjustedSize.destination.width; if (verticalPadding > 0) { verticalPadding = verticalPadding / 2; } else { verticalPadding = 0; } if (horizontalPadding > 0) { horizontalPadding = horizontalPadding / 2; } else { horizontalPadding = 0; } final double ratioWidth; final double ratioHeight; if (!kIsWeb && defaultTargetPlatform == TargetPlatform.iOS) { ratioWidth = barcodeSize.width / adjustedSize.destination.width; ratioHeight = barcodeSize.height / adjustedSize.destination.height; } else { ratioWidth = cameraPreviewSize.width / adjustedSize.destination.width; ratioHeight = cameraPreviewSize.height / adjustedSize.destination.height; } final List adjustedOffset = [ for (final offset in barcodeCorners) Offset( offset.dx / ratioWidth + horizontalPadding, offset.dy / ratioHeight + verticalPadding, ), ]; final cutoutPath = Path()..addPolygon(adjustedOffset, true); final backgroundPaint = Paint() ..color = Colors.red.withOpacity(0.3) ..style = PaintingStyle.fill ..blendMode = BlendMode.dstOut; canvas.drawPath(cutoutPath, backgroundPaint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) { return false; } }