Skip to content

Commit bafacb5

Browse files
Merge pull request #1188 from jsroest/master
feat: Add picklist mode sample to the example app
2 parents b4a084c + 49bb424 commit bafacb5

File tree

7 files changed

+331
-1
lines changed

7 files changed

+331
-1
lines changed

example/ios/Podfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Uncomment this line to define a global platform for your project
2-
# platform :ios, '15.5.0'
2+
platform :ios, '15.5.0'
33

44
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
55
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

example/lib/main.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:mobile_scanner_example/barcode_scanner_simple.dart';
88
import 'package:mobile_scanner_example/barcode_scanner_window.dart';
99
import 'package:mobile_scanner_example/barcode_scanner_zoom.dart';
1010
import 'package:mobile_scanner_example/mobile_scanner_overlay.dart';
11+
import 'package:mobile_scanner_example/picklist/picklist_result.dart';
1112

1213
void main() {
1314
runApp(
@@ -91,6 +92,11 @@ class MyHome extends StatelessWidget {
9192
'Analyze image from file',
9293
const BarcodeScannerAnalyzeImage(),
9394
),
95+
_buildItem(
96+
context,
97+
'Picklist mode',
98+
const PicklistResult(),
99+
),
94100
],
95101
),
96102
),
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import 'dart:async';
2+
3+
import 'package:flutter/material.dart';
4+
import 'package:flutter/services.dart';
5+
import 'package:mobile_scanner/mobile_scanner.dart';
6+
import 'package:mobile_scanner_example/picklist/classes/barcode_at_center.dart';
7+
import 'package:mobile_scanner_example/picklist/widgets/crosshair.dart';
8+
import 'package:mobile_scanner_example/scanner_error_widget.dart';
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.
17+
class BarcodeScannerPicklist extends StatefulWidget {
18+
const BarcodeScannerPicklist({super.key});
19+
20+
@override
21+
State<BarcodeScannerPicklist> createState() => _BarcodeScannerPicklistState();
22+
}
23+
24+
class _BarcodeScannerPicklistState extends State<BarcodeScannerPicklist> {
25+
final _mobileScannerController = MobileScannerController(
26+
// The controller is started from the initState method.
27+
autoStart: false,
28+
);
29+
30+
final orientation = DeviceOrientation.portraitUp;
31+
32+
// On this subscription the barcodes are received.
33+
StreamSubscription<Object?>? _subscription;
34+
35+
// This boolean indicates if the detection of barcodes is enabled or
36+
// temporarily suspended.
37+
final _scannerEnabled = ValueNotifier(true);
38+
39+
// This boolean is used to prevent multiple pops.
40+
var _validBarcodeFound = false;
41+
42+
@override
43+
void initState() {
44+
// Lock to portrait (may not work on iPad with multitasking).
45+
SystemChrome.setPreferredOrientations([orientation]);
46+
// Get a stream subscription and listen to received barcodes.
47+
_subscription = _mobileScannerController.barcodes.listen(_handleBarcodes);
48+
super.initState();
49+
// Start the controller to start scanning.
50+
unawaited(_mobileScannerController.start());
51+
}
52+
53+
@override
54+
void dispose() {
55+
// Cancel the stream subscription.
56+
unawaited(_subscription?.cancel());
57+
_subscription = null;
58+
super.dispose();
59+
// Dispose the controller.
60+
_mobileScannerController.dispose();
61+
}
62+
63+
// Check the list of barcodes only if scannerEnables is true.
64+
// Only take the barcode that is at the center of the image.
65+
// Return the barcode found to the calling page with the help of the
66+
// navigator.
67+
void _handleBarcodes(BarcodeCapture barcodeCapture) {
68+
// Discard all events when the scanner is disabled or when already a valid
69+
// barcode is found.
70+
if (!_scannerEnabled.value || _validBarcodeFound) {
71+
return;
72+
}
73+
final barcode = findBarcodeAtCenter(barcodeCapture, orientation);
74+
if (barcode != null) {
75+
_validBarcodeFound = true;
76+
Navigator.of(context).pop(barcode);
77+
}
78+
}
79+
80+
@override
81+
Widget build(BuildContext context) {
82+
return PopScope(
83+
onPopInvokedWithResult: (didPop, result) {
84+
// Reset the page orientation to the system default values, when this page is popped
85+
if (!didPop) {
86+
return;
87+
}
88+
SystemChrome.setPreferredOrientations(<DeviceOrientation>[]);
89+
},
90+
child: Scaffold(
91+
appBar: AppBar(title: const Text('Picklist scanner')),
92+
backgroundColor: Colors.black,
93+
body: Listener(
94+
// Detect if the user touches the screen and disable/enable the scanner accordingly
95+
behavior: HitTestBehavior.opaque,
96+
onPointerDown: (_) => _scannerEnabled.value = false,
97+
onPointerUp: (_) => _scannerEnabled.value = true,
98+
onPointerCancel: (_) => _scannerEnabled.value = true,
99+
// A stack containing the image feed and the crosshair
100+
// 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.
101+
child: Stack(
102+
fit: StackFit.expand,
103+
children: [
104+
MobileScanner(
105+
controller: _mobileScannerController,
106+
errorBuilder: (context, error, child) =>
107+
ScannerErrorWidget(error: error),
108+
fit: BoxFit.contain,
109+
),
110+
Crosshair(_scannerEnabled),
111+
],
112+
),
113+
),
114+
),
115+
);
116+
}
117+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import 'package:flutter/services.dart';
2+
import 'package:mobile_scanner/mobile_scanner.dart';
3+
import 'package:mobile_scanner_example/picklist/classes/fix_coordinate_space.dart';
4+
5+
/// This function finds the barcode that touches the center of the
6+
/// image. If no barcode is found that touches the center, null is returned.
7+
/// See [_BarcodeScannerPicklistState] and the returnImage option for more info.
8+
///
9+
/// https://github.com/juliansteenbakker/mobile_scanner/issues/1183
10+
Barcode? findBarcodeAtCenter(
11+
BarcodeCapture barcodeCapture,
12+
DeviceOrientation orientation,
13+
) {
14+
final imageSize = fixPortraitLandscape(barcodeCapture.size, orientation);
15+
for (final barcode in barcodeCapture.barcodes) {
16+
final corners = fixCorners(barcode.corners);
17+
if (_isPolygonTouchingTheCenter(
18+
imageSize: imageSize,
19+
polygon: corners,
20+
)) {
21+
return barcode;
22+
}
23+
}
24+
return null;
25+
}
26+
27+
/// Check if the polygon, represented by a list of offsets, touches the center of
28+
/// an image when the size of the image is given.
29+
bool _isPolygonTouchingTheCenter({
30+
required Size imageSize,
31+
required List<Offset> polygon,
32+
}) {
33+
final centerOfCameraOutput = Offset(
34+
imageSize.width / 2,
35+
imageSize.height / 2,
36+
);
37+
return _isPointInPolygon(
38+
point: centerOfCameraOutput,
39+
polygon: polygon,
40+
);
41+
}
42+
43+
/// Credits to chatGPT:
44+
/// Checks if a given [point] is inside the [polygon] boundaries.
45+
///
46+
/// Parameters:
47+
/// - [point]: The `Offset` (usually represents a point in 2D space) to check.
48+
/// - [polygon]: A List of `Offset` representing the vertices of the polygon.
49+
///
50+
/// Returns:
51+
/// - A boolean value: `true` if the point is inside the polygon, or `false` otherwise.
52+
///
53+
/// Uses the ray-casting algorithm based on the Jordan curve theorem.
54+
bool _isPointInPolygon({
55+
required Offset point,
56+
required List<Offset> polygon,
57+
}) {
58+
// Initial variables:
59+
int i; // Loop variable for current vertex
60+
int j = polygon.length -
61+
1; // Last vertex index, initialized to the last vertex of the polygon
62+
bool inside = false; // Boolean flag initialized to false
63+
64+
// Loop through each edge of the polygon
65+
for (i = 0; i < polygon.length; j = i++) {
66+
// Check if point's y-coordinate is within the y-boundaries of the edge
67+
if (((polygon[i].dy > point.dy) != (polygon[j].dy > point.dy)) &&
68+
// Check if the point's x-coordinate is to the left of the edge
69+
(point.dx <
70+
(polygon[j].dx -
71+
polygon[i]
72+
.dx) * // Horizontal distance between the vertices of the edge
73+
(point.dy -
74+
polygon[i]
75+
.dy) / // Scale factor based on the y-distance of the point to the lower vertex
76+
(polygon[j].dy -
77+
polygon[i]
78+
.dy) + // Vertical distance between the vertices of the edge
79+
polygon[i].dx)) {
80+
// Horizontal position of the lower vertex
81+
// If the ray intersects the polygon edge, invert the inside flag
82+
inside = !inside;
83+
}
84+
}
85+
// Return the status of the inside flag which tells if the point is inside the polygon or not
86+
return inside;
87+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import 'package:flutter/services.dart';
2+
3+
Size fixPortraitLandscape(
4+
Size imageSize,
5+
DeviceOrientation orientation,
6+
) {
7+
switch (orientation) {
8+
case DeviceOrientation.portraitUp:
9+
case DeviceOrientation.portraitDown:
10+
return Size(imageSize.shortestSide, imageSize.longestSide);
11+
case DeviceOrientation.landscapeLeft:
12+
case DeviceOrientation.landscapeRight:
13+
return Size(imageSize.longestSide, imageSize.shortestSide);
14+
}
15+
}
16+
17+
List<Offset> fixCorners(List<Offset> corners) {
18+
// Clone the original list to avoid side-effects
19+
final sorted = List<Offset>.from(corners);
20+
21+
sorted.sort((a, b) {
22+
// Prioritize y-axis (dy), and within that, the x-axis (dx)
23+
int compare = a.dy.compareTo(b.dy);
24+
if (compare == 0) {
25+
compare = a.dx.compareTo(b.dx);
26+
}
27+
return compare;
28+
});
29+
30+
final topLeft = sorted.first; // smallest x, smallest y
31+
final topRight = sorted[1]; // larger x, smaller y
32+
final bottomLeft = sorted[2]; // smaller x, larger y
33+
final bottomRight = sorted.last; // larger x, larger y
34+
35+
return [topLeft, topRight, bottomRight, bottomLeft];
36+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:mobile_scanner/mobile_scanner.dart';
3+
import 'package:mobile_scanner_example/picklist/barcode_scanner_picklist.dart';
4+
5+
class PicklistResult extends StatefulWidget {
6+
const PicklistResult({super.key});
7+
8+
@override
9+
State<PicklistResult> createState() => _PicklistResultState();
10+
}
11+
12+
class _PicklistResultState extends State<PicklistResult> {
13+
String barcode = 'Scan Something!';
14+
15+
@override
16+
Widget build(BuildContext context) {
17+
return Scaffold(
18+
appBar: AppBar(title: const Text('Picklist result')),
19+
body: SafeArea(
20+
child: Padding(
21+
padding: const EdgeInsets.symmetric(horizontal: 16.0),
22+
child: Center(
23+
child: Column(
24+
mainAxisAlignment: MainAxisAlignment.center,
25+
children: [
26+
Text(barcode),
27+
ElevatedButton(
28+
onPressed: () async {
29+
final scannedBarcode =
30+
await Navigator.of(context).push<Barcode>(
31+
MaterialPageRoute(
32+
builder: (context) => const BarcodeScannerPicklist(),
33+
),
34+
);
35+
setState(
36+
() {
37+
if (scannedBarcode == null) {
38+
barcode = 'Scan Something!';
39+
return;
40+
}
41+
if (scannedBarcode.displayValue == null) {
42+
barcode = '>>binary<<';
43+
return;
44+
}
45+
barcode = scannedBarcode.displayValue!;
46+
},
47+
);
48+
},
49+
child: const Text('Scan'),
50+
),
51+
],
52+
),
53+
),
54+
),
55+
),
56+
);
57+
}
58+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import 'package:flutter/foundation.dart';
2+
import 'package:flutter/material.dart';
3+
4+
class Crosshair extends StatelessWidget {
5+
const Crosshair(
6+
this.scannerEnabled, {
7+
super.key,
8+
});
9+
10+
final ValueListenable<bool> scannerEnabled;
11+
12+
@override
13+
Widget build(BuildContext context) {
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+
},
24+
);
25+
}
26+
}

0 commit comments

Comments
 (0)