import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'lifecycle_event_handler.dart'; import 'qr_scanner_overlay_shape.dart'; import 'types/barcode.dart'; import 'types/barcode_format.dart'; import 'types/camera.dart'; import 'types/camera_exception.dart'; import 'types/features.dart'; import 'web/flutter_qr_stub.dart' // ignore: uri_does_not_exist if (dart.library.html) 'web/flutter_qr_web.dart'; typedef QRViewCreatedCallback = void Function(QRViewController); typedef PermissionSetCallback = void Function(QRViewController, bool); /// The [QRView] is the view where the camera /// and the barcode scanner gets displayed. class QRView extends StatefulWidget { const QRView({ required Key key, required this.onQRViewCreated, this.overlay, this.overlayMargin = EdgeInsets.zero, this.cameraFacing = CameraFacing.back, this.onPermissionSet, this.formatsAllowed = const [], }) : super(key: key); /// [onQRViewCreated] gets called when the view is created final QRViewCreatedCallback onQRViewCreated; /// Use [overlay] to provide an overlay for the view. /// This can be used to create a certain scan area. final QrScannerOverlayShape? overlay; /// Use [overlayMargin] to provide a margin to [overlay] final EdgeInsetsGeometry overlayMargin; /// Set which camera to use on startup. /// /// [cameraFacing] can either be CameraFacing.front or CameraFacing.back. /// Defaults to CameraFacing.back final CameraFacing cameraFacing; /// Calls the provided [onPermissionSet] callback when the permission is set. final PermissionSetCallback? onPermissionSet; /// Use [formatsAllowed] to specify which formats needs to be scanned. final List formatsAllowed; @override State createState() => _QRViewState(); } class _QRViewState extends State { late MethodChannel _channel; late LifecycleEventHandler _observer; @override void initState() { super.initState(); _observer = LifecycleEventHandler(resumeCallBack: updateDimensions); WidgetsBinding.instance!.addObserver(_observer); } @override Widget build(BuildContext context) { return NotificationListener( onNotification: onNotification, child: SizeChangedLayoutNotifier( child: (widget.overlay != null) ? _getPlatformQrViewWithOverlay() : _getPlatformQrView(), ), ); } @override void dispose() { super.dispose(); WidgetsBinding.instance!.removeObserver(_observer); } Future updateDimensions() async { await QRViewController.updateDimensions( widget.key as GlobalKey>, _channel, overlay: widget.overlay); } bool onNotification(notification) { updateDimensions(); return false; } Widget _getPlatformQrViewWithOverlay() { return Stack( children: [ _getPlatformQrView(), Container( padding: widget.overlayMargin, decoration: ShapeDecoration( shape: widget.overlay!, ), ) ], ); } Widget _getPlatformQrView() { Widget _platformQrView; if (kIsWeb) { _platformQrView = createWebQrView( onPlatformViewCreated: widget.onQRViewCreated, cameraFacing: widget.cameraFacing, ); } else { switch (defaultTargetPlatform) { case TargetPlatform.android: _platformQrView = AndroidView( viewType: 'net.touchcapture.qr.flutterqr/qrview', onPlatformViewCreated: _onPlatformViewCreated, creationParams: _QrCameraSettings(cameraFacing: widget.cameraFacing).toMap(), creationParamsCodec: StandardMessageCodec(), ); break; case TargetPlatform.iOS: _platformQrView = UiKitView( viewType: 'net.touchcapture.qr.flutterqr/qrview', onPlatformViewCreated: _onPlatformViewCreated, creationParams: _QrCameraSettings(cameraFacing: widget.cameraFacing).toMap(), creationParamsCodec: StandardMessageCodec(), ); break; default: throw UnsupportedError( "Trying to use the default qrview implementation for $defaultTargetPlatform but there isn't a default one"); } } return _platformQrView; } void _onPlatformViewCreated(int id) { _channel = MethodChannel('net.touchcapture.qr.flutterqr/qrview_$id'); // Start scan after creation of the view final controller = QRViewController._( _channel, widget.key as GlobalKey>?, widget.onPermissionSet, widget.cameraFacing) .._startScan(widget.key as GlobalKey>, widget.overlay, widget.formatsAllowed); // Initialize the controller for controlling the QRView widget.onQRViewCreated(controller); } } class _QrCameraSettings { _QrCameraSettings({ this.cameraFacing = CameraFacing.unknown, }); final CameraFacing cameraFacing; Map toMap() { return { 'cameraFacing': cameraFacing.index, }; } } class QRViewController { QRViewController._(MethodChannel channel, GlobalKey? qrKey, PermissionSetCallback? onPermissionSet, CameraFacing cameraFacing) : _channel = channel, _cameraFacing = cameraFacing { _channel.setMethodCallHandler((call) async { switch (call.method) { case 'onRecognizeQR': if (call.arguments != null) { final args = call.arguments as Map; final code = args['code'] as String?; final rawType = args['type'] as String; // Raw bytes are only supported by Android. final rawBytes = args['rawBytes'] as List?; final format = BarcodeTypesExtension.fromString(rawType); if (format != BarcodeFormat.unknown) { final barcode = Barcode(code, format, rawBytes); _scanUpdateController.sink.add(barcode); } else { throw Exception('Unexpected barcode type $rawType'); } } break; case 'onPermissionSet': if (call.arguments != null && call.arguments is bool) { _hasPermissions = call.arguments; if (onPermissionSet != null) { onPermissionSet(this, _hasPermissions); } } break; } }); } final MethodChannel _channel; final CameraFacing _cameraFacing; final StreamController _scanUpdateController = StreamController(); Stream get scannedDataStream => _scanUpdateController.stream; bool _hasPermissions = false; bool get hasPermissions => _hasPermissions; /// Starts the barcode scanner Future _startScan(GlobalKey key, QrScannerOverlayShape? overlay, List? barcodeFormats) async { // We need to update the dimension before the scan is started. try { await QRViewController.updateDimensions(key, _channel, overlay: overlay); return await _channel.invokeMethod( 'startScan', barcodeFormats?.map((e) => e.asInt()).toList() ?? []); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } } /// Gets information about which camera is active. Future getCameraInfo() async { try { var cameraFacing = await _channel.invokeMethod('getCameraInfo') as int; if (cameraFacing == -1) return _cameraFacing; return CameraFacing .values[await _channel.invokeMethod('getCameraInfo') as int]; } on PlatformException catch (e) { throw CameraException(e.code, e.message); } } /// Flips the camera between available modes Future flipCamera() async { try { return CameraFacing .values[await _channel.invokeMethod('flipCamera') as int]; } on PlatformException catch (e) { throw CameraException(e.code, e.message); } } /// Get flashlight status Future getFlashStatus() async { try { return await _channel.invokeMethod('getFlashInfo'); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } } /// Toggles the flashlight between available modes Future toggleFlash() async { try { await _channel.invokeMethod('toggleFlash') as bool?; } on PlatformException catch (e) { throw CameraException(e.code, e.message); } } /// Pauses the camera and barcode scanning Future pauseCamera() async { try { await _channel.invokeMethod('pauseCamera'); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } } /// Stops barcode scanning and the camera Future stopCamera() async { try { await _channel.invokeMethod('stopCamera'); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } } /// Resumes barcode scanning Future resumeCamera() async { try { await _channel.invokeMethod('resumeCamera'); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } } /// Returns which features are available on device. Future getSystemFeatures() async { try { var features = await _channel.invokeMapMethod('getSystemFeatures'); if (features != null) { return SystemFeatures.fromJson(features); } throw CameraException('Error', 'Could not get system features'); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } } /// Stops the camera and disposes the barcode stream. void dispose() { if (defaultTargetPlatform == TargetPlatform.iOS) stopCamera(); _scanUpdateController.close(); } /// Updates the view dimensions for iOS. static Future updateDimensions(GlobalKey key, MethodChannel channel, {QrScannerOverlayShape? overlay}) async { if (defaultTargetPlatform == TargetPlatform.iOS) { // Add small delay to ensure the render box is loaded await Future.delayed(Duration(milliseconds: 300)); if (key.currentContext == null) return false; final renderBox = key.currentContext!.findRenderObject() as RenderBox; try { await channel.invokeMethod('setDimensions', { 'width': renderBox.size.width, 'height': renderBox.size.height, 'scanAreaWidth': overlay?.cutOutWidth ?? 0, 'scanAreaHeight': overlay?.cutOutHeight ?? 0, 'scanAreaOffset': overlay?.cutOutBottomOffset ?? 0 }); return true; } on PlatformException catch (e) { throw CameraException(e.code, e.message); } } else if (defaultTargetPlatform == TargetPlatform.android) { if (overlay == null) { return false; } await channel.invokeMethod('changeScanArea', { 'scanAreaWidth': overlay.cutOutWidth, 'scanAreaHeight': overlay.cutOutHeight, 'cutOutBottomOffset': overlay.cutOutBottomOffset }); return true; } return false; } }