Julian Steenbakker
Committed by GitHub

Merge pull request #1188 from jsroest/master

feat: Add picklist mode sample to the example app
# Uncomment this line to define a global platform for your project
# platform :ios, '15.5.0'
platform :ios, '15.5.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
... ...
... ... @@ -8,6 +8,7 @@ import 'package:mobile_scanner_example/barcode_scanner_simple.dart';
import 'package:mobile_scanner_example/barcode_scanner_window.dart';
import 'package:mobile_scanner_example/barcode_scanner_zoom.dart';
import 'package:mobile_scanner_example/mobile_scanner_overlay.dart';
import 'package:mobile_scanner_example/picklist/picklist_result.dart';
void main() {
runApp(
... ... @@ -91,6 +92,11 @@ class MyHome extends StatelessWidget {
'Analyze image from file',
const BarcodeScannerAnalyzeImage(),
),
_buildItem(
context,
'Picklist mode',
const PicklistResult(),
),
],
),
),
... ...
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:mobile_scanner_example/picklist/classes/barcode_at_center.dart';
import 'package:mobile_scanner_example/picklist/widgets/crosshair.dart';
import 'package:mobile_scanner_example/scanner_error_widget.dart';
// This sample implements picklist functionality.
// The scanning can temporarily be suspended by the user by touching the screen.
// When the scanning is active, the crosshair turns red.
// When the scanning is suspended, the crosshair turns green.
// A barcode has to touch the center of viewfinder to be scanned.
// Therefore the Crosshair widget needs to be placed at the center of the
// MobileScanner widget to visually line up.
class BarcodeScannerPicklist extends StatefulWidget {
const BarcodeScannerPicklist({super.key});
@override
State<BarcodeScannerPicklist> createState() => _BarcodeScannerPicklistState();
}
class _BarcodeScannerPicklistState extends State<BarcodeScannerPicklist> {
final _mobileScannerController = MobileScannerController(
// The controller is started from the initState method.
autoStart: false,
);
final orientation = DeviceOrientation.portraitUp;
// On this subscription the barcodes are received.
StreamSubscription<Object?>? _subscription;
// This boolean indicates if the detection of barcodes is enabled or
// temporarily suspended.
final _scannerEnabled = ValueNotifier(true);
// This boolean is used to prevent multiple pops.
var _validBarcodeFound = false;
@override
void initState() {
// Lock to portrait (may not work on iPad with multitasking).
SystemChrome.setPreferredOrientations([orientation]);
// Get a stream subscription and listen to received barcodes.
_subscription = _mobileScannerController.barcodes.listen(_handleBarcodes);
super.initState();
// Start the controller to start scanning.
unawaited(_mobileScannerController.start());
}
@override
void dispose() {
// Cancel the stream subscription.
unawaited(_subscription?.cancel());
_subscription = null;
super.dispose();
// Dispose the controller.
_mobileScannerController.dispose();
}
// Check the list of barcodes only if scannerEnables is true.
// Only take the barcode that is at the center of the image.
// Return the barcode found to the calling page with the help of the
// navigator.
void _handleBarcodes(BarcodeCapture barcodeCapture) {
// Discard all events when the scanner is disabled or when already a valid
// barcode is found.
if (!_scannerEnabled.value || _validBarcodeFound) {
return;
}
final barcode = findBarcodeAtCenter(barcodeCapture, orientation);
if (barcode != null) {
_validBarcodeFound = true;
Navigator.of(context).pop(barcode);
}
}
@override
Widget build(BuildContext context) {
return PopScope(
onPopInvokedWithResult: (didPop, result) {
// Reset the page orientation to the system default values, when this page is popped
if (!didPop) {
return;
}
SystemChrome.setPreferredOrientations(<DeviceOrientation>[]);
},
child: Scaffold(
appBar: AppBar(title: const Text('Picklist scanner')),
backgroundColor: Colors.black,
body: Listener(
// Detect if the user touches the screen and disable/enable the scanner accordingly
behavior: HitTestBehavior.opaque,
onPointerDown: (_) => _scannerEnabled.value = false,
onPointerUp: (_) => _scannerEnabled.value = true,
onPointerCancel: (_) => _scannerEnabled.value = true,
// A stack containing the image feed and the crosshair
// 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.
child: Stack(
fit: StackFit.expand,
children: [
MobileScanner(
controller: _mobileScannerController,
errorBuilder: (context, error, child) =>
ScannerErrorWidget(error: error),
fit: BoxFit.contain,
),
Crosshair(_scannerEnabled),
],
),
),
),
);
}
}
... ...
import 'package:flutter/services.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:mobile_scanner_example/picklist/classes/fix_coordinate_space.dart';
/// This function finds the barcode that touches the center of the
/// image. If no barcode is found that touches the center, null is returned.
/// See [_BarcodeScannerPicklistState] and the returnImage option for more info.
///
/// https://github.com/juliansteenbakker/mobile_scanner/issues/1183
Barcode? findBarcodeAtCenter(
BarcodeCapture barcodeCapture,
DeviceOrientation orientation,
) {
final imageSize = fixPortraitLandscape(barcodeCapture.size, orientation);
for (final barcode in barcodeCapture.barcodes) {
final corners = fixCorners(barcode.corners);
if (_isPolygonTouchingTheCenter(
imageSize: imageSize,
polygon: corners,
)) {
return barcode;
}
}
return null;
}
/// Check if the polygon, represented by a list of offsets, touches the center of
/// an image when the size of the image is given.
bool _isPolygonTouchingTheCenter({
required Size imageSize,
required List<Offset> polygon,
}) {
final centerOfCameraOutput = Offset(
imageSize.width / 2,
imageSize.height / 2,
);
return _isPointInPolygon(
point: centerOfCameraOutput,
polygon: polygon,
);
}
/// Credits to chatGPT:
/// Checks if a given [point] is inside the [polygon] boundaries.
///
/// Parameters:
/// - [point]: The `Offset` (usually represents a point in 2D space) to check.
/// - [polygon]: A List of `Offset` representing the vertices of the polygon.
///
/// Returns:
/// - A boolean value: `true` if the point is inside the polygon, or `false` otherwise.
///
/// Uses the ray-casting algorithm based on the Jordan curve theorem.
bool _isPointInPolygon({
required Offset point,
required List<Offset> polygon,
}) {
// Initial variables:
int i; // Loop variable for current vertex
int j = polygon.length -
1; // Last vertex index, initialized to the last vertex of the polygon
bool inside = false; // Boolean flag initialized to false
// Loop through each edge of the polygon
for (i = 0; i < polygon.length; j = i++) {
// Check if point's y-coordinate is within the y-boundaries of the edge
if (((polygon[i].dy > point.dy) != (polygon[j].dy > point.dy)) &&
// Check if the point's x-coordinate is to the left of the edge
(point.dx <
(polygon[j].dx -
polygon[i]
.dx) * // Horizontal distance between the vertices of the edge
(point.dy -
polygon[i]
.dy) / // Scale factor based on the y-distance of the point to the lower vertex
(polygon[j].dy -
polygon[i]
.dy) + // Vertical distance between the vertices of the edge
polygon[i].dx)) {
// Horizontal position of the lower vertex
// If the ray intersects the polygon edge, invert the inside flag
inside = !inside;
}
}
// Return the status of the inside flag which tells if the point is inside the polygon or not
return inside;
}
... ...
import 'package:flutter/services.dart';
Size fixPortraitLandscape(
Size imageSize,
DeviceOrientation orientation,
) {
switch (orientation) {
case DeviceOrientation.portraitUp:
case DeviceOrientation.portraitDown:
return Size(imageSize.shortestSide, imageSize.longestSide);
case DeviceOrientation.landscapeLeft:
case DeviceOrientation.landscapeRight:
return Size(imageSize.longestSide, imageSize.shortestSide);
}
}
List<Offset> fixCorners(List<Offset> corners) {
// Clone the original list to avoid side-effects
final sorted = List<Offset>.from(corners);
sorted.sort((a, b) {
// Prioritize y-axis (dy), and within that, the x-axis (dx)
int compare = a.dy.compareTo(b.dy);
if (compare == 0) {
compare = a.dx.compareTo(b.dx);
}
return compare;
});
final topLeft = sorted.first; // smallest x, smallest y
final topRight = sorted[1]; // larger x, smaller y
final bottomLeft = sorted[2]; // smaller x, larger y
final bottomRight = sorted.last; // larger x, larger y
return [topLeft, topRight, bottomRight, bottomLeft];
}
... ...
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:mobile_scanner_example/picklist/barcode_scanner_picklist.dart';
class PicklistResult extends StatefulWidget {
const PicklistResult({super.key});
@override
State<PicklistResult> createState() => _PicklistResultState();
}
class _PicklistResultState extends State<PicklistResult> {
String barcode = 'Scan Something!';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Picklist result')),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(barcode),
ElevatedButton(
onPressed: () async {
final scannedBarcode =
await Navigator.of(context).push<Barcode>(
MaterialPageRoute(
builder: (context) => const BarcodeScannerPicklist(),
),
);
setState(
() {
if (scannedBarcode == null) {
barcode = 'Scan Something!';
return;
}
if (scannedBarcode.displayValue == null) {
barcode = '>>binary<<';
return;
}
barcode = scannedBarcode.displayValue!;
},
);
},
child: const Text('Scan'),
),
],
),
),
),
),
);
}
}
... ...
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class Crosshair extends StatelessWidget {
const Crosshair(
this.scannerEnabled, {
super.key,
});
final ValueListenable<bool> scannerEnabled;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: scannerEnabled,
builder: (context, value, child) {
return Center(
child: Icon(
Icons.close,
color: scannerEnabled.value ? Colors.red : Colors.green,
),
);
},
);
}
}
... ...