import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; class ScanQRScreen extends StatefulWidget { @override State createState() => _ScanQRScreenState(); } class _ScanQRScreenState extends State { final MobileScannerController cameraController = MobileScannerController( autoStart: true, formats: [BarcodeFormat.qrCode], ); @override Widget build(BuildContext context) { final scanWindow = Rect.fromCenter( center: MediaQuery.sizeOf(context).center(Offset.zero), width: 250, height: 250, ); return Scaffold( appBar: AppBar(title: const Text('Scan QR')), backgroundColor: Colors.black, body: Stack(fit: StackFit.expand, children: [ Center( child: MobileScanner( fit: BoxFit.contain, controller: cameraController, scanWindow: scanWindow, onDetect: (BarcodeCapture barcodes) { var barcode = barcodes.barcodes.firstOrNull; if (barcode != null && mounted) { cameraController.stop().then((_) { Navigator.pop(context, barcode.rawValue); }); } }), ), ValueListenableBuilder( valueListenable: cameraController, builder: (context, value, child) { if (!value.isInitialized || !value.isRunning || value.error != null) { return const SizedBox(); } return CustomPaint( painter: ScannerOverlay(scanWindow: scanWindow), ); }, ), Align( alignment: Alignment.bottomCenter, child: Padding( padding: const EdgeInsets.all(16.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ToggleFlashlightButton(controller: cameraController), SwitchCameraButton(controller: cameraController), ], ), ), ), ])); } } class ScannerOverlay extends CustomPainter { const ScannerOverlay({ required this.scanWindow, this.borderRadius = 12.0, }); final Rect scanWindow; final double borderRadius; @override void paint(Canvas canvas, Size size) { // we need to pass the size to the custom paint widget final backgroundPath = Path()..addRect(Rect.largest); final cutoutPath = Path() ..addRRect( RRect.fromRectAndCorners( scanWindow, topLeft: Radius.circular(borderRadius), topRight: Radius.circular(borderRadius), bottomLeft: Radius.circular(borderRadius), bottomRight: Radius.circular(borderRadius), ), ); final backgroundPaint = Paint() ..color = Colors.black.withOpacity(0.5) ..style = PaintingStyle.fill ..blendMode = BlendMode.dstOut; final backgroundWithCutout = Path.combine( PathOperation.difference, backgroundPath, cutoutPath, ); final borderPaint = Paint() ..color = Colors.white ..style = PaintingStyle.stroke ..strokeWidth = 4.0; final borderRect = RRect.fromRectAndCorners( scanWindow, topLeft: Radius.circular(borderRadius), topRight: Radius.circular(borderRadius), bottomLeft: Radius.circular(borderRadius), bottomRight: Radius.circular(borderRadius), ); // First, draw the background, // with a cutout area that is a bit larger than the scan window. // Finally, draw the scan window itself. canvas.drawPath(backgroundWithCutout, backgroundPaint); canvas.drawRRect(borderRect, borderPaint); } @override bool shouldRepaint(ScannerOverlay oldDelegate) { return scanWindow != oldDelegate.scanWindow || borderRadius != oldDelegate.borderRadius; } } class SwitchCameraButton extends StatelessWidget { const SwitchCameraButton({required this.controller, super.key}); final MobileScannerController controller; @override Widget build(BuildContext context) { return ValueListenableBuilder( valueListenable: controller, builder: (context, state, child) { if (!state.isInitialized || !state.isRunning) { return const SizedBox.shrink(); } final int? availableCameras = state.availableCameras; if (availableCameras != null && availableCameras < 2) { return const SizedBox.shrink(); } final Widget icon; switch (state.cameraDirection) { case CameraFacing.front: icon = const Icon(Icons.camera_front); case CameraFacing.back: icon = const Icon(Icons.camera_rear); } return IconButton( iconSize: 32.0, color: Colors.white, icon: icon, onPressed: () async { await controller.switchCamera(); }, ); }, ); } } class ToggleFlashlightButton extends StatelessWidget { const ToggleFlashlightButton({required this.controller, super.key}); final MobileScannerController controller; @override Widget build(BuildContext context) { return ValueListenableBuilder( valueListenable: controller, builder: (context, controllerState, child) { if (!controllerState.isInitialized || !controllerState.isRunning) { return const SizedBox.shrink(); } switch (controllerState.torchState) { case TorchState.auto: return IconButton( color: Colors.white, iconSize: 32.0, icon: const Icon(Icons.flash_auto), onPressed: () async { await controller.toggleTorch(); }, ); case TorchState.off: return IconButton( color: Colors.white, iconSize: 32.0, icon: const Icon(Icons.flash_off), onPressed: () async { await controller.toggleTorch(); }, ); case TorchState.on: return IconButton( color: Colors.white, iconSize: 32.0, icon: const Icon(Icons.flash_on), onPressed: () async { await controller.toggleTorch(); }, ); case TorchState.unavailable: return const SizedBox.square( dimension: 48.0, child: Icon( Icons.no_flash, size: 32.0, color: Colors.grey, ), ); } }, ); } }