Navaron Bracke
Committed by GitHub

Merge pull request #916 from navaronbracke/mobile_scanner_platform_interface

feat: Mobile scanner platform interface
Showing 37 changed files with 2003 additions and 2095 deletions

Too many changes to show.

To preserve performance only 37 of 37+ files are displayed.

... ... @@ -36,7 +36,7 @@ jobs:
- uses: subosito/flutter-action@v2.12.0
with:
cache: true
flutter-version: '3.13'
flutter-version: '3.19'
channel: 'stable'
- name: Version
run: flutter doctor -v
... ...
## 5.0.0-beta.1
**BREAKING CHANGES:**
* Flutter 3.19.0 is now required.
* The `width` and `height` of `BarcodeCapture` have been removed, in favor of `size`.
* The `raw` attribute is now `Object?` instead of `dynamic`, so that it participates in type promotion.
* The `MobileScannerArguments` class has been removed from the public API, as it is an internal type.
* The `cameraFacingOverride` named argument for the `start()` method has been renamed to `cameraDirection`.
* The `analyzeImage` function now correctly returns a `BarcodeCapture?` instead of a boolean.
* The `formats` attribute of the `MobileScannerController` is now non-null.
* The `MobileScannerState` enum has been renamed to `MobileScannerAuthorizationState`.
* The various `ValueNotifier`s for the camera state have been removed. Use the `value` of the `MobileScannerController` instead.
* The `hasTorch` getter has been removed. Instead, use the torch state of the controller's value.
The `TorchState` enum now provides a new value for unavailable flashlights.
* The `autoStart` attribute has been removed from the `MobileScannerController`. The controller should be manually started on-demand.
* A controller is now required for the `MobileScanner` widget.
* The `onPermissionSet`, `onStart` and `onScannerStarted` methods have been removed from the `MobileScanner` widget. Instead, await `MobileScannerController.start()`.
* The `startDelay` has been removed from the `MobileScanner` widget. Instead, use a delay between manual starts of one or more controllers.
* The `onDetect` method has been removed from the `MobileScanner` widget. Instead, listen to `MobileScannerController.barcodes` directly.
* The `overlay` widget of the `MobileScanner` has been replaced by a new property, `overlayBuilder`, which provides the constraints for the overlay.
* The torch can no longer be toggled on the web, as this is only available for image tracks and not video tracks. As a result the torch state for the web will always be `TorchState.unavailable`.
* The zoom scale can no longer be modified on the web, as this is only available for image tracks and not video tracks. As a result, the zoom scale will always be `1.0`.
Improvements:
* The `MobileScannerController` is now a ChangeNotifier, with `MobileScannerState` as its model.
* The web implementation now supports alternate URLs for loading the barcode library.
## 4.0.1
Bugs fixed:
* [iOS] Fixed a crash with a nil capture session when starting the camera. (thanks @navaronbracke !)
## 4.0.0
BREAKING CHANGES:
**BREAKING CHANGES:**
* [Android] compileSdk has been upgraded to version 34.
* [Android] Java version has been upgraded to version 17.
... ... @@ -186,7 +216,8 @@ Deprecated:
* The `onStart` method has been renamed to `onScannerStarted`.
* The `onPermissionSet` argument of the `MobileScannerController` is now deprecated.
Breaking changes:
**BREAKING CHANGES:**
* `MobileScannerException` now uses an `errorCode` instead of a `message`.
* `MobileScannerException` now contains additional details from the original error.
* Refactored `MobileScannerController.start()` to throw `MobileScannerException`s
... ... @@ -223,7 +254,9 @@ Fixes:
* [iOS] Fix crash when changing torch state
## 3.0.0-beta.2
Breaking changes:
**BREAKING CHANGES:**
* The arguments parameter of onDetect is removed. The data is now returned by the onStart callback
in the MobileScanner widget.
* onDetect now returns the object BarcodeCapture, which contains a List of barcodes and, if enabled, an image.
... ... @@ -243,7 +276,9 @@ Other improvements:
* [iOS] Updated POD dependencies
## 3.0.0-beta.1
Breaking changes:
**BREAKING CHANGES:**
* [Android] SDK updated to SDK 33.
Features:
... ... @@ -259,7 +294,9 @@ Other changes:
* Several minor code improvements
## 2.0.0
Breaking changes:
**BREAKING CHANGES:**
This version is only compatible with flutter 3.0.0 and later.
## 1.1.2-play-services
... ... @@ -293,7 +330,9 @@ Bugfixes:
* Upgraded several dependencies.
## 1.0.0
BREAKING CHANGES:
**BREAKING CHANGES:**
This version adds a new allowDuplicates option which now defaults to FALSE. this means that it will only call onDetect once after a scan.
If you still want duplicates, you can set allowDuplicates to true.
This also means that you don't have to check for duplicates yourself anymore.
... ...
... ... @@ -7,17 +7,15 @@
A universal scanner for Flutter based on MLKit. Uses CameraX on Android and AVFoundation on iOS.
## Features Supported
See the example app for detailed implementation information.
| Features | Android | iOS | macOS | Web |
|------------------------|--------------------|--------------------|-------|-----|
| analyzeImage (Gallery) | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: |
| returnImage | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: |
| scanWindow | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: |
| barcodeOverlay | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: |
| Features | Android | iOS | macOS | Web |
|------------------------|--------------------|--------------------|----------------------|-----|
| analyzeImage (Gallery) | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: |
| returnImage | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: |
| scanWindow | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: |
## Platform Support
... ... @@ -26,6 +24,7 @@ See the example app for detailed implementation information.
| ✔ | ✔ | ✔ | ✔ | :x: | :x: |
## Platform specific setup
### Android
This package uses by default the **bundled version** of MLKit Barcode-scanning for Android. This version is immediately available to the device. But it will increase the size of the app by approximately 3 to 10 MB.
... ... @@ -61,194 +60,110 @@ Ensure that you granted camera permission in XCode -> Signing & Capabilities:
<img width="696" alt="Screenshot of XCode where Camera is checked" src="https://user-images.githubusercontent.com/24459435/193464115-d76f81d0-6355-4cb2-8bee-538e413a3ad0.png">
## Web
This package uses ZXing on web to read barcodes so it needs to be included in `index.html` as script.
```html
<script src="https://unpkg.com/@zxing/library@0.19.1" type="application/javascript"></script>
```
## Usage
As of version 5.0.0 adding the library to the `index.html` is no longer required,
as the library is automatically loaded on first use.
Import `package:mobile_scanner/mobile_scanner.dart`, and use the widget with or without the controller.
### Providing a mirror for the barcode scanning library
If you don't provide a controller, you can't control functions like the torch(flash) or switching camera.
If you don't set `detectionSpeed` to `DetectionSpeed.noDuplicates`, you can get multiple scans in a very short time, causing things like pop() to fire lots of times.
Example without controller:
If a different mirror is needed to load the barcode scanning library,
the source URL can be set beforehand.
```dart
import 'package:flutter/foundation.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Mobile Scanner')),
body: MobileScanner(
// fit: BoxFit.contain,
onDetect: (capture) {
final List<Barcode> barcodes = capture.barcodes;
final Uint8List? image = capture.image;
for (final barcode in barcodes) {
debugPrint('Barcode found! ${barcode.rawValue}');
}
},
),
);
}
final String scriptUrl = // ...
if (kIsWeb) {
MobileScannerPlatform.instance.setBarcodeLibraryScriptUrl(scriptUrl);
}
```
Example with controller and initial values:
## Usage
Import the package with `package:mobile_scanner/mobile_scanner.dart`.
Create a new `MobileScannerController` controller, using the required options.
Provide a `StreamSubscription` for the barcode events.
```dart
import 'package:mobile_scanner/mobile_scanner.dart';
final MobileScannerController controller = MobileScannerController(
// required options for the scanner
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Mobile Scanner')),
body: MobileScanner(
// fit: BoxFit.contain,
controller: MobileScannerController(
detectionSpeed: DetectionSpeed.normal,
facing: CameraFacing.front,
torchEnabled: true,
),
onDetect: (capture) {
final List<Barcode> barcodes = capture.barcodes;
final Uint8List? image = capture.image;
for (final barcode in barcodes) {
debugPrint('Barcode found! ${barcode.rawValue}');
}
},
),
);
}
StreamSubscription<Object?>? _subscription;
```
Example with controller and torch & camera controls:
Ensure that your `State` class mixes in `WidgetsBindingObserver`, to handle lifecyle changes:
```dart
import 'package:mobile_scanner/mobile_scanner.dart';
MobileScannerController cameraController = MobileScannerController();
class MyState extends State<MyStatefulWidget> with WidgetsBindingObserver {
// ...
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Mobile Scanner'),
actions: [
IconButton(
color: Colors.white,
icon: ValueListenableBuilder(
valueListenable: cameraController.torchState,
builder: (context, state, child) {
switch (state as TorchState) {
case TorchState.off:
return const Icon(Icons.flash_off, color: Colors.grey);
case TorchState.on:
return const Icon(Icons.flash_on, color: Colors.yellow);
}
},
),
iconSize: 32.0,
onPressed: () => cameraController.toggleTorch(),
),
IconButton(
color: Colors.white,
icon: ValueListenableBuilder(
valueListenable: cameraController.cameraFacingState,
builder: (context, state, child) {
switch (state as CameraFacing) {
case CameraFacing.front:
return const Icon(Icons.camera_front);
case CameraFacing.back:
return const Icon(Icons.camera_rear);
}
},
),
iconSize: 32.0,
onPressed: () => cameraController.switchCamera(),
),
],
),
body: MobileScanner(
// fit: BoxFit.contain,
controller: cameraController,
onDetect: (capture) {
final List<Barcode> barcodes = capture.barcodes;
final Uint8List? image = capture.image;
for (final barcode in barcodes) {
debugPrint('Barcode found! ${barcode.rawValue}');
}
},
),
);
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
switch (state) {
case AppLifecycleState.detached:
case AppLifecycleState.hidden:
case AppLifecycleState.paused:
return;
case AppLifecycleState.resumed:
// Restart the scanner when the app is resumed.
// Don't forget to resume listening to the barcode events.
_subscription = controller.barcodes.listen(_handleBarcode);
unawaited(controller.start());
case AppLifecycleState.inactive:
// Stop the scanner when the app is paused.
// Also stop the barcode events subscription.
unawaited(_subscription?.cancel());
_subscription = null;
unawaited(controller.stop());
}
}
// ...
}
```
Example with controller and returning images
Then, start the scanner in `void initState()`:
```dart
import 'package:mobile_scanner/mobile_scanner.dart';
@override
void initState() {
super.initState();
// Start listening to lifecycle changes.
WidgetsBinding.instance.addObserver(this);
// Start listening to the barcode events.
_subscription = controller.barcodes.listen(_handleBarcode);
// Finally, start the scanner itself.
unawaited(controller.start());
}
```
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Mobile Scanner')),
body: MobileScanner(
fit: BoxFit.contain,
controller: MobileScannerController(
// facing: CameraFacing.back,
// torchEnabled: false,
returnImage: true,
),
onDetect: (capture) {
final List<Barcode> barcodes = capture.barcodes;
final Uint8List? image = capture.image;
for (final barcode in barcodes) {
debugPrint('Barcode found! ${barcode.rawValue}');
}
if (image != null) {
showDialog(
context: context,
builder: (context) =>
Image(image: MemoryImage(image)),
);
Future.delayed(const Duration(seconds: 5), () {
Navigator.pop(context);
});
}
},
),
);
}
Finally, dispose of the the `MobileScannerController` when you are done with it.
```dart
@override
Future<void> dispose() async {
// Stop listening to lifecycle changes.
WidgetsBinding.instance.removeObserver(this);
// Stop listening to the barcode events.
unawaited(_subscription?.cancel());
_subscription = null;
// Dispose the widget itself.
super.dispose();
// Finally, dispose of the controller.
await controller.dispose();
}
```
### BarcodeCapture
The onDetect function returns a BarcodeCapture objects which contains the following items.
| Property name | Type | Description |
|---------------|---------------|-----------------------------------|
| barcodes | List<Barcode> | A list with scanned barcodes. |
| image | Uint8List? | If enabled, an image of the scan. |
You can use the following properties of the Barcode object.
| Property name | Type | Description |
|---------------|----------------|-------------------------------------|
| format | BarcodeFormat | |
| rawBytes | Uint8List? | binary scan result |
| rawValue | String? | Value if barcode is in UTF-8 format |
| displayValue | String? | |
| type | BarcodeType | |
| calendarEvent | CalendarEvent? | |
| contactInfo | ContactInfo? | |
| driverLicense | DriverLicense? | |
| email | Email? | |
| geoPoint | GeoPoint? | |
| phone | Phone? | |
| sms | SMS? | |
| url | UrlBookmark? | |
| wifi | WiFi? | WiFi Access-Point details |
To display the camera preview, pass the controller to a `MobileScanner` widget.
See the examples for runnable examples of various usages,
such as the basic usage, applying a scan window, or retrieving images from the barcodes.
... ...
... ... @@ -78,7 +78,7 @@ class MobileScanner(
scanner.process(inputImage)
.addOnSuccessListener { barcodes ->
if (detectionSpeed == DetectionSpeed.NO_DUPLICATES) {
val newScannedBarcodes = barcodes.mapNotNull({ barcode -> barcode.rawValue }).sorted()
val newScannedBarcodes = barcodes.mapNotNull { barcode -> barcode.rawValue }.sorted()
if (newScannedBarcodes == lastScanned) {
// New scanned is duplicate, returning
return@addOnSuccessListener
... ... @@ -424,7 +424,7 @@ class MobileScanner(
/**
* Analyze a single image.
*/
fun analyzeImage(image: Uri, analyzerCallback: AnalyzerCallback) {
fun analyzeImage(image: Uri, onSuccess: AnalyzerSuccessCallback, onError: AnalyzerErrorCallback) {
val inputImage = InputImage.fromFilePath(activity, image)
scanner.process(inputImage)
... ... @@ -432,15 +432,13 @@ class MobileScanner(
val barcodeMap = barcodes.map { barcode -> barcode.data }
if (barcodeMap.isNotEmpty()) {
analyzerCallback(barcodeMap)
onSuccess(barcodeMap)
} else {
analyzerCallback(null)
onSuccess(null)
}
}
.addOnFailureListener { e ->
mobileScannerErrorCallback(
e.localizedMessage ?: e.toString()
)
onError(e.localizedMessage ?: e.toString())
}
}
... ...
... ... @@ -3,7 +3,8 @@ package dev.steenbakker.mobile_scanner
import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters
typealias MobileScannerCallback = (barcodes: List<Map<String, Any?>>, image: ByteArray?, width: Int?, height: Int?) -> Unit
typealias AnalyzerCallback = (barcodes: List<Map<String, Any?>>?) -> Unit
typealias AnalyzerErrorCallback = (message: String) -> Unit
typealias AnalyzerSuccessCallback = (barcodes: List<Map<String, Any?>>?) -> Unit
typealias MobileScannerErrorCallback = (error: String) -> Unit
typealias TorchStateCallback = (state: Int) -> Unit
typealias ZoomScaleStateCallback = (zoomScale: Double) -> Unit
... ...
... ... @@ -26,16 +26,19 @@ class MobileScannerHandler(
private val addPermissionListener: (RequestPermissionsResultListener) -> Unit,
textureRegistry: TextureRegistry): MethodChannel.MethodCallHandler {
private val analyzerCallback: AnalyzerCallback = { barcodes: List<Map<String, Any?>>?->
if (barcodes != null) {
barcodeHandler.publishEvent(mapOf(
"name" to "barcode",
"data" to barcodes
))
private val analyzeImageErrorCallback: AnalyzerErrorCallback = {
Handler(Looper.getMainLooper()).post {
analyzerResult?.error("MobileScanner", it, null)
analyzerResult = null
}
}
private val analyzeImageSuccessCallback: AnalyzerSuccessCallback = {
Handler(Looper.getMainLooper()).post {
analyzerResult?.success(barcodes != null)
analyzerResult?.success(mapOf(
"name" to "barcode",
"data" to it
))
analyzerResult = null
}
}
... ... @@ -236,7 +239,8 @@ class MobileScannerHandler(
private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) {
analyzerResult = result
val uri = Uri.fromFile(File(call.arguments.toString()))
mobileScanner!!.analyzeImage(uri, analyzerCallback)
mobileScanner!!.analyzeImage(uri, analyzeImageSuccessCallback, analyzeImageErrorCallback)
}
private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) {
... ... @@ -265,7 +269,7 @@ class MobileScannerHandler(
}
private fun updateScanWindow(call: MethodCall, result: MethodChannel.Result) {
mobileScanner!!.scanWindow = call.argument<List<Float>?>("rect")
mobileScanner?.scanWindow = call.argument<List<Float>?>("rect")
result.success(null)
}
... ...
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:mobile_scanner_example/scanner_error_widget.dart';
class BarcodeListScannerWithController extends StatefulWidget {
const BarcodeListScannerWithController({super.key});
@override
State<BarcodeListScannerWithController> createState() =>
_BarcodeListScannerWithControllerState();
}
class _BarcodeListScannerWithControllerState
extends State<BarcodeListScannerWithController>
with SingleTickerProviderStateMixin {
BarcodeCapture? barcodeCapture;
final MobileScannerController controller = MobileScannerController(
torchEnabled: true,
// formats: [BarcodeFormat.qrCode]
// facing: CameraFacing.front,
// detectionSpeed: DetectionSpeed.normal
// detectionTimeoutMs: 1000,
// returnImage: false,
);
bool isStarted = true;
void _startOrStop() {
try {
if (isStarted) {
controller.stop();
} else {
controller.start();
}
setState(() {
isStarted = !isStarted;
});
} on Exception catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Something went wrong! $e'),
backgroundColor: Colors.red,
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('With ValueListenableBuilder')),
backgroundColor: Colors.black,
body: Builder(
builder: (context) {
return Stack(
children: [
MobileScanner(
controller: controller,
errorBuilder: (context, error, child) {
return ScannerErrorWidget(error: error);
},
fit: BoxFit.contain,
onDetect: (barcodeCapture) {
setState(() {
this.barcodeCapture = barcodeCapture;
});
},
onScannerStarted: (arguments) {
// Do something with arguments.
},
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
alignment: Alignment.bottomCenter,
height: 100,
color: Colors.black.withOpacity(0.4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
color: Colors.white,
icon: ValueListenableBuilder<TorchState>(
valueListenable: controller.torchState,
builder: (context, state, child) {
switch (state) {
case TorchState.off:
return const Icon(
Icons.flash_off,
color: Colors.grey,
);
case TorchState.on:
return const Icon(
Icons.flash_on,
color: Colors.yellow,
);
}
},
),
iconSize: 32.0,
onPressed: () => controller.toggleTorch(),
),
IconButton(
color: Colors.white,
icon: isStarted
? const Icon(Icons.stop)
: const Icon(Icons.play_arrow),
iconSize: 32.0,
onPressed: _startOrStop,
),
Center(
child: SizedBox(
width: MediaQuery.of(context).size.width - 200,
height: 50,
child: FittedBox(
child: Text(
'${barcodeCapture?.barcodes.map((e) => e.rawValue) ?? 'Scan something!'}',
overflow: TextOverflow.fade,
style: Theme.of(context)
.textTheme
.headlineMedium!
.copyWith(color: Colors.white),
),
),
),
),
IconButton(
color: Colors.white,
icon: ValueListenableBuilder<CameraFacing>(
valueListenable: controller.cameraFacingState,
builder: (context, state, child) {
switch (state) {
case CameraFacing.front:
return const Icon(Icons.camera_front);
case CameraFacing.back:
return const Icon(Icons.camera_rear);
}
},
),
iconSize: 32.0,
onPressed: () => controller.switchCamera(),
),
IconButton(
color: Colors.white,
icon: const Icon(Icons.image),
iconSize: 32.0,
onPressed: () async {
final ImagePicker picker = ImagePicker();
// Pick an image
final XFile? image = await picker.pickImage(
source: ImageSource.gallery,
);
if (image != null) {
if (await controller.analyzeImage(image.path)) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Barcode found!'),
backgroundColor: Colors.green,
),
);
} else {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('No barcode found!'),
backgroundColor: Colors.red,
),
);
}
}
},
),
],
),
),
),
],
);
},
),
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:mobile_scanner_example/scanner_button_widgets.dart';
import 'package:mobile_scanner_example/scanner_error_widget.dart';
class BarcodeScannerWithController extends StatefulWidget {
... ... @@ -12,10 +14,7 @@ class BarcodeScannerWithController extends StatefulWidget {
}
class _BarcodeScannerWithControllerState
extends State<BarcodeScannerWithController>
with SingleTickerProviderStateMixin {
BarcodeCapture? barcode;
extends State<BarcodeScannerWithController> with WidgetsBindingObserver {
final MobileScannerController controller = MobileScannerController(
torchEnabled: true, useNewCameraSelector: true,
// formats: [BarcodeFormat.qrCode]
... ... @@ -25,178 +24,106 @@ class _BarcodeScannerWithControllerState
// returnImage: false,
);
bool isStarted = true;
Barcode? _barcode;
StreamSubscription<Object?>? _subscription;
Widget _buildBarcode(Barcode? value) {
if (value == null) {
return const Text(
'Scan something!',
overflow: TextOverflow.fade,
style: TextStyle(color: Colors.white),
);
}
return Text(
value.displayValue ?? 'No display value.',
overflow: TextOverflow.fade,
style: const TextStyle(color: Colors.white),
);
}
void _startOrStop() {
try {
if (isStarted) {
controller.stop();
} else {
controller.start();
}
void _handleBarcode(BarcodeCapture barcodes) {
if (mounted) {
setState(() {
isStarted = !isStarted;
_barcode = barcodes.barcodes.firstOrNull;
});
} on Exception catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Something went wrong! $e'),
backgroundColor: Colors.red,
),
);
}
}
int? numberOfCameras;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_subscription = controller.barcodes.listen(_handleBarcode);
unawaited(controller.start());
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
switch (state) {
case AppLifecycleState.detached:
case AppLifecycleState.hidden:
case AppLifecycleState.paused:
return;
case AppLifecycleState.resumed:
_subscription = controller.barcodes.listen(_handleBarcode);
unawaited(controller.start());
case AppLifecycleState.inactive:
unawaited(_subscription?.cancel());
_subscription = null;
unawaited(controller.stop());
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('With controller')),
backgroundColor: Colors.black,
body: Builder(
builder: (context) {
return Stack(
children: [
MobileScanner(
onScannerStarted: (arguments) {
if (mounted && arguments?.numberOfCameras != null) {
numberOfCameras = arguments!.numberOfCameras;
setState(() {});
}
},
controller: controller,
errorBuilder: (context, error, child) {
return ScannerErrorWidget(error: error);
},
fit: BoxFit.contain,
onDetect: (barcode) {
setState(() {
this.barcode = barcode;
});
},
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
alignment: Alignment.bottomCenter,
height: 100,
color: Colors.black.withOpacity(0.4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ValueListenableBuilder(
valueListenable: controller.hasTorchState,
builder: (context, state, child) {
if (state != true) {
return const SizedBox.shrink();
}
return IconButton(
color: Colors.white,
icon: ValueListenableBuilder<TorchState>(
valueListenable: controller.torchState,
builder: (context, state, child) {
switch (state) {
case TorchState.off:
return const Icon(
Icons.flash_off,
color: Colors.grey,
);
case TorchState.on:
return const Icon(
Icons.flash_on,
color: Colors.yellow,
);
}
},
),
iconSize: 32.0,
onPressed: () => controller.toggleTorch(),
);
},
),
IconButton(
color: Colors.white,
icon: isStarted
? const Icon(Icons.stop)
: const Icon(Icons.play_arrow),
iconSize: 32.0,
onPressed: _startOrStop,
),
Center(
child: SizedBox(
width: MediaQuery.of(context).size.width - 200,
height: 50,
child: FittedBox(
child: Text(
barcode?.barcodes.first.rawValue ??
'Scan something!',
overflow: TextOverflow.fade,
style: Theme.of(context)
.textTheme
.headlineMedium!
.copyWith(color: Colors.white),
),
),
),
),
IconButton(
color: Colors.white,
icon: ValueListenableBuilder<CameraFacing>(
valueListenable: controller.cameraFacingState,
builder: (context, state, child) {
switch (state) {
case CameraFacing.front:
return const Icon(Icons.camera_front);
case CameraFacing.back:
return const Icon(Icons.camera_rear);
}
},
),
iconSize: 32.0,
onPressed: (numberOfCameras ?? 0) < 2
? null
: () => controller.switchCamera(),
),
IconButton(
color: Colors.white,
icon: const Icon(Icons.image),
iconSize: 32.0,
onPressed: () async {
final ImagePicker picker = ImagePicker();
// Pick an image
final XFile? image = await picker.pickImage(
source: ImageSource.gallery,
);
if (image != null) {
if (await controller.analyzeImage(image.path)) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Barcode found!'),
backgroundColor: Colors.green,
),
);
} else {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('No barcode found!'),
backgroundColor: Colors.red,
),
);
}
}
},
),
],
),
),
body: Stack(
children: [
MobileScanner(
controller: controller,
errorBuilder: (context, error, child) {
return ScannerErrorWidget(error: error);
},
fit: BoxFit.contain,
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
alignment: Alignment.bottomCenter,
height: 100,
color: Colors.black.withOpacity(0.4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ToggleFlashlightButton(controller: controller),
StartStopMobileScannerButton(controller: controller),
Expanded(child: Center(child: _buildBarcode(_barcode))),
SwitchCameraButton(controller: controller),
AnalyzeImageFromGalleryButton(controller: controller),
],
),
],
);
},
),
),
],
),
);
}
@override
Future<void> dispose() async {
WidgetsBinding.instance.removeObserver(this);
unawaited(_subscription?.cancel());
_subscription = null;
super.dispose();
await controller.dispose();
}
}
... ...
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:mobile_scanner_example/scanner_button_widgets.dart';
import 'package:mobile_scanner_example/scanner_error_widget.dart';
class BarcodeScannerListView extends StatefulWidget {
const BarcodeScannerListView({super.key});
@override
State<BarcodeScannerListView> createState() => _BarcodeScannerListViewState();
}
class _BarcodeScannerListViewState extends State<BarcodeScannerListView> {
final MobileScannerController controller = MobileScannerController(
torchEnabled: true,
// formats: [BarcodeFormat.qrCode]
// facing: CameraFacing.front,
// detectionSpeed: DetectionSpeed.normal
// detectionTimeoutMs: 1000,
// returnImage: false,
);
@override
void initState() {
super.initState();
controller.start();
}
Widget _buildBarcodesListView() {
return StreamBuilder<BarcodeCapture>(
stream: controller.barcodes,
builder: (context, snapshot) {
final barcodes = snapshot.data?.barcodes;
if (barcodes == null || barcodes.isEmpty) {
return const Center(
child: Text(
'Scan Something!',
style: TextStyle(color: Colors.white, fontSize: 20),
),
);
}
return ListView.builder(
itemCount: barcodes.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
barcodes[index].rawValue ?? 'No raw value',
overflow: TextOverflow.fade,
style: const TextStyle(color: Colors.white),
),
);
},
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('With ListView')),
backgroundColor: Colors.black,
body: Stack(
children: [
MobileScanner(
controller: controller,
errorBuilder: (context, error, child) {
return ScannerErrorWidget(error: error);
},
fit: BoxFit.contain,
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
alignment: Alignment.bottomCenter,
height: 100,
color: Colors.black.withOpacity(0.4),
child: Column(
children: [
Expanded(
child: _buildBarcodesListView(),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ToggleFlashlightButton(controller: controller),
StartStopMobileScannerButton(controller: controller),
const Spacer(),
SwitchCameraButton(controller: controller),
AnalyzeImageFromGalleryButton(controller: controller),
],
),
],
),
),
),
],
),
);
}
@override
Future<void> dispose() async {
super.dispose();
await controller.dispose();
}
}
... ...
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:mobile_scanner_example/scanned_barcode_label.dart';
import 'package:mobile_scanner_example/scanner_error_widget.dart';
class BarcodeScannerPageView extends StatefulWidget {
... ... @@ -9,62 +12,15 @@ class BarcodeScannerPageView extends StatefulWidget {
State<BarcodeScannerPageView> createState() => _BarcodeScannerPageViewState();
}
class _BarcodeScannerPageViewState extends State<BarcodeScannerPageView>
with SingleTickerProviderStateMixin {
BarcodeCapture? capture;
class _BarcodeScannerPageViewState extends State<BarcodeScannerPageView> {
final MobileScannerController controller = MobileScannerController();
Widget cameraView() {
return Builder(
builder: (context) {
return Stack(
children: [
MobileScanner(
startDelay: true,
controller: MobileScannerController(torchEnabled: true),
fit: BoxFit.contain,
errorBuilder: (context, error, child) {
return ScannerErrorWidget(error: error);
},
onDetect: (capture) {
setState(() {
this.capture = capture;
});
},
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
alignment: Alignment.bottomCenter,
height: 100,
color: Colors.black.withOpacity(0.4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Center(
child: SizedBox(
width: MediaQuery.of(context).size.width - 120,
height: 50,
child: FittedBox(
child: Text(
capture?.barcodes.first.rawValue ??
'Scan something!',
overflow: TextOverflow.fade,
style: Theme.of(context)
.textTheme
.headlineMedium!
.copyWith(color: Colors.white),
),
),
),
),
],
),
),
),
],
);
},
);
final PageController pageController = PageController();
@override
void initState() {
super.initState();
unawaited(controller.start());
}
@override
... ... @@ -73,13 +29,68 @@ class _BarcodeScannerPageViewState extends State<BarcodeScannerPageView>
appBar: AppBar(title: const Text('With PageView')),
backgroundColor: Colors.black,
body: PageView(
controller: pageController,
onPageChanged: (index) async {
// Stop the camera view for the current page,
// and then restart the camera for the new page.
await controller.stop();
// When switching pages, add a delay to the next start call.
// Otherwise the camera will start before the next page is displayed.
await Future.delayed(const Duration(seconds: 1, milliseconds: 500));
if (!mounted) {
return;
}
unawaited(controller.start());
},
children: [
cameraView(),
Container(),
cameraView(),
cameraView(),
_BarcodeScannerPage(controller: controller),
const SizedBox(),
_BarcodeScannerPage(controller: controller),
_BarcodeScannerPage(controller: controller),
],
),
);
}
@override
Future<void> dispose() async {
pageController.dispose();
super.dispose();
await controller.dispose();
}
}
class _BarcodeScannerPage extends StatelessWidget {
const _BarcodeScannerPage({required this.controller});
final MobileScannerController controller;
@override
Widget build(BuildContext context) {
return Stack(
children: [
MobileScanner(
controller: controller,
fit: BoxFit.contain,
errorBuilder: (context, error, child) {
return ScannerErrorWidget(error: error);
},
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
alignment: Alignment.bottomCenter,
height: 100,
color: Colors.black.withOpacity(0.4),
child: Center(
child: ScannedBarcodeLabel(barcodes: controller.barcodes),
),
),
),
],
);
}
}
... ...
... ... @@ -2,6 +2,8 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:mobile_scanner_example/scanned_barcode_label.dart';
import 'package:mobile_scanner_example/scanner_button_widgets.dart';
import 'package:mobile_scanner_example/scanner_error_widget.dart';
class BarcodeScannerReturningImage extends StatefulWidget {
... ... @@ -13,11 +15,7 @@ class BarcodeScannerReturningImage extends StatefulWidget {
}
class _BarcodeScannerReturningImageState
extends State<BarcodeScannerReturningImage>
with SingleTickerProviderStateMixin {
BarcodeCapture? barcode;
// MobileScannerArguments? arguments;
extends State<BarcodeScannerReturningImage> {
final MobileScannerController controller = MobileScannerController(
torchEnabled: true,
// formats: [BarcodeFormat.qrCode]
... ... @@ -27,26 +25,10 @@ class _BarcodeScannerReturningImageState
returnImage: true,
);
bool isStarted = true;
void _startOrStop() {
try {
if (isStarted) {
controller.stop();
} else {
controller.start();
}
setState(() {
isStarted = !isStarted;
});
} on Exception catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Something went wrong! $e'),
backgroundColor: Colors.red,
),
);
}
@override
void initState() {
super.initState();
controller.start();
}
@override
... ... @@ -57,20 +39,55 @@ class _BarcodeScannerReturningImageState
child: Column(
children: [
Expanded(
child: barcode?.image != null
? Transform.rotate(
angle: 90 * pi / 180,
child: Image(
gaplessPlayback: true,
image: MemoryImage(barcode!.image!),
fit: BoxFit.contain,
),
)
: const Center(
child: StreamBuilder<BarcodeCapture>(
stream: controller.barcodes,
builder: (context, snapshot) {
final barcode = snapshot.data;
if (barcode == null) {
return const Center(
child: Text(
'Your scanned barcode will appear here!',
),
),
);
}
final barcodeImage = barcode.image;
if (barcodeImage == null) {
return const Center(
child: Text('No image for this barcode.'),
);
}
return Image.memory(
barcodeImage,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return Center(
child: Text('Could not decode image bytes. $error'),
);
},
frameBuilder: (
BuildContext context,
Widget child,
int? frame,
bool? wasSynchronouslyLoaded,
) {
if (wasSynchronouslyLoaded == true || frame != null) {
return Transform.rotate(
angle: 90 * pi / 180,
child: child,
);
}
return const Center(
child: CircularProgressIndicator(),
);
},
);
},
),
),
Expanded(
flex: 2,
... ... @@ -84,11 +101,6 @@ class _BarcodeScannerReturningImageState
return ScannerErrorWidget(error: error);
},
fit: BoxFit.contain,
onDetect: (barcode) {
setState(() {
this.barcode = barcode;
});
},
),
Align(
alignment: Alignment.bottomCenter,
... ... @@ -99,69 +111,18 @@ class _BarcodeScannerReturningImageState
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
color: Colors.white,
icon: ValueListenableBuilder<TorchState>(
valueListenable: controller.torchState,
builder: (context, state, child) {
switch (state) {
case TorchState.off:
return const Icon(
Icons.flash_off,
color: Colors.grey,
);
case TorchState.on:
return const Icon(
Icons.flash_on,
color: Colors.yellow,
);
}
},
),
iconSize: 32.0,
onPressed: () => controller.toggleTorch(),
),
IconButton(
color: Colors.white,
icon: isStarted
? const Icon(Icons.stop)
: const Icon(Icons.play_arrow),
iconSize: 32.0,
onPressed: _startOrStop,
ToggleFlashlightButton(controller: controller),
StartStopMobileScannerButton(
controller: controller,
),
Center(
child: SizedBox(
width: MediaQuery.of(context).size.width - 200,
height: 50,
child: FittedBox(
child: Text(
barcode?.barcodes.first.rawValue ??
'Scan something!',
overflow: TextOverflow.fade,
style: Theme.of(context)
.textTheme
.headlineMedium!
.copyWith(color: Colors.white),
),
Expanded(
child: Center(
child: ScannedBarcodeLabel(
barcodes: controller.barcodes,
),
),
),
IconButton(
color: Colors.white,
icon: ValueListenableBuilder<CameraFacing>(
valueListenable: controller.cameraFacingState,
builder: (context, state, child) {
switch (state) {
case CameraFacing.front:
return const Icon(Icons.camera_front);
case CameraFacing.back:
return const Icon(Icons.camera_rear);
}
},
),
iconSize: 32.0,
onPressed: () => controller.switchCamera(),
),
SwitchCameraButton(controller: controller),
],
),
),
... ... @@ -177,8 +138,8 @@ class _BarcodeScannerReturningImageState
}
@override
void dispose() {
controller.dispose();
Future<void> dispose() async {
super.dispose();
await controller.dispose();
}
}
... ...
... ... @@ -3,6 +3,7 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:mobile_scanner_example/scanned_barcode_label.dart';
import 'package:mobile_scanner_example/scanner_error_widget.dart';
... ... @@ -16,95 +17,120 @@ class BarcodeScannerWithScanWindow extends StatefulWidget {
class _BarcodeScannerWithScanWindowState
extends State<BarcodeScannerWithScanWindow> {
late MobileScannerController controller = MobileScannerController();
Barcode? barcode;
BarcodeCapture? capture;
final MobileScannerController controller = MobileScannerController();
Future<void> onDetect(BarcodeCapture barcode) async {
capture = barcode;
setState(() => this.barcode = barcode.barcodes.first);
@override
void initState() {
super.initState();
controller.start();
}
MobileScannerArguments? arguments;
Widget _buildBarcodeOverlay() {
return ValueListenableBuilder(
valueListenable: controller,
builder: (context, value, child) {
// Not ready.
if (!value.isInitialized || !value.isRunning || value.error != null) {
return const SizedBox();
}
return StreamBuilder<BarcodeCapture>(
stream: controller.barcodes,
builder: (context, snapshot) {
final BarcodeCapture? barcodeCapture = snapshot.data;
// No barcode.
if (barcodeCapture == null || barcodeCapture.barcodes.isEmpty) {
return const SizedBox();
}
final scannedBarcode = barcodeCapture.barcodes.first;
// No barcode corners, or size, or no camera preview size.
if (scannedBarcode.corners.isEmpty ||
value.size.isEmpty ||
barcodeCapture.size.isEmpty) {
return const SizedBox();
}
return CustomPaint(
painter: BarcodeOverlay(
barcodeCorners: scannedBarcode.corners,
barcodeSize: barcodeCapture.size,
boxFit: BoxFit.contain,
cameraPreviewSize: value.size,
),
);
},
);
},
);
}
Widget _buildScanWindow(Rect scanWindowRect) {
return ValueListenableBuilder(
valueListenable: controller,
builder: (context, value, child) {
// Not ready.
if (!value.isInitialized ||
!value.isRunning ||
value.error != null ||
value.size.isEmpty) {
return const SizedBox();
}
return CustomPaint(
painter: ScannerOverlay(scanWindowRect),
);
},
);
}
@override
Widget build(BuildContext context) {
final scanWindow = Rect.fromCenter(
center: MediaQuery.of(context).size.center(Offset.zero),
center: MediaQuery.sizeOf(context).center(Offset.zero),
width: 200,
height: 200,
);
return Scaffold(
appBar: AppBar(title: const Text('With Scan window')),
backgroundColor: Colors.black,
body: Builder(
builder: (context) {
return Stack(
fit: StackFit.expand,
children: [
MobileScanner(
fit: BoxFit.contain,
scanWindow: scanWindow,
controller: controller,
onScannerStarted: (arguments) {
setState(() {
this.arguments = arguments;
});
},
errorBuilder: (context, error, child) {
return ScannerErrorWidget(error: error);
},
onDetect: onDetect,
),
if (barcode != null &&
barcode?.corners != null &&
arguments != null)
CustomPaint(
painter: BarcodeOverlay(
barcode: barcode!,
arguments: arguments!,
boxFit: BoxFit.contain,
capture: capture!,
),
),
CustomPaint(
painter: ScannerOverlay(scanWindow),
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
alignment: Alignment.bottomCenter,
height: 100,
color: Colors.black.withOpacity(0.4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Center(
child: SizedBox(
width: MediaQuery.of(context).size.width - 120,
height: 50,
child: FittedBox(
child: Text(
barcode?.displayValue ?? 'Scan something!',
overflow: TextOverflow.fade,
style: Theme.of(context)
.textTheme
.headlineMedium!
.copyWith(color: Colors.white),
),
),
),
),
],
),
),
),
],
);
},
body: Stack(
fit: StackFit.expand,
children: [
MobileScanner(
fit: BoxFit.contain,
scanWindow: scanWindow,
controller: controller,
errorBuilder: (context, error, child) {
return ScannerErrorWidget(error: error);
},
),
_buildBarcodeOverlay(),
_buildScanWindow(scanWindow),
Align(
alignment: Alignment.bottomCenter,
child: Container(
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
height: 100,
color: Colors.black.withOpacity(0.4),
child: ScannedBarcodeLabel(barcodes: controller.barcodes),
),
),
],
),
);
}
@override
Future<void> dispose() async {
super.dispose();
await controller.dispose();
}
}
class ScannerOverlay extends CustomPainter {
... ... @@ -114,6 +140,8 @@ class ScannerOverlay extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// TODO: use `Offset.zero & size` instead of Rect.largest
// we need to pass the size to the custom paint widget
final backgroundPath = Path()..addRect(Rect.largest);
final cutoutPath = Path()..addRect(scanWindow);
... ... @@ -138,24 +166,26 @@ class ScannerOverlay extends CustomPainter {
class BarcodeOverlay extends CustomPainter {
BarcodeOverlay({
required this.barcode,
required this.arguments,
required this.barcodeCorners,
required this.barcodeSize,
required this.boxFit,
required this.capture,
required this.cameraPreviewSize,
});
final BarcodeCapture capture;
final Barcode barcode;
final MobileScannerArguments arguments;
final List<Offset> barcodeCorners;
final Size barcodeSize;
final BoxFit boxFit;
final Size cameraPreviewSize;
@override
void paint(Canvas canvas, Size size) {
if (barcode.corners.isEmpty) {
if (barcodeCorners.isEmpty ||
barcodeSize.isEmpty ||
cameraPreviewSize.isEmpty) {
return;
}
final adjustedSize = applyBoxFit(boxFit, arguments.size, size);
final adjustedSize = applyBoxFit(boxFit, cameraPreviewSize, size);
double verticalPadding = size.height - adjustedSize.destination.height;
double horizontalPadding = size.width - adjustedSize.destination.width;
... ... @@ -175,22 +205,21 @@ class BarcodeOverlay extends CustomPainter {
final double ratioHeight;
if (!kIsWeb && Platform.isIOS) {
ratioWidth = capture.size.width / adjustedSize.destination.width;
ratioHeight = capture.size.height / adjustedSize.destination.height;
ratioWidth = barcodeSize.width / adjustedSize.destination.width;
ratioHeight = barcodeSize.height / adjustedSize.destination.height;
} else {
ratioWidth = arguments.size.width / adjustedSize.destination.width;
ratioHeight = arguments.size.height / adjustedSize.destination.height;
ratioWidth = cameraPreviewSize.width / adjustedSize.destination.width;
ratioHeight = cameraPreviewSize.height / adjustedSize.destination.height;
}
final List<Offset> adjustedOffset = [];
for (final offset in barcode.corners) {
adjustedOffset.add(
final List<Offset> adjustedOffset = [
for (final offset in barcodeCorners)
Offset(
offset.dx / ratioWidth + horizontalPadding,
offset.dy / ratioHeight + verticalPadding,
),
);
}
];
final cutoutPath = Path()..addPolygon(adjustedOffset, true);
final backgroundPaint = Paint()
... ...
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:mobile_scanner_example/scanner_error_widget.dart';
class BarcodeScannerWithoutController extends StatefulWidget {
const BarcodeScannerWithoutController({super.key});
@override
State<BarcodeScannerWithoutController> createState() =>
_BarcodeScannerWithoutControllerState();
}
class _BarcodeScannerWithoutControllerState
extends State<BarcodeScannerWithoutController>
with SingleTickerProviderStateMixin {
BarcodeCapture? capture;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Without controller')),
backgroundColor: Colors.black,
body: Builder(
builder: (context) {
return Stack(
children: [
MobileScanner(
fit: BoxFit.contain,
errorBuilder: (context, error, child) {
return ScannerErrorWidget(error: error);
},
onDetect: (capture) {
setState(() {
this.capture = capture;
});
},
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
alignment: Alignment.bottomCenter,
height: 100,
color: Colors.black.withOpacity(0.4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Center(
child: SizedBox(
width: MediaQuery.of(context).size.width - 120,
height: 50,
child: FittedBox(
child: Text(
capture?.barcodes.first.rawValue ??
'Scan something!',
overflow: TextOverflow.fade,
style: Theme.of(context)
.textTheme
.headlineMedium!
.copyWith(color: Colors.white),
),
),
),
),
],
),
),
),
],
);
},
),
);
}
}
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:mobile_scanner_example/scanned_barcode_label.dart';
import 'package:mobile_scanner_example/scanner_button_widgets.dart';
import 'package:mobile_scanner_example/scanner_error_widget.dart';
class BarcodeScannerWithZoom extends StatefulWidget {
... ... @@ -11,195 +15,115 @@ class BarcodeScannerWithZoom extends StatefulWidget {
State<BarcodeScannerWithZoom> createState() => _BarcodeScannerWithZoomState();
}
class _BarcodeScannerWithZoomState extends State<BarcodeScannerWithZoom>
with SingleTickerProviderStateMixin {
BarcodeCapture? barcode;
MobileScannerController controller = MobileScannerController(
class _BarcodeScannerWithZoomState extends State<BarcodeScannerWithZoom> {
final MobileScannerController controller = MobileScannerController(
torchEnabled: true,
);
bool isStarted = true;
double _zoomFactor = 0.0;
@override
void initState() {
super.initState();
controller.start();
}
Widget _buildZoomScaleSlider() {
return ValueListenableBuilder(
valueListenable: controller,
builder: (context, state, child) {
if (!state.isInitialized || !state.isRunning) {
return const SizedBox.shrink();
}
final TextStyle labelStyle = Theme.of(context)
.textTheme
.headlineMedium!
.copyWith(color: Colors.white);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
children: [
Text(
'0%',
overflow: TextOverflow.fade,
style: labelStyle,
),
Expanded(
child: Slider(
value: _zoomFactor,
onChanged: (value) {
setState(() {
_zoomFactor = value;
controller.setZoomScale(value);
});
},
),
),
Text(
'100%',
overflow: TextOverflow.fade,
style: labelStyle,
),
],
),
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('With zoom slider')),
backgroundColor: Colors.black,
body: Builder(
builder: (context) {
return Stack(
children: [
MobileScanner(
controller: controller,
fit: BoxFit.contain,
errorBuilder: (context, error, child) {
return ScannerErrorWidget(error: error);
},
onDetect: (barcode) {
setState(() {
this.barcode = barcode;
});
},
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
alignment: Alignment.bottomCenter,
height: 100,
color: Colors.black.withOpacity(0.4),
child: Column(
body: Stack(
children: [
MobileScanner(
controller: controller,
fit: BoxFit.contain,
errorBuilder: (context, error, child) {
return ScannerErrorWidget(error: error);
},
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
alignment: Alignment.bottomCenter,
height: 100,
color: Colors.black.withOpacity(0.4),
child: Column(
children: [
if (!kIsWeb) _buildZoomScaleSlider(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
children: [
Text(
"0%",
overflow: TextOverflow.fade,
style: Theme.of(context)
.textTheme
.headlineMedium!
.copyWith(color: Colors.white),
),
Expanded(
child: Slider(
max: 100,
divisions: 100,
value: _zoomFactor,
label: "${_zoomFactor.round()} %",
onChanged: (value) {
setState(() {
_zoomFactor = value;
controller.setZoomScale(value);
});
},
),
),
Text(
"100%",
overflow: TextOverflow.fade,
style: Theme.of(context)
.textTheme
.headlineMedium!
.copyWith(color: Colors.white),
),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
color: Colors.white,
icon: ValueListenableBuilder<TorchState>(
valueListenable: controller.torchState,
builder: (context, state, child) {
switch (state) {
case TorchState.off:
return const Icon(
Icons.flash_off,
color: Colors.grey,
);
case TorchState.on:
return const Icon(
Icons.flash_on,
color: Colors.yellow,
);
}
},
),
iconSize: 32.0,
onPressed: () => controller.toggleTorch(),
),
IconButton(
color: Colors.white,
icon: isStarted
? const Icon(Icons.stop)
: const Icon(Icons.play_arrow),
iconSize: 32.0,
onPressed: () => setState(() {
isStarted
? controller.stop()
: controller.start();
isStarted = !isStarted;
}),
),
Center(
child: SizedBox(
width: MediaQuery.of(context).size.width - 200,
height: 50,
child: FittedBox(
child: Text(
barcode?.barcodes.first.rawValue ??
'Scan something!',
overflow: TextOverflow.fade,
style: Theme.of(context)
.textTheme
.headlineMedium!
.copyWith(color: Colors.white),
),
),
),
),
IconButton(
color: Colors.white,
icon: ValueListenableBuilder<CameraFacing>(
valueListenable: controller.cameraFacingState,
builder: (context, state, child) {
switch (state) {
case CameraFacing.front:
return const Icon(Icons.camera_front);
case CameraFacing.back:
return const Icon(Icons.camera_rear);
}
},
),
iconSize: 32.0,
onPressed: () => controller.switchCamera(),
ToggleFlashlightButton(controller: controller),
StartStopMobileScannerButton(controller: controller),
Expanded(
child: Center(
child: ScannedBarcodeLabel(
barcodes: controller.barcodes,
),
IconButton(
color: Colors.white,
icon: const Icon(Icons.image),
iconSize: 32.0,
onPressed: () async {
final ImagePicker picker = ImagePicker();
// Pick an image
final XFile? image = await picker.pickImage(
source: ImageSource.gallery,
);
if (image != null) {
if (await controller.analyzeImage(image.path)) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Barcode found!'),
backgroundColor: Colors.green,
),
);
} else {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('No barcode found!'),
backgroundColor: Colors.red,
),
);
}
}
},
),
],
),
),
SwitchCameraButton(controller: controller),
AnalyzeImageFromGalleryButton(controller: controller),
],
),
),
],
),
],
);
},
),
),
],
),
);
}
@override
Future<void> dispose() async {
super.dispose();
await controller.dispose();
}
}
... ...
import 'package:flutter/material.dart';
import 'package:mobile_scanner_example/barcode_list_scanner_controller.dart';
import 'package:mobile_scanner_example/barcode_scanner_controller.dart';
import 'package:mobile_scanner_example/barcode_scanner_listview.dart';
import 'package:mobile_scanner_example/barcode_scanner_pageview.dart';
import 'package:mobile_scanner_example/barcode_scanner_returning_image.dart';
import 'package:mobile_scanner_example/barcode_scanner_window.dart';
import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart';
import 'package:mobile_scanner_example/barcode_scanner_zoom.dart';
import 'package:mobile_scanner_example/mobile_scanner_overlay.dart';
void main() => runApp(const MaterialApp(home: MyHome()));
void main() {
runApp(
const MaterialApp(
title: 'Mobile Scanner Example',
home: MyHome(),
),
);
}
class MyHome extends StatelessWidget {
const MyHome({super.key});
... ... @@ -16,23 +22,20 @@ class MyHome extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Flutter Demo Home Page')),
body: SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
appBar: AppBar(title: const Text('Mobile Scanner Example')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
const BarcodeListScannerWithController(),
builder: (context) => const BarcodeScannerListView(),
),
);
},
child: const Text('MobileScanner with List Controller'),
child: const Text('MobileScanner with ListView'),
),
ElevatedButton(
onPressed: () {
... ... @@ -62,19 +65,9 @@ class MyHome extends StatelessWidget {
),
);
},
child:
const Text('MobileScanner with Controller (returning image)'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
const BarcodeScannerWithoutController(),
),
);
},
child: const Text('MobileScanner without Controller'),
child: const Text(
'MobileScanner with Controller (returning image)',
),
),
ElevatedButton(
onPressed: () {
... ...
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:mobile_scanner_example/scanned_barcode_label.dart';
import 'package:mobile_scanner_example/scanner_button_widgets.dart';
import 'package:mobile_scanner_example/scanner_error_widget.dart';
class BarcodeScannerWithOverlay extends StatefulWidget {
... ... @@ -9,174 +11,105 @@ class BarcodeScannerWithOverlay extends StatefulWidget {
}
class _BarcodeScannerWithOverlayState extends State<BarcodeScannerWithOverlay> {
String overlayText = "Please scan QR Code";
bool camStarted = false;
final MobileScannerController controller = MobileScannerController(
formats: const [BarcodeFormat.qrCode],
autoStart: false,
);
@override
void dispose() {
controller.dispose();
super.dispose();
}
void startCamera() {
if (camStarted) {
return;
}
controller.start().then((_) {
if (mounted) {
setState(() {
camStarted = true;
});
}
}).catchError((Object error, StackTrace stackTrace) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Something went wrong! $error'),
backgroundColor: Colors.red,
),
);
}
});
}
void onBarcodeDetect(BarcodeCapture barcodeCapture) {
final barcode = barcodeCapture.barcodes.last;
setState(() {
overlayText = barcodeCapture.barcodes.last.displayValue ??
barcode.rawValue ??
'Barcode has no displayable value';
});
void initState() {
super.initState();
controller.start();
}
@override
Widget build(BuildContext context) {
final scanWindow = Rect.fromCenter(
center: MediaQuery.of(context).size.center(Offset.zero),
center: MediaQuery.sizeOf(context).center(Offset.zero),
width: 200,
height: 200,
);
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
title: const Text('Scanner with Overlay Example app'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
child: camStarted
? Stack(
fit: StackFit.expand,
children: [
Center(
child: MobileScanner(
fit: BoxFit.contain,
onDetect: onBarcodeDetect,
overlay: Padding(
padding: const EdgeInsets.all(16.0),
child: Align(
alignment: Alignment.bottomCenter,
child: Opacity(
opacity: 0.7,
child: Text(
overlayText,
style: const TextStyle(
backgroundColor: Colors.black26,
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 24,
overflow: TextOverflow.ellipsis,
),
maxLines: 1,
),
),
),
),
controller: controller,
scanWindow: scanWindow,
errorBuilder: (context, error, child) {
return ScannerErrorWidget(error: error);
},
),
),
CustomPaint(
painter: ScannerOverlay(scanWindow),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Align(
alignment: Alignment.bottomCenter,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ValueListenableBuilder<TorchState>(
valueListenable: controller.torchState,
builder: (context, value, child) {
final Color iconColor;
switch (value) {
case TorchState.off:
iconColor = Colors.black;
case TorchState.on:
iconColor = Colors.yellow;
}
return IconButton(
onPressed: () => controller.toggleTorch(),
icon: Icon(
Icons.flashlight_on,
color: iconColor,
),
);
},
),
IconButton(
onPressed: () => controller.switchCamera(),
icon: const Icon(
Icons.cameraswitch_rounded,
color: Colors.white,
),
),
],
),
),
),
],
)
: const Center(
child: Text("Tap on Camera to activate QR Scanner"),
),
body: Stack(
fit: StackFit.expand,
children: [
Center(
child: MobileScanner(
fit: BoxFit.contain,
controller: controller,
scanWindow: scanWindow,
errorBuilder: (context, error, child) {
return ScannerErrorWidget(error: error);
},
overlayBuilder: (context, constraints) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Align(
alignment: Alignment.bottomCenter,
child: ScannedBarcodeLabel(barcodes: controller.barcodes),
),
);
},
),
],
),
),
floatingActionButton: camStarted
? null
: FloatingActionButton(
onPressed: startCamera,
child: const Icon(Icons.camera_alt),
),
ValueListenableBuilder(
valueListenable: controller,
builder: (context, value, child) {
if (!value.isInitialized ||
!value.isRunning ||
value.error != null) {
return const SizedBox();
}
return CustomPaint(
painter: ScannerOverlay(scanWindow: scanWindow),
);
},
),
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ToggleFlashlightButton(controller: controller),
SwitchCameraButton(controller: controller),
],
),
),
),
],
),
);
}
@override
Future<void> dispose() async {
super.dispose();
await controller.dispose();
}
}
class ScannerOverlay extends CustomPainter {
ScannerOverlay(this.scanWindow);
const ScannerOverlay({
required this.scanWindow,
this.borderRadius = 12.0,
});
final Rect scanWindow;
final double borderRadius = 12.0;
final double borderRadius;
@override
void paint(Canvas canvas, Size size) {
// TODO: use `Offset.zero & size` instead of Rect.largest
// we need to pass the size to the custom paint widget
final backgroundPath = Path()..addRect(Rect.largest);
final cutoutPath = Path()
..addRRect(
RRect.fromRectAndCorners(
... ... @@ -199,14 +132,11 @@ class ScannerOverlay extends CustomPainter {
cutoutPath,
);
// Create a Paint object for the white border
final borderPaint = Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 4.0; // Adjust the border width as needed
..strokeWidth = 4.0;
// Calculate the border rectangle with rounded corners
// Adjust the radius as needed
final borderRect = RRect.fromRectAndCorners(
scanWindow,
topLeft: Radius.circular(borderRadius),
... ... @@ -215,13 +145,16 @@ class ScannerOverlay extends CustomPainter {
bottomRight: Radius.circular(borderRadius),
);
// Draw the white border
// First, draw the background,
// with a cutout area that is a bit larger than the scan window.
// Finally, draw the scan window itself.
canvas.drawPath(backgroundWithCutout, backgroundPaint);
canvas.drawRRect(borderRect, borderPaint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
bool shouldRepaint(ScannerOverlay oldDelegate) {
return scanWindow != oldDelegate.scanWindow ||
borderRadius != oldDelegate.borderRadius;
}
}
... ...
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class ScannedBarcodeLabel extends StatelessWidget {
const ScannedBarcodeLabel({
super.key,
required this.barcodes,
});
final Stream<BarcodeCapture> barcodes;
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: barcodes,
builder: (context, snaphot) {
final scannedBarcodes = snaphot.data?.barcodes ?? [];
if (scannedBarcodes.isEmpty) {
return const Text(
'Scan something!',
overflow: TextOverflow.fade,
style: TextStyle(color: Colors.white),
);
}
return Text(
scannedBarcodes.first.displayValue ?? 'No display value.',
overflow: TextOverflow.fade,
style: const TextStyle(color: Colors.white),
);
},
);
}
}
... ...
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class AnalyzeImageFromGalleryButton extends StatelessWidget {
const AnalyzeImageFromGalleryButton({required this.controller, super.key});
final MobileScannerController controller;
@override
Widget build(BuildContext context) {
return IconButton(
color: Colors.white,
icon: const Icon(Icons.image),
iconSize: 32.0,
onPressed: () async {
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(
source: ImageSource.gallery,
);
if (image == null) {
return;
}
final BarcodeCapture? barcodes = await controller.analyzeImage(
image.path,
);
if (!context.mounted) {
return;
}
final SnackBar snackbar = barcodes != null
? const SnackBar(
content: Text('Barcode found!'),
backgroundColor: Colors.green,
)
: const SnackBar(
content: Text('No barcode found!'),
backgroundColor: Colors.red,
);
ScaffoldMessenger.of(context).showSnackBar(snackbar);
},
);
}
}
class StartStopMobileScannerButton extends StatelessWidget {
const StartStopMobileScannerButton({required this.controller, super.key});
final MobileScannerController controller;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: controller,
builder: (context, state, child) {
if (!state.isInitialized || !state.isRunning) {
return IconButton(
color: Colors.white,
icon: const Icon(Icons.play_arrow),
iconSize: 32.0,
onPressed: () async {
await controller.start();
},
);
}
return IconButton(
color: Colors.white,
icon: const Icon(Icons.stop),
iconSize: 32.0,
onPressed: () async {
await controller.stop();
},
);
},
);
}
}
class SwitchCameraButton extends StatelessWidget {
const SwitchCameraButton({required this.controller, super.key});
final MobileScannerController controller;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: controller,
builder: (context, state, child) {
if (!state.isInitialized || !state.isRunning) {
return const SizedBox.shrink();
}
final int? availableCameras = state.availableCameras;
if (availableCameras != null && availableCameras < 2) {
return const SizedBox.shrink();
}
final Widget icon;
switch (state.cameraDirection) {
case CameraFacing.front:
icon = const Icon(Icons.camera_front);
case CameraFacing.back:
icon = const Icon(Icons.camera_rear);
}
return IconButton(
iconSize: 32.0,
icon: icon,
onPressed: () async {
await controller.switchCamera();
},
);
},
);
}
}
class ToggleFlashlightButton extends StatelessWidget {
const ToggleFlashlightButton({required this.controller, super.key});
final MobileScannerController controller;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: controller,
builder: (context, state, child) {
if (!state.isInitialized || !state.isRunning) {
return const SizedBox.shrink();
}
switch (state.torchState) {
case TorchState.off:
return IconButton(
color: Colors.white,
iconSize: 32.0,
icon: const Icon(Icons.flash_off),
onPressed: () async {
await controller.toggleTorch();
},
);
case TorchState.on:
return IconButton(
color: Colors.white,
iconSize: 32.0,
icon: const Icon(Icons.flash_on),
onPressed: () async {
await controller.toggleTorch();
},
);
case TorchState.unavailable:
return const Icon(
Icons.no_flash,
color: Colors.grey,
);
}
},
);
}
}
... ...
... ... @@ -6,8 +6,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 0.0.1
environment:
sdk: ">=3.1.0 <4.0.0"
flutter: ">=3.13.0"
sdk: ">=3.3.0 <4.0.0"
flutter: ">=3.19.0"
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
... ...
... ... @@ -40,7 +40,6 @@
<script src="flutter.js" defer></script>
</head>
<body>
<script src="https://unpkg.com/@zxing/library@0.19.1" type="application/javascript"></script>
<script>
window.addEventListener('load', function(ev) {
// Download main.dart.js
... ...
... ... @@ -245,12 +245,12 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
return
}
mobileScanner.analyzeImage(image: uiImage!, position: AVCaptureDevice.Position.back, callback: { [self] barcodes, error in
mobileScanner.analyzeImage(image: uiImage!, position: AVCaptureDevice.Position.back, callback: { barcodes, error in
if error != nil {
barcodeHandler.publishEvent(["name": "error", "message": error?.localizedDescription])
DispatchQueue.main.async {
result(false)
result(FlutterError(code: "MobileScanner",
message: error?.localizedDescription,
details: nil))
}
return
... ... @@ -258,15 +258,13 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin {
if (barcodes == nil || barcodes!.isEmpty) {
DispatchQueue.main.async {
result(false)
result(nil)
}
} else {
let barcodesMap: [Any?] = barcodes!.compactMap { barcode in barcode.data }
let event: [String: Any?] = ["name": "barcode", "data": barcodesMap]
barcodeHandler.publishEvent(event)
DispatchQueue.main.async {
result(true)
result(["name": "barcode", "data": barcodesMap])
}
}
})
... ...
... ... @@ -5,13 +5,15 @@ export 'src/enums/camera_facing.dart';
export 'src/enums/detection_speed.dart';
export 'src/enums/email_type.dart';
export 'src/enums/encryption_type.dart';
export 'src/enums/mobile_scanner_authorization_state.dart';
export 'src/enums/mobile_scanner_error_code.dart';
export 'src/enums/mobile_scanner_state.dart';
export 'src/enums/phone_type.dart';
export 'src/enums/torch_state.dart';
export 'src/mobile_scanner.dart';
export 'src/mobile_scanner_controller.dart';
export 'src/mobile_scanner_exception.dart';
export 'src/mobile_scanner_exception.dart'
hide PermissionRequestPendingException;
export 'src/mobile_scanner_platform_interface.dart';
export 'src/objects/address.dart';
export 'src/objects/barcode.dart';
export 'src/objects/barcode_capture.dart';
... ... @@ -20,7 +22,7 @@ export 'src/objects/contact_info.dart';
export 'src/objects/driver_license.dart';
export 'src/objects/email.dart';
export 'src/objects/geo_point.dart';
export 'src/objects/mobile_scanner_arguments.dart';
export 'src/objects/mobile_scanner_state.dart';
export 'src/objects/person_name.dart';
export 'src/objects/phone.dart';
export 'src/objects/sms.dart';
... ...
export 'src/web/base.dart';
export 'src/web/jsqr.dart';
export 'src/web/zxing.dart';
import 'dart:async';
import 'dart:html' as html;
import 'dart:ui_web' as ui;
import 'package:flutter/services.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
import 'package:mobile_scanner/mobile_scanner_web.dart';
import 'package:mobile_scanner/src/enums/barcode_format.dart';
import 'package:mobile_scanner/src/enums/camera_facing.dart';
/// This plugin is the web implementation of mobile_scanner.
/// It only supports QR codes.
class MobileScannerWebPlugin {
static void registerWith(Registrar registrar) {
final PluginEventChannel event = PluginEventChannel(
'dev.steenbakker.mobile_scanner/scanner/event',
const StandardMethodCodec(),
registrar,
);
final MethodChannel channel = MethodChannel(
'dev.steenbakker.mobile_scanner/scanner/method',
const StandardMethodCodec(),
registrar,
);
final MobileScannerWebPlugin instance = MobileScannerWebPlugin();
channel.setMethodCallHandler(instance.handleMethodCall);
event.setController(instance.controller);
}
// Controller to send events back to the framework
StreamController controller = StreamController.broadcast();
// ID of the video feed
String viewID = 'WebScanner-${DateTime.now().millisecondsSinceEpoch}';
static final html.DivElement vidDiv = html.DivElement();
/// Represents barcode reader library.
/// Change this property if you want to use a custom implementation.
///
/// Example of using the jsQR library:
/// void main() {
/// if (kIsWeb) {
/// MobileScannerWebPlugin.barCodeReader =
/// JsQrCodeReader(videoContainer: MobileScannerWebPlugin.vidDiv);
/// }
/// runApp(const MaterialApp(home: MyHome()));
/// }
static WebBarcodeReaderBase barCodeReader =
ZXingBarcodeReader(videoContainer: vidDiv);
StreamSubscription? _barCodeStreamSubscription;
/// Handle incomming messages
Future<dynamic> handleMethodCall(MethodCall call) async {
switch (call.method) {
case 'start':
return _start(call.arguments as Map);
case 'torch':
return _torch(call.arguments);
case 'stop':
return cancel();
case 'updateScanWindow':
return Future<void>.value();
default:
throw PlatformException(
code: 'Unimplemented',
details: "The mobile_scanner plugin for web doesn't implement "
"the method '${call.method}'",
);
}
}
/// Can enable or disable the flash if available
Future<void> _torch(arguments) async {
barCodeReader.toggleTorch(enabled: arguments == 1);
}
/// Starts the video stream and the scanner
Future<Map> _start(Map arguments) async {
var cameraFacing = CameraFacing.front;
if (arguments.containsKey('facing')) {
cameraFacing = CameraFacing.values[arguments['facing'] as int];
}
ui.platformViewRegistry.registerViewFactory(
viewID,
(int id) {
return vidDiv
..style.width = '100%'
..style.height = '100%';
},
);
// Check if stream is running
if (barCodeReader.isStarted) {
final hasTorch = await barCodeReader.hasTorch();
return {
'ViewID': viewID,
'videoWidth': barCodeReader.videoWidth,
'videoHeight': barCodeReader.videoHeight,
'torchable': hasTorch,
};
}
try {
List<BarcodeFormat>? formats;
if (arguments.containsKey('formats')) {
formats = (arguments['formats'] as List)
.cast<int>()
.map(BarcodeFormat.fromRawValue)
.toList();
}
final Duration? detectionTimeout;
if (arguments.containsKey('timeout')) {
detectionTimeout = Duration(milliseconds: arguments['timeout'] as int);
} else {
detectionTimeout = null;
}
await barCodeReader.start(
cameraFacing: cameraFacing,
formats: formats,
detectionTimeout: detectionTimeout,
);
_barCodeStreamSubscription =
barCodeReader.detectBarcodeContinuously().listen((code) {
if (code != null) {
controller.add({
'name': 'barcodeWeb',
'data': {
'rawValue': code.rawValue,
'rawBytes': code.rawBytes,
'format': code.format.rawValue,
'displayValue': code.displayValue,
'type': code.type.rawValue,
if (code.corners.isNotEmpty)
'corners': code.corners
.map(
(Offset c) => <Object?, Object?>{'x': c.dx, 'y': c.dy},
)
.toList(),
},
});
}
});
final hasTorch = await barCodeReader.hasTorch();
final bool? enableTorch = arguments['torch'] as bool?;
if (hasTorch && enableTorch != null) {
await barCodeReader.toggleTorch(enabled: enableTorch);
}
return {
'ViewID': viewID,
'videoWidth': barCodeReader.videoWidth,
'videoHeight': barCodeReader.videoHeight,
'torchable': hasTorch,
};
} catch (e, stackTrace) {
throw PlatformException(
code: 'MobileScannerWeb',
message: '$e',
details: stackTrace.toString(),
);
}
}
/// Check if any camera's are available
static Future<bool> cameraAvailable() async {
final sources =
await html.window.navigator.mediaDevices!.enumerateDevices();
for (final e in sources) {
// TODO:
// ignore: avoid_dynamic_calls
if (e.kind == 'videoinput') {
return true;
}
}
return false;
}
/// Stops the video feed and analyzer
Future<void> cancel() async {
await barCodeReader.stop();
await barCodeReader.stopDetectBarcodeContinuously();
await _barCodeStreamSubscription?.cancel();
_barCodeStreamSubscription = null;
}
}
/// The authorization state of the scanner.
enum MobileScannerState {
enum MobileScannerAuthorizationState {
/// The scanner has not yet requested the required permissions.
undetermined(0),
... ... @@ -9,16 +9,16 @@ enum MobileScannerState {
/// The user denied the required permissions.
denied(2);
const MobileScannerState(this.rawValue);
const MobileScannerAuthorizationState(this.rawValue);
factory MobileScannerState.fromRawValue(int value) {
factory MobileScannerAuthorizationState.fromRawValue(int value) {
switch (value) {
case 0:
return MobileScannerState.undetermined;
return MobileScannerAuthorizationState.undetermined;
case 1:
return MobileScannerState.authorized;
return MobileScannerAuthorizationState.authorized;
case 2:
return MobileScannerState.denied;
return MobileScannerAuthorizationState.denied;
default:
throw ArgumentError.value(value, 'value', 'Invalid raw value.');
}
... ...
import 'package:mobile_scanner/src/mobile_scanner_controller.dart';
/// This enum defines the different error codes for the mobile scanner.
enum MobileScannerErrorCode {
/// The controller was already started.
///
/// The controller should be stopped using [MobileScannerController.stop],
/// before restarting it.
controllerAlreadyInitialized,
/// The controller was used after being disposed.
controllerDisposed,
/// The controller was used
/// while it was not yet initialized using [MobileScannerController.start].
controllerUninitialized,
... ...
... ... @@ -4,7 +4,10 @@ enum TorchState {
off(0),
/// The flashlight is on.
on(1);
on(1),
/// The flashlight is unavailable.
unavailable(2);
const TorchState(this.rawValue);
... ... @@ -14,6 +17,8 @@ enum TorchState {
return TorchState.off;
case 1:
return TorchState.on;
case 2:
return TorchState.unavailable;
default:
throw ArgumentError.value(value, 'value', 'Invalid raw value.');
}
... ...
import 'dart:async';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:mobile_scanner/src/enums/barcode_format.dart';
import 'package:mobile_scanner/src/enums/mobile_scanner_authorization_state.dart';
import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart';
import 'package:mobile_scanner/src/enums/torch_state.dart';
import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
import 'package:mobile_scanner/src/mobile_scanner_platform_interface.dart';
import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart';
import 'package:mobile_scanner/src/objects/barcode.dart';
import 'package:mobile_scanner/src/objects/barcode_capture.dart';
import 'package:mobile_scanner/src/objects/start_options.dart';
/// An implementation of [MobileScannerPlatform] that uses method channels.
class MethodChannelMobileScanner extends MobileScannerPlatform {
/// The method channel used to interact with the native platform.
@visibleForTesting
final methodChannel = const MethodChannel(
'dev.steenbakker.mobile_scanner/scanner/method',
);
/// The event channel that sends back scanned barcode events.
@visibleForTesting
final eventChannel = const EventChannel(
'dev.steenbakker.mobile_scanner/scanner/event',
);
Stream<Map<Object?, Object?>>? _eventsStream;
Stream<Map<Object?, Object?>> get eventsStream {
_eventsStream ??=
eventChannel.receiveBroadcastStream().cast<Map<Object?, Object?>>();
return _eventsStream!;
}
int? _textureId;
/// Parse a [BarcodeCapture] from the given [event].
BarcodeCapture? _parseBarcode(Map<Object?, Object?>? event) {
if (event == null) {
return null;
}
final Object? data = event['data'];
if (data == null || data is! List<Object?>) {
return null;
}
final List<Map<Object?, Object?>> barcodes =
data.cast<Map<Object?, Object?>>();
if (Platform.isMacOS) {
return BarcodeCapture(
raw: event,
barcodes: barcodes
.map(
(barcode) => Barcode(
rawValue: barcode['payload'] as String?,
format: BarcodeFormat.fromRawValue(
barcode['symbology'] as int? ?? -1,
),
),
)
.toList(),
);
}
if (Platform.isAndroid || Platform.isIOS) {
final double? width = event['width'] as double?;
final double? height = event['height'] as double?;
return BarcodeCapture(
raw: data,
barcodes: barcodes.map(Barcode.fromNative).toList(),
image: event['image'] as Uint8List?,
size: width == null || height == null ? Size.zero : Size(width, height),
);
}
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.genericError,
errorDetails: MobileScannerErrorDetails(
message: 'Only Android, iOS and macOS are supported.',
),
);
}
/// Request permission to access the camera.
///
/// Throws a [MobileScannerException] if the permission is not granted.
Future<void> _requestCameraPermission() async {
final MobileScannerAuthorizationState authorizationState;
try {
authorizationState = MobileScannerAuthorizationState.fromRawValue(
await methodChannel.invokeMethod<int>('state') ?? 0,
);
} on PlatformException catch (error) {
// If the permission state is invalid, that is an error.
throw MobileScannerException(
errorCode: MobileScannerErrorCode.genericError,
errorDetails: MobileScannerErrorDetails(
code: error.code,
details: error.details as Object?,
message: error.message,
),
);
}
switch (authorizationState) {
case MobileScannerAuthorizationState.authorized:
return; // Already authorized.
// Android does not have an undetermined authorization state.
// So if the permission was denied, request it again.
case MobileScannerAuthorizationState.denied:
case MobileScannerAuthorizationState.undetermined:
try {
final bool granted =
await methodChannel.invokeMethod<bool>('request') ?? false;
if (granted) {
return; // Authorization was granted.
}
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.permissionDenied,
);
} on PlatformException catch (error) {
// If the permission state is invalid, that is an error.
throw MobileScannerException(
errorCode: MobileScannerErrorCode.genericError,
errorDetails: MobileScannerErrorDetails(
code: error.code,
details: error.details as Object?,
message: error.message,
),
);
}
}
}
@override
Stream<BarcodeCapture?> get barcodesStream {
return eventsStream
.where((event) => event['name'] == 'barcode')
.map((event) => _parseBarcode(event));
}
@override
Stream<TorchState> get torchStateStream {
return eventsStream
.where((event) => event['name'] == 'torchState')
.map((event) => TorchState.fromRawValue(event['data'] as int? ?? 0));
}
@override
Stream<double> get zoomScaleStateStream {
return eventsStream
.where((event) => event['name'] == 'zoomScaleState')
.map((event) => event['data'] as double? ?? 0.0);
}
@override
Future<BarcodeCapture?> analyzeImage(String path) async {
final Map<String, Object?>? result =
await methodChannel.invokeMapMethod<String, Object?>(
'analyzeImage',
path,
);
return _parseBarcode(result);
}
@override
Widget buildCameraView() {
if (_textureId == null) {
return const SizedBox();
}
return Texture(textureId: _textureId!);
}
@override
Future<void> resetZoomScale() async {
await methodChannel.invokeMethod<void>('resetScale');
}
@override
Future<void> setTorchState(TorchState torchState) async {
if (torchState == TorchState.unavailable) {
return;
}
await methodChannel.invokeMethod<void>('torch', torchState.rawValue);
}
@override
Future<void> setZoomScale(double zoomScale) async {
await methodChannel.invokeMethod<void>('setScale', zoomScale);
}
@override
Future<MobileScannerViewAttributes> start(StartOptions startOptions) async {
if (_textureId != null) {
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.controllerAlreadyInitialized,
errorDetails: MobileScannerErrorDetails(
message:
'The scanner was already started. Call stop() before calling start() again.',
),
);
}
await _requestCameraPermission();
Map<String, Object?>? startResult;
try {
startResult = await methodChannel.invokeMapMethod<String, Object?>(
'start',
startOptions.toMap(),
);
} on PlatformException catch (error) {
throw MobileScannerException(
errorCode: MobileScannerErrorCode.genericError,
errorDetails: MobileScannerErrorDetails(
code: error.code,
details: error.details as Object?,
message: error.message,
),
);
}
if (startResult == null) {
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.genericError,
errorDetails: MobileScannerErrorDetails(
message: 'The start method did not return a view configuration.',
),
);
}
final int? textureId = startResult['textureId'] as int?;
if (textureId == null) {
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.genericError,
errorDetails: MobileScannerErrorDetails(
message: 'The start method did not return a texture id.',
),
);
}
_textureId = textureId;
final int? numberOfCameras = startResult['numberOfCameras'] as int?;
final bool hasTorch = startResult['torchable'] as bool? ?? false;
final Map<Object?, Object?>? sizeInfo =
startResult['size'] as Map<Object?, Object?>?;
final double? width = sizeInfo?['width'] as double?;
final double? height = sizeInfo?['height'] as double?;
final Size size;
if (width == null || height == null) {
size = Size.zero;
} else {
size = Size(width, height);
}
return MobileScannerViewAttributes(
hasTorch: hasTorch,
numberOfCameras: numberOfCameras,
size: size,
);
}
@override
Future<void> stop() async {
if (_textureId == null) {
return;
}
_textureId = null;
await methodChannel.invokeMethod<void>('stop');
}
@override
Future<void> updateScanWindow(Rect? window) async {
if (_textureId == null) {
return;
}
List<double>? points;
if (window != null) {
points = [window.left, window.top, window.right, window.bottom];
}
await methodChannel.invokeMethod<void>(
'updateScanWindow',
{'rect': points},
);
}
@override
Future<void> dispose() async {
await stop();
}
}
... ...
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart';
import 'package:mobile_scanner/src/mobile_scanner_controller.dart';
import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
import 'package:mobile_scanner/src/objects/barcode_capture.dart';
import 'package:mobile_scanner/src/objects/mobile_scanner_arguments.dart';
import 'package:mobile_scanner/src/mobile_scanner_platform_interface.dart';
import 'package:mobile_scanner/src/objects/mobile_scanner_state.dart';
import 'package:mobile_scanner/src/scan_window_calculation.dart';
/// The function signature for the error builder.
... ... @@ -17,18 +14,26 @@ typedef MobileScannerErrorBuilder = Widget Function(
Widget?,
);
/// The [MobileScanner] widget displays a live camera preview.
/// This widget displays a live camera preview for the barcode scanner.
class MobileScanner extends StatefulWidget {
/// The controller that manages the barcode scanner.
///
/// If this is null, the scanner will manage its own controller.
final MobileScannerController? controller;
/// Create a new [MobileScanner] using the provided [controller].
const MobileScanner({
required this.controller,
this.fit = BoxFit.cover,
this.errorBuilder,
this.overlayBuilder,
this.placeholderBuilder,
this.scanWindow,
super.key,
});
/// The controller for the camera preview.
final MobileScannerController controller;
/// The function that builds an error widget when the scanner
/// could not be started.
/// The error builder for the camera preview.
///
/// If this is null, defaults to a black [ColoredBox]
/// with a centered white [Icons.error] icon.
/// If this is null, a black [ColoredBox],
/// with a centered white [Icons.error] icon is used as error widget.
final MobileScannerErrorBuilder? errorBuilder;
/// The [BoxFit] for the camera preview.
... ... @@ -36,250 +41,153 @@ class MobileScanner extends StatefulWidget {
/// Defaults to [BoxFit.cover].
final BoxFit fit;
/// The function that signals when new codes were detected by the [controller].
final void Function(BarcodeCapture barcodes) onDetect;
/// The function that signals when the barcode scanner is started.
@Deprecated('Use onScannerStarted() instead.')
final void Function(MobileScannerArguments? arguments)? onStart;
/// The function that signals when the barcode scanner is started.
final void Function(MobileScannerArguments? arguments)? onScannerStarted;
/// The builder for the overlay above the camera preview.
///
/// The resulting widget can be combined with the [scanWindow] rectangle
/// to create a cutout for the camera preview.
///
/// The [BoxConstraints] for this builder
/// are the same constraints that are used to compute the effective [scanWindow].
///
/// The overlay is only displayed when the camera preview is visible.
final LayoutWidgetBuilder? overlayBuilder;
/// The function that builds a placeholder widget when the scanner
/// is not yet displaying its camera preview.
/// The placeholder builder for the camera preview.
///
/// If this is null, a black [ColoredBox] is used as placeholder.
///
/// The placeholder is displayed when the camera preview is being initialized.
final Widget Function(BuildContext, Widget?)? placeholderBuilder;
/// if set barcodes will only be scanned if they fall within this [Rect]
/// useful for having a cut-out overlay for example. these [Rect]
/// coordinates are relative to the widget size, so by how much your
/// rectangle overlays the actual image can depend on things like the
/// [BoxFit]
final Rect? scanWindow;
/// Only set this to true if you are starting another instance of mobile_scanner
/// right after disposing the first one, like in a PageView.
/// The scan window rectangle for the barcode scanner.
///
/// Default: false
final bool startDelay;
/// The overlay which will be painted above the scanner when has started successful.
/// Will no be pointed when an error occurs or the scanner hasn't been started yet.
final Widget? overlay;
/// Create a new [MobileScanner] using the provided [controller]
/// and [onBarcodeDetected] callback.
const MobileScanner({
this.controller,
this.errorBuilder,
this.fit = BoxFit.cover,
required this.onDetect,
@Deprecated('Use onScannerStarted() instead.') this.onStart,
this.onScannerStarted,
this.placeholderBuilder,
this.scanWindow,
this.startDelay = false,
this.overlay,
super.key,
});
/// If this is not null, the barcode scanner will only scan barcodes
/// which intersect this rectangle.
///
/// This rectangle is relative to the layout size
/// of the *camera preview widget* in the widget tree,
/// rather than the actual size of the camera preview output.
/// This is because the size of the camera preview widget
/// might not be the same as the size of the camera output.
///
/// For example, the applied [fit] has an effect on the size of the camera preview widget,
/// while the camera preview size remains the same.
///
/// The following example shows a scan window that is centered,
/// fills half the height and one third of the width of the layout:
///
/// ```dart
/// LayoutBuider(
/// builder: (BuildContext context, BoxConstraints constraints) {
/// final Size layoutSize = constraints.biggest;
///
/// final double scanWindowWidth = layoutSize.width / 3;
/// final double scanWindowHeight = layoutSize.height / 2;
///
/// final Rect scanWindow = Rect.fromCenter(
/// center: layoutSize.center(Offset.zero),
/// width: scanWindowWidth,
/// height: scanWindowHeight,
/// );
/// }
/// );
/// ```
final Rect? scanWindow;
@override
State<MobileScanner> createState() => _MobileScannerState();
}
class _MobileScannerState extends State<MobileScanner>
with WidgetsBindingObserver {
/// The subscription that listens to barcode detection.
StreamSubscription<BarcodeCapture>? _barcodesSubscription;
class _MobileScannerState extends State<MobileScanner> {
/// The current scan window.
Rect? scanWindow;
/// The internally managed controller.
late MobileScannerController _controller;
/// Calculate the scan window based on the given [constraints].
///
/// If the [scanWindow] is already set, this method does nothing.
void _maybeUpdateScanWindow(
MobileScannerState scannerState,
BoxConstraints constraints,
) {
if (widget.scanWindow != null && scanWindow == null) {
scanWindow = calculateScanWindowRelativeToTextureInPercentage(
widget.fit,
widget.scanWindow!,
textureSize: scannerState.size,
widgetSize: constraints.biggest,
);
/// Whether the controller should resume
/// when the application comes back to the foreground.
bool _resumeFromBackground = false;
unawaited(widget.controller.updateScanWindow(scanWindow));
}
}
MobileScannerException? _startException;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<MobileScannerState>(
valueListenable: widget.controller,
builder: (BuildContext context, MobileScannerState value, Widget? child) {
if (!value.isInitialized) {
const Widget defaultPlaceholder = ColoredBox(color: Colors.black);
return widget.placeholderBuilder?.call(context, child) ??
defaultPlaceholder;
}
Widget _buildPlaceholderOrError(BuildContext context, Widget? child) {
final error = _startException;
final MobileScannerException? error = value.error;
if (error != null) {
return widget.errorBuilder?.call(context, error, child) ??
const ColoredBox(
if (error != null) {
const Widget defaultError = ColoredBox(
color: Colors.black,
child: Center(child: Icon(Icons.error, color: Colors.white)),
);
}
return widget.placeholderBuilder?.call(context, child) ??
const ColoredBox(color: Colors.black);
}
/// Start the given [scanner].
Future<void> _startScanner() async {
if (widget.startDelay) {
await Future.delayed(const Duration(seconds: 1, milliseconds: 500));
}
_barcodesSubscription ??= _controller.barcodes.listen(
widget.onDetect,
);
if (!_controller.autoStart) {
debugPrint(
'mobile_scanner: not starting automatically because autoStart is set to false in the controller.',
);
return;
}
_controller.start().then((arguments) {
// ignore: deprecated_member_use_from_same_package
widget.onStart?.call(arguments);
widget.onScannerStarted?.call(arguments);
}).catchError((error) {
if (!mounted) {
return;
}
if (error is MobileScannerException) {
_startException = error;
} else if (error is PlatformException) {
_startException = MobileScannerException(
errorCode: MobileScannerErrorCode.genericError,
errorDetails: MobileScannerErrorDetails(
code: error.code,
message: error.message,
details: error.details,
),
);
} else {
_startException = MobileScannerException(
errorCode: MobileScannerErrorCode.genericError,
errorDetails: MobileScannerErrorDetails(
details: error,
),
);
}
setState(() {});
});
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_controller = widget.controller ?? MobileScannerController();
_startScanner();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// App state changed before the controller was initialized.
if (_controller.isStarting) {
return;
}
switch (state) {
case AppLifecycleState.resumed:
if (_resumeFromBackground) {
_startScanner();
return widget.errorBuilder?.call(context, error, child) ??
defaultError;
}
case AppLifecycleState.inactive:
_resumeFromBackground = true;
_controller.stop();
default:
break;
}
}
Rect? scanWindow;
return LayoutBuilder(
builder: (context, constraints) {
_maybeUpdateScanWindow(value, constraints);
final Widget? overlay =
widget.overlayBuilder?.call(context, constraints);
final Size cameraPreviewSize = value.size;
final Widget scannerWidget = ClipRect(
child: SizedBox.fromSize(
size: constraints.biggest,
child: FittedBox(
fit: widget.fit,
child: SizedBox(
width: cameraPreviewSize.width,
height: cameraPreviewSize.height,
child: MobileScannerPlatform.instance.buildCameraView(),
),
),
),
);
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
return ValueListenableBuilder<MobileScannerArguments?>(
valueListenable: _controller.startArguments,
builder: (context, value, child) {
if (value == null) {
return _buildPlaceholderOrError(context, child);
if (overlay == null) {
return scannerWidget;
}
if (widget.scanWindow != null && scanWindow == null) {
scanWindow = calculateScanWindowRelativeToTextureInPercentage(
widget.fit,
widget.scanWindow!,
textureSize: value.size,
widgetSize: constraints.biggest,
);
_controller.updateScanWindow(scanWindow);
}
if (widget.overlay != null) {
return Stack(
alignment: Alignment.center,
children: [
_scanner(
value.size,
value.webId,
value.textureId,
value.numberOfCameras,
),
widget.overlay!,
],
);
} else {
return _scanner(
value.size,
value.webId,
value.textureId,
value.numberOfCameras,
);
}
return Stack(
alignment: Alignment.center,
children: <Widget>[
scannerWidget,
overlay,
],
);
},
);
},
);
}
Widget _scanner(
Size size,
String? webId,
int? textureId,
int? numberOfCameras,
) {
return ClipRect(
child: LayoutBuilder(
builder: (_, constraints) {
return SizedBox.fromSize(
size: constraints.biggest,
child: FittedBox(
fit: widget.fit,
child: SizedBox(
width: size.width,
height: size.height,
child: kIsWeb
? HtmlElementView(viewType: webId!)
: Texture(textureId: textureId!),
),
),
);
},
),
);
}
@override
void dispose() {
_controller.updateScanWindow(null);
WidgetsBinding.instance.removeObserver(this);
_barcodesSubscription?.cancel();
_barcodesSubscription = null;
_controller.dispose();
super.dispose();
// When this widget is unmounted, reset the scan window.
unawaited(widget.controller.updateScanWindow(null));
}
}
... ...
import 'dart:async';
import 'dart:io';
// ignore: unnecessary_import
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:mobile_scanner/src/enums/barcode_format.dart';
import 'package:mobile_scanner/src/enums/camera_facing.dart';
import 'package:mobile_scanner/src/enums/detection_speed.dart';
import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart';
import 'package:mobile_scanner/src/enums/mobile_scanner_state.dart';
import 'package:mobile_scanner/src/enums/torch_state.dart';
import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
import 'package:mobile_scanner/src/objects/barcode.dart';
import 'package:mobile_scanner/src/mobile_scanner_platform_interface.dart';
import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart';
import 'package:mobile_scanner/src/objects/barcode_capture.dart';
import 'package:mobile_scanner/src/objects/mobile_scanner_arguments.dart';
import 'package:mobile_scanner/src/objects/mobile_scanner_state.dart';
import 'package:mobile_scanner/src/objects/start_options.dart';
/// The [MobileScannerController] holds all the logic of this plugin,
/// where as the [MobileScanner] class is the frontend of this plugin.
class MobileScannerController {
/// The controller for the [MobileScanner] widget.
class MobileScannerController extends ValueNotifier<MobileScannerState> {
/// Construct a new [MobileScannerController] instance.
MobileScannerController({
this.facing = CameraFacing.back,
this.cameraResolution,
this.detectionSpeed = DetectionSpeed.normal,
this.detectionTimeoutMs = 250,
this.torchEnabled = false,
this.formats,
int detectionTimeoutMs = 250,
this.facing = CameraFacing.back,
this.formats = const <BarcodeFormat>[],
this.returnImage = false,
@Deprecated(
'Instead, use the result of calling `start()` to determine if permissions were granted.',
)
this.onPermissionSet,
this.autoStart = true,
this.cameraResolution,
this.torchEnabled = false,
this.useNewCameraSelector = false,
});
}) : detectionTimeoutMs =
detectionSpeed == DetectionSpeed.normal ? detectionTimeoutMs : 0,
assert(
detectionTimeoutMs >= 0,
'The detection timeout must be greater than or equal to 0.',
),
super(MobileScannerState.uninitialized(facing));
/// Select which camera should be used.
/// The desired resolution for the camera.
///
/// Default: CameraFacing.back
final CameraFacing facing;
/// Enable or disable the torch (Flash) on start
/// When this value is provided, the camera will try to match this resolution,
/// or fallback to the closest available resolution.
/// When this is null, Android defaults to a resolution of 640x480.
///
/// Default: disabled
final bool torchEnabled;
/// Set to true if you want to return the image buffer with the Barcode event
/// Bear in mind that changing the resolution has an effect on the aspect ratio.
///
/// Only supported on iOS and Android
final bool returnImage;
/// If provided, the scanner will only detect those specific formats
final List<BarcodeFormat>? formats;
/// When the camera orientation changes,
/// the resolution will be flipped to match the new dimensions of the display.
///
/// Currently only supported on Android.
final Size? cameraResolution;
/// Sets the speed of detections.
/// The detection speed for the scanner.
///
/// WARNING: DetectionSpeed.unrestricted can cause memory issues on some devices
/// Defaults to [DetectionSpeed.normal].
final DetectionSpeed detectionSpeed;
/// Sets the timeout, in milliseconds, of the scanner.
/// The detection timeout, in milliseconds, for the scanner.
///
/// This timeout is ignored if the [detectionSpeed]
/// is not set to [DetectionSpeed.normal].
... ... @@ -67,439 +61,342 @@ class MobileScannerController {
/// which prevents memory issues on older devices.
final int detectionTimeoutMs;
/// Automatically start the mobileScanner on initialization.
final bool autoStart;
/// The facing direction for the camera.
///
/// Defaults to the back-facing camera.
final CameraFacing facing;
/// The desired resolution for the camera.
/// The formats that the scanner should detect.
///
/// When this value is provided, the camera will try to match this resolution,
/// or fallback to the closest available resolution.
/// When this is null, Android defaults to a resolution of 640x480.
/// If this is empty, all supported formats are detected.
final List<BarcodeFormat> formats;
/// Whether scanned barcodes should contain the image
/// that is embedded into the barcode.
///
/// Bear in mind that changing the resolution has an effect on the aspect ratio.
/// If this is false, [BarcodeCapture.image] will always be null.
///
/// When the camera orientation changes,
/// the resolution will be flipped to match the new dimensions of the display.
/// Defaults to false, and is only supported on iOS and Android.
final bool returnImage;
/// Whether the flashlight should be turned on when the camera is started.
///
/// Currently only supported on Android.
final Size? cameraResolution;
/// Defaults to false.
final bool torchEnabled;
/// Use the new resolution selector. Warning: not fully tested, may produce
/// unwanted/zoomed images.
/// Use the new resolution selector.
///
/// This feature is experimental and not fully tested yet.
/// Use caution when using this flag,
/// as the new resolution selector may produce unwanted or zoomed images.
///
/// Only supported on Android
/// Only supported on Android.
final bool useNewCameraSelector;
/// Sets the barcode stream
/// The internal barcode controller, that listens for detected barcodes.
final StreamController<BarcodeCapture> _barcodesController =
StreamController.broadcast();
/// Get the stream of scanned barcodes.
Stream<BarcodeCapture> get barcodes => _barcodesController.stream;
static const MethodChannel _methodChannel =
MethodChannel('dev.steenbakker.mobile_scanner/scanner/method');
static const EventChannel _eventChannel =
EventChannel('dev.steenbakker.mobile_scanner/scanner/event');
StreamSubscription<BarcodeCapture?>? _barcodesSubscription;
StreamSubscription<TorchState>? _torchStateSubscription;
StreamSubscription<double>? _zoomScaleSubscription;
@Deprecated(
'Instead, use the result of calling `start()` to determine if permissions were granted.',
)
Function(bool permissionGranted)? onPermissionSet;
bool _isDisposed = false;
/// Listen to events from the platform specific code
StreamSubscription? events;
void _disposeListeners() {
_barcodesSubscription?.cancel();
_torchStateSubscription?.cancel();
_zoomScaleSubscription?.cancel();
/// A notifier that provides several arguments about the MobileScanner
final ValueNotifier<MobileScannerArguments?> startArguments =
ValueNotifier(null);
_barcodesSubscription = null;
_torchStateSubscription = null;
_zoomScaleSubscription = null;
}
/// A notifier that provides the state of the Torch (Flash)
final ValueNotifier<TorchState> torchState = ValueNotifier(TorchState.off);
void _setupListeners() {
_barcodesSubscription = MobileScannerPlatform.instance.barcodesStream
.listen((BarcodeCapture? barcode) {
if (_barcodesController.isClosed || barcode == null) {
return;
}
/// A notifier that provides the state of which camera is being used
late final ValueNotifier<CameraFacing> cameraFacingState =
ValueNotifier(facing);
_barcodesController.add(barcode);
});
/// A notifier that provides zoomScale.
final ValueNotifier<double> zoomScaleState = ValueNotifier(0.0);
_torchStateSubscription = MobileScannerPlatform.instance.torchStateStream
.listen((TorchState torchState) {
if (_isDisposed) {
return;
}
bool isStarting = false;
value = value.copyWith(torchState: torchState);
});
/// A notifier that provides availability of the Torch (Flash)
final ValueNotifier<bool?> hasTorchState = ValueNotifier(false);
_zoomScaleSubscription = MobileScannerPlatform.instance.zoomScaleStateStream
.listen((double zoomScale) {
if (_isDisposed) {
return;
}
/// Returns whether the device has a torch.
///
/// Throws an error if the controller is not initialized.
bool get hasTorch {
final hasTorch = hasTorchState.value;
if (hasTorch == null) {
value = value.copyWith(zoomScale: zoomScale);
});
}
void _throwIfNotInitialized() {
if (!value.isInitialized) {
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.controllerUninitialized,
errorDetails: MobileScannerErrorDetails(
message: 'The MobileScannerController has not been initialized.',
),
);
}
return hasTorch;
if (_isDisposed) {
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.controllerDisposed,
errorDetails: MobileScannerErrorDetails(
message:
'The MobileScannerController was used after it has been disposed.',
),
);
}
}
/// Set the starting arguments for the camera
Map<String, dynamic> _argumentsToMap({CameraFacing? cameraFacingOverride}) {
final Map<String, dynamic> arguments = {};
cameraFacingState.value = cameraFacingOverride ?? facing;
arguments['facing'] = cameraFacingState.value.rawValue;
arguments['torch'] = torchEnabled;
arguments['speed'] = detectionSpeed.rawValue;
arguments['timeout'] = detectionTimeoutMs;
arguments['returnImage'] = returnImage;
arguments['useNewCameraSelector'] = useNewCameraSelector;
/* if (scanWindow != null) {
arguments['scanWindow'] = [
scanWindow!.left,
scanWindow!.top,
scanWindow!.right,
scanWindow!.bottom,
];
} */
if (formats != null) {
if (kIsWeb || Platform.isIOS || Platform.isMacOS || Platform.isAndroid) {
arguments['formats'] = formats!.map((e) => e.rawValue).toList();
}
}
/// Analyze an image file.
///
/// The [path] points to a file on the device.
///
/// This is only supported on Android and iOS.
///
/// Returns the [BarcodeCapture] that was found in the image.
Future<BarcodeCapture?> analyzeImage(String path) {
return MobileScannerPlatform.instance.analyzeImage(path);
}
if (cameraResolution != null) {
arguments['cameraResolution'] = <int>[
cameraResolution!.width.toInt(),
cameraResolution!.height.toInt(),
];
}
/// Build a camera preview widget.
Widget buildCameraView() {
_throwIfNotInitialized();
return arguments;
return MobileScannerPlatform.instance.buildCameraView();
}
/// Start scanning for barcodes.
/// Upon calling this method, the necessary camera permission will be requested.
/// Reset the zoom scale of the camera.
///
/// Returns an instance of [MobileScannerArguments]
/// when the scanner was successfully started.
/// Returns null if the scanner is currently starting.
///
/// Throws a [MobileScannerException] if starting the scanner failed.
Future<MobileScannerArguments?> start({
CameraFacing? cameraFacingOverride,
}) async {
if (isStarting) {
debugPrint("Called start() while starting.");
return null;
}
events ??= _eventChannel
.receiveBroadcastStream()
.listen((data) => _handleEvent(data as Map));
/// Does nothing if the camera is not running.
Future<void> resetZoomScale() async {
_throwIfNotInitialized();
isStarting = true;
if (!value.isRunning) {
return;
}
// Check authorization status
if (!kIsWeb) {
final MobileScannerState state;
// When the platform has updated the zoom scale,
// it will send an update through the zoom scale state event stream.
await MobileScannerPlatform.instance.resetZoomScale();
}
try {
state = MobileScannerState.fromRawValue(
await _methodChannel.invokeMethod('state') as int? ?? 0,
);
} on PlatformException catch (error) {
isStarting = false;
throw MobileScannerException(
errorCode: MobileScannerErrorCode.genericError,
errorDetails: MobileScannerErrorDetails(
code: error.code,
details: error.details as Object?,
message: error.message,
),
);
}
/// Set the zoom scale of the camera.
///
/// The [zoomScale] must be between 0.0 and 1.0 (both inclusive).
///
/// If the [zoomScale] is out of range,
/// it is adjusted to fit within the allowed range.
///
/// Does nothing if the camera is not running.
Future<void> setZoomScale(double zoomScale) async {
_throwIfNotInitialized();
switch (state) {
// Android does not have an undetermined permission state.
// So if the permission state is denied, just request it now.
case MobileScannerState.undetermined:
case MobileScannerState.denied:
try {
final bool granted =
await _methodChannel.invokeMethod('request') as bool? ?? false;
if (!granted) {
isStarting = false;
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.permissionDenied,
);
}
} on PlatformException catch (error) {
isStarting = false;
throw MobileScannerException(
errorCode: MobileScannerErrorCode.genericError,
errorDetails: MobileScannerErrorDetails(
code: error.code,
details: error.details as Object?,
message: error.message,
),
);
}
case MobileScannerState.authorized:
break;
}
if (!value.isRunning) {
return;
}
// Start the camera with arguments
Map<String, dynamic>? startResult = {};
try {
startResult = await _methodChannel.invokeMapMethod<String, dynamic>(
'start',
_argumentsToMap(cameraFacingOverride: cameraFacingOverride),
);
} on PlatformException catch (error) {
MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError;
final String? errorMessage = error.message;
if (kIsWeb) {
if (errorMessage == null) {
errorCode = MobileScannerErrorCode.genericError;
} else if (errorMessage.contains('NotFoundError') ||
errorMessage.contains('NotSupportedError')) {
errorCode = MobileScannerErrorCode.unsupported;
} else if (errorMessage.contains('NotAllowedError')) {
errorCode = MobileScannerErrorCode.permissionDenied;
} else {
errorCode = MobileScannerErrorCode.genericError;
}
}
final double clampedZoomScale = zoomScale.clamp(0.0, 1.0);
isStarting = false;
// Update the zoom scale state to the new state.
// When the platform has updated the zoom scale,
// it will send an update through the zoom scale state event stream.
await MobileScannerPlatform.instance.setZoomScale(clampedZoomScale);
}
throw MobileScannerException(
errorCode: errorCode,
/// Start scanning for barcodes.
/// Upon calling this method, the necessary camera permission will be requested.
///
/// The [cameraDirection] can be used to specify the camera direction.
/// If this is null, this defaults to the [facing] value.
///
/// Does nothing if the camera is already running.
Future<void> start({CameraFacing? cameraDirection}) async {
if (_isDisposed) {
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.controllerDisposed,
errorDetails: MobileScannerErrorDetails(
code: error.code,
details: error.details as Object?,
message: error.message,
message:
'The MobileScannerController was used after it has been disposed.',
),
);
}
if (startResult == null) {
isStarting = false;
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.genericError,
);
// Do nothing if the camera is already running.
if (value.isRunning) {
return;
}
final hasTorch = startResult['torchable'] as bool? ?? false;
hasTorchState.value = hasTorch;
final CameraFacing effectiveDirection = cameraDirection ?? facing;
final Size size;
final StartOptions options = StartOptions(
cameraDirection: effectiveDirection,
cameraResolution: cameraResolution,
detectionSpeed: detectionSpeed,
detectionTimeoutMs: detectionTimeoutMs,
formats: formats,
returnImage: returnImage,
torchEnabled: torchEnabled,
);
if (kIsWeb) {
size = Size(
startResult['videoWidth'] as double? ?? 0,
startResult['videoHeight'] as double? ?? 0,
try {
_setupListeners();
final MobileScannerViewAttributes viewAttributes =
await MobileScannerPlatform.instance.start(
options,
);
} else {
final Map<Object?, Object?>? sizeInfo =
startResult['size'] as Map<Object?, Object?>?;
size = Size(
sizeInfo?['width'] as double? ?? 0,
sizeInfo?['height'] as double? ?? 0,
value = value.copyWith(
availableCameras: viewAttributes.numberOfCameras,
cameraDirection: effectiveDirection,
isInitialized: true,
isRunning: true,
size: viewAttributes.size,
// If the device has a flashlight, let the platform update the torch state.
// If it does not have one, provide the unavailable state directly.
torchState: viewAttributes.hasTorch ? null : TorchState.unavailable,
);
} on MobileScannerException catch (error) {
// The initialization finished with an error.
// To avoid stale values, reset the output size,
// torch state and zoom scale to the defaults.
if (!_isDisposed) {
value = value.copyWith(
cameraDirection: facing,
isInitialized: true,
isRunning: false,
error: error,
size: Size.zero,
torchState: TorchState.unavailable,
zoomScale: 1.0,
);
}
} on PermissionRequestPendingException catch (_) {
// If a permission request was already pending, do nothing.
}
isStarting = false;
return startArguments.value = MobileScannerArguments(
numberOfCameras: startResult['numberOfCameras'] as int?,
size: size,
hasTorch: hasTorch,
textureId: kIsWeb ? null : startResult['textureId'] as int?,
webId: kIsWeb ? startResult['ViewID'] as String? : null,
);
}
/// Stops the camera, but does not dispose this controller.
/// Stop the camera.
///
/// After calling this method, the camera can be restarted using [start].
///
/// Does nothing if the camera is already stopped.
Future<void> stop() async {
await _methodChannel.invokeMethod('stop');
// Do nothing if not initialized or already stopped.
// On the web, the permission popup triggers a lifecycle change from resumed to inactive,
// due to the permission popup gaining focus.
// This would 'stop' the camera while it is not ready yet.
if (!value.isInitialized || !value.isRunning || _isDisposed) {
return;
}
_disposeListeners();
// After the camera stopped, set the torch state to off,
// as the torch state callback is never called when the camera is stopped.
torchState.value = TorchState.off;
value = value.copyWith(
isRunning: false,
torchState: TorchState.off,
);
await MobileScannerPlatform.instance.stop();
}
/// Switches the torch on or off.
/// Switch between the front and back camera.
///
/// Does nothing if the device has no torch.
///
/// Throws if the controller was not initialized.
Future<void> toggleTorch() async {
final hasTorch = hasTorchState.value;
/// Does nothing if the device has less than 2 cameras.
Future<void> switchCamera() async {
_throwIfNotInitialized();
if (hasTorch == null) {
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.controllerUninitialized,
);
}
final int? availableCameras = value.availableCameras;
if (!hasTorch) {
// Do nothing if the amount of cameras is less than 2 cameras.
// If the the current platform does not provide the amount of cameras,
// continue anyway.
if (availableCameras != null && availableCameras < 2) {
return;
}
final TorchState newState =
torchState.value == TorchState.off ? TorchState.on : TorchState.off;
await stop();
await _methodChannel.invokeMethod('torch', newState.rawValue);
}
final CameraFacing cameraDirection = value.cameraDirection;
/// Changes the state of the camera (front or back).
///
/// Does nothing if the device has no front camera.
Future<void> switchCamera() async {
await _methodChannel.invokeMethod('stop');
final CameraFacing facingToUse =
cameraFacingState.value == CameraFacing.back
? CameraFacing.front
: CameraFacing.back;
await start(cameraFacingOverride: facingToUse);
await start(
cameraDirection: cameraDirection == CameraFacing.front
? CameraFacing.back
: CameraFacing.front,
);
}
/// Handles a local image file.
/// Returns true if a barcode or QR code is found.
/// Returns false if nothing is found.
/// Switches the flashlight on or off.
///
/// [path] The path of the image on the devices
Future<bool> analyzeImage(String path) async {
events ??= _eventChannel
.receiveBroadcastStream()
.listen((data) => _handleEvent(data as Map));
return _methodChannel
.invokeMethod<bool>('analyzeImage', path)
.then<bool>((bool? value) => value ?? false);
}
/// Does nothing if the device has no torch,
/// or if the camera is not running.
Future<void> toggleTorch() async {
_throwIfNotInitialized();
/// Set the zoomScale of the camera.
///
/// [zoomScale] must be within 0.0 and 1.0, where 1.0 is the max zoom, and 0.0
/// is zoomed out.
Future<void> setZoomScale(double zoomScale) async {
if (zoomScale < 0 || zoomScale > 1) {
throw const MobileScannerException(
errorCode: MobileScannerErrorCode.genericError,
errorDetails: MobileScannerErrorDetails(
message: 'The zoomScale must be between 0 and 1.',
),
);
if (!value.isRunning) {
return;
}
await _methodChannel.invokeMethod('setScale', zoomScale);
}
/// Reset the zoomScale of the camera to use standard scale 1x.
Future<void> resetZoomScale() async {
await _methodChannel.invokeMethod('resetScale');
}
final TorchState torchState = value.torchState;
/// Disposes the MobileScannerController and closes all listeners.
///
/// If you call this, you cannot use this controller object anymore.
void dispose() {
stop();
events?.cancel();
_barcodesController.close();
if (torchState == TorchState.unavailable) {
return;
}
final TorchState newState =
torchState == TorchState.off ? TorchState.on : TorchState.off;
// Update the torch state to the new state.
// When the platform has updated the torch state,
// it will send an update through the torch state event stream.
await MobileScannerPlatform.instance.setTorchState(newState);
}
/// Handles a returning event from the platform side
void _handleEvent(Map event) {
final name = event['name'];
final data = event['data'];
switch (name) {
case 'torchState':
final state = TorchState.values[data as int? ?? 0];
torchState.value = state;
case 'zoomScaleState':
zoomScaleState.value = data as double? ?? 0.0;
case 'barcode':
if (data == null) return;
final parsed = (data as List)
.map((value) => Barcode.fromNative(value as Map))
.toList();
_barcodesController.add(
BarcodeCapture(
raw: data,
barcodes: parsed,
image: event['image'] as Uint8List?,
width: event['width'] as double?,
height: event['height'] as double?,
),
);
case 'barcodeMac':
_barcodesController.add(
BarcodeCapture(
raw: data,
barcodes: [
Barcode(
rawValue: (data as Map)['payload'] as String?,
format: BarcodeFormat.fromRawValue(
data['symbology'] as int? ?? -1,
),
),
],
),
);
case 'barcodeWeb':
final barcode = data as Map?;
final corners = barcode?['corners'] as List<Object?>? ?? <Object?>[];
_barcodesController.add(
BarcodeCapture(
raw: data,
barcodes: [
if (barcode != null)
Barcode(
rawValue: barcode['rawValue'] as String?,
rawBytes: barcode['rawBytes'] as Uint8List?,
format: BarcodeFormat.fromRawValue(
barcode['format'] as int? ?? -1,
),
corners: List.unmodifiable(
corners.cast<Map<Object?, Object?>>().map(
(Map<Object?, Object?> e) {
return Offset(e['x']! as double, e['y']! as double);
},
),
),
),
],
),
);
case 'error':
throw MobileScannerException(
errorCode: MobileScannerErrorCode.genericError,
errorDetails: MobileScannerErrorDetails(message: data as String?),
);
default:
throw UnimplementedError(name as String?);
/// Update the scan window with the given [window] rectangle.
///
/// If [window] is null, the scan window will be reset to the full camera preview.
Future<void> updateScanWindow(Rect? window) async {
if (_isDisposed || !value.isInitialized) {
return;
}
await MobileScannerPlatform.instance.updateScanWindow(window);
}
/// updates the native ScanWindow
Future<void> updateScanWindow(Rect? window) async {
List? data;
if (window != null) {
data = [window.left, window.top, window.right, window.bottom];
/// Dispose the controller.
///
/// Once the controller is disposed, it cannot be used anymore.
@override
Future<void> dispose() async {
if (_isDisposed) {
return;
}
await _methodChannel.invokeMethod('updateScanWindow', {'rect': data});
_isDisposed = true;
unawaited(_barcodesController.close());
super.dispose();
await MobileScannerPlatform.instance.dispose();
}
}
... ...
... ... @@ -39,3 +39,10 @@ class MobileScannerErrorDetails {
/// The error message from the [PlatformException].
final String? message;
}
/// This class represents an exception that is thrown
/// when the scanner was (re)started while a permission request was pending.
///
/// This exception type is only used internally,
/// and is not part of the public API.
class PermissionRequestPendingException implements Exception {}
... ...
import 'package:flutter/widgets.dart';
import 'package:mobile_scanner/src/enums/torch_state.dart';
import 'package:mobile_scanner/src/method_channel/mobile_scanner_method_channel.dart';
import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart';
import 'package:mobile_scanner/src/objects/barcode_capture.dart';
import 'package:mobile_scanner/src/objects/start_options.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
/// The platform interface for the `mobile_scanner` plugin.
abstract class MobileScannerPlatform extends PlatformInterface {
/// Constructs a MobileScannerPlatform.
MobileScannerPlatform() : super(token: _token);
static final Object _token = Object();
static MobileScannerPlatform _instance = MethodChannelMobileScanner();
/// The default instance of [MobileScannerPlatform] to use.
///
/// Defaults to [MethodChannelMobileScanner].
static MobileScannerPlatform get instance => _instance;
/// Platform-specific implementations should set this with their own
/// platform-specific class that extends [MobileScannerPlatform] when
/// they register themselves.
static set instance(MobileScannerPlatform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
/// Get the stream of barcode captures.
Stream<BarcodeCapture?> get barcodesStream {
throw UnimplementedError('barcodesStream has not been implemented.');
}
/// Get the stream of torch state changes.
Stream<TorchState> get torchStateStream {
throw UnimplementedError('torchStateStream has not been implemented.');
}
/// Get the stream of zoom scale changes.
Stream<double> get zoomScaleStateStream {
throw UnimplementedError('zoomScaleStateStream has not been implemented.');
}
/// Analyze a local image file for barcodes.
///
/// The [path] is the path to the file on disk.
///
/// Returns the barcodes that were found in the image.
Future<BarcodeCapture?> analyzeImage(String path) {
throw UnimplementedError('analyzeImage() has not been implemented.');
}
/// Build the camera view for the barcode scanner.
Widget buildCameraView() {
throw UnimplementedError('buildCameraView() has not been implemented.');
}
/// Reset the zoom scale, so that the camera is fully zoomed out.
Future<void> resetZoomScale() {
throw UnimplementedError('resetZoomScale() has not been implemented.');
}
/// Set the source url for the barcode library.
///
/// This is only supported on the web.
void setBarcodeLibraryScriptUrl(String scriptUrl) {}
/// Set the torch state of the active camera.
Future<void> setTorchState(TorchState torchState) {
throw UnimplementedError('setTorchState() has not been implemented.');
}
/// Set the zoom scale of the camera.
///
/// The [zoomScale] must be between `0.0` and `1.0` (both inclusive).
/// A value of `0.0` indicates that the camera is fully zoomed out,
/// while `1.0` indicates that the camera is fully zoomed in.
Future<void> setZoomScale(double zoomScale) {
throw UnimplementedError('setZoomScale() has not been implemented.');
}
/// Start the barcode scanner and prepare a scanner view.
///
/// Upon calling this method, the necessary camera permission will be requested.
///
/// The given [StartOptions.cameraDirection] is used as the direction for the camera that needs to be set up.
Future<MobileScannerViewAttributes> start(StartOptions startOptions) {
throw UnimplementedError('start() has not been implemented.');
}
/// Stop the camera.
Future<void> stop() {
throw UnimplementedError('stop() has not been implemented.');
}
/// Update the scan window to the given [window] rectangle.
///
/// Any barcodes that do not intersect with the given [window] will be ignored.
///
/// If [window] is `null`, the scan window will be reset to the full screen.
Future<void> updateScanWindow(Rect? window) {
throw UnimplementedError('updateScanWindow() has not been implemented.');
}
/// Dispose of this [MobileScannerPlatform] instance.
Future<void> dispose() {
throw UnimplementedError('dispose() has not been implemented.');
}
}
... ...
import 'dart:ui';
/// This class defines the attributes for the mobile scanner view.
class MobileScannerViewAttributes {
const MobileScannerViewAttributes({
required this.hasTorch,
this.numberOfCameras,
required this.size,
});
/// Whether the current active camera has a torch.
final bool hasTorch;
/// The number of available cameras.
final int? numberOfCameras;
/// The size of the camera output.
final Size size;
}
... ...
... ... @@ -147,7 +147,9 @@ class Barcode {
/// The SMS message that is embedded in the barcode.
final SMS? sms;
/// The type of the [format] of the barcode.
/// The contextual type of the [format] of the barcode.
///
/// For example: TYPE_TEXT, TYPE_PRODUCT, TYPE_URL, etc.
///
/// For types that are recognized,
/// but could not be parsed correctly, [BarcodeType.text] will be returned.
... ...
... ... @@ -6,38 +6,25 @@ import 'package:mobile_scanner/src/objects/barcode.dart';
/// This class represents a scanned barcode.
class BarcodeCapture {
/// Create a new [BarcodeCapture] instance.
BarcodeCapture({
const BarcodeCapture({
this.barcodes = const <Barcode>[],
double? height,
this.image,
this.raw,
double? width,
}) : size =
width == null && height == null ? Size.zero : Size(width!, height!);
this.size = Size.zero,
});
/// The list of scanned barcodes.
final List<Barcode> barcodes;
/// The bytes of the image that is embedded in the barcode.
///
/// This null if [MobileScannerController.returnImage] is false.
/// This null if [MobileScannerController.returnImage] is false,
/// or if there is no available image.
final Uint8List? image;
/// The raw data of the scanned barcode.
final dynamic raw; // TODO: this should be `Object?` instead of dynamic
final Object? raw;
/// The size of the scanned barcode.
final Size size;
/// The width of the scanned barcode.
///
/// Prefer using `size.width` instead,
/// as this getter will be removed in the future.
double get width => size.width;
/// The height of the scanned barcode.
///
/// Prefer using `size.height` instead,
/// as this getter will be removed in the future.
double get height => size.height;
}
... ...
import 'package:flutter/material.dart';
/// The start arguments of the scanner.
class MobileScannerArguments {
/// The output size of the camera.
/// This value can be used to draw a box in the image.
final Size size;
/// A bool which is true if the device has a torch.
final bool hasTorch;
/// The texture id of the capture used internally.
final int? textureId;
/// The texture id of the capture used internally if device is web.
final String? webId;
/// Indicates how many cameras are available.
///
/// Currently only supported on Android.
final int? numberOfCameras;
MobileScannerArguments({
required this.size,
required this.hasTorch,
this.textureId,
this.webId,
this.numberOfCameras,
});
}
import 'dart:ui';
import 'package:mobile_scanner/src/enums/camera_facing.dart';
import 'package:mobile_scanner/src/enums/torch_state.dart';
import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
/// This class represents the current state of a [MobileScannerController].
class MobileScannerState {
/// Create a new [MobileScannerState] instance.
const MobileScannerState({
required this.availableCameras,
required this.cameraDirection,
required this.isInitialized,
required this.isRunning,
required this.size,
required this.torchState,
required this.zoomScale,
this.error,
});
/// Create a new [MobileScannerState] instance that is uninitialized.
const MobileScannerState.uninitialized(CameraFacing facing)
: this(
availableCameras: null,
cameraDirection: facing,
isInitialized: false,
isRunning: false,
size: Size.zero,
torchState: TorchState.unavailable,
zoomScale: 1.0,
);
/// The number of available cameras.
///
/// This is null if the number of cameras is unknown.
final int? availableCameras;
/// The facing direction of the camera.
final CameraFacing cameraDirection;
/// The error that occurred while setting up or using the canera.
final MobileScannerException? error;
/// Whether the mobile scanner has initialized successfully.
///
/// This is `true` if the camera is ready to be used.
final bool isInitialized;
/// Whether the mobile scanner is currently running.
///
/// This is `true` if the camera is active.
final bool isRunning;
/// The size of the camera output.
final Size size;
/// The current state of the flashlight of the camera.
final TorchState torchState;
/// The current zoom scale of the camera.
final double zoomScale;
/// Create a copy of this state with the given parameters.
MobileScannerState copyWith({
int? availableCameras,
CameraFacing? cameraDirection,
MobileScannerException? error,
bool? isInitialized,
bool? isRunning,
Size? size,
TorchState? torchState,
double? zoomScale,
}) {
return MobileScannerState(
availableCameras: availableCameras ?? this.availableCameras,
cameraDirection: cameraDirection ?? this.cameraDirection,
error: error,
isInitialized: isInitialized ?? this.isInitialized,
isRunning: isRunning ?? this.isRunning,
size: size ?? this.size,
torchState: torchState ?? this.torchState,
zoomScale: zoomScale ?? this.zoomScale,
);
}
}
... ...