Sander Roest

Finalize the picklist sample

1 import 'dart:async'; 1 import 'dart:async';
2 2
3 import 'package:flutter/material.dart'; 3 import 'package:flutter/material.dart';
  4 +import 'package:flutter/services.dart';
4 import 'package:mobile_scanner/mobile_scanner.dart'; 5 import 'package:mobile_scanner/mobile_scanner.dart';
5 import 'package:mobile_scanner_example/picklist/classes/barcode_at_center.dart'; 6 import 'package:mobile_scanner_example/picklist/classes/barcode_at_center.dart';
6 -  
7 import 'package:mobile_scanner_example/picklist/widgets/crosshair.dart'; 7 import 'package:mobile_scanner_example/picklist/widgets/crosshair.dart';
8 import 'package:mobile_scanner_example/scanner_error_widget.dart'; 8 import 'package:mobile_scanner_example/scanner_error_widget.dart';
9 9
  10 +// This sample implements picklist functionality.
  11 +// The scanning can temporarily be suspended by the user by touching the screen.
  12 +// When the scanning is active, the crosshair turns red.
  13 +// When the scanning is suspended, the crosshair turns green.
  14 +// A barcode has to touch the center of viewfinder to be scanned.
  15 +// Therefore the Crosshair widget needs to be placed at the center of the
  16 +// MobileScanner widget to visually line up.
10 class BarcodeScannerPicklist extends StatefulWidget { 17 class BarcodeScannerPicklist extends StatefulWidget {
11 const BarcodeScannerPicklist({super.key}); 18 const BarcodeScannerPicklist({super.key});
12 19
@@ -14,113 +21,109 @@ class BarcodeScannerPicklist extends StatefulWidget { @@ -14,113 +21,109 @@ class BarcodeScannerPicklist extends StatefulWidget {
14 State<BarcodeScannerPicklist> createState() => _BarcodeScannerPicklistState(); 21 State<BarcodeScannerPicklist> createState() => _BarcodeScannerPicklistState();
15 } 22 }
16 23
17 -class _BarcodeScannerPicklistState extends State<BarcodeScannerPicklist>  
18 - with WidgetsBindingObserver { 24 +class _BarcodeScannerPicklistState extends State<BarcodeScannerPicklist> {
19 final _mobileScannerController = MobileScannerController( 25 final _mobileScannerController = MobileScannerController(
  26 + // The controller is started from the initState method.
20 autoStart: false, 27 autoStart: false,
21 - useNewCameraSelector: true, 28 + // The know the placing of the barcodes, we need to know the size of the
  29 + // canvas they are placed on. Unfortunately the only known reliable way
  30 + // to get the dimensions, is to receive the complete image from the native
  31 + // side.
  32 + // https://github.com/juliansteenbakker/mobile_scanner/issues/1183
  33 + returnImage: true,
22 ); 34 );
23 - StreamSubscription<Object?>? _barcodesSubscription;  
24 35
  36 + // On this subscription the barcodes are received.
  37 + StreamSubscription<Object?>? _subscription;
  38 +
  39 + // This boolean indicates if the detection of barcodes is enabled or
  40 + // temporarily suspended.
25 final _scannerEnabled = ValueNotifier(true); 41 final _scannerEnabled = ValueNotifier(true);
26 42
27 - bool barcodeDetected = false; 43 + // This boolean is used to prevent multiple pops.
  44 + var _validBarcodeFound = false;
28 45
29 @override 46 @override
30 void initState() { 47 void initState() {
31 - WidgetsBinding.instance.addObserver(this);  
32 - _barcodesSubscription = _mobileScannerController.barcodes.listen(  
33 - _handleBarcodes,  
34 - ); 48 + // Enable and disable scanning on the native side, so we don't get a stream
  49 + // of images when not needed. This also improves the behavior (false
  50 + // positives) when the user switches quickly to another barcode after
  51 + // enabling the scanner by releasing the finger.
  52 + _scannerEnabled.addListener(() {
  53 + _scannerEnabled.value
  54 + ? _mobileScannerController.updateScanWindow(null)
  55 + : _mobileScannerController.updateScanWindow(Rect.zero);
  56 + });
  57 + // Lock to portrait (may not work on iPad with multitasking).
  58 + SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
  59 + // Get a stream subscription and listen to received barcodes.
  60 + _subscription = _mobileScannerController.barcodes.listen(_handleBarcodes);
35 super.initState(); 61 super.initState();
  62 + // Start the controller to start scanning.
36 unawaited(_mobileScannerController.start()); 63 unawaited(_mobileScannerController.start());
37 } 64 }
38 65
39 @override 66 @override
40 - void didChangeAppLifecycleState(AppLifecycleState state) {  
41 - if (!_mobileScannerController.value.isInitialized) {  
42 - return;  
43 - }  
44 -  
45 - switch (state) {  
46 - case AppLifecycleState.detached:  
47 - case AppLifecycleState.hidden:  
48 - case AppLifecycleState.paused:  
49 - return;  
50 - case AppLifecycleState.resumed:  
51 - _barcodesSubscription =  
52 - _mobileScannerController.barcodes.listen(_handleBarcodes);  
53 -  
54 - unawaited(_mobileScannerController.start());  
55 - case AppLifecycleState.inactive:  
56 - unawaited(_barcodesSubscription?.cancel());  
57 - _barcodesSubscription = null;  
58 - unawaited(_mobileScannerController.stop());  
59 - }  
60 - }  
61 -  
62 - @override  
63 void dispose() { 67 void dispose() {
64 - WidgetsBinding.instance.removeObserver(this);  
65 - unawaited(_barcodesSubscription?.cancel());  
66 - _barcodesSubscription = null; 68 + // Cancel the stream subscription.
  69 + unawaited(_subscription?.cancel());
  70 + _subscription = null;
67 super.dispose(); 71 super.dispose();
  72 + // Dispose the controller.
68 _mobileScannerController.dispose(); 73 _mobileScannerController.dispose();
69 } 74 }
70 75
71 - void _handleBarcodes(BarcodeCapture capture) {  
72 - if (!_scannerEnabled.value) { 76 + // Check the list of barcodes only if scannerEnables is true.
  77 + // Only take the barcode that is at the center of the image.
  78 + // Return the barcode found to the calling page with the help of the
  79 + // navigator.
  80 + void _handleBarcodes(BarcodeCapture barcodeCapture) {
  81 + // Discard all events when the scanner is disabled or when already a valid
  82 + // barcode is found.
  83 + if (!_scannerEnabled.value || _validBarcodeFound) {
73 return; 84 return;
74 } 85 }
75 -  
76 - for (final barcode in capture.barcodes) {  
77 - if (isBarcodeAtCenterOfImage(  
78 - cameraOutputSize: _mobileScannerController.value.size,  
79 - barcode: barcode,  
80 - )) {  
81 - if (!barcodeDetected) {  
82 - barcodeDetected = true;  
83 - Navigator.of(context).pop(barcode);  
84 - }  
85 - return;  
86 - } 86 + final barcode = findBarcodeAtCenter(barcodeCapture);
  87 + if (barcode != null) {
  88 + _validBarcodeFound = true;
  89 + Navigator.of(context).pop(barcode);
87 } 90 }
88 } 91 }
89 92
90 @override 93 @override
91 Widget build(BuildContext context) { 94 Widget build(BuildContext context) {
92 - return Scaffold(  
93 - appBar: AppBar(title: const Text('Picklist scanner')),  
94 - backgroundColor: Colors.black,  
95 - body: StreamBuilder(  
96 - stream: _mobileScannerController.barcodes,  
97 - builder: (context, snapshot) {  
98 - return Listener(  
99 - behavior: HitTestBehavior.opaque,  
100 - onPointerDown: (_) => _scannerEnabled.value = false,  
101 - onPointerUp: (_) => _scannerEnabled.value = true,  
102 - onPointerCancel: (_) => _scannerEnabled.value = true,  
103 - child: Stack(  
104 - fit: StackFit.expand,  
105 - children: [  
106 - MobileScanner(  
107 - controller: _mobileScannerController,  
108 - errorBuilder: (context, error, child) =>  
109 - ScannerErrorWidget(error: error),  
110 - fit: BoxFit.contain,  
111 - ),  
112 - ValueListenableBuilder(  
113 - valueListenable: _scannerEnabled,  
114 - builder: (context, value, child) {  
115 - return Crosshair(  
116 - scannerEnabled: value,  
117 - );  
118 - },  
119 - ),  
120 - ],  
121 - ),  
122 - );  
123 - }, 95 + return PopScope(
  96 + onPopInvokedWithResult: (didPop, result) {
  97 + // Reset the page orientation to the system default values, when this page is popped
  98 + if (!didPop) {
  99 + return;
  100 + }
  101 + SystemChrome.setPreferredOrientations(<DeviceOrientation>[]);
  102 + },
  103 + child: Scaffold(
  104 + appBar: AppBar(title: const Text('Picklist scanner')),
  105 + backgroundColor: Colors.black,
  106 + body: Listener(
  107 + // Detect if the user touches the screen and disable/enable the scanner accordingly
  108 + behavior: HitTestBehavior.opaque,
  109 + onPointerDown: (_) => _scannerEnabled.value = false,
  110 + onPointerUp: (_) => _scannerEnabled.value = true,
  111 + onPointerCancel: (_) => _scannerEnabled.value = true,
  112 + // A stack containing the image feed and the crosshair
  113 + // The location of the crosshair must be at the center of the MobileScanner, otherwise the detection area and the visual representation do not line up.
  114 + child: Stack(
  115 + fit: StackFit.expand,
  116 + children: [
  117 + MobileScanner(
  118 + controller: _mobileScannerController,
  119 + errorBuilder: (context, error, child) =>
  120 + ScannerErrorWidget(error: error),
  121 + fit: BoxFit.contain,
  122 + ),
  123 + Crosshair(_scannerEnabled),
  124 + ],
  125 + ),
  126 + ),
124 ), 127 ),
125 ); 128 );
126 } 129 }
1 import 'package:flutter/material.dart'; 1 import 'package:flutter/material.dart';
2 import 'package:mobile_scanner/mobile_scanner.dart'; 2 import 'package:mobile_scanner/mobile_scanner.dart';
3 3
4 -bool isBarcodeAtCenterOfImage({  
5 - required Size cameraOutputSize,  
6 - required Barcode barcode, 4 +/// This function finds the barcode that touches the center of the
  5 +/// image. If no barcode is found that touches the center, null is returned.
  6 +/// See [_BarcodeScannerPicklistState] and the returnImage option for more info.
  7 +///
  8 +/// https://github.com/juliansteenbakker/mobile_scanner/issues/1183
  9 +Barcode? findBarcodeAtCenter(BarcodeCapture barcodeCapture) {
  10 + final imageSize = barcodeCapture.size;
  11 + for (final barcode in barcodeCapture.barcodes) {
  12 + if (_isPolygonTouchingTheCenter(
  13 + imageSize: imageSize,
  14 + polygon: barcode.corners,
  15 + )) {
  16 + return barcode;
  17 + }
  18 + }
  19 + return null;
  20 +}
  21 +
  22 +/// Check if the polygon, represented by a list of offsets, touches the center of
  23 +/// an image when the size of the image is given.
  24 +bool _isPolygonTouchingTheCenter({
  25 + required Size imageSize,
  26 + required List<Offset> polygon,
7 }) { 27 }) {
8 final centerOfCameraOutput = Offset( 28 final centerOfCameraOutput = Offset(
9 - cameraOutputSize.width / 2,  
10 - cameraOutputSize.height / 2, 29 + imageSize.width / 2,
  30 + imageSize.height / 2,
11 ); 31 );
12 - debugPrint(cameraOutputSize.toString());  
13 return _isPointInPolygon( 32 return _isPointInPolygon(
14 point: centerOfCameraOutput, 33 point: centerOfCameraOutput,
15 - polygon: barcode.corners, 34 + polygon: polygon,
16 ); 35 );
17 } 36 }
18 37
19 -//This is what chatGPT came up with.  
20 -//https://en.wikipedia.org/wiki/Point_in_polygon 38 +/// Credits to chatGPT:
  39 +/// Checks if a given [point] is inside the [polygon] boundaries.
  40 +///
  41 +/// Parameters:
  42 +/// - [point]: The `Offset` (usually represents a point in 2D space) to check.
  43 +/// - [polygon]: A List of `Offset` representing the vertices of the polygon.
  44 +///
  45 +/// Returns:
  46 +/// - A boolean value: `true` if the point is inside the polygon, or `false` otherwise.
  47 +///
  48 +/// Uses the ray-casting algorithm based on the Jordan curve theorem.
21 bool _isPointInPolygon({ 49 bool _isPointInPolygon({
22 required Offset point, 50 required Offset point,
23 required List<Offset> polygon, 51 required List<Offset> polygon,
24 }) { 52 }) {
25 - int i;  
26 - int j = polygon.length - 1;  
27 - bool inside = false; 53 + // Initial variables:
  54 + int i; // Loop variable for current vertex
  55 + int j = polygon.length -
  56 + 1; // Last vertex index, initialized to the last vertex of the polygon
  57 + bool inside = false; // Boolean flag initialized to false
28 58
  59 + // Loop through each edge of the polygon
29 for (i = 0; i < polygon.length; j = i++) { 60 for (i = 0; i < polygon.length; j = i++) {
  61 + // Check if point's y-coordinate is within the y-boundaries of the edge
30 if (((polygon[i].dy > point.dy) != (polygon[j].dy > point.dy)) && 62 if (((polygon[i].dy > point.dy) != (polygon[j].dy > point.dy)) &&
  63 + // Check if the point's x-coordinate is to the left of the edge
31 (point.dx < 64 (point.dx <
32 - (polygon[j].dx - polygon[i].dx) *  
33 - (point.dy - polygon[i].dy) /  
34 - (polygon[j].dy - polygon[i].dy) + 65 + (polygon[j].dx -
  66 + polygon[i]
  67 + .dx) * // Horizontal distance between the vertices of the edge
  68 + (point.dy -
  69 + polygon[i]
  70 + .dy) / // Scale factor based on the y-distance of the point to the lower vertex
  71 + (polygon[j].dy -
  72 + polygon[i]
  73 + .dy) + // Vertical distance between the vertices of the edge
35 polygon[i].dx)) { 74 polygon[i].dx)) {
  75 + // Horizontal position of the lower vertex
  76 + // If the ray intersects the polygon edge, invert the inside flag
36 inside = !inside; 77 inside = !inside;
37 } 78 }
38 } 79 }
  80 + // Return the status of the inside flag which tells if the point is inside the polygon or not
39 return inside; 81 return inside;
40 } 82 }
@@ -15,7 +15,7 @@ class _PicklistResultState extends State<PicklistResult> { @@ -15,7 +15,7 @@ class _PicklistResultState extends State<PicklistResult> {
15 @override 15 @override
16 Widget build(BuildContext context) { 16 Widget build(BuildContext context) {
17 return Scaffold( 17 return Scaffold(
18 - appBar: AppBar(title: const Text('Picklist mode')), 18 + appBar: AppBar(title: const Text('Picklist result')),
19 body: SafeArea( 19 body: SafeArea(
20 child: Padding( 20 child: Padding(
21 padding: const EdgeInsets.symmetric(horizontal: 16.0), 21 padding: const EdgeInsets.symmetric(horizontal: 16.0),
  1 +import 'package:flutter/foundation.dart';
1 import 'package:flutter/material.dart'; 2 import 'package:flutter/material.dart';
2 3
3 class Crosshair extends StatelessWidget { 4 class Crosshair extends StatelessWidget {
4 - const Crosshair({ 5 + const Crosshair(
  6 + this.scannerEnabled, {
5 super.key, 7 super.key,
6 - required this.scannerEnabled,  
7 }); 8 });
8 9
9 - final bool scannerEnabled; 10 + final ValueListenable<bool> scannerEnabled;
10 11
11 @override 12 @override
12 Widget build(BuildContext context) { 13 Widget build(BuildContext context) {
13 - return Center(  
14 - child: Icon(  
15 - Icons.close,  
16 - color: scannerEnabled ? Colors.red : Colors.green,  
17 - ), 14 + return ValueListenableBuilder(
  15 + valueListenable: scannerEnabled,
  16 + builder: (context, value, child) {
  17 + return Center(
  18 + child: Icon(
  19 + Icons.close,
  20 + color: scannerEnabled.value ? Colors.red : Colors.green,
  21 + ),
  22 + );
  23 + },
18 ); 24 );
19 } 25 }
20 } 26 }