Sander Roest

Add a "picklist mode" to the example app

@@ -8,8 +8,9 @@ import 'package:mobile_scanner_example/barcode_scanner_simple.dart'; @@ -8,8 +8,9 @@ import 'package:mobile_scanner_example/barcode_scanner_simple.dart';
8 import 'package:mobile_scanner_example/barcode_scanner_window.dart'; 8 import 'package:mobile_scanner_example/barcode_scanner_window.dart';
9 import 'package:mobile_scanner_example/barcode_scanner_zoom.dart'; 9 import 'package:mobile_scanner_example/barcode_scanner_zoom.dart';
10 import 'package:mobile_scanner_example/mobile_scanner_overlay.dart'; 10 import 'package:mobile_scanner_example/mobile_scanner_overlay.dart';
  11 +import 'package:mobile_scanner_example/picklist/picklist_result.dart';
11 12
12 -void main() { 13 +void main() async {
13 runApp( 14 runApp(
14 const MaterialApp( 15 const MaterialApp(
15 title: 'Mobile Scanner Example', 16 title: 'Mobile Scanner Example',
@@ -91,6 +92,11 @@ class MyHome extends StatelessWidget { @@ -91,6 +92,11 @@ class MyHome extends StatelessWidget {
91 'Analyze image from file', 92 'Analyze image from file',
92 const BarcodeScannerAnalyzeImage(), 93 const BarcodeScannerAnalyzeImage(),
93 ), 94 ),
  95 + _buildItem(
  96 + context,
  97 + 'Picklist mode',
  98 + const PicklistResult(),
  99 + ),
94 ], 100 ],
95 ), 101 ),
96 ), 102 ),
  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 + // 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,
  34 + );
  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.
  41 + final _scannerEnabled = ValueNotifier(true);
  42 +
  43 + // This boolean is used to prevent multiple pops.
  44 + var _validBarcodeFound = false;
  45 +
  46 + @override
  47 + void initState() {
  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);
  61 + super.initState();
  62 + // Start the controller to start scanning.
  63 + unawaited(_mobileScannerController.start());
  64 + }
  65 +
  66 + @override
  67 + void dispose() {
  68 + // Cancel the stream subscription.
  69 + unawaited(_subscription?.cancel());
  70 + _subscription = null;
  71 + super.dispose();
  72 + // Dispose the controller.
  73 + _mobileScannerController.dispose();
  74 + }
  75 +
  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) {
  84 + return;
  85 + }
  86 + final barcode = findBarcodeAtCenter(barcodeCapture);
  87 + if (barcode != null) {
  88 + _validBarcodeFound = true;
  89 + Navigator.of(context).pop(barcode);
  90 + }
  91 + }
  92 +
  93 + @override
  94 + Widget build(BuildContext context) {
  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 + ),
  127 + ),
  128 + );
  129 + }
  130 +}
  1 +import 'package:flutter/material.dart';
  2 +import 'package:mobile_scanner/mobile_scanner.dart';
  3 +
  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,
  27 +}) {
  28 + final centerOfCameraOutput = Offset(
  29 + imageSize.width / 2,
  30 + imageSize.height / 2,
  31 + );
  32 + return _isPointInPolygon(
  33 + point: centerOfCameraOutput,
  34 + polygon: polygon,
  35 + );
  36 +}
  37 +
  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.
  49 +bool _isPointInPolygon({
  50 + required Offset point,
  51 + required List<Offset> polygon,
  52 +}) {
  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
  58 +
  59 + // Loop through each edge of the polygon
  60 + for (i = 0; i < polygon.length; j = i++) {
  61 + // Check if point's y-coordinate is within the y-boundaries of the edge
  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
  64 + (point.dx <
  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
  74 + polygon[i].dx)) {
  75 + // Horizontal position of the lower vertex
  76 + // If the ray intersects the polygon edge, invert the inside flag
  77 + inside = !inside;
  78 + }
  79 + }
  80 + // Return the status of the inside flag which tells if the point is inside the polygon or not
  81 + return inside;
  82 +}
  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 +}
  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 +}