Flutter plugin for scanning and generating QR codes using the ZXing library, supporting Android, iOS, and desktop platforms
				
			
			
				
		flutterbarcode-generatorbarcode-scannergeneratorqrqrcodeqrcode-generatorqrcode-scannerscannerzxingbarcodezxscanner
		
		
		
		
			You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							273 lines
						
					
					
						
							7.9 KiB
						
					
					
				
			
		
		
	
	
							273 lines
						
					
					
						
							7.9 KiB
						
					
					
				import 'dart:async'; | 
						|
import 'dart:io'; | 
						|
import 'dart:math'; | 
						|
 | 
						|
import 'package:camera/camera.dart'; | 
						|
import 'package:flutter/material.dart'; | 
						|
 | 
						|
// import 'package:flutter_beep/flutter_beep.dart'; | 
						|
 | 
						|
import '../../generated_bindings.dart'; | 
						|
import '../logic/zxing.dart'; | 
						|
import '../utils/extentions.dart'; | 
						|
import 'scanner_overlay.dart'; | 
						|
 | 
						|
class ReaderWidget extends StatefulWidget { | 
						|
  const ReaderWidget({ | 
						|
    super.key, | 
						|
    required this.onScan, | 
						|
    this.onControllerCreated, | 
						|
    this.codeFormat = Format.Any, | 
						|
    this.showCroppingRect = true, | 
						|
    this.scannerOverlay, | 
						|
    this.showFlashlight = true, | 
						|
    this.allowPinchZoom = true, | 
						|
    this.scanDelay = const Duration(milliseconds: 1000), // 1000ms delay | 
						|
    this.scanDelaySuccess = const Duration(milliseconds: 1000), // 1000ms delay | 
						|
    this.cropPercent = 0.5, // 50% of the screen | 
						|
    this.resolution = ResolutionPreset.high, | 
						|
    this.loading = const DecoratedBox(decoration: BoxDecoration(color: Colors.black)), | 
						|
  }); | 
						|
 | 
						|
  final Function(CodeResult) onScan; | 
						|
  final Function(CameraController?)? onControllerCreated; | 
						|
  final int codeFormat; | 
						|
  final bool showCroppingRect; | 
						|
  final ScannerOverlay? scannerOverlay; | 
						|
  final bool showFlashlight; | 
						|
  final bool allowPinchZoom; | 
						|
  final Duration scanDelay; | 
						|
  final double cropPercent; | 
						|
  final ResolutionPreset resolution; | 
						|
  final Duration scanDelaySuccess; | 
						|
  final Widget loading; | 
						|
 | 
						|
  @override | 
						|
  State<ReaderWidget> createState() => _ReaderWidgetState(); | 
						|
} | 
						|
 | 
						|
class _ReaderWidgetState extends State<ReaderWidget> with TickerProviderStateMixin, WidgetsBindingObserver { | 
						|
  List<CameraDescription> cameras = <CameraDescription>[]; | 
						|
  CameraController? controller; | 
						|
  bool _cameraOn = false; | 
						|
 | 
						|
  double _zoom = 1.0; | 
						|
  double _scaleFactor = 1.0; | 
						|
  double _maxZoomLevel = 1.0; | 
						|
  double _minZoomLevel = 1.0; | 
						|
 | 
						|
  bool isAndroid() => Theme.of(context).platform == TargetPlatform.android; | 
						|
 | 
						|
  // true when code detecting is ongoing | 
						|
  bool _isProcessing = false; | 
						|
 | 
						|
  @override | 
						|
  void initState() { | 
						|
    super.initState(); | 
						|
    WidgetsBinding.instance.addObserver(this); | 
						|
    initStateAsync(); | 
						|
  } | 
						|
 | 
						|
  Future<void> initStateAsync() async { | 
						|
    // Spawn a new isolate | 
						|
    await startCameraProcessing(); | 
						|
 | 
						|
    availableCameras().then((List<CameraDescription> cameras) { | 
						|
      setState(() { | 
						|
        this.cameras = cameras; | 
						|
        if (cameras.isNotEmpty) { | 
						|
          onNewCameraSelected(cameras.first); | 
						|
        } | 
						|
      }); | 
						|
    }); | 
						|
  } | 
						|
 | 
						|
  @override | 
						|
  void didChangeAppLifecycleState(AppLifecycleState state) { | 
						|
    switch (state) { | 
						|
      case AppLifecycleState.resumed: | 
						|
        if (cameras.isNotEmpty && !_cameraOn) { | 
						|
          onNewCameraSelected(cameras.first); | 
						|
        } | 
						|
 | 
						|
        break; | 
						|
      case AppLifecycleState.inactive: | 
						|
        break; | 
						|
      case AppLifecycleState.paused: | 
						|
        controller?.stopImageStream().then((_) => controller?.dispose()); | 
						|
        setState(() { | 
						|
          _cameraOn = false; | 
						|
        }); | 
						|
        break; | 
						|
      case AppLifecycleState.detached: | 
						|
        break; | 
						|
    } | 
						|
  } | 
						|
 | 
						|
  @override | 
						|
  void dispose() { | 
						|
    stopCameraProcessing(); | 
						|
    controller?.removeListener(rebuildOnMount); | 
						|
    controller?.dispose(); | 
						|
    WidgetsBinding.instance.removeObserver(this); | 
						|
    super.dispose(); | 
						|
  } | 
						|
 | 
						|
  void rebuildOnMount() { | 
						|
    if (mounted) { | 
						|
      setState(() { | 
						|
        _cameraOn = true; | 
						|
      }); | 
						|
    } | 
						|
  } | 
						|
 | 
						|
  Future<void> onNewCameraSelected(CameraDescription cameraDescription) async { | 
						|
    if (controller != null) { | 
						|
      controller?.removeListener(rebuildOnMount); | 
						|
      await controller!.dispose(); | 
						|
    } | 
						|
 | 
						|
    controller = CameraController( | 
						|
      cameraDescription, | 
						|
      widget.resolution, | 
						|
      enableAudio: false, | 
						|
      imageFormatGroup: isAndroid() ? ImageFormatGroup.yuv420 : ImageFormatGroup.bgra8888, | 
						|
    ); | 
						|
    if (controller == null) { | 
						|
      return; | 
						|
    } | 
						|
    try { | 
						|
      await controller!.initialize(); | 
						|
      await controller!.setFlashMode(FlashMode.off); | 
						|
      _maxZoomLevel = await controller!.getMaxZoomLevel(); | 
						|
      _minZoomLevel = await controller!.getMinZoomLevel(); | 
						|
      controller!.startImageStream(processImageStream); | 
						|
    } on CameraException catch (e) { | 
						|
      debugPrint('${e.code}: ${e.description}'); | 
						|
    } | 
						|
 | 
						|
    controller!.addListener(rebuildOnMount); | 
						|
    rebuildOnMount(); | 
						|
    widget.onControllerCreated?.call(controller); | 
						|
  } | 
						|
 | 
						|
  Future<void> processImageStream(CameraImage image) async { | 
						|
    if (!_isProcessing) { | 
						|
      _isProcessing = true; | 
						|
      try { | 
						|
        final CodeResult result = await processCameraImage( | 
						|
          image, | 
						|
          format: widget.codeFormat, | 
						|
          cropPercent: widget.showCroppingRect ? widget.cropPercent : 0, | 
						|
        ); | 
						|
        if (result.isValidBool) { | 
						|
          widget.onScan(result); | 
						|
          setState(() {}); | 
						|
          await Future<void>.delayed(widget.scanDelaySuccess); | 
						|
        } | 
						|
      } on FileSystemException catch (e) { | 
						|
        debugPrint(e.message); | 
						|
      } catch (e) { | 
						|
        debugPrint(e.toString()); | 
						|
      } | 
						|
      await Future<void>.delayed(widget.scanDelay); | 
						|
      _isProcessing = false; | 
						|
    } | 
						|
 | 
						|
    return; | 
						|
  } | 
						|
 | 
						|
  @override | 
						|
  Widget build(BuildContext context) { | 
						|
    final bool isCameraReady = cameras.isNotEmpty && _cameraOn && controller != null && controller!.value.isInitialized; | 
						|
    final Size size = MediaQuery.of(context).size; | 
						|
    final double cameraMaxSize = max(size.width, size.height); | 
						|
    final double cropSize = min(size.width, size.height) * widget.cropPercent; | 
						|
    return Stack( | 
						|
      children: <Widget>[ | 
						|
        if (!isCameraReady) widget.loading, | 
						|
        if (isCameraReady) | 
						|
          SizedBox( | 
						|
            width: cameraMaxSize, | 
						|
            height: cameraMaxSize, | 
						|
            child: ClipRRect( | 
						|
              child: OverflowBox( | 
						|
                child: FittedBox( | 
						|
                  fit: BoxFit.cover, | 
						|
                  child: SizedBox( | 
						|
                    width: cameraMaxSize, | 
						|
                    child: CameraPreview( | 
						|
                      controller!, | 
						|
                    ), | 
						|
                  ), | 
						|
                ), | 
						|
              ), | 
						|
            ), | 
						|
          ), | 
						|
        if (widget.showCroppingRect) | 
						|
          Container( | 
						|
            decoration: ShapeDecoration( | 
						|
              shape: widget.scannerOverlay ?? | 
						|
                  ScannerOverlay( | 
						|
                    borderColor: Theme.of(context).primaryColor, | 
						|
                    overlayColor: Colors.black45, | 
						|
                    borderRadius: 1, | 
						|
                    borderLength: 16, | 
						|
                    borderWidth: 8, | 
						|
                    cutOutSize: cropSize, | 
						|
                  ), | 
						|
            ), | 
						|
          ), | 
						|
        if (widget.allowPinchZoom) | 
						|
          GestureDetector( | 
						|
            onScaleStart: (ScaleStartDetails details) { | 
						|
              _zoom = _scaleFactor; | 
						|
            }, | 
						|
            onScaleUpdate: (ScaleUpdateDetails details) { | 
						|
              _scaleFactor = (_zoom * details.scale).clamp(_minZoomLevel, _maxZoomLevel); | 
						|
              controller?.setZoomLevel(_scaleFactor); | 
						|
            }, | 
						|
          ), | 
						|
        if (widget.showFlashlight) | 
						|
          Positioned( | 
						|
            bottom: 20, | 
						|
            left: 20, | 
						|
            child: FloatingActionButton( | 
						|
                onPressed: () { | 
						|
                  if (controller != null) { | 
						|
                    FlashMode mode = controller!.value.flashMode; | 
						|
                    if (mode == FlashMode.torch) { | 
						|
                      mode = FlashMode.off; | 
						|
                    } else { | 
						|
                      mode = FlashMode.torch; | 
						|
                    } | 
						|
                    controller!.setFlashMode(mode); | 
						|
                    setState(() {}); | 
						|
                  } | 
						|
                }, | 
						|
                backgroundColor: Colors.black26, | 
						|
                child: _FlashIcon(flashMode: controller!.value.flashMode)), | 
						|
          ), | 
						|
      ], | 
						|
    ); | 
						|
  } | 
						|
} | 
						|
 | 
						|
class _FlashIcon extends StatelessWidget { | 
						|
  const _FlashIcon({required this.flashMode}); | 
						|
  final FlashMode flashMode; | 
						|
 | 
						|
  @override | 
						|
  Widget build(BuildContext context) { | 
						|
    switch (flashMode) { | 
						|
      case FlashMode.torch: | 
						|
        return const Icon(Icons.flash_on); | 
						|
      case FlashMode.off: | 
						|
        return const Icon(Icons.flash_off); | 
						|
      case FlashMode.always: | 
						|
        return const Icon(Icons.flash_on); | 
						|
      case FlashMode.auto: | 
						|
        return const Icon(Icons.flash_auto); | 
						|
    } | 
						|
  } | 
						|
}
 | 
						|
 |