From e6b74ac752d118ddd8bf96ed172e9a83f8d9ba11 Mon Sep 17 00:00:00 2001 From: Julian Steenbakker Date: Wed, 23 Dec 2020 11:24:01 +0100 Subject: [PATCH] Added possibility to choose facing camera on start. Moved the size listener to plugin side. --- .../net/touchcapture/qr/flutterqr/QRView.kt | 23 ++- .../qr/flutterqr/QRViewFactory.kt | 5 +- example/.gitignore | 1 + example/ios/Runner.xcodeproj/project.pbxproj | 2 - example/lib/main.dart | 31 ++-- ios/Classes/QRView.swift | 119 ++++++------ ios/Classes/QRViewFactory.swift | 4 +- lib/src/qr_code_scanner.dart | 169 +++++++++++------- 8 files changed, 211 insertions(+), 143 deletions(-) diff --git a/android/src/main/kotlin/net/touchcapture/qr/flutterqr/QRView.kt b/android/src/main/kotlin/net/touchcapture/qr/flutterqr/QRView.kt index 48329c5..945cb37 100644 --- a/android/src/main/kotlin/net/touchcapture/qr/flutterqr/QRView.kt +++ b/android/src/main/kotlin/net/touchcapture/qr/flutterqr/QRView.kt @@ -17,7 +17,7 @@ import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.platform.PlatformView -class QRView(messenger: BinaryMessenger, id: Int, private val context: Context) : +class QRView(messenger: BinaryMessenger, id: Int, private val context: Context, private val params: HashMap) : PlatformView, MethodChannel.MethodCallHandler { private var isTorchOn: Boolean = false @@ -66,6 +66,12 @@ class QRView(messenger: BinaryMessenger, id: Int, private val context: Context) override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when(call.method){ + "startScan" -> { + startScan() + } + "stopScan" -> { + stopScan() + } "flipCamera" -> { flipCamera() } @@ -127,14 +133,16 @@ class QRView(messenger: BinaryMessenger, id: Int, private val context: Context) private fun initBarCodeView(): BarcodeView? { if (barcodeView == null) { - barcodeView = createBarCodeView() + barcodeView = BarcodeView(Shared.activity) + if (params["cameraFacing"] as Int == 1) { + barcodeView?.cameraSettings?.requestedCameraId = CameraInfo.CAMERA_FACING_FRONT + } } return barcodeView } - private fun createBarCodeView(): BarcodeView { - val barcode = BarcodeView(Shared.activity) - barcode.decodeContinuous( + private fun startScan() { + barcodeView?.decodeContinuous( object : BarcodeCallback { override fun barcodeResult(result: BarcodeResult) { val code = mapOf( @@ -147,7 +155,10 @@ class QRView(messenger: BinaryMessenger, id: Int, private val context: Context) override fun possibleResultPoints(resultPoints: List) {} } ) - return barcode + } + + private fun stopScan() { + barcodeView?.stopDecoding() } private fun hasCameraPermission(): Boolean { diff --git a/android/src/main/kotlin/net/touchcapture/qr/flutterqr/QRViewFactory.kt b/android/src/main/kotlin/net/touchcapture/qr/flutterqr/QRViewFactory.kt index a797084..e81303d 100644 --- a/android/src/main/kotlin/net/touchcapture/qr/flutterqr/QRViewFactory.kt +++ b/android/src/main/kotlin/net/touchcapture/qr/flutterqr/QRViewFactory.kt @@ -10,8 +10,9 @@ import io.flutter.plugin.platform.PlatformViewFactory class QRViewFactory(private val messenger: BinaryMessenger) : PlatformViewFactory(StandardMessageCodec.INSTANCE) { - override fun create(context: Context, id: Int, obj: Any?): PlatformView { - return QRView(messenger, id, context) + override fun create(context: Context, id: Int, args: Any?): PlatformView { + val params = args as HashMap + return QRView(messenger, id, context, params) } } \ No newline at end of file diff --git a/example/.gitignore b/example/.gitignore index 47e0b4d..4736dd6 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -9,6 +9,7 @@ .buildlog/ .history .svn/ +.last_build_id # IntelliJ related *.iml diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index d28e5f5..d3ce8b9 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -264,13 +264,11 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${PODS_ROOT}/../Flutter/Flutter.framework", "${BUILT_PRODUCTS_DIR}/MTBBarcodeScanner/MTBBarcodeScanner.framework", "${BUILT_PRODUCTS_DIR}/qr_code_scanner/qr_code_scanner.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MTBBarcodeScanner.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/qr_code_scanner.framework", ); diff --git a/example/lib/main.dart b/example/lib/main.dart index beb8d51..72fdd08 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -154,25 +154,18 @@ class _QRViewExampleState extends State { : 300.0; // To ensure the Scanner view is properly sizes after rotation // we need to listen for Flutter SizeChanged notification and update controller - return NotificationListener( - onNotification: (notification) { - Future.microtask( - () => controller?.updateDimensions(qrKey, scanArea: scanArea)); - return false; - }, - child: SizeChangedLayoutNotifier( - key: const Key('qr-size-notifier'), - child: QRView( - key: qrKey, - onQRViewCreated: _onQRViewCreated, - overlay: QrScannerOverlayShape( - borderColor: Colors.red, - borderRadius: 10, - borderLength: 30, - borderWidth: 10, - cutOutSize: scanArea, - ), - ))); + return QRView( + key: qrKey, + cameraFacing: CameraFacing.front, + onQRViewCreated: _onQRViewCreated, + overlay: QrScannerOverlayShape( + borderColor: Colors.red, + borderRadius: 10, + borderLength: 30, + borderWidth: 10, + cutOutSize: scanArea, + ), + ); } void _onQRViewCreated(QRViewController controller) { diff --git a/ios/Classes/QRView.swift b/ios/Classes/QRView.swift index 298f7a9..3e36441 100644 --- a/ios/Classes/QRView.swift +++ b/ios/Classes/QRView.swift @@ -13,67 +13,28 @@ public class QRView:NSObject,FlutterPlatformView { var scanner: MTBBarcodeScanner? var registrar: FlutterPluginRegistrar var channel: FlutterMethodChannel + var cameraFacing: MTBCamera - public init(withFrame frame: CGRect, withRegistrar registrar: FlutterPluginRegistrar, withId id: Int64){ + public init(withFrame frame: CGRect, withRegistrar registrar: FlutterPluginRegistrar, withId id: Int64, params: Dictionary){ self.registrar = registrar previewView = UIView(frame: frame) + cameraFacing = MTBCamera.init(rawValue: UInt(Int(params["cameraFacing"] as! Double))) ?? MTBCamera.back channel = FlutterMethodChannel(name: "net.touchcapture.qr.flutterqr/qrview_\(id)", binaryMessenger: registrar.messenger()) } - func isCameraAvailable(success: Bool) -> Void { - if success { - do { - try scanner?.startScanning(resultBlock: { [weak self] codes in - if let codes = codes { - for code in codes { - var typeString: String; - switch(code.type) { - case AVMetadataObject.ObjectType.aztec: - typeString = "AZTEC" - case AVMetadataObject.ObjectType.code39: - typeString = "CODE_39" - case AVMetadataObject.ObjectType.code93: - typeString = "CODE_93" - case AVMetadataObject.ObjectType.code128: - typeString = "CODE_128" - case AVMetadataObject.ObjectType.dataMatrix: - typeString = "DATA_MATRIX" - case AVMetadataObject.ObjectType.ean8: - typeString = "EAN_8" - case AVMetadataObject.ObjectType.ean13: - typeString = "EAN_13" - case AVMetadataObject.ObjectType.itf14: - typeString = "ITF" - case AVMetadataObject.ObjectType.pdf417: - typeString = "PDF_417" - case AVMetadataObject.ObjectType.qr: - typeString = "QR_CODE" - case AVMetadataObject.ObjectType.upce: - typeString = "UPC_E" - default: - return - } - guard let stringValue = code.stringValue else { continue } - let result = ["code": stringValue, "type": typeString] - self?.channel.invokeMethod("onRecognizeQR", arguments: result) - } - } - }) - } catch { - NSLog("Unable to start scanning") - } - } else { - UIAlertView(title: "Scanning Unavailable", message: "This app does not have permission to access the camera", delegate: nil, cancelButtonTitle: nil, otherButtonTitles: "Ok").show() - } + deinit { + scanner?.stopScanning() } public func view() -> UIView { channel.setMethodCallHandler({ - [weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in + [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in switch(call.method){ case "setDimensions": let arguments = call.arguments as! Dictionary self?.setDimensions(width: arguments["width"] ?? 0, height: arguments["height"] ?? 0, scanArea: arguments["scanArea"] ?? 0) + case "startScan": + self?.startScan(result) case "flipCamera": self?.flipCamera() case "toggleFlash": @@ -106,9 +67,67 @@ public class QRView:NSObject,FlutterPlatformView { self.scanner?.scanRect = CGRect(x: Double(midX) - (scanArea / 2), y: Double(midY) - (scanArea / 2), width: scanArea, height: scanArea) } } - - - MTBBarcodeScanner.requestCameraPermission(success: isCameraAvailable) + } + } + + func startScan(_ result: @escaping FlutterResult) -> Void { + scanner = MTBBarcodeScanner(previewView: previewView) + + MTBBarcodeScanner.requestCameraPermission(success: { permissionGranted in + if permissionGranted { + do { + try self.scanner?.startScanning(with: self.cameraFacing, resultBlock: { [weak self] codes in + if let codes = codes { + for code in codes { + var typeString: String; + switch(code.type) { + case AVMetadataObject.ObjectType.aztec: + typeString = "AZTEC" + case AVMetadataObject.ObjectType.code39: + typeString = "CODE_39" + case AVMetadataObject.ObjectType.code93: + typeString = "CODE_93" + case AVMetadataObject.ObjectType.code128: + typeString = "CODE_128" + case AVMetadataObject.ObjectType.dataMatrix: + typeString = "DATA_MATRIX" + case AVMetadataObject.ObjectType.ean8: + typeString = "EAN_8" + case AVMetadataObject.ObjectType.ean13: + typeString = "EAN_13" + case AVMetadataObject.ObjectType.itf14: + typeString = "ITF" + case AVMetadataObject.ObjectType.pdf417: + typeString = "PDF_417" + case AVMetadataObject.ObjectType.qr: + typeString = "QR_CODE" + case AVMetadataObject.ObjectType.upce: + typeString = "UPC_E" + default: + return + } + guard let stringValue = code.stringValue else { continue } + let result = ["code": stringValue, "type": typeString] + self?.channel.invokeMethod("onRecognizeQR", arguments: result) + } + } + }) + } catch { + let error = FlutterError(code: "unknown-error", message: "Unable to start scanning", details: nil) + result(error) + } + } else { + let error = FlutterError(code: "cameraPermission", message: "Permission denied to access the camera", details: nil) + result(error) + } + }) + } + + func stopScan(){ + if let sc: MTBBarcodeScanner = scanner { + if sc.isScanning() { + sc.stopScanning() + } } } diff --git a/ios/Classes/QRViewFactory.swift b/ios/Classes/QRViewFactory.swift index fff745b..6f851f6 100644 --- a/ios/Classes/QRViewFactory.swift +++ b/ios/Classes/QRViewFactory.swift @@ -17,8 +17,8 @@ public class QRViewFactory: NSObject, FlutterPlatformViewFactory { } public func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?) -> FlutterPlatformView { - let dictionary = args as! Dictionary - return QRView(withFrame: CGRect(x: 0, y: 0, width: dictionary["width"] ?? 0, height: dictionary["height"] ?? 0), withRegistrar: registrar!,withId: viewId) + let params = args as! Dictionary + return QRView(withFrame: frame, withRegistrar: registrar!,withId: viewId, params: params) } public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol { diff --git a/lib/src/qr_code_scanner.dart b/lib/src/qr_code_scanner.dart index 7a60428..be42b6e 100644 --- a/lib/src/qr_code_scanner.dart +++ b/lib/src/qr_code_scanner.dart @@ -7,6 +7,14 @@ import 'package:qr_code_scanner/qr_code_scanner.dart'; typedef QRViewCreatedCallback = void Function(QRViewController); +enum CameraFacing { + /// Shows back facing camera. + back, + + /// Shows front facing camera. + front +} + enum BarcodeFormat { /// Aztec 2D barcode format. aztec, @@ -80,6 +88,11 @@ const _formatNames = { 'UPC_EAN_EXTENSION': BarcodeFormat.upcEanExtension, }; +/// The [Barcode] object holds information about the barcode or qr code. +/// +/// [code] is the content of the barcode. +/// [format] displays which type the code is. +/// Only for Android, [rawBytes] gives a list of bytes of the result. class Barcode { Barcode(this.code, this.format, this.rawBytes); @@ -90,60 +103,84 @@ class Barcode { final List rawBytes; } +/// 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, }) : assert(key != null), assert(onQRViewCreated != null), super(key: key); final QRViewCreatedCallback onQRViewCreated; - final ShapeBorder overlay; final EdgeInsetsGeometry overlayMargin; + final CameraFacing cameraFacing; @override State createState() => _QRViewState(); } class _QRViewState extends State { + + var _channel; + @override Widget build(BuildContext context) { + return NotificationListener( + onNotification: onNotification, + child: SizeChangedLayoutNotifier( + child: (widget.overlay != null) ? + _getPlatformQrViewWithOverlay() : + _getPlatformQrView(), + ), + ); + } + + bool onNotification(notification) { + Future.microtask(() => { + QRViewController.updateDimensions( + widget.key, + _channel, + scanArea: widget.overlay != null ? + (widget.overlay as QrScannerOverlayShape).cutOutSize : 0.0) + }); + return false; + } + + Widget _getPlatformQrViewWithOverlay() { return Stack( children: [ - _getPlatformQrView(widget.key), - if (widget.overlay != null) - Container( - padding: widget.overlayMargin, - decoration: ShapeDecoration( - shape: widget.overlay, - ), - ) - else - Container(), + _getPlatformQrView(), + Container( + padding: widget.overlayMargin, + decoration: ShapeDecoration( + shape: widget.overlay, + ), + ) ], ); } - Widget _getPlatformQrView(GlobalKey key) { + Widget _getPlatformQrView() { Widget _platformQrView; 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: - _CreationParams.fromWidget(MediaQuery.of(context).size.width, 400) - .toMap(), + creationParams: _QrCameraSettings(cameraFacing: widget.cameraFacing).toMap(), creationParamsCodec: StandardMessageCodec(), ); break; @@ -155,101 +192,109 @@ class _QRViewState extends State { } void _onPlatformViewCreated(int id) { - if (widget.onQRViewCreated == null) { - return; - } - // We pass the cutout size so that the scanner respects the scan area. var cutOutSize = 0.0; if (widget.overlay != null) { cutOutSize = (widget.overlay as QrScannerOverlayShape).cutOutSize; } - widget.onQRViewCreated(QRViewController._(id, widget.key, cutOutSize)); - } -} + _channel = MethodChannel('net.touchcapture.qr.flutterqr/qrview_$id'); -class _CreationParams { - _CreationParams({this.width, this.height}); + // Start scan after creation of the view + final controller = QRViewController._(_channel, widget.key, cutOutSize) + .._startScan(widget.key, cutOutSize); - static _CreationParams fromWidget(double width, double height) { - return _CreationParams( - width: width, - height: height, - ); + // Initialize the controller for controlling the QRView + if (widget.onQRViewCreated != null) { + widget.onQRViewCreated(controller); + } } +} + +class _QrCameraSettings { + _QrCameraSettings({ + this.cameraFacing, + }); - final double width; - final double height; + final CameraFacing cameraFacing; Map toMap() { return { - 'width': width, - 'height': height, + 'cameraFacing': cameraFacing.index, }; } + } class QRViewController { - QRViewController._(int id, GlobalKey qrKey, double scanArea) - : _channel = MethodChannel('net.touchcapture.qr.flutterqr/qrview_$id') { - updateDimensions(qrKey, scanArea: scanArea); - _channel.setMethodCallHandler( - (call) async { - switch (call.method) { - case scanMethodCall: - 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 = _formatNames[rawType]; - if (format != null) { - final barcode = Barcode(code, format, rawBytes); - _scanUpdateController.sink.add(barcode); - } else { - throw Exception('Unexpected barcode type $rawType'); - } - } - } - }, - ); + QRViewController._(MethodChannel channel, GlobalKey qrKey, double scanArea) + : _channel = channel { + _channel.setMethodCallHandler(_onMethodCall); } - static const scanMethodCall = 'onRecognizeQR'; - final MethodChannel _channel; - final StreamController _scanUpdateController = StreamController(); Stream get scannedDataStream => _scanUpdateController.stream; + Future _onMethodCall(MethodCall 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 = _formatNames[rawType]; + if (format != null) { + final barcode = Barcode(code, format, rawBytes); + _scanUpdateController.sink.add(barcode); + } else { + throw Exception('Unexpected barcode type $rawType'); + } + } + } + } + + /// Starts the barcode scanner + Future _startScan(GlobalKey key, double cutOutSize, ) async { + // We need to update the dimension before the scan is started. + QRViewController.updateDimensions(key, _channel, scanArea: cutOutSize); + return _channel.invokeMethod('startScan'); + } + + /// Flips the camera between available modes void flipCamera() { _channel.invokeMethod('flipCamera'); } + /// Toggles the flashlight between available modes void toggleFlash() { _channel.invokeMethod('toggleFlash'); } + /// Pauses barcode scanning void pauseCamera() { _channel.invokeMethod('pauseCamera'); } + /// Resumes barcode scanning void resumeCamera() { _channel.invokeMethod('resumeCamera'); } + /// Disposes the barcode stream. void dispose() { _scanUpdateController.close(); } - void updateDimensions(GlobalKey key, {double scanArea}) { + /// Updates the view dimensions for iOS. + static void updateDimensions(GlobalKey key, MethodChannel channel, {double scanArea}) { if (defaultTargetPlatform == TargetPlatform.iOS) { final RenderBox renderBox = key.currentContext.findRenderObject(); - _channel.invokeMethod('setDimensions', { + channel.invokeMethod('setDimensions', { 'width': renderBox.size.width, 'height': renderBox.size.height, 'scanArea': scanArea ?? 0