The QR Code Scanner package for Appeto SDK.
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.
 
 
 
 
 
 

361 rivejä
11 KiB

  1. import 'dart:async';
  2. import 'package:flutter/foundation.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:flutter/services.dart';
  5. import 'lifecycle_event_handler.dart';
  6. import 'qr_scanner_overlay_shape.dart';
  7. import 'types/barcode.dart';
  8. import 'types/barcode_format.dart';
  9. import 'types/camera.dart';
  10. import 'types/camera_exception.dart';
  11. import 'types/features.dart';
  12. import 'web/flutter_qr_stub.dart'
  13. // ignore: uri_does_not_exist
  14. if (dart.library.html) 'web/flutter_qr_web.dart';
  15. typedef QRViewCreatedCallback = void Function(QRViewController);
  16. typedef PermissionSetCallback = void Function(QRViewController, bool);
  17. /// The [QRView] is the view where the camera
  18. /// and the barcode scanner gets displayed.
  19. class QRView extends StatefulWidget {
  20. const QRView({
  21. required Key key,
  22. required this.onQRViewCreated,
  23. this.overlay,
  24. this.overlayMargin = EdgeInsets.zero,
  25. this.cameraFacing = CameraFacing.back,
  26. this.onPermissionSet,
  27. this.formatsAllowed = const <BarcodeFormat>[],
  28. }) : super(key: key);
  29. /// [onQRViewCreated] gets called when the view is created
  30. final QRViewCreatedCallback onQRViewCreated;
  31. /// Use [overlay] to provide an overlay for the view.
  32. /// This can be used to create a certain scan area.
  33. final QrScannerOverlayShape? overlay;
  34. /// Use [overlayMargin] to provide a margin to [overlay]
  35. final EdgeInsetsGeometry overlayMargin;
  36. /// Set which camera to use on startup.
  37. ///
  38. /// [cameraFacing] can either be CameraFacing.front or CameraFacing.back.
  39. /// Defaults to CameraFacing.back
  40. final CameraFacing cameraFacing;
  41. /// Calls the provided [onPermissionSet] callback when the permission is set.
  42. final PermissionSetCallback? onPermissionSet;
  43. /// Use [formatsAllowed] to specify which formats needs to be scanned.
  44. final List<BarcodeFormat> formatsAllowed;
  45. @override
  46. State<StatefulWidget> createState() => _QRViewState();
  47. }
  48. class _QRViewState extends State<QRView> {
  49. late MethodChannel _channel;
  50. late LifecycleEventHandler _observer;
  51. @override
  52. void initState() {
  53. super.initState();
  54. _observer = LifecycleEventHandler(resumeCallBack: updateDimensions);
  55. WidgetsBinding.instance!.addObserver(_observer);
  56. }
  57. @override
  58. Widget build(BuildContext context) {
  59. return NotificationListener(
  60. onNotification: onNotification,
  61. child: SizeChangedLayoutNotifier(
  62. child: (widget.overlay != null)
  63. ? _getPlatformQrViewWithOverlay()
  64. : _getPlatformQrView(),
  65. ),
  66. );
  67. }
  68. @override
  69. void dispose() {
  70. super.dispose();
  71. WidgetsBinding.instance!.removeObserver(_observer);
  72. }
  73. Future<void> updateDimensions() async {
  74. await QRViewController.updateDimensions(
  75. widget.key as GlobalKey<State<StatefulWidget>>, _channel,
  76. overlay: widget.overlay);
  77. }
  78. bool onNotification(notification) {
  79. updateDimensions();
  80. return false;
  81. }
  82. Widget _getPlatformQrViewWithOverlay() {
  83. return Stack(
  84. children: [
  85. _getPlatformQrView(),
  86. Padding(
  87. padding: widget.overlayMargin,
  88. child: Container(
  89. decoration: ShapeDecoration(
  90. shape: widget.overlay!,
  91. ),
  92. ),
  93. )
  94. ],
  95. );
  96. }
  97. Widget _getPlatformQrView() {
  98. Widget _platformQrView;
  99. if (kIsWeb) {
  100. _platformQrView = createWebQrView(
  101. onPlatformViewCreated: widget.onQRViewCreated,
  102. cameraFacing: widget.cameraFacing,
  103. );
  104. } else {
  105. switch (defaultTargetPlatform) {
  106. case TargetPlatform.android:
  107. _platformQrView = AndroidView(
  108. viewType: 'net.touchcapture.qr.flutterqr/qrview',
  109. onPlatformViewCreated: _onPlatformViewCreated,
  110. creationParams:
  111. _QrCameraSettings(cameraFacing: widget.cameraFacing).toMap(),
  112. creationParamsCodec: const StandardMessageCodec(),
  113. );
  114. break;
  115. case TargetPlatform.iOS:
  116. _platformQrView = UiKitView(
  117. viewType: 'net.touchcapture.qr.flutterqr/qrview',
  118. onPlatformViewCreated: _onPlatformViewCreated,
  119. creationParams:
  120. _QrCameraSettings(cameraFacing: widget.cameraFacing).toMap(),
  121. creationParamsCodec: const StandardMessageCodec(),
  122. );
  123. break;
  124. default:
  125. throw UnsupportedError(
  126. "Trying to use the default qrview implementation for $defaultTargetPlatform but there isn't a default one");
  127. }
  128. }
  129. return _platformQrView;
  130. }
  131. void _onPlatformViewCreated(int id) {
  132. _channel = MethodChannel('net.touchcapture.qr.flutterqr/qrview_$id');
  133. // Start scan after creation of the view
  134. final controller = QRViewController._(
  135. _channel,
  136. widget.key as GlobalKey<State<StatefulWidget>>?,
  137. widget.onPermissionSet,
  138. widget.cameraFacing)
  139. .._startScan(widget.key as GlobalKey<State<StatefulWidget>>,
  140. widget.overlay, widget.formatsAllowed);
  141. // Initialize the controller for controlling the QRView
  142. widget.onQRViewCreated(controller);
  143. }
  144. }
  145. class _QrCameraSettings {
  146. _QrCameraSettings({
  147. this.cameraFacing = CameraFacing.unknown,
  148. });
  149. final CameraFacing cameraFacing;
  150. Map<String, dynamic> toMap() {
  151. return <String, dynamic>{
  152. 'cameraFacing': cameraFacing.index,
  153. };
  154. }
  155. }
  156. class QRViewController {
  157. QRViewController._(MethodChannel channel, GlobalKey? qrKey,
  158. PermissionSetCallback? onPermissionSet, CameraFacing cameraFacing)
  159. : _channel = channel,
  160. _cameraFacing = cameraFacing {
  161. _channel.setMethodCallHandler((call) async {
  162. switch (call.method) {
  163. case 'onRecognizeQR':
  164. if (call.arguments != null) {
  165. final args = call.arguments as Map;
  166. final code = args['code'] as String?;
  167. final rawType = args['type'] as String;
  168. // Raw bytes are only supported by Android.
  169. final rawBytes = args['rawBytes'] as List<int>?;
  170. final format = BarcodeTypesExtension.fromString(rawType);
  171. if (format != BarcodeFormat.unknown) {
  172. final barcode = Barcode(code, format, rawBytes);
  173. _scanUpdateController.sink.add(barcode);
  174. } else {
  175. throw Exception('Unexpected barcode type $rawType');
  176. }
  177. }
  178. break;
  179. case 'onPermissionSet':
  180. if (call.arguments != null && call.arguments is bool) {
  181. _hasPermissions = call.arguments;
  182. if (onPermissionSet != null) {
  183. onPermissionSet(this, _hasPermissions);
  184. }
  185. }
  186. break;
  187. }
  188. });
  189. }
  190. final MethodChannel _channel;
  191. final CameraFacing _cameraFacing;
  192. final StreamController<Barcode> _scanUpdateController =
  193. StreamController<Barcode>();
  194. Stream<Barcode> get scannedDataStream => _scanUpdateController.stream;
  195. bool _hasPermissions = false;
  196. bool get hasPermissions => _hasPermissions;
  197. /// Starts the barcode scanner
  198. Future<void> _startScan(GlobalKey key, QrScannerOverlayShape? overlay,
  199. List<BarcodeFormat>? barcodeFormats) async {
  200. // We need to update the dimension before the scan is started.
  201. try {
  202. await QRViewController.updateDimensions(key, _channel, overlay: overlay);
  203. return await _channel.invokeMethod(
  204. 'startScan', barcodeFormats?.map((e) => e.asInt()).toList() ?? []);
  205. } on PlatformException catch (e) {
  206. throw CameraException(e.code, e.message);
  207. }
  208. }
  209. /// Gets information about which camera is active.
  210. Future<CameraFacing> getCameraInfo() async {
  211. try {
  212. var cameraFacing = await _channel.invokeMethod('getCameraInfo') as int;
  213. if (cameraFacing == -1) return _cameraFacing;
  214. return CameraFacing
  215. .values[await _channel.invokeMethod('getCameraInfo') as int];
  216. } on PlatformException catch (e) {
  217. throw CameraException(e.code, e.message);
  218. }
  219. }
  220. /// Flips the camera between available modes
  221. Future<CameraFacing> flipCamera() async {
  222. try {
  223. return CameraFacing
  224. .values[await _channel.invokeMethod('flipCamera') as int];
  225. } on PlatformException catch (e) {
  226. throw CameraException(e.code, e.message);
  227. }
  228. }
  229. /// Get flashlight status
  230. Future<bool?> getFlashStatus() async {
  231. try {
  232. return await _channel.invokeMethod('getFlashInfo');
  233. } on PlatformException catch (e) {
  234. throw CameraException(e.code, e.message);
  235. }
  236. }
  237. /// Toggles the flashlight between available modes
  238. Future<void> toggleFlash() async {
  239. try {
  240. await _channel.invokeMethod('toggleFlash') as bool?;
  241. } on PlatformException catch (e) {
  242. throw CameraException(e.code, e.message);
  243. }
  244. }
  245. /// Pauses the camera and barcode scanning
  246. Future<void> pauseCamera() async {
  247. try {
  248. await _channel.invokeMethod('pauseCamera');
  249. } on PlatformException catch (e) {
  250. throw CameraException(e.code, e.message);
  251. }
  252. }
  253. /// Stops barcode scanning and the camera
  254. Future<void> stopCamera() async {
  255. try {
  256. await _channel.invokeMethod('stopCamera');
  257. } on PlatformException catch (e) {
  258. throw CameraException(e.code, e.message);
  259. }
  260. }
  261. /// Resumes barcode scanning
  262. Future<void> resumeCamera() async {
  263. try {
  264. await _channel.invokeMethod('resumeCamera');
  265. } on PlatformException catch (e) {
  266. throw CameraException(e.code, e.message);
  267. }
  268. }
  269. /// Returns which features are available on device.
  270. Future<SystemFeatures> getSystemFeatures() async {
  271. try {
  272. var features =
  273. await _channel.invokeMapMethod<String, dynamic>('getSystemFeatures');
  274. if (features != null) {
  275. return SystemFeatures.fromJson(features);
  276. }
  277. throw CameraException('Error', 'Could not get system features');
  278. } on PlatformException catch (e) {
  279. throw CameraException(e.code, e.message);
  280. }
  281. }
  282. /// Stops the camera and disposes the barcode stream.
  283. void dispose() {
  284. if (defaultTargetPlatform == TargetPlatform.iOS) stopCamera();
  285. _scanUpdateController.close();
  286. }
  287. /// Updates the view dimensions for iOS.
  288. static Future<bool> updateDimensions(GlobalKey key, MethodChannel channel,
  289. {QrScannerOverlayShape? overlay}) async {
  290. if (defaultTargetPlatform == TargetPlatform.iOS) {
  291. // Add small delay to ensure the render box is loaded
  292. await Future.delayed(const Duration(milliseconds: 300));
  293. if (key.currentContext == null) return false;
  294. final renderBox = key.currentContext!.findRenderObject() as RenderBox;
  295. try {
  296. await channel.invokeMethod('setDimensions', {
  297. 'width': renderBox.size.width,
  298. 'height': renderBox.size.height,
  299. 'scanAreaWidth': overlay?.cutOutWidth ?? 0,
  300. 'scanAreaHeight': overlay?.cutOutHeight ?? 0,
  301. 'scanAreaOffset': overlay?.cutOutBottomOffset ?? 0
  302. });
  303. return true;
  304. } on PlatformException catch (e) {
  305. throw CameraException(e.code, e.message);
  306. }
  307. } else if (defaultTargetPlatform == TargetPlatform.android) {
  308. if (overlay == null) {
  309. return false;
  310. }
  311. await channel.invokeMethod('changeScanArea', {
  312. 'scanAreaWidth': overlay.cutOutWidth,
  313. 'scanAreaHeight': overlay.cutOutHeight,
  314. 'cutOutBottomOffset': overlay.cutOutBottomOffset
  315. });
  316. return true;
  317. }
  318. return false;
  319. }
  320. }