Committed by
GitHub
Merge pull request #916 from navaronbracke/mobile_scanner_platform_interface
feat: Mobile scanner platform interface
Showing
38 changed files
with
1940 additions
and
1976 deletions
Too many changes to show.
To preserve performance only 38 of 38+ files are displayed.
| @@ -36,7 +36,7 @@ jobs: | @@ -36,7 +36,7 @@ jobs: | ||
| 36 | - uses: subosito/flutter-action@v2.12.0 | 36 | - uses: subosito/flutter-action@v2.12.0 |
| 37 | with: | 37 | with: |
| 38 | cache: true | 38 | cache: true |
| 39 | - flutter-version: '3.13' | 39 | + flutter-version: '3.19' |
| 40 | channel: 'stable' | 40 | channel: 'stable' |
| 41 | - name: Version | 41 | - name: Version |
| 42 | run: flutter doctor -v | 42 | run: flutter doctor -v |
| 1 | +## 5.0.0-beta.1 | ||
| 2 | + | ||
| 3 | +**BREAKING CHANGES:** | ||
| 4 | + | ||
| 5 | +* Flutter 3.19.0 is now required. | ||
| 6 | +* The `width` and `height` of `BarcodeCapture` have been removed, in favor of `size`. | ||
| 7 | +* The `raw` attribute is now `Object?` instead of `dynamic`, so that it participates in type promotion. | ||
| 8 | +* The `MobileScannerArguments` class has been removed from the public API, as it is an internal type. | ||
| 9 | +* The `cameraFacingOverride` named argument for the `start()` method has been renamed to `cameraDirection`. | ||
| 10 | +* The `analyzeImage` function now correctly returns a `BarcodeCapture?` instead of a boolean. | ||
| 11 | +* The `formats` attribute of the `MobileScannerController` is now non-null. | ||
| 12 | +* The `MobileScannerState` enum has been renamed to `MobileScannerAuthorizationState`. | ||
| 13 | +* The various `ValueNotifier`s for the camera state have been removed. Use the `value` of the `MobileScannerController` instead. | ||
| 14 | +* The `hasTorch` getter has been removed. Instead, use the torch state of the controller's value. | ||
| 15 | + The `TorchState` enum now provides a new value for unavailable flashlights. | ||
| 16 | +* The `autoStart` attribute has been removed from the `MobileScannerController`. The controller should be manually started on-demand. | ||
| 17 | +* A controller is now required for the `MobileScanner` widget. | ||
| 18 | +* The `onPermissionSet`, `onStart` and `onScannerStarted` methods have been removed from the `MobileScanner` widget. Instead, await `MobileScannerController.start()`. | ||
| 19 | +* The `startDelay` has been removed from the `MobileScanner` widget. Instead, use a delay between manual starts of one or more controllers. | ||
| 20 | +* The `onDetect` method has been removed from the `MobileScanner` widget. Instead, listen to `MobileScannerController.barcodes` directly. | ||
| 21 | +* The `overlay` widget of the `MobileScanner` has been replaced by a new property, `overlayBuilder`, which provides the constraints for the overlay. | ||
| 22 | +* 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`. | ||
| 23 | +* 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`. | ||
| 24 | + | ||
| 25 | +Improvements: | ||
| 26 | +* The `MobileScannerController` is now a ChangeNotifier, with `MobileScannerState` as its model. | ||
| 27 | +* The web implementation now supports alternate URLs for loading the barcode library. | ||
| 28 | + | ||
| 1 | ## 4.0.1 | 29 | ## 4.0.1 |
| 2 | Bugs fixed: | 30 | Bugs fixed: |
| 3 | * [iOS] Fixed a crash with a nil capture session when starting the camera. (thanks @navaronbracke !) | 31 | * [iOS] Fixed a crash with a nil capture session when starting the camera. (thanks @navaronbracke !) |
| 4 | 32 | ||
| 5 | ## 4.0.0 | 33 | ## 4.0.0 |
| 6 | -BREAKING CHANGES: | 34 | + |
| 35 | +**BREAKING CHANGES:** | ||
| 36 | + | ||
| 7 | * [Android] compileSdk has been upgraded to version 34. | 37 | * [Android] compileSdk has been upgraded to version 34. |
| 8 | * [Android] Java version has been upgraded to version 17. | 38 | * [Android] Java version has been upgraded to version 17. |
| 9 | 39 | ||
| @@ -186,7 +216,8 @@ Deprecated: | @@ -186,7 +216,8 @@ Deprecated: | ||
| 186 | * The `onStart` method has been renamed to `onScannerStarted`. | 216 | * The `onStart` method has been renamed to `onScannerStarted`. |
| 187 | * The `onPermissionSet` argument of the `MobileScannerController` is now deprecated. | 217 | * The `onPermissionSet` argument of the `MobileScannerController` is now deprecated. |
| 188 | 218 | ||
| 189 | -Breaking changes: | 219 | +**BREAKING CHANGES:** |
| 220 | + | ||
| 190 | * `MobileScannerException` now uses an `errorCode` instead of a `message`. | 221 | * `MobileScannerException` now uses an `errorCode` instead of a `message`. |
| 191 | * `MobileScannerException` now contains additional details from the original error. | 222 | * `MobileScannerException` now contains additional details from the original error. |
| 192 | * Refactored `MobileScannerController.start()` to throw `MobileScannerException`s | 223 | * Refactored `MobileScannerController.start()` to throw `MobileScannerException`s |
| @@ -223,7 +254,9 @@ Fixes: | @@ -223,7 +254,9 @@ Fixes: | ||
| 223 | * [iOS] Fix crash when changing torch state | 254 | * [iOS] Fix crash when changing torch state |
| 224 | 255 | ||
| 225 | ## 3.0.0-beta.2 | 256 | ## 3.0.0-beta.2 |
| 226 | -Breaking changes: | 257 | + |
| 258 | +**BREAKING CHANGES:** | ||
| 259 | + | ||
| 227 | * The arguments parameter of onDetect is removed. The data is now returned by the onStart callback | 260 | * The arguments parameter of onDetect is removed. The data is now returned by the onStart callback |
| 228 | in the MobileScanner widget. | 261 | in the MobileScanner widget. |
| 229 | * onDetect now returns the object BarcodeCapture, which contains a List of barcodes and, if enabled, an image. | 262 | * onDetect now returns the object BarcodeCapture, which contains a List of barcodes and, if enabled, an image. |
| @@ -243,7 +276,9 @@ Other improvements: | @@ -243,7 +276,9 @@ Other improvements: | ||
| 243 | * [iOS] Updated POD dependencies | 276 | * [iOS] Updated POD dependencies |
| 244 | 277 | ||
| 245 | ## 3.0.0-beta.1 | 278 | ## 3.0.0-beta.1 |
| 246 | -Breaking changes: | 279 | + |
| 280 | +**BREAKING CHANGES:** | ||
| 281 | + | ||
| 247 | * [Android] SDK updated to SDK 33. | 282 | * [Android] SDK updated to SDK 33. |
| 248 | 283 | ||
| 249 | Features: | 284 | Features: |
| @@ -259,7 +294,9 @@ Other changes: | @@ -259,7 +294,9 @@ Other changes: | ||
| 259 | * Several minor code improvements | 294 | * Several minor code improvements |
| 260 | 295 | ||
| 261 | ## 2.0.0 | 296 | ## 2.0.0 |
| 262 | -Breaking changes: | 297 | + |
| 298 | +**BREAKING CHANGES:** | ||
| 299 | + | ||
| 263 | This version is only compatible with flutter 3.0.0 and later. | 300 | This version is only compatible with flutter 3.0.0 and later. |
| 264 | 301 | ||
| 265 | ## 1.1.2-play-services | 302 | ## 1.1.2-play-services |
| @@ -293,7 +330,9 @@ Bugfixes: | @@ -293,7 +330,9 @@ Bugfixes: | ||
| 293 | * Upgraded several dependencies. | 330 | * Upgraded several dependencies. |
| 294 | 331 | ||
| 295 | ## 1.0.0 | 332 | ## 1.0.0 |
| 296 | -BREAKING CHANGES: | 333 | + |
| 334 | +**BREAKING CHANGES:** | ||
| 335 | + | ||
| 297 | This version adds a new allowDuplicates option which now defaults to FALSE. this means that it will only call onDetect once after a scan. | 336 | This version adds a new allowDuplicates option which now defaults to FALSE. this means that it will only call onDetect once after a scan. |
| 298 | If you still want duplicates, you can set allowDuplicates to true. | 337 | If you still want duplicates, you can set allowDuplicates to true. |
| 299 | This also means that you don't have to check for duplicates yourself anymore. | 338 | This also means that you don't have to check for duplicates yourself anymore. |
| @@ -7,17 +7,15 @@ | @@ -7,17 +7,15 @@ | ||
| 7 | 7 | ||
| 8 | A universal scanner for Flutter based on MLKit. Uses CameraX on Android and AVFoundation on iOS. | 8 | A universal scanner for Flutter based on MLKit. Uses CameraX on Android and AVFoundation on iOS. |
| 9 | 9 | ||
| 10 | - | ||
| 11 | ## Features Supported | 10 | ## Features Supported |
| 12 | 11 | ||
| 13 | See the example app for detailed implementation information. | 12 | See the example app for detailed implementation information. |
| 14 | 13 | ||
| 15 | | Features | Android | iOS | macOS | Web | | 14 | | Features | Android | iOS | macOS | Web | |
| 16 | -|------------------------|--------------------|--------------------|-------|-----| | 15 | +|------------------------|--------------------|--------------------|----------------------|-----| |
| 17 | | analyzeImage (Gallery) | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | | 16 | | analyzeImage (Gallery) | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | |
| 18 | | returnImage | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | | 17 | | returnImage | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | |
| 19 | -| scanWindow | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | | ||
| 20 | -| barcodeOverlay | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | | 18 | +| scanWindow | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: | |
| 21 | 19 | ||
| 22 | ## Platform Support | 20 | ## Platform Support |
| 23 | 21 | ||
| @@ -26,6 +24,7 @@ See the example app for detailed implementation information. | @@ -26,6 +24,7 @@ See the example app for detailed implementation information. | ||
| 26 | | ✔ | ✔ | ✔ | ✔ | :x: | :x: | | 24 | | ✔ | ✔ | ✔ | ✔ | :x: | :x: | |
| 27 | 25 | ||
| 28 | ## Platform specific setup | 26 | ## Platform specific setup |
| 27 | + | ||
| 29 | ### Android | 28 | ### Android |
| 30 | 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. | 29 | 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. |
| 31 | 30 | ||
| @@ -61,194 +60,110 @@ Ensure that you granted camera permission in XCode -> Signing & Capabilities: | @@ -61,194 +60,110 @@ Ensure that you granted camera permission in XCode -> Signing & Capabilities: | ||
| 61 | <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"> | 60 | <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"> |
| 62 | 61 | ||
| 63 | ## Web | 62 | ## Web |
| 64 | -This package uses ZXing on web to read barcodes so it needs to be included in `index.html` as script. | ||
| 65 | -```html | ||
| 66 | -<script src="https://unpkg.com/@zxing/library@0.19.1" type="application/javascript"></script> | ||
| 67 | -``` | ||
| 68 | - | ||
| 69 | -## Usage | ||
| 70 | - | ||
| 71 | -Import `package:mobile_scanner/mobile_scanner.dart`, and use the widget with or without the controller. | ||
| 72 | 63 | ||
| 73 | -If you don't provide a controller, you can't control functions like the torch(flash) or switching camera. | 64 | +As of version 5.0.0 adding the library to the `index.html` is no longer required, |
| 65 | +as the library is automatically loaded on first use. | ||
| 74 | 66 | ||
| 75 | -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. | 67 | +### Providing a mirror for the barcode scanning library |
| 76 | 68 | ||
| 77 | -Example without controller: | 69 | +If a different mirror is needed to load the barcode scanning library, |
| 70 | +the source URL can be set beforehand. | ||
| 78 | 71 | ||
| 79 | ```dart | 72 | ```dart |
| 73 | +import 'package:flutter/foundation.dart'; | ||
| 80 | import 'package:mobile_scanner/mobile_scanner.dart'; | 74 | import 'package:mobile_scanner/mobile_scanner.dart'; |
| 81 | 75 | ||
| 82 | - @override | ||
| 83 | - Widget build(BuildContext context) { | ||
| 84 | - return Scaffold( | ||
| 85 | - appBar: AppBar(title: const Text('Mobile Scanner')), | ||
| 86 | - body: MobileScanner( | ||
| 87 | - // fit: BoxFit.contain, | ||
| 88 | - onDetect: (capture) { | ||
| 89 | - final List<Barcode> barcodes = capture.barcodes; | ||
| 90 | - final Uint8List? image = capture.image; | ||
| 91 | - for (final barcode in barcodes) { | ||
| 92 | - debugPrint('Barcode found! ${barcode.rawValue}'); | ||
| 93 | - } | ||
| 94 | - }, | ||
| 95 | - ), | ||
| 96 | - ); | ||
| 97 | - } | 76 | +final String scriptUrl = // ... |
| 77 | + | ||
| 78 | +if (kIsWeb) { | ||
| 79 | + MobileScannerPlatform.instance.setBarcodeLibraryScriptUrl(scriptUrl); | ||
| 80 | +} | ||
| 98 | ``` | 81 | ``` |
| 99 | 82 | ||
| 100 | -Example with controller and initial values: | 83 | +## Usage |
| 84 | + | ||
| 85 | +Import the package with `package:mobile_scanner/mobile_scanner.dart`. | ||
| 86 | + | ||
| 87 | +Create a new `MobileScannerController` controller, using the required options. | ||
| 88 | +Provide a `StreamSubscription` for the barcode events. | ||
| 101 | 89 | ||
| 102 | ```dart | 90 | ```dart |
| 103 | -import 'package:mobile_scanner/mobile_scanner.dart'; | 91 | +final MobileScannerController controller = MobileScannerController( |
| 92 | + // required options for the scanner | ||
| 93 | +); | ||
| 104 | 94 | ||
| 105 | - @override | ||
| 106 | - Widget build(BuildContext context) { | ||
| 107 | - return Scaffold( | ||
| 108 | - appBar: AppBar(title: const Text('Mobile Scanner')), | ||
| 109 | - body: MobileScanner( | ||
| 110 | - // fit: BoxFit.contain, | ||
| 111 | - controller: MobileScannerController( | ||
| 112 | - detectionSpeed: DetectionSpeed.normal, | ||
| 113 | - facing: CameraFacing.front, | ||
| 114 | - torchEnabled: true, | ||
| 115 | - ), | ||
| 116 | - onDetect: (capture) { | ||
| 117 | - final List<Barcode> barcodes = capture.barcodes; | ||
| 118 | - final Uint8List? image = capture.image; | ||
| 119 | - for (final barcode in barcodes) { | ||
| 120 | - debugPrint('Barcode found! ${barcode.rawValue}'); | ||
| 121 | - } | ||
| 122 | - }, | ||
| 123 | - ), | ||
| 124 | - ); | ||
| 125 | - } | 95 | +StreamSubscription<Object?>? _subscription; |
| 126 | ``` | 96 | ``` |
| 127 | 97 | ||
| 128 | -Example with controller and torch & camera controls: | 98 | +Ensure that your `State` class mixes in `WidgetsBindingObserver`, to handle lifecyle changes: |
| 129 | 99 | ||
| 130 | ```dart | 100 | ```dart |
| 131 | -import 'package:mobile_scanner/mobile_scanner.dart'; | ||
| 132 | - | ||
| 133 | - MobileScannerController cameraController = MobileScannerController(); | 101 | +class MyState extends State<MyStatefulWidget> with WidgetsBindingObserver { |
| 102 | + // ... | ||
| 134 | 103 | ||
| 135 | @override | 104 | @override |
| 136 | - Widget build(BuildContext context) { | ||
| 137 | - return Scaffold( | ||
| 138 | - appBar: AppBar( | ||
| 139 | - title: const Text('Mobile Scanner'), | ||
| 140 | - actions: [ | ||
| 141 | - IconButton( | ||
| 142 | - color: Colors.white, | ||
| 143 | - icon: ValueListenableBuilder( | ||
| 144 | - valueListenable: cameraController.torchState, | ||
| 145 | - builder: (context, state, child) { | ||
| 146 | - switch (state as TorchState) { | ||
| 147 | - case TorchState.off: | ||
| 148 | - return const Icon(Icons.flash_off, color: Colors.grey); | ||
| 149 | - case TorchState.on: | ||
| 150 | - return const Icon(Icons.flash_on, color: Colors.yellow); | ||
| 151 | - } | ||
| 152 | - }, | ||
| 153 | - ), | ||
| 154 | - iconSize: 32.0, | ||
| 155 | - onPressed: () => cameraController.toggleTorch(), | ||
| 156 | - ), | ||
| 157 | - IconButton( | ||
| 158 | - color: Colors.white, | ||
| 159 | - icon: ValueListenableBuilder( | ||
| 160 | - valueListenable: cameraController.cameraFacingState, | ||
| 161 | - builder: (context, state, child) { | ||
| 162 | - switch (state as CameraFacing) { | ||
| 163 | - case CameraFacing.front: | ||
| 164 | - return const Icon(Icons.camera_front); | ||
| 165 | - case CameraFacing.back: | ||
| 166 | - return const Icon(Icons.camera_rear); | 105 | + void didChangeAppLifecycleState(AppLifecycleState state) { |
| 106 | + super.didChangeAppLifecycleState(state); | ||
| 107 | + | ||
| 108 | + switch (state) { | ||
| 109 | + case AppLifecycleState.detached: | ||
| 110 | + case AppLifecycleState.hidden: | ||
| 111 | + case AppLifecycleState.paused: | ||
| 112 | + return; | ||
| 113 | + case AppLifecycleState.resumed: | ||
| 114 | + // Restart the scanner when the app is resumed. | ||
| 115 | + // Don't forget to resume listening to the barcode events. | ||
| 116 | + _subscription = controller.barcodes.listen(_handleBarcode); | ||
| 117 | + | ||
| 118 | + unawaited(controller.start()); | ||
| 119 | + case AppLifecycleState.inactive: | ||
| 120 | + // Stop the scanner when the app is paused. | ||
| 121 | + // Also stop the barcode events subscription. | ||
| 122 | + unawaited(_subscription?.cancel()); | ||
| 123 | + _subscription = null; | ||
| 124 | + unawaited(controller.stop()); | ||
| 167 | } | 125 | } |
| 168 | - }, | ||
| 169 | - ), | ||
| 170 | - iconSize: 32.0, | ||
| 171 | - onPressed: () => cameraController.switchCamera(), | ||
| 172 | - ), | ||
| 173 | - ], | ||
| 174 | - ), | ||
| 175 | - body: MobileScanner( | ||
| 176 | - // fit: BoxFit.contain, | ||
| 177 | - controller: cameraController, | ||
| 178 | - onDetect: (capture) { | ||
| 179 | - final List<Barcode> barcodes = capture.barcodes; | ||
| 180 | - final Uint8List? image = capture.image; | ||
| 181 | - for (final barcode in barcodes) { | ||
| 182 | - debugPrint('Barcode found! ${barcode.rawValue}'); | ||
| 183 | - } | ||
| 184 | - }, | ||
| 185 | - ), | ||
| 186 | - ); | ||
| 187 | } | 126 | } |
| 127 | + | ||
| 128 | + // ... | ||
| 129 | +} | ||
| 188 | ``` | 130 | ``` |
| 189 | 131 | ||
| 190 | -Example with controller and returning images | 132 | +Then, start the scanner in `void initState()`: |
| 191 | 133 | ||
| 192 | ```dart | 134 | ```dart |
| 193 | -import 'package:mobile_scanner/mobile_scanner.dart'; | 135 | +@override |
| 136 | +void initState() { | ||
| 137 | + super.initState(); | ||
| 138 | + // Start listening to lifecycle changes. | ||
| 139 | + WidgetsBinding.instance.addObserver(this); | ||
| 140 | + | ||
| 141 | + // Start listening to the barcode events. | ||
| 142 | + _subscription = controller.barcodes.listen(_handleBarcode); | ||
| 143 | + | ||
| 144 | + // Finally, start the scanner itself. | ||
| 145 | + unawaited(controller.start()); | ||
| 146 | +} | ||
| 147 | +``` | ||
| 194 | 148 | ||
| 195 | - @override | ||
| 196 | - Widget build(BuildContext context) { | ||
| 197 | - return Scaffold( | ||
| 198 | - appBar: AppBar(title: const Text('Mobile Scanner')), | ||
| 199 | - body: MobileScanner( | ||
| 200 | - fit: BoxFit.contain, | ||
| 201 | - controller: MobileScannerController( | ||
| 202 | - // facing: CameraFacing.back, | ||
| 203 | - // torchEnabled: false, | ||
| 204 | - returnImage: true, | ||
| 205 | - ), | ||
| 206 | - onDetect: (capture) { | ||
| 207 | - final List<Barcode> barcodes = capture.barcodes; | ||
| 208 | - final Uint8List? image = capture.image; | ||
| 209 | - for (final barcode in barcodes) { | ||
| 210 | - debugPrint('Barcode found! ${barcode.rawValue}'); | ||
| 211 | - } | ||
| 212 | - if (image != null) { | ||
| 213 | - showDialog( | ||
| 214 | - context: context, | ||
| 215 | - builder: (context) => | ||
| 216 | - Image(image: MemoryImage(image)), | ||
| 217 | - ); | ||
| 218 | - Future.delayed(const Duration(seconds: 5), () { | ||
| 219 | - Navigator.pop(context); | ||
| 220 | - }); | ||
| 221 | - } | ||
| 222 | - }, | ||
| 223 | - ), | ||
| 224 | - ); | ||
| 225 | - } | 149 | +Finally, dispose of the the `MobileScannerController` when you are done with it. |
| 150 | + | ||
| 151 | +```dart | ||
| 152 | +@override | ||
| 153 | +Future<void> dispose() async { | ||
| 154 | + // Stop listening to lifecycle changes. | ||
| 155 | + WidgetsBinding.instance.removeObserver(this); | ||
| 156 | + // Stop listening to the barcode events. | ||
| 157 | + unawaited(_subscription?.cancel()); | ||
| 158 | + _subscription = null; | ||
| 159 | + // Dispose the widget itself. | ||
| 160 | + super.dispose(); | ||
| 161 | + // Finally, dispose of the controller. | ||
| 162 | + await controller.dispose(); | ||
| 163 | +} | ||
| 226 | ``` | 164 | ``` |
| 227 | 165 | ||
| 228 | -### BarcodeCapture | ||
| 229 | - | ||
| 230 | -The onDetect function returns a BarcodeCapture objects which contains the following items. | ||
| 231 | - | ||
| 232 | -| Property name | Type | Description | | ||
| 233 | -|---------------|---------------|-----------------------------------| | ||
| 234 | -| barcodes | List<Barcode> | A list with scanned barcodes. | | ||
| 235 | -| image | Uint8List? | If enabled, an image of the scan. | | ||
| 236 | - | ||
| 237 | -You can use the following properties of the Barcode object. | ||
| 238 | - | ||
| 239 | -| Property name | Type | Description | | ||
| 240 | -|---------------|----------------|-------------------------------------| | ||
| 241 | -| format | BarcodeFormat | | | ||
| 242 | -| rawBytes | Uint8List? | binary scan result | | ||
| 243 | -| rawValue | String? | Value if barcode is in UTF-8 format | | ||
| 244 | -| displayValue | String? | | | ||
| 245 | -| type | BarcodeType | | | ||
| 246 | -| calendarEvent | CalendarEvent? | | | ||
| 247 | -| contactInfo | ContactInfo? | | | ||
| 248 | -| driverLicense | DriverLicense? | | | ||
| 249 | -| email | Email? | | | ||
| 250 | -| geoPoint | GeoPoint? | | | ||
| 251 | -| phone | Phone? | | | ||
| 252 | -| sms | SMS? | | | ||
| 253 | -| url | UrlBookmark? | | | ||
| 254 | -| wifi | WiFi? | WiFi Access-Point details | | 166 | +To display the camera preview, pass the controller to a `MobileScanner` widget. |
| 167 | + | ||
| 168 | +See the examples for runnable examples of various usages, | ||
| 169 | +such as the basic usage, applying a scan window, or retrieving images from the barcodes. |
| @@ -78,7 +78,7 @@ class MobileScanner( | @@ -78,7 +78,7 @@ class MobileScanner( | ||
| 78 | scanner.process(inputImage) | 78 | scanner.process(inputImage) |
| 79 | .addOnSuccessListener { barcodes -> | 79 | .addOnSuccessListener { barcodes -> |
| 80 | if (detectionSpeed == DetectionSpeed.NO_DUPLICATES) { | 80 | if (detectionSpeed == DetectionSpeed.NO_DUPLICATES) { |
| 81 | - val newScannedBarcodes = barcodes.mapNotNull({ barcode -> barcode.rawValue }).sorted() | 81 | + val newScannedBarcodes = barcodes.mapNotNull { barcode -> barcode.rawValue }.sorted() |
| 82 | if (newScannedBarcodes == lastScanned) { | 82 | if (newScannedBarcodes == lastScanned) { |
| 83 | // New scanned is duplicate, returning | 83 | // New scanned is duplicate, returning |
| 84 | return@addOnSuccessListener | 84 | return@addOnSuccessListener |
| @@ -424,7 +424,7 @@ class MobileScanner( | @@ -424,7 +424,7 @@ class MobileScanner( | ||
| 424 | /** | 424 | /** |
| 425 | * Analyze a single image. | 425 | * Analyze a single image. |
| 426 | */ | 426 | */ |
| 427 | - fun analyzeImage(image: Uri, analyzerCallback: AnalyzerCallback) { | 427 | + fun analyzeImage(image: Uri, onSuccess: AnalyzerSuccessCallback, onError: AnalyzerErrorCallback) { |
| 428 | val inputImage = InputImage.fromFilePath(activity, image) | 428 | val inputImage = InputImage.fromFilePath(activity, image) |
| 429 | 429 | ||
| 430 | scanner.process(inputImage) | 430 | scanner.process(inputImage) |
| @@ -432,15 +432,13 @@ class MobileScanner( | @@ -432,15 +432,13 @@ class MobileScanner( | ||
| 432 | val barcodeMap = barcodes.map { barcode -> barcode.data } | 432 | val barcodeMap = barcodes.map { barcode -> barcode.data } |
| 433 | 433 | ||
| 434 | if (barcodeMap.isNotEmpty()) { | 434 | if (barcodeMap.isNotEmpty()) { |
| 435 | - analyzerCallback(barcodeMap) | 435 | + onSuccess(barcodeMap) |
| 436 | } else { | 436 | } else { |
| 437 | - analyzerCallback(null) | 437 | + onSuccess(null) |
| 438 | } | 438 | } |
| 439 | } | 439 | } |
| 440 | .addOnFailureListener { e -> | 440 | .addOnFailureListener { e -> |
| 441 | - mobileScannerErrorCallback( | ||
| 442 | - e.localizedMessage ?: e.toString() | ||
| 443 | - ) | 441 | + onError(e.localizedMessage ?: e.toString()) |
| 444 | } | 442 | } |
| 445 | } | 443 | } |
| 446 | 444 |
| @@ -3,7 +3,8 @@ package dev.steenbakker.mobile_scanner | @@ -3,7 +3,8 @@ package dev.steenbakker.mobile_scanner | ||
| 3 | import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters | 3 | import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters |
| 4 | 4 | ||
| 5 | typealias MobileScannerCallback = (barcodes: List<Map<String, Any?>>, image: ByteArray?, width: Int?, height: Int?) -> Unit | 5 | typealias MobileScannerCallback = (barcodes: List<Map<String, Any?>>, image: ByteArray?, width: Int?, height: Int?) -> Unit |
| 6 | -typealias AnalyzerCallback = (barcodes: List<Map<String, Any?>>?) -> Unit | 6 | +typealias AnalyzerErrorCallback = (message: String) -> Unit |
| 7 | +typealias AnalyzerSuccessCallback = (barcodes: List<Map<String, Any?>>?) -> Unit | ||
| 7 | typealias MobileScannerErrorCallback = (error: String) -> Unit | 8 | typealias MobileScannerErrorCallback = (error: String) -> Unit |
| 8 | typealias TorchStateCallback = (state: Int) -> Unit | 9 | typealias TorchStateCallback = (state: Int) -> Unit |
| 9 | typealias ZoomScaleStateCallback = (zoomScale: Double) -> Unit | 10 | typealias ZoomScaleStateCallback = (zoomScale: Double) -> Unit |
| @@ -26,16 +26,19 @@ class MobileScannerHandler( | @@ -26,16 +26,19 @@ class MobileScannerHandler( | ||
| 26 | private val addPermissionListener: (RequestPermissionsResultListener) -> Unit, | 26 | private val addPermissionListener: (RequestPermissionsResultListener) -> Unit, |
| 27 | textureRegistry: TextureRegistry): MethodChannel.MethodCallHandler { | 27 | textureRegistry: TextureRegistry): MethodChannel.MethodCallHandler { |
| 28 | 28 | ||
| 29 | - private val analyzerCallback: AnalyzerCallback = { barcodes: List<Map<String, Any?>>?-> | ||
| 30 | - if (barcodes != null) { | ||
| 31 | - barcodeHandler.publishEvent(mapOf( | ||
| 32 | - "name" to "barcode", | ||
| 33 | - "data" to barcodes | ||
| 34 | - )) | 29 | + private val analyzeImageErrorCallback: AnalyzerErrorCallback = { |
| 30 | + Handler(Looper.getMainLooper()).post { | ||
| 31 | + analyzerResult?.error("MobileScanner", it, null) | ||
| 32 | + analyzerResult = null | ||
| 33 | + } | ||
| 35 | } | 34 | } |
| 36 | 35 | ||
| 36 | + private val analyzeImageSuccessCallback: AnalyzerSuccessCallback = { | ||
| 37 | Handler(Looper.getMainLooper()).post { | 37 | Handler(Looper.getMainLooper()).post { |
| 38 | - analyzerResult?.success(barcodes != null) | 38 | + analyzerResult?.success(mapOf( |
| 39 | + "name" to "barcode", | ||
| 40 | + "data" to it | ||
| 41 | + )) | ||
| 39 | analyzerResult = null | 42 | analyzerResult = null |
| 40 | } | 43 | } |
| 41 | } | 44 | } |
| @@ -236,7 +239,8 @@ class MobileScannerHandler( | @@ -236,7 +239,8 @@ class MobileScannerHandler( | ||
| 236 | private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) { | 239 | private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) { |
| 237 | analyzerResult = result | 240 | analyzerResult = result |
| 238 | val uri = Uri.fromFile(File(call.arguments.toString())) | 241 | val uri = Uri.fromFile(File(call.arguments.toString())) |
| 239 | - mobileScanner!!.analyzeImage(uri, analyzerCallback) | 242 | + |
| 243 | + mobileScanner!!.analyzeImage(uri, analyzeImageSuccessCallback, analyzeImageErrorCallback) | ||
| 240 | } | 244 | } |
| 241 | 245 | ||
| 242 | private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) { | 246 | private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) { |
| @@ -265,7 +269,7 @@ class MobileScannerHandler( | @@ -265,7 +269,7 @@ class MobileScannerHandler( | ||
| 265 | } | 269 | } |
| 266 | 270 | ||
| 267 | private fun updateScanWindow(call: MethodCall, result: MethodChannel.Result) { | 271 | private fun updateScanWindow(call: MethodCall, result: MethodChannel.Result) { |
| 268 | - mobileScanner!!.scanWindow = call.argument<List<Float>?>("rect") | 272 | + mobileScanner?.scanWindow = call.argument<List<Float>?>("rect") |
| 269 | 273 | ||
| 270 | result.success(null) | 274 | result.success(null) |
| 271 | } | 275 | } |
| 1 | -import 'package:flutter/material.dart'; | ||
| 2 | -import 'package:image_picker/image_picker.dart'; | ||
| 3 | -import 'package:mobile_scanner/mobile_scanner.dart'; | ||
| 4 | -import 'package:mobile_scanner_example/scanner_error_widget.dart'; | ||
| 5 | - | ||
| 6 | -class BarcodeListScannerWithController extends StatefulWidget { | ||
| 7 | - const BarcodeListScannerWithController({super.key}); | ||
| 8 | - | ||
| 9 | - @override | ||
| 10 | - State<BarcodeListScannerWithController> createState() => | ||
| 11 | - _BarcodeListScannerWithControllerState(); | ||
| 12 | -} | ||
| 13 | - | ||
| 14 | -class _BarcodeListScannerWithControllerState | ||
| 15 | - extends State<BarcodeListScannerWithController> | ||
| 16 | - with SingleTickerProviderStateMixin { | ||
| 17 | - BarcodeCapture? barcodeCapture; | ||
| 18 | - | ||
| 19 | - final MobileScannerController controller = MobileScannerController( | ||
| 20 | - torchEnabled: true, | ||
| 21 | - // formats: [BarcodeFormat.qrCode] | ||
| 22 | - // facing: CameraFacing.front, | ||
| 23 | - // detectionSpeed: DetectionSpeed.normal | ||
| 24 | - // detectionTimeoutMs: 1000, | ||
| 25 | - // returnImage: false, | ||
| 26 | - ); | ||
| 27 | - | ||
| 28 | - bool isStarted = true; | ||
| 29 | - | ||
| 30 | - void _startOrStop() { | ||
| 31 | - try { | ||
| 32 | - if (isStarted) { | ||
| 33 | - controller.stop(); | ||
| 34 | - } else { | ||
| 35 | - controller.start(); | ||
| 36 | - } | ||
| 37 | - setState(() { | ||
| 38 | - isStarted = !isStarted; | ||
| 39 | - }); | ||
| 40 | - } on Exception catch (e) { | ||
| 41 | - ScaffoldMessenger.of(context).showSnackBar( | ||
| 42 | - SnackBar( | ||
| 43 | - content: Text('Something went wrong! $e'), | ||
| 44 | - backgroundColor: Colors.red, | ||
| 45 | - ), | ||
| 46 | - ); | ||
| 47 | - } | ||
| 48 | - } | ||
| 49 | - | ||
| 50 | - @override | ||
| 51 | - Widget build(BuildContext context) { | ||
| 52 | - return Scaffold( | ||
| 53 | - appBar: AppBar(title: const Text('With ValueListenableBuilder')), | ||
| 54 | - backgroundColor: Colors.black, | ||
| 55 | - body: Builder( | ||
| 56 | - builder: (context) { | ||
| 57 | - return Stack( | ||
| 58 | - children: [ | ||
| 59 | - MobileScanner( | ||
| 60 | - controller: controller, | ||
| 61 | - errorBuilder: (context, error, child) { | ||
| 62 | - return ScannerErrorWidget(error: error); | ||
| 63 | - }, | ||
| 64 | - fit: BoxFit.contain, | ||
| 65 | - onDetect: (barcodeCapture) { | ||
| 66 | - setState(() { | ||
| 67 | - this.barcodeCapture = barcodeCapture; | ||
| 68 | - }); | ||
| 69 | - }, | ||
| 70 | - onScannerStarted: (arguments) { | ||
| 71 | - // Do something with arguments. | ||
| 72 | - }, | ||
| 73 | - ), | ||
| 74 | - Align( | ||
| 75 | - alignment: Alignment.bottomCenter, | ||
| 76 | - child: Container( | ||
| 77 | - alignment: Alignment.bottomCenter, | ||
| 78 | - height: 100, | ||
| 79 | - color: Colors.black.withOpacity(0.4), | ||
| 80 | - child: Row( | ||
| 81 | - mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||
| 82 | - children: [ | ||
| 83 | - IconButton( | ||
| 84 | - color: Colors.white, | ||
| 85 | - icon: ValueListenableBuilder<TorchState>( | ||
| 86 | - valueListenable: controller.torchState, | ||
| 87 | - builder: (context, state, child) { | ||
| 88 | - switch (state) { | ||
| 89 | - case TorchState.off: | ||
| 90 | - return const Icon( | ||
| 91 | - Icons.flash_off, | ||
| 92 | - color: Colors.grey, | ||
| 93 | - ); | ||
| 94 | - case TorchState.on: | ||
| 95 | - return const Icon( | ||
| 96 | - Icons.flash_on, | ||
| 97 | - color: Colors.yellow, | ||
| 98 | - ); | ||
| 99 | - } | ||
| 100 | - }, | ||
| 101 | - ), | ||
| 102 | - iconSize: 32.0, | ||
| 103 | - onPressed: () => controller.toggleTorch(), | ||
| 104 | - ), | ||
| 105 | - IconButton( | ||
| 106 | - color: Colors.white, | ||
| 107 | - icon: isStarted | ||
| 108 | - ? const Icon(Icons.stop) | ||
| 109 | - : const Icon(Icons.play_arrow), | ||
| 110 | - iconSize: 32.0, | ||
| 111 | - onPressed: _startOrStop, | ||
| 112 | - ), | ||
| 113 | - Center( | ||
| 114 | - child: SizedBox( | ||
| 115 | - width: MediaQuery.of(context).size.width - 200, | ||
| 116 | - height: 50, | ||
| 117 | - child: FittedBox( | ||
| 118 | - child: Text( | ||
| 119 | - '${barcodeCapture?.barcodes.map((e) => e.rawValue) ?? 'Scan something!'}', | ||
| 120 | - overflow: TextOverflow.fade, | ||
| 121 | - style: Theme.of(context) | ||
| 122 | - .textTheme | ||
| 123 | - .headlineMedium! | ||
| 124 | - .copyWith(color: Colors.white), | ||
| 125 | - ), | ||
| 126 | - ), | ||
| 127 | - ), | ||
| 128 | - ), | ||
| 129 | - IconButton( | ||
| 130 | - color: Colors.white, | ||
| 131 | - icon: ValueListenableBuilder<CameraFacing>( | ||
| 132 | - valueListenable: controller.cameraFacingState, | ||
| 133 | - builder: (context, state, child) { | ||
| 134 | - switch (state) { | ||
| 135 | - case CameraFacing.front: | ||
| 136 | - return const Icon(Icons.camera_front); | ||
| 137 | - case CameraFacing.back: | ||
| 138 | - return const Icon(Icons.camera_rear); | ||
| 139 | - } | ||
| 140 | - }, | ||
| 141 | - ), | ||
| 142 | - iconSize: 32.0, | ||
| 143 | - onPressed: () => controller.switchCamera(), | ||
| 144 | - ), | ||
| 145 | - IconButton( | ||
| 146 | - color: Colors.white, | ||
| 147 | - icon: const Icon(Icons.image), | ||
| 148 | - iconSize: 32.0, | ||
| 149 | - onPressed: () async { | ||
| 150 | - final ImagePicker picker = ImagePicker(); | ||
| 151 | - // Pick an image | ||
| 152 | - final XFile? image = await picker.pickImage( | ||
| 153 | - source: ImageSource.gallery, | ||
| 154 | - ); | ||
| 155 | - if (image != null) { | ||
| 156 | - if (await controller.analyzeImage(image.path)) { | ||
| 157 | - if (!context.mounted) return; | ||
| 158 | - ScaffoldMessenger.of(context).showSnackBar( | ||
| 159 | - const SnackBar( | ||
| 160 | - content: Text('Barcode found!'), | ||
| 161 | - backgroundColor: Colors.green, | ||
| 162 | - ), | ||
| 163 | - ); | ||
| 164 | - } else { | ||
| 165 | - if (!context.mounted) return; | ||
| 166 | - ScaffoldMessenger.of(context).showSnackBar( | ||
| 167 | - const SnackBar( | ||
| 168 | - content: Text('No barcode found!'), | ||
| 169 | - backgroundColor: Colors.red, | ||
| 170 | - ), | ||
| 171 | - ); | ||
| 172 | - } | ||
| 173 | - } | ||
| 174 | - }, | ||
| 175 | - ), | ||
| 176 | - ], | ||
| 177 | - ), | ||
| 178 | - ), | ||
| 179 | - ), | ||
| 180 | - ], | ||
| 181 | - ); | ||
| 182 | - }, | ||
| 183 | - ), | ||
| 184 | - ); | ||
| 185 | - } | ||
| 186 | - | ||
| 187 | - @override | ||
| 188 | - void dispose() { | ||
| 189 | - controller.dispose(); | ||
| 190 | - super.dispose(); | ||
| 191 | - } | ||
| 192 | -} |
| 1 | +import 'dart:async'; | ||
| 2 | + | ||
| 1 | import 'package:flutter/material.dart'; | 3 | import 'package:flutter/material.dart'; |
| 2 | -import 'package:image_picker/image_picker.dart'; | ||
| 3 | import 'package:mobile_scanner/mobile_scanner.dart'; | 4 | import 'package:mobile_scanner/mobile_scanner.dart'; |
| 5 | +import 'package:mobile_scanner_example/scanner_button_widgets.dart'; | ||
| 4 | import 'package:mobile_scanner_example/scanner_error_widget.dart'; | 6 | import 'package:mobile_scanner_example/scanner_error_widget.dart'; |
| 5 | 7 | ||
| 6 | class BarcodeScannerWithController extends StatefulWidget { | 8 | class BarcodeScannerWithController extends StatefulWidget { |
| @@ -12,10 +14,7 @@ class BarcodeScannerWithController extends StatefulWidget { | @@ -12,10 +14,7 @@ class BarcodeScannerWithController extends StatefulWidget { | ||
| 12 | } | 14 | } |
| 13 | 15 | ||
| 14 | class _BarcodeScannerWithControllerState | 16 | class _BarcodeScannerWithControllerState |
| 15 | - extends State<BarcodeScannerWithController> | ||
| 16 | - with SingleTickerProviderStateMixin { | ||
| 17 | - BarcodeCapture? barcode; | ||
| 18 | - | 17 | + extends State<BarcodeScannerWithController> with WidgetsBindingObserver { |
| 19 | final MobileScannerController controller = MobileScannerController( | 18 | final MobileScannerController controller = MobileScannerController( |
| 20 | torchEnabled: true, useNewCameraSelector: true, | 19 | torchEnabled: true, useNewCameraSelector: true, |
| 21 | // formats: [BarcodeFormat.qrCode] | 20 | // formats: [BarcodeFormat.qrCode] |
| @@ -25,56 +24,76 @@ class _BarcodeScannerWithControllerState | @@ -25,56 +24,76 @@ class _BarcodeScannerWithControllerState | ||
| 25 | // returnImage: false, | 24 | // returnImage: false, |
| 26 | ); | 25 | ); |
| 27 | 26 | ||
| 28 | - bool isStarted = true; | 27 | + Barcode? _barcode; |
| 28 | + StreamSubscription<Object?>? _subscription; | ||
| 29 | + | ||
| 30 | + Widget _buildBarcode(Barcode? value) { | ||
| 31 | + if (value == null) { | ||
| 32 | + return const Text( | ||
| 33 | + 'Scan something!', | ||
| 34 | + overflow: TextOverflow.fade, | ||
| 35 | + style: TextStyle(color: Colors.white), | ||
| 36 | + ); | ||
| 37 | + } | ||
| 29 | 38 | ||
| 30 | - void _startOrStop() { | ||
| 31 | - try { | ||
| 32 | - if (isStarted) { | ||
| 33 | - controller.stop(); | ||
| 34 | - } else { | ||
| 35 | - controller.start(); | 39 | + return Text( |
| 40 | + value.displayValue ?? 'No display value.', | ||
| 41 | + overflow: TextOverflow.fade, | ||
| 42 | + style: const TextStyle(color: Colors.white), | ||
| 43 | + ); | ||
| 36 | } | 44 | } |
| 45 | + | ||
| 46 | + void _handleBarcode(BarcodeCapture barcodes) { | ||
| 47 | + if (mounted) { | ||
| 37 | setState(() { | 48 | setState(() { |
| 38 | - isStarted = !isStarted; | 49 | + _barcode = barcodes.barcodes.firstOrNull; |
| 39 | }); | 50 | }); |
| 40 | - } on Exception catch (e) { | ||
| 41 | - ScaffoldMessenger.of(context).showSnackBar( | ||
| 42 | - SnackBar( | ||
| 43 | - content: Text('Something went wrong! $e'), | ||
| 44 | - backgroundColor: Colors.red, | ||
| 45 | - ), | ||
| 46 | - ); | ||
| 47 | } | 51 | } |
| 48 | } | 52 | } |
| 49 | 53 | ||
| 50 | - int? numberOfCameras; | 54 | + @override |
| 55 | + void initState() { | ||
| 56 | + super.initState(); | ||
| 57 | + WidgetsBinding.instance.addObserver(this); | ||
| 58 | + | ||
| 59 | + _subscription = controller.barcodes.listen(_handleBarcode); | ||
| 60 | + | ||
| 61 | + unawaited(controller.start()); | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + @override | ||
| 65 | + void didChangeAppLifecycleState(AppLifecycleState state) { | ||
| 66 | + super.didChangeAppLifecycleState(state); | ||
| 67 | + | ||
| 68 | + switch (state) { | ||
| 69 | + case AppLifecycleState.detached: | ||
| 70 | + case AppLifecycleState.hidden: | ||
| 71 | + case AppLifecycleState.paused: | ||
| 72 | + return; | ||
| 73 | + case AppLifecycleState.resumed: | ||
| 74 | + _subscription = controller.barcodes.listen(_handleBarcode); | ||
| 75 | + | ||
| 76 | + unawaited(controller.start()); | ||
| 77 | + case AppLifecycleState.inactive: | ||
| 78 | + unawaited(_subscription?.cancel()); | ||
| 79 | + _subscription = null; | ||
| 80 | + unawaited(controller.stop()); | ||
| 81 | + } | ||
| 82 | + } | ||
| 51 | 83 | ||
| 52 | @override | 84 | @override |
| 53 | Widget build(BuildContext context) { | 85 | Widget build(BuildContext context) { |
| 54 | return Scaffold( | 86 | return Scaffold( |
| 55 | appBar: AppBar(title: const Text('With controller')), | 87 | appBar: AppBar(title: const Text('With controller')), |
| 56 | backgroundColor: Colors.black, | 88 | backgroundColor: Colors.black, |
| 57 | - body: Builder( | ||
| 58 | - builder: (context) { | ||
| 59 | - return Stack( | 89 | + body: Stack( |
| 60 | children: [ | 90 | children: [ |
| 61 | MobileScanner( | 91 | MobileScanner( |
| 62 | - onScannerStarted: (arguments) { | ||
| 63 | - if (mounted && arguments?.numberOfCameras != null) { | ||
| 64 | - numberOfCameras = arguments!.numberOfCameras; | ||
| 65 | - setState(() {}); | ||
| 66 | - } | ||
| 67 | - }, | ||
| 68 | controller: controller, | 92 | controller: controller, |
| 69 | errorBuilder: (context, error, child) { | 93 | errorBuilder: (context, error, child) { |
| 70 | return ScannerErrorWidget(error: error); | 94 | return ScannerErrorWidget(error: error); |
| 71 | }, | 95 | }, |
| 72 | fit: BoxFit.contain, | 96 | fit: BoxFit.contain, |
| 73 | - onDetect: (barcode) { | ||
| 74 | - setState(() { | ||
| 75 | - this.barcode = barcode; | ||
| 76 | - }); | ||
| 77 | - }, | ||
| 78 | ), | 97 | ), |
| 79 | Align( | 98 | Align( |
| 80 | alignment: Alignment.bottomCenter, | 99 | alignment: Alignment.bottomCenter, |
| @@ -85,118 +104,26 @@ class _BarcodeScannerWithControllerState | @@ -85,118 +104,26 @@ class _BarcodeScannerWithControllerState | ||
| 85 | child: Row( | 104 | child: Row( |
| 86 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, | 105 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, |
| 87 | children: [ | 106 | children: [ |
| 88 | - ValueListenableBuilder( | ||
| 89 | - valueListenable: controller.hasTorchState, | ||
| 90 | - builder: (context, state, child) { | ||
| 91 | - if (state != true) { | ||
| 92 | - return const SizedBox.shrink(); | ||
| 93 | - } | ||
| 94 | - return IconButton( | ||
| 95 | - color: Colors.white, | ||
| 96 | - icon: ValueListenableBuilder<TorchState>( | ||
| 97 | - valueListenable: controller.torchState, | ||
| 98 | - builder: (context, state, child) { | ||
| 99 | - switch (state) { | ||
| 100 | - case TorchState.off: | ||
| 101 | - return const Icon( | ||
| 102 | - Icons.flash_off, | ||
| 103 | - color: Colors.grey, | ||
| 104 | - ); | ||
| 105 | - case TorchState.on: | ||
| 106 | - return const Icon( | ||
| 107 | - Icons.flash_on, | ||
| 108 | - color: Colors.yellow, | ||
| 109 | - ); | ||
| 110 | - } | ||
| 111 | - }, | ||
| 112 | - ), | ||
| 113 | - iconSize: 32.0, | ||
| 114 | - onPressed: () => controller.toggleTorch(), | ||
| 115 | - ); | ||
| 116 | - }, | ||
| 117 | - ), | ||
| 118 | - IconButton( | ||
| 119 | - color: Colors.white, | ||
| 120 | - icon: isStarted | ||
| 121 | - ? const Icon(Icons.stop) | ||
| 122 | - : const Icon(Icons.play_arrow), | ||
| 123 | - iconSize: 32.0, | ||
| 124 | - onPressed: _startOrStop, | ||
| 125 | - ), | ||
| 126 | - Center( | ||
| 127 | - child: SizedBox( | ||
| 128 | - width: MediaQuery.of(context).size.width - 200, | ||
| 129 | - height: 50, | ||
| 130 | - child: FittedBox( | ||
| 131 | - child: Text( | ||
| 132 | - barcode?.barcodes.first.rawValue ?? | ||
| 133 | - 'Scan something!', | ||
| 134 | - overflow: TextOverflow.fade, | ||
| 135 | - style: Theme.of(context) | ||
| 136 | - .textTheme | ||
| 137 | - .headlineMedium! | ||
| 138 | - .copyWith(color: Colors.white), | ||
| 139 | - ), | ||
| 140 | - ), | ||
| 141 | - ), | ||
| 142 | - ), | ||
| 143 | - IconButton( | ||
| 144 | - color: Colors.white, | ||
| 145 | - icon: ValueListenableBuilder<CameraFacing>( | ||
| 146 | - valueListenable: controller.cameraFacingState, | ||
| 147 | - builder: (context, state, child) { | ||
| 148 | - switch (state) { | ||
| 149 | - case CameraFacing.front: | ||
| 150 | - return const Icon(Icons.camera_front); | ||
| 151 | - case CameraFacing.back: | ||
| 152 | - return const Icon(Icons.camera_rear); | ||
| 153 | - } | ||
| 154 | - }, | ||
| 155 | - ), | ||
| 156 | - iconSize: 32.0, | ||
| 157 | - onPressed: (numberOfCameras ?? 0) < 2 | ||
| 158 | - ? null | ||
| 159 | - : () => controller.switchCamera(), | ||
| 160 | - ), | ||
| 161 | - IconButton( | ||
| 162 | - color: Colors.white, | ||
| 163 | - icon: const Icon(Icons.image), | ||
| 164 | - iconSize: 32.0, | ||
| 165 | - onPressed: () async { | ||
| 166 | - final ImagePicker picker = ImagePicker(); | ||
| 167 | - // Pick an image | ||
| 168 | - final XFile? image = await picker.pickImage( | ||
| 169 | - source: ImageSource.gallery, | ||
| 170 | - ); | ||
| 171 | - if (image != null) { | ||
| 172 | - if (await controller.analyzeImage(image.path)) { | ||
| 173 | - if (!context.mounted) return; | ||
| 174 | - ScaffoldMessenger.of(context).showSnackBar( | ||
| 175 | - const SnackBar( | ||
| 176 | - content: Text('Barcode found!'), | ||
| 177 | - backgroundColor: Colors.green, | ||
| 178 | - ), | ||
| 179 | - ); | ||
| 180 | - } else { | ||
| 181 | - if (!context.mounted) return; | ||
| 182 | - ScaffoldMessenger.of(context).showSnackBar( | ||
| 183 | - const SnackBar( | ||
| 184 | - content: Text('No barcode found!'), | ||
| 185 | - backgroundColor: Colors.red, | ||
| 186 | - ), | ||
| 187 | - ); | ||
| 188 | - } | ||
| 189 | - } | ||
| 190 | - }, | ||
| 191 | - ), | 107 | + ToggleFlashlightButton(controller: controller), |
| 108 | + StartStopMobileScannerButton(controller: controller), | ||
| 109 | + Expanded(child: Center(child: _buildBarcode(_barcode))), | ||
| 110 | + SwitchCameraButton(controller: controller), | ||
| 111 | + AnalyzeImageFromGalleryButton(controller: controller), | ||
| 192 | ], | 112 | ], |
| 193 | ), | 113 | ), |
| 194 | ), | 114 | ), |
| 195 | ), | 115 | ), |
| 196 | ], | 116 | ], |
| 197 | - ); | ||
| 198 | - }, | ||
| 199 | ), | 117 | ), |
| 200 | ); | 118 | ); |
| 201 | } | 119 | } |
| 120 | + | ||
| 121 | + @override | ||
| 122 | + Future<void> dispose() async { | ||
| 123 | + WidgetsBinding.instance.removeObserver(this); | ||
| 124 | + unawaited(_subscription?.cancel()); | ||
| 125 | + _subscription = null; | ||
| 126 | + super.dispose(); | ||
| 127 | + await controller.dispose(); | ||
| 128 | + } | ||
| 202 | } | 129 | } |
example/lib/barcode_scanner_listview.dart
0 → 100644
| 1 | +import 'dart:async'; | ||
| 2 | + | ||
| 3 | +import 'package:flutter/material.dart'; | ||
| 4 | +import 'package:mobile_scanner/mobile_scanner.dart'; | ||
| 5 | +import 'package:mobile_scanner_example/scanner_button_widgets.dart'; | ||
| 6 | +import 'package:mobile_scanner_example/scanner_error_widget.dart'; | ||
| 7 | + | ||
| 8 | +class BarcodeScannerListView extends StatefulWidget { | ||
| 9 | + const BarcodeScannerListView({super.key}); | ||
| 10 | + | ||
| 11 | + @override | ||
| 12 | + State<BarcodeScannerListView> createState() => _BarcodeScannerListViewState(); | ||
| 13 | +} | ||
| 14 | + | ||
| 15 | +class _BarcodeScannerListViewState extends State<BarcodeScannerListView> { | ||
| 16 | + final MobileScannerController controller = MobileScannerController( | ||
| 17 | + torchEnabled: true, | ||
| 18 | + // formats: [BarcodeFormat.qrCode] | ||
| 19 | + // facing: CameraFacing.front, | ||
| 20 | + // detectionSpeed: DetectionSpeed.normal | ||
| 21 | + // detectionTimeoutMs: 1000, | ||
| 22 | + // returnImage: false, | ||
| 23 | + ); | ||
| 24 | + | ||
| 25 | + @override | ||
| 26 | + void initState() { | ||
| 27 | + super.initState(); | ||
| 28 | + | ||
| 29 | + controller.start(); | ||
| 30 | + } | ||
| 31 | + | ||
| 32 | + Widget _buildBarcodesListView() { | ||
| 33 | + return StreamBuilder<BarcodeCapture>( | ||
| 34 | + stream: controller.barcodes, | ||
| 35 | + builder: (context, snapshot) { | ||
| 36 | + final barcodes = snapshot.data?.barcodes; | ||
| 37 | + | ||
| 38 | + if (barcodes == null || barcodes.isEmpty) { | ||
| 39 | + return const Center( | ||
| 40 | + child: Text( | ||
| 41 | + 'Scan Something!', | ||
| 42 | + style: TextStyle(color: Colors.white, fontSize: 20), | ||
| 43 | + ), | ||
| 44 | + ); | ||
| 45 | + } | ||
| 46 | + | ||
| 47 | + return ListView.builder( | ||
| 48 | + itemCount: barcodes.length, | ||
| 49 | + itemBuilder: (context, index) { | ||
| 50 | + return Padding( | ||
| 51 | + padding: const EdgeInsets.all(8.0), | ||
| 52 | + child: Text( | ||
| 53 | + barcodes[index].rawValue ?? 'No raw value', | ||
| 54 | + overflow: TextOverflow.fade, | ||
| 55 | + style: const TextStyle(color: Colors.white), | ||
| 56 | + ), | ||
| 57 | + ); | ||
| 58 | + }, | ||
| 59 | + ); | ||
| 60 | + }, | ||
| 61 | + ); | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + @override | ||
| 65 | + Widget build(BuildContext context) { | ||
| 66 | + return Scaffold( | ||
| 67 | + appBar: AppBar(title: const Text('With ListView')), | ||
| 68 | + backgroundColor: Colors.black, | ||
| 69 | + body: Stack( | ||
| 70 | + children: [ | ||
| 71 | + MobileScanner( | ||
| 72 | + controller: controller, | ||
| 73 | + errorBuilder: (context, error, child) { | ||
| 74 | + return ScannerErrorWidget(error: error); | ||
| 75 | + }, | ||
| 76 | + fit: BoxFit.contain, | ||
| 77 | + ), | ||
| 78 | + Align( | ||
| 79 | + alignment: Alignment.bottomCenter, | ||
| 80 | + child: Container( | ||
| 81 | + alignment: Alignment.bottomCenter, | ||
| 82 | + height: 100, | ||
| 83 | + color: Colors.black.withOpacity(0.4), | ||
| 84 | + child: Column( | ||
| 85 | + children: [ | ||
| 86 | + Expanded( | ||
| 87 | + child: _buildBarcodesListView(), | ||
| 88 | + ), | ||
| 89 | + Row( | ||
| 90 | + mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||
| 91 | + children: [ | ||
| 92 | + ToggleFlashlightButton(controller: controller), | ||
| 93 | + StartStopMobileScannerButton(controller: controller), | ||
| 94 | + const Spacer(), | ||
| 95 | + SwitchCameraButton(controller: controller), | ||
| 96 | + AnalyzeImageFromGalleryButton(controller: controller), | ||
| 97 | + ], | ||
| 98 | + ), | ||
| 99 | + ], | ||
| 100 | + ), | ||
| 101 | + ), | ||
| 102 | + ), | ||
| 103 | + ], | ||
| 104 | + ), | ||
| 105 | + ); | ||
| 106 | + } | ||
| 107 | + | ||
| 108 | + @override | ||
| 109 | + Future<void> dispose() async { | ||
| 110 | + super.dispose(); | ||
| 111 | + await controller.dispose(); | ||
| 112 | + } | ||
| 113 | +} |
| 1 | +import 'dart:async'; | ||
| 2 | + | ||
| 1 | import 'package:flutter/material.dart'; | 3 | import 'package:flutter/material.dart'; |
| 2 | import 'package:mobile_scanner/mobile_scanner.dart'; | 4 | import 'package:mobile_scanner/mobile_scanner.dart'; |
| 5 | +import 'package:mobile_scanner_example/scanned_barcode_label.dart'; | ||
| 3 | import 'package:mobile_scanner_example/scanner_error_widget.dart'; | 6 | import 'package:mobile_scanner_example/scanner_error_widget.dart'; |
| 4 | 7 | ||
| 5 | class BarcodeScannerPageView extends StatefulWidget { | 8 | class BarcodeScannerPageView extends StatefulWidget { |
| @@ -9,27 +12,72 @@ class BarcodeScannerPageView extends StatefulWidget { | @@ -9,27 +12,72 @@ class BarcodeScannerPageView extends StatefulWidget { | ||
| 9 | State<BarcodeScannerPageView> createState() => _BarcodeScannerPageViewState(); | 12 | State<BarcodeScannerPageView> createState() => _BarcodeScannerPageViewState(); |
| 10 | } | 13 | } |
| 11 | 14 | ||
| 12 | -class _BarcodeScannerPageViewState extends State<BarcodeScannerPageView> | ||
| 13 | - with SingleTickerProviderStateMixin { | ||
| 14 | - BarcodeCapture? capture; | 15 | +class _BarcodeScannerPageViewState extends State<BarcodeScannerPageView> { |
| 16 | + final MobileScannerController controller = MobileScannerController(); | ||
| 17 | + | ||
| 18 | + final PageController pageController = PageController(); | ||
| 19 | + | ||
| 20 | + @override | ||
| 21 | + void initState() { | ||
| 22 | + super.initState(); | ||
| 23 | + unawaited(controller.start()); | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + @override | ||
| 27 | + Widget build(BuildContext context) { | ||
| 28 | + return Scaffold( | ||
| 29 | + appBar: AppBar(title: const Text('With PageView')), | ||
| 30 | + backgroundColor: Colors.black, | ||
| 31 | + body: PageView( | ||
| 32 | + controller: pageController, | ||
| 33 | + onPageChanged: (index) async { | ||
| 34 | + // Stop the camera view for the current page, | ||
| 35 | + // and then restart the camera for the new page. | ||
| 36 | + await controller.stop(); | ||
| 37 | + | ||
| 38 | + // When switching pages, add a delay to the next start call. | ||
| 39 | + // Otherwise the camera will start before the next page is displayed. | ||
| 40 | + await Future.delayed(const Duration(seconds: 1, milliseconds: 500)); | ||
| 41 | + | ||
| 42 | + if (!mounted) { | ||
| 43 | + return; | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + unawaited(controller.start()); | ||
| 47 | + }, | ||
| 48 | + children: [ | ||
| 49 | + _BarcodeScannerPage(controller: controller), | ||
| 50 | + const SizedBox(), | ||
| 51 | + _BarcodeScannerPage(controller: controller), | ||
| 52 | + _BarcodeScannerPage(controller: controller), | ||
| 53 | + ], | ||
| 54 | + ), | ||
| 55 | + ); | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + @override | ||
| 59 | + Future<void> dispose() async { | ||
| 60 | + pageController.dispose(); | ||
| 61 | + super.dispose(); | ||
| 62 | + await controller.dispose(); | ||
| 63 | + } | ||
| 64 | +} | ||
| 15 | 65 | ||
| 16 | - Widget cameraView() { | ||
| 17 | - return Builder( | ||
| 18 | - builder: (context) { | 66 | +class _BarcodeScannerPage extends StatelessWidget { |
| 67 | + const _BarcodeScannerPage({required this.controller}); | ||
| 68 | + | ||
| 69 | + final MobileScannerController controller; | ||
| 70 | + | ||
| 71 | + @override | ||
| 72 | + Widget build(BuildContext context) { | ||
| 19 | return Stack( | 73 | return Stack( |
| 20 | children: [ | 74 | children: [ |
| 21 | MobileScanner( | 75 | MobileScanner( |
| 22 | - startDelay: true, | ||
| 23 | - controller: MobileScannerController(torchEnabled: true), | 76 | + controller: controller, |
| 24 | fit: BoxFit.contain, | 77 | fit: BoxFit.contain, |
| 25 | errorBuilder: (context, error, child) { | 78 | errorBuilder: (context, error, child) { |
| 26 | return ScannerErrorWidget(error: error); | 79 | return ScannerErrorWidget(error: error); |
| 27 | }, | 80 | }, |
| 28 | - onDetect: (capture) { | ||
| 29 | - setState(() { | ||
| 30 | - this.capture = capture; | ||
| 31 | - }); | ||
| 32 | - }, | ||
| 33 | ), | 81 | ), |
| 34 | Align( | 82 | Align( |
| 35 | alignment: Alignment.bottomCenter, | 83 | alignment: Alignment.bottomCenter, |
| @@ -37,49 +85,12 @@ class _BarcodeScannerPageViewState extends State<BarcodeScannerPageView> | @@ -37,49 +85,12 @@ class _BarcodeScannerPageViewState extends State<BarcodeScannerPageView> | ||
| 37 | alignment: Alignment.bottomCenter, | 85 | alignment: Alignment.bottomCenter, |
| 38 | height: 100, | 86 | height: 100, |
| 39 | color: Colors.black.withOpacity(0.4), | 87 | color: Colors.black.withOpacity(0.4), |
| 40 | - child: Row( | ||
| 41 | - mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||
| 42 | - children: [ | ||
| 43 | - Center( | ||
| 44 | - child: SizedBox( | ||
| 45 | - width: MediaQuery.of(context).size.width - 120, | ||
| 46 | - height: 50, | ||
| 47 | - child: FittedBox( | ||
| 48 | - child: Text( | ||
| 49 | - capture?.barcodes.first.rawValue ?? | ||
| 50 | - 'Scan something!', | ||
| 51 | - overflow: TextOverflow.fade, | ||
| 52 | - style: Theme.of(context) | ||
| 53 | - .textTheme | ||
| 54 | - .headlineMedium! | ||
| 55 | - .copyWith(color: Colors.white), | ||
| 56 | - ), | 88 | + child: Center( |
| 89 | + child: ScannedBarcodeLabel(barcodes: controller.barcodes), | ||
| 57 | ), | 90 | ), |
| 58 | ), | 91 | ), |
| 59 | ), | 92 | ), |
| 60 | ], | 93 | ], |
| 61 | - ), | ||
| 62 | - ), | ||
| 63 | - ), | ||
| 64 | - ], | ||
| 65 | - ); | ||
| 66 | - }, | ||
| 67 | - ); | ||
| 68 | - } | ||
| 69 | - | ||
| 70 | - @override | ||
| 71 | - Widget build(BuildContext context) { | ||
| 72 | - return Scaffold( | ||
| 73 | - appBar: AppBar(title: const Text('With PageView')), | ||
| 74 | - backgroundColor: Colors.black, | ||
| 75 | - body: PageView( | ||
| 76 | - children: [ | ||
| 77 | - cameraView(), | ||
| 78 | - Container(), | ||
| 79 | - cameraView(), | ||
| 80 | - cameraView(), | ||
| 81 | - ], | ||
| 82 | - ), | ||
| 83 | ); | 94 | ); |
| 84 | } | 95 | } |
| 85 | } | 96 | } |
| @@ -2,6 +2,8 @@ import 'dart:math'; | @@ -2,6 +2,8 @@ import 'dart:math'; | ||
| 2 | 2 | ||
| 3 | import 'package:flutter/material.dart'; | 3 | import 'package:flutter/material.dart'; |
| 4 | import 'package:mobile_scanner/mobile_scanner.dart'; | 4 | import 'package:mobile_scanner/mobile_scanner.dart'; |
| 5 | +import 'package:mobile_scanner_example/scanned_barcode_label.dart'; | ||
| 6 | +import 'package:mobile_scanner_example/scanner_button_widgets.dart'; | ||
| 5 | import 'package:mobile_scanner_example/scanner_error_widget.dart'; | 7 | import 'package:mobile_scanner_example/scanner_error_widget.dart'; |
| 6 | 8 | ||
| 7 | class BarcodeScannerReturningImage extends StatefulWidget { | 9 | class BarcodeScannerReturningImage extends StatefulWidget { |
| @@ -13,11 +15,7 @@ class BarcodeScannerReturningImage extends StatefulWidget { | @@ -13,11 +15,7 @@ class BarcodeScannerReturningImage extends StatefulWidget { | ||
| 13 | } | 15 | } |
| 14 | 16 | ||
| 15 | class _BarcodeScannerReturningImageState | 17 | class _BarcodeScannerReturningImageState |
| 16 | - extends State<BarcodeScannerReturningImage> | ||
| 17 | - with SingleTickerProviderStateMixin { | ||
| 18 | - BarcodeCapture? barcode; | ||
| 19 | - // MobileScannerArguments? arguments; | ||
| 20 | - | 18 | + extends State<BarcodeScannerReturningImage> { |
| 21 | final MobileScannerController controller = MobileScannerController( | 19 | final MobileScannerController controller = MobileScannerController( |
| 22 | torchEnabled: true, | 20 | torchEnabled: true, |
| 23 | // formats: [BarcodeFormat.qrCode] | 21 | // formats: [BarcodeFormat.qrCode] |
| @@ -27,27 +25,11 @@ class _BarcodeScannerReturningImageState | @@ -27,27 +25,11 @@ class _BarcodeScannerReturningImageState | ||
| 27 | returnImage: true, | 25 | returnImage: true, |
| 28 | ); | 26 | ); |
| 29 | 27 | ||
| 30 | - bool isStarted = true; | ||
| 31 | - | ||
| 32 | - void _startOrStop() { | ||
| 33 | - try { | ||
| 34 | - if (isStarted) { | ||
| 35 | - controller.stop(); | ||
| 36 | - } else { | 28 | + @override |
| 29 | + void initState() { | ||
| 30 | + super.initState(); | ||
| 37 | controller.start(); | 31 | controller.start(); |
| 38 | } | 32 | } |
| 39 | - setState(() { | ||
| 40 | - isStarted = !isStarted; | ||
| 41 | - }); | ||
| 42 | - } on Exception catch (e) { | ||
| 43 | - ScaffoldMessenger.of(context).showSnackBar( | ||
| 44 | - SnackBar( | ||
| 45 | - content: Text('Something went wrong! $e'), | ||
| 46 | - backgroundColor: Colors.red, | ||
| 47 | - ), | ||
| 48 | - ); | ||
| 49 | - } | ||
| 50 | - } | ||
| 51 | 33 | ||
| 52 | @override | 34 | @override |
| 53 | Widget build(BuildContext context) { | 35 | Widget build(BuildContext context) { |
| @@ -57,19 +39,54 @@ class _BarcodeScannerReturningImageState | @@ -57,19 +39,54 @@ class _BarcodeScannerReturningImageState | ||
| 57 | child: Column( | 39 | child: Column( |
| 58 | children: [ | 40 | children: [ |
| 59 | Expanded( | 41 | Expanded( |
| 60 | - child: barcode?.image != null | ||
| 61 | - ? Transform.rotate( | ||
| 62 | - angle: 90 * pi / 180, | ||
| 63 | - child: Image( | ||
| 64 | - gaplessPlayback: true, | ||
| 65 | - image: MemoryImage(barcode!.image!), | ||
| 66 | - fit: BoxFit.contain, | ||
| 67 | - ), | ||
| 68 | - ) | ||
| 69 | - : const Center( | 42 | + child: StreamBuilder<BarcodeCapture>( |
| 43 | + stream: controller.barcodes, | ||
| 44 | + builder: (context, snapshot) { | ||
| 45 | + final barcode = snapshot.data; | ||
| 46 | + | ||
| 47 | + if (barcode == null) { | ||
| 48 | + return const Center( | ||
| 70 | child: Text( | 49 | child: Text( |
| 71 | 'Your scanned barcode will appear here!', | 50 | 'Your scanned barcode will appear here!', |
| 72 | ), | 51 | ), |
| 52 | + ); | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + final barcodeImage = barcode.image; | ||
| 56 | + | ||
| 57 | + if (barcodeImage == null) { | ||
| 58 | + return const Center( | ||
| 59 | + child: Text('No image for this barcode.'), | ||
| 60 | + ); | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | + return Image.memory( | ||
| 64 | + barcodeImage, | ||
| 65 | + fit: BoxFit.contain, | ||
| 66 | + errorBuilder: (context, error, stackTrace) { | ||
| 67 | + return Center( | ||
| 68 | + child: Text('Could not decode image bytes. $error'), | ||
| 69 | + ); | ||
| 70 | + }, | ||
| 71 | + frameBuilder: ( | ||
| 72 | + BuildContext context, | ||
| 73 | + Widget child, | ||
| 74 | + int? frame, | ||
| 75 | + bool? wasSynchronouslyLoaded, | ||
| 76 | + ) { | ||
| 77 | + if (wasSynchronouslyLoaded == true || frame != null) { | ||
| 78 | + return Transform.rotate( | ||
| 79 | + angle: 90 * pi / 180, | ||
| 80 | + child: child, | ||
| 81 | + ); | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + return const Center( | ||
| 85 | + child: CircularProgressIndicator(), | ||
| 86 | + ); | ||
| 87 | + }, | ||
| 88 | + ); | ||
| 89 | + }, | ||
| 73 | ), | 90 | ), |
| 74 | ), | 91 | ), |
| 75 | Expanded( | 92 | Expanded( |
| @@ -84,11 +101,6 @@ class _BarcodeScannerReturningImageState | @@ -84,11 +101,6 @@ class _BarcodeScannerReturningImageState | ||
| 84 | return ScannerErrorWidget(error: error); | 101 | return ScannerErrorWidget(error: error); |
| 85 | }, | 102 | }, |
| 86 | fit: BoxFit.contain, | 103 | fit: BoxFit.contain, |
| 87 | - onDetect: (barcode) { | ||
| 88 | - setState(() { | ||
| 89 | - this.barcode = barcode; | ||
| 90 | - }); | ||
| 91 | - }, | ||
| 92 | ), | 104 | ), |
| 93 | Align( | 105 | Align( |
| 94 | alignment: Alignment.bottomCenter, | 106 | alignment: Alignment.bottomCenter, |
| @@ -99,69 +111,18 @@ class _BarcodeScannerReturningImageState | @@ -99,69 +111,18 @@ class _BarcodeScannerReturningImageState | ||
| 99 | child: Row( | 111 | child: Row( |
| 100 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, | 112 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, |
| 101 | children: [ | 113 | children: [ |
| 102 | - IconButton( | ||
| 103 | - color: Colors.white, | ||
| 104 | - icon: ValueListenableBuilder<TorchState>( | ||
| 105 | - valueListenable: controller.torchState, | ||
| 106 | - builder: (context, state, child) { | ||
| 107 | - switch (state) { | ||
| 108 | - case TorchState.off: | ||
| 109 | - return const Icon( | ||
| 110 | - Icons.flash_off, | ||
| 111 | - color: Colors.grey, | ||
| 112 | - ); | ||
| 113 | - case TorchState.on: | ||
| 114 | - return const Icon( | ||
| 115 | - Icons.flash_on, | ||
| 116 | - color: Colors.yellow, | ||
| 117 | - ); | ||
| 118 | - } | ||
| 119 | - }, | 114 | + ToggleFlashlightButton(controller: controller), |
| 115 | + StartStopMobileScannerButton( | ||
| 116 | + controller: controller, | ||
| 117 | + ), | ||
| 118 | + Expanded( | ||
| 119 | + child: Center( | ||
| 120 | + child: ScannedBarcodeLabel( | ||
| 121 | + barcodes: controller.barcodes, | ||
| 120 | ), | 122 | ), |
| 121 | - iconSize: 32.0, | ||
| 122 | - onPressed: () => controller.toggleTorch(), | ||
| 123 | - ), | ||
| 124 | - IconButton( | ||
| 125 | - color: Colors.white, | ||
| 126 | - icon: isStarted | ||
| 127 | - ? const Icon(Icons.stop) | ||
| 128 | - : const Icon(Icons.play_arrow), | ||
| 129 | - iconSize: 32.0, | ||
| 130 | - onPressed: _startOrStop, | ||
| 131 | - ), | ||
| 132 | - Center( | ||
| 133 | - child: SizedBox( | ||
| 134 | - width: MediaQuery.of(context).size.width - 200, | ||
| 135 | - height: 50, | ||
| 136 | - child: FittedBox( | ||
| 137 | - child: Text( | ||
| 138 | - barcode?.barcodes.first.rawValue ?? | ||
| 139 | - 'Scan something!', | ||
| 140 | - overflow: TextOverflow.fade, | ||
| 141 | - style: Theme.of(context) | ||
| 142 | - .textTheme | ||
| 143 | - .headlineMedium! | ||
| 144 | - .copyWith(color: Colors.white), | ||
| 145 | - ), | ||
| 146 | - ), | ||
| 147 | - ), | ||
| 148 | - ), | ||
| 149 | - IconButton( | ||
| 150 | - color: Colors.white, | ||
| 151 | - icon: ValueListenableBuilder<CameraFacing>( | ||
| 152 | - valueListenable: controller.cameraFacingState, | ||
| 153 | - builder: (context, state, child) { | ||
| 154 | - switch (state) { | ||
| 155 | - case CameraFacing.front: | ||
| 156 | - return const Icon(Icons.camera_front); | ||
| 157 | - case CameraFacing.back: | ||
| 158 | - return const Icon(Icons.camera_rear); | ||
| 159 | - } | ||
| 160 | - }, | ||
| 161 | ), | 123 | ), |
| 162 | - iconSize: 32.0, | ||
| 163 | - onPressed: () => controller.switchCamera(), | ||
| 164 | ), | 124 | ), |
| 125 | + SwitchCameraButton(controller: controller), | ||
| 165 | ], | 126 | ], |
| 166 | ), | 127 | ), |
| 167 | ), | 128 | ), |
| @@ -177,8 +138,8 @@ class _BarcodeScannerReturningImageState | @@ -177,8 +138,8 @@ class _BarcodeScannerReturningImageState | ||
| 177 | } | 138 | } |
| 178 | 139 | ||
| 179 | @override | 140 | @override |
| 180 | - void dispose() { | ||
| 181 | - controller.dispose(); | 141 | + Future<void> dispose() async { |
| 182 | super.dispose(); | 142 | super.dispose(); |
| 143 | + await controller.dispose(); | ||
| 183 | } | 144 | } |
| 184 | } | 145 | } |
| @@ -3,6 +3,7 @@ import 'dart:io'; | @@ -3,6 +3,7 @@ import 'dart:io'; | ||
| 3 | import 'package:flutter/foundation.dart'; | 3 | import 'package:flutter/foundation.dart'; |
| 4 | import 'package:flutter/material.dart'; | 4 | import 'package:flutter/material.dart'; |
| 5 | import 'package:mobile_scanner/mobile_scanner.dart'; | 5 | import 'package:mobile_scanner/mobile_scanner.dart'; |
| 6 | +import 'package:mobile_scanner_example/scanned_barcode_label.dart'; | ||
| 6 | 7 | ||
| 7 | import 'package:mobile_scanner_example/scanner_error_widget.dart'; | 8 | import 'package:mobile_scanner_example/scanner_error_widget.dart'; |
| 8 | 9 | ||
| @@ -16,95 +17,120 @@ class BarcodeScannerWithScanWindow extends StatefulWidget { | @@ -16,95 +17,120 @@ class BarcodeScannerWithScanWindow extends StatefulWidget { | ||
| 16 | 17 | ||
| 17 | class _BarcodeScannerWithScanWindowState | 18 | class _BarcodeScannerWithScanWindowState |
| 18 | extends State<BarcodeScannerWithScanWindow> { | 19 | extends State<BarcodeScannerWithScanWindow> { |
| 19 | - late MobileScannerController controller = MobileScannerController(); | ||
| 20 | - Barcode? barcode; | ||
| 21 | - BarcodeCapture? capture; | 20 | + final MobileScannerController controller = MobileScannerController(); |
| 22 | 21 | ||
| 23 | - Future<void> onDetect(BarcodeCapture barcode) async { | ||
| 24 | - capture = barcode; | ||
| 25 | - setState(() => this.barcode = barcode.barcodes.first); | 22 | + @override |
| 23 | + void initState() { | ||
| 24 | + super.initState(); | ||
| 25 | + | ||
| 26 | + controller.start(); | ||
| 27 | + } | ||
| 28 | + | ||
| 29 | + Widget _buildBarcodeOverlay() { | ||
| 30 | + return ValueListenableBuilder( | ||
| 31 | + valueListenable: controller, | ||
| 32 | + builder: (context, value, child) { | ||
| 33 | + // Not ready. | ||
| 34 | + if (!value.isInitialized || !value.isRunning || value.error != null) { | ||
| 35 | + return const SizedBox(); | ||
| 36 | + } | ||
| 37 | + | ||
| 38 | + return StreamBuilder<BarcodeCapture>( | ||
| 39 | + stream: controller.barcodes, | ||
| 40 | + builder: (context, snapshot) { | ||
| 41 | + final BarcodeCapture? barcodeCapture = snapshot.data; | ||
| 42 | + | ||
| 43 | + // No barcode. | ||
| 44 | + if (barcodeCapture == null || barcodeCapture.barcodes.isEmpty) { | ||
| 45 | + return const SizedBox(); | ||
| 26 | } | 46 | } |
| 27 | 47 | ||
| 28 | - MobileScannerArguments? arguments; | 48 | + final scannedBarcode = barcodeCapture.barcodes.first; |
| 49 | + | ||
| 50 | + // No barcode corners, or size, or no camera preview size. | ||
| 51 | + if (scannedBarcode.corners.isEmpty || | ||
| 52 | + value.size.isEmpty || | ||
| 53 | + barcodeCapture.size.isEmpty) { | ||
| 54 | + return const SizedBox(); | ||
| 55 | + } | ||
| 56 | + | ||
| 57 | + return CustomPaint( | ||
| 58 | + painter: BarcodeOverlay( | ||
| 59 | + barcodeCorners: scannedBarcode.corners, | ||
| 60 | + barcodeSize: barcodeCapture.size, | ||
| 61 | + boxFit: BoxFit.contain, | ||
| 62 | + cameraPreviewSize: value.size, | ||
| 63 | + ), | ||
| 64 | + ); | ||
| 65 | + }, | ||
| 66 | + ); | ||
| 67 | + }, | ||
| 68 | + ); | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + Widget _buildScanWindow(Rect scanWindowRect) { | ||
| 72 | + return ValueListenableBuilder( | ||
| 73 | + valueListenable: controller, | ||
| 74 | + builder: (context, value, child) { | ||
| 75 | + // Not ready. | ||
| 76 | + if (!value.isInitialized || | ||
| 77 | + !value.isRunning || | ||
| 78 | + value.error != null || | ||
| 79 | + value.size.isEmpty) { | ||
| 80 | + return const SizedBox(); | ||
| 81 | + } | ||
| 82 | + | ||
| 83 | + return CustomPaint( | ||
| 84 | + painter: ScannerOverlay(scanWindowRect), | ||
| 85 | + ); | ||
| 86 | + }, | ||
| 87 | + ); | ||
| 88 | + } | ||
| 29 | 89 | ||
| 30 | @override | 90 | @override |
| 31 | Widget build(BuildContext context) { | 91 | Widget build(BuildContext context) { |
| 32 | final scanWindow = Rect.fromCenter( | 92 | final scanWindow = Rect.fromCenter( |
| 33 | - center: MediaQuery.of(context).size.center(Offset.zero), | 93 | + center: MediaQuery.sizeOf(context).center(Offset.zero), |
| 34 | width: 200, | 94 | width: 200, |
| 35 | height: 200, | 95 | height: 200, |
| 36 | ); | 96 | ); |
| 97 | + | ||
| 37 | return Scaffold( | 98 | return Scaffold( |
| 38 | appBar: AppBar(title: const Text('With Scan window')), | 99 | appBar: AppBar(title: const Text('With Scan window')), |
| 39 | backgroundColor: Colors.black, | 100 | backgroundColor: Colors.black, |
| 40 | - body: Builder( | ||
| 41 | - builder: (context) { | ||
| 42 | - return Stack( | 101 | + body: Stack( |
| 43 | fit: StackFit.expand, | 102 | fit: StackFit.expand, |
| 44 | children: [ | 103 | children: [ |
| 45 | MobileScanner( | 104 | MobileScanner( |
| 46 | fit: BoxFit.contain, | 105 | fit: BoxFit.contain, |
| 47 | scanWindow: scanWindow, | 106 | scanWindow: scanWindow, |
| 48 | controller: controller, | 107 | controller: controller, |
| 49 | - onScannerStarted: (arguments) { | ||
| 50 | - setState(() { | ||
| 51 | - this.arguments = arguments; | ||
| 52 | - }); | ||
| 53 | - }, | ||
| 54 | errorBuilder: (context, error, child) { | 108 | errorBuilder: (context, error, child) { |
| 55 | return ScannerErrorWidget(error: error); | 109 | return ScannerErrorWidget(error: error); |
| 56 | }, | 110 | }, |
| 57 | - onDetect: onDetect, | ||
| 58 | - ), | ||
| 59 | - if (barcode != null && | ||
| 60 | - barcode?.corners != null && | ||
| 61 | - arguments != null) | ||
| 62 | - CustomPaint( | ||
| 63 | - painter: BarcodeOverlay( | ||
| 64 | - barcode: barcode!, | ||
| 65 | - arguments: arguments!, | ||
| 66 | - boxFit: BoxFit.contain, | ||
| 67 | - capture: capture!, | ||
| 68 | - ), | ||
| 69 | - ), | ||
| 70 | - CustomPaint( | ||
| 71 | - painter: ScannerOverlay(scanWindow), | ||
| 72 | ), | 111 | ), |
| 112 | + _buildBarcodeOverlay(), | ||
| 113 | + _buildScanWindow(scanWindow), | ||
| 73 | Align( | 114 | Align( |
| 74 | alignment: Alignment.bottomCenter, | 115 | alignment: Alignment.bottomCenter, |
| 75 | child: Container( | 116 | child: Container( |
| 76 | - alignment: Alignment.bottomCenter, | 117 | + alignment: Alignment.center, |
| 118 | + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), | ||
| 77 | height: 100, | 119 | height: 100, |
| 78 | color: Colors.black.withOpacity(0.4), | 120 | color: Colors.black.withOpacity(0.4), |
| 79 | - child: Row( | ||
| 80 | - mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||
| 81 | - children: [ | ||
| 82 | - Center( | ||
| 83 | - child: SizedBox( | ||
| 84 | - width: MediaQuery.of(context).size.width - 120, | ||
| 85 | - height: 50, | ||
| 86 | - child: FittedBox( | ||
| 87 | - child: Text( | ||
| 88 | - barcode?.displayValue ?? 'Scan something!', | ||
| 89 | - overflow: TextOverflow.fade, | ||
| 90 | - style: Theme.of(context) | ||
| 91 | - .textTheme | ||
| 92 | - .headlineMedium! | ||
| 93 | - .copyWith(color: Colors.white), | ||
| 94 | - ), | ||
| 95 | - ), | 121 | + child: ScannedBarcodeLabel(barcodes: controller.barcodes), |
| 96 | ), | 122 | ), |
| 97 | ), | 123 | ), |
| 98 | ], | 124 | ], |
| 99 | ), | 125 | ), |
| 100 | - ), | ||
| 101 | - ), | ||
| 102 | - ], | ||
| 103 | - ); | ||
| 104 | - }, | ||
| 105 | - ), | ||
| 106 | ); | 126 | ); |
| 107 | } | 127 | } |
| 128 | + | ||
| 129 | + @override | ||
| 130 | + Future<void> dispose() async { | ||
| 131 | + super.dispose(); | ||
| 132 | + await controller.dispose(); | ||
| 133 | + } | ||
| 108 | } | 134 | } |
| 109 | 135 | ||
| 110 | class ScannerOverlay extends CustomPainter { | 136 | class ScannerOverlay extends CustomPainter { |
| @@ -114,6 +140,8 @@ class ScannerOverlay extends CustomPainter { | @@ -114,6 +140,8 @@ class ScannerOverlay extends CustomPainter { | ||
| 114 | 140 | ||
| 115 | @override | 141 | @override |
| 116 | void paint(Canvas canvas, Size size) { | 142 | void paint(Canvas canvas, Size size) { |
| 143 | + // TODO: use `Offset.zero & size` instead of Rect.largest | ||
| 144 | + // we need to pass the size to the custom paint widget | ||
| 117 | final backgroundPath = Path()..addRect(Rect.largest); | 145 | final backgroundPath = Path()..addRect(Rect.largest); |
| 118 | final cutoutPath = Path()..addRect(scanWindow); | 146 | final cutoutPath = Path()..addRect(scanWindow); |
| 119 | 147 | ||
| @@ -138,24 +166,26 @@ class ScannerOverlay extends CustomPainter { | @@ -138,24 +166,26 @@ class ScannerOverlay extends CustomPainter { | ||
| 138 | 166 | ||
| 139 | class BarcodeOverlay extends CustomPainter { | 167 | class BarcodeOverlay extends CustomPainter { |
| 140 | BarcodeOverlay({ | 168 | BarcodeOverlay({ |
| 141 | - required this.barcode, | ||
| 142 | - required this.arguments, | 169 | + required this.barcodeCorners, |
| 170 | + required this.barcodeSize, | ||
| 143 | required this.boxFit, | 171 | required this.boxFit, |
| 144 | - required this.capture, | 172 | + required this.cameraPreviewSize, |
| 145 | }); | 173 | }); |
| 146 | 174 | ||
| 147 | - final BarcodeCapture capture; | ||
| 148 | - final Barcode barcode; | ||
| 149 | - final MobileScannerArguments arguments; | 175 | + final List<Offset> barcodeCorners; |
| 176 | + final Size barcodeSize; | ||
| 150 | final BoxFit boxFit; | 177 | final BoxFit boxFit; |
| 178 | + final Size cameraPreviewSize; | ||
| 151 | 179 | ||
| 152 | @override | 180 | @override |
| 153 | void paint(Canvas canvas, Size size) { | 181 | void paint(Canvas canvas, Size size) { |
| 154 | - if (barcode.corners.isEmpty) { | 182 | + if (barcodeCorners.isEmpty || |
| 183 | + barcodeSize.isEmpty || | ||
| 184 | + cameraPreviewSize.isEmpty) { | ||
| 155 | return; | 185 | return; |
| 156 | } | 186 | } |
| 157 | 187 | ||
| 158 | - final adjustedSize = applyBoxFit(boxFit, arguments.size, size); | 188 | + final adjustedSize = applyBoxFit(boxFit, cameraPreviewSize, size); |
| 159 | 189 | ||
| 160 | double verticalPadding = size.height - adjustedSize.destination.height; | 190 | double verticalPadding = size.height - adjustedSize.destination.height; |
| 161 | double horizontalPadding = size.width - adjustedSize.destination.width; | 191 | double horizontalPadding = size.width - adjustedSize.destination.width; |
| @@ -175,22 +205,21 @@ class BarcodeOverlay extends CustomPainter { | @@ -175,22 +205,21 @@ class BarcodeOverlay extends CustomPainter { | ||
| 175 | final double ratioHeight; | 205 | final double ratioHeight; |
| 176 | 206 | ||
| 177 | if (!kIsWeb && Platform.isIOS) { | 207 | if (!kIsWeb && Platform.isIOS) { |
| 178 | - ratioWidth = capture.size.width / adjustedSize.destination.width; | ||
| 179 | - ratioHeight = capture.size.height / adjustedSize.destination.height; | 208 | + ratioWidth = barcodeSize.width / adjustedSize.destination.width; |
| 209 | + ratioHeight = barcodeSize.height / adjustedSize.destination.height; | ||
| 180 | } else { | 210 | } else { |
| 181 | - ratioWidth = arguments.size.width / adjustedSize.destination.width; | ||
| 182 | - ratioHeight = arguments.size.height / adjustedSize.destination.height; | 211 | + ratioWidth = cameraPreviewSize.width / adjustedSize.destination.width; |
| 212 | + ratioHeight = cameraPreviewSize.height / adjustedSize.destination.height; | ||
| 183 | } | 213 | } |
| 184 | 214 | ||
| 185 | - final List<Offset> adjustedOffset = []; | ||
| 186 | - for (final offset in barcode.corners) { | ||
| 187 | - adjustedOffset.add( | 215 | + final List<Offset> adjustedOffset = [ |
| 216 | + for (final offset in barcodeCorners) | ||
| 188 | Offset( | 217 | Offset( |
| 189 | offset.dx / ratioWidth + horizontalPadding, | 218 | offset.dx / ratioWidth + horizontalPadding, |
| 190 | offset.dy / ratioHeight + verticalPadding, | 219 | offset.dy / ratioHeight + verticalPadding, |
| 191 | ), | 220 | ), |
| 192 | - ); | ||
| 193 | - } | 221 | + ]; |
| 222 | + | ||
| 194 | final cutoutPath = Path()..addPolygon(adjustedOffset, true); | 223 | final cutoutPath = Path()..addPolygon(adjustedOffset, true); |
| 195 | 224 | ||
| 196 | final backgroundPaint = Paint() | 225 | final backgroundPaint = Paint() |
| 1 | -import 'package:flutter/material.dart'; | ||
| 2 | -import 'package:mobile_scanner/mobile_scanner.dart'; | ||
| 3 | -import 'package:mobile_scanner_example/scanner_error_widget.dart'; | ||
| 4 | - | ||
| 5 | -class BarcodeScannerWithoutController extends StatefulWidget { | ||
| 6 | - const BarcodeScannerWithoutController({super.key}); | ||
| 7 | - | ||
| 8 | - @override | ||
| 9 | - State<BarcodeScannerWithoutController> createState() => | ||
| 10 | - _BarcodeScannerWithoutControllerState(); | ||
| 11 | -} | ||
| 12 | - | ||
| 13 | -class _BarcodeScannerWithoutControllerState | ||
| 14 | - extends State<BarcodeScannerWithoutController> | ||
| 15 | - with SingleTickerProviderStateMixin { | ||
| 16 | - BarcodeCapture? capture; | ||
| 17 | - | ||
| 18 | - @override | ||
| 19 | - Widget build(BuildContext context) { | ||
| 20 | - return Scaffold( | ||
| 21 | - appBar: AppBar(title: const Text('Without controller')), | ||
| 22 | - backgroundColor: Colors.black, | ||
| 23 | - body: Builder( | ||
| 24 | - builder: (context) { | ||
| 25 | - return Stack( | ||
| 26 | - children: [ | ||
| 27 | - MobileScanner( | ||
| 28 | - fit: BoxFit.contain, | ||
| 29 | - errorBuilder: (context, error, child) { | ||
| 30 | - return ScannerErrorWidget(error: error); | ||
| 31 | - }, | ||
| 32 | - onDetect: (capture) { | ||
| 33 | - setState(() { | ||
| 34 | - this.capture = capture; | ||
| 35 | - }); | ||
| 36 | - }, | ||
| 37 | - ), | ||
| 38 | - Align( | ||
| 39 | - alignment: Alignment.bottomCenter, | ||
| 40 | - child: Container( | ||
| 41 | - alignment: Alignment.bottomCenter, | ||
| 42 | - height: 100, | ||
| 43 | - color: Colors.black.withOpacity(0.4), | ||
| 44 | - child: Row( | ||
| 45 | - mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||
| 46 | - children: [ | ||
| 47 | - Center( | ||
| 48 | - child: SizedBox( | ||
| 49 | - width: MediaQuery.of(context).size.width - 120, | ||
| 50 | - height: 50, | ||
| 51 | - child: FittedBox( | ||
| 52 | - child: Text( | ||
| 53 | - capture?.barcodes.first.rawValue ?? | ||
| 54 | - 'Scan something!', | ||
| 55 | - overflow: TextOverflow.fade, | ||
| 56 | - style: Theme.of(context) | ||
| 57 | - .textTheme | ||
| 58 | - .headlineMedium! | ||
| 59 | - .copyWith(color: Colors.white), | ||
| 60 | - ), | ||
| 61 | - ), | ||
| 62 | - ), | ||
| 63 | - ), | ||
| 64 | - ], | ||
| 65 | - ), | ||
| 66 | - ), | ||
| 67 | - ), | ||
| 68 | - ], | ||
| 69 | - ); | ||
| 70 | - }, | ||
| 71 | - ), | ||
| 72 | - ); | ||
| 73 | - } | ||
| 74 | -} |
| 1 | +import 'dart:async'; | ||
| 2 | + | ||
| 3 | +import 'package:flutter/foundation.dart'; | ||
| 1 | import 'package:flutter/material.dart'; | 4 | import 'package:flutter/material.dart'; |
| 2 | -import 'package:image_picker/image_picker.dart'; | ||
| 3 | import 'package:mobile_scanner/mobile_scanner.dart'; | 5 | import 'package:mobile_scanner/mobile_scanner.dart'; |
| 6 | +import 'package:mobile_scanner_example/scanned_barcode_label.dart'; | ||
| 4 | 7 | ||
| 8 | +import 'package:mobile_scanner_example/scanner_button_widgets.dart'; | ||
| 5 | import 'package:mobile_scanner_example/scanner_error_widget.dart'; | 9 | import 'package:mobile_scanner_example/scanner_error_widget.dart'; |
| 6 | 10 | ||
| 7 | class BarcodeScannerWithZoom extends StatefulWidget { | 11 | class BarcodeScannerWithZoom extends StatefulWidget { |
| @@ -11,64 +15,44 @@ class BarcodeScannerWithZoom extends StatefulWidget { | @@ -11,64 +15,44 @@ class BarcodeScannerWithZoom extends StatefulWidget { | ||
| 11 | State<BarcodeScannerWithZoom> createState() => _BarcodeScannerWithZoomState(); | 15 | State<BarcodeScannerWithZoom> createState() => _BarcodeScannerWithZoomState(); |
| 12 | } | 16 | } |
| 13 | 17 | ||
| 14 | -class _BarcodeScannerWithZoomState extends State<BarcodeScannerWithZoom> | ||
| 15 | - with SingleTickerProviderStateMixin { | ||
| 16 | - BarcodeCapture? barcode; | ||
| 17 | - | ||
| 18 | - MobileScannerController controller = MobileScannerController( | 18 | +class _BarcodeScannerWithZoomState extends State<BarcodeScannerWithZoom> { |
| 19 | + final MobileScannerController controller = MobileScannerController( | ||
| 19 | torchEnabled: true, | 20 | torchEnabled: true, |
| 20 | ); | 21 | ); |
| 21 | 22 | ||
| 22 | - bool isStarted = true; | ||
| 23 | double _zoomFactor = 0.0; | 23 | double _zoomFactor = 0.0; |
| 24 | 24 | ||
| 25 | @override | 25 | @override |
| 26 | - Widget build(BuildContext context) { | ||
| 27 | - return Scaffold( | ||
| 28 | - appBar: AppBar(title: const Text('With zoom slider')), | ||
| 29 | - backgroundColor: Colors.black, | ||
| 30 | - body: Builder( | ||
| 31 | - builder: (context) { | ||
| 32 | - return Stack( | ||
| 33 | - children: [ | ||
| 34 | - MobileScanner( | ||
| 35 | - controller: controller, | ||
| 36 | - fit: BoxFit.contain, | ||
| 37 | - errorBuilder: (context, error, child) { | ||
| 38 | - return ScannerErrorWidget(error: error); | ||
| 39 | - }, | ||
| 40 | - onDetect: (barcode) { | ||
| 41 | - setState(() { | ||
| 42 | - this.barcode = barcode; | ||
| 43 | - }); | ||
| 44 | - }, | ||
| 45 | - ), | ||
| 46 | - Align( | ||
| 47 | - alignment: Alignment.bottomCenter, | ||
| 48 | - child: Container( | ||
| 49 | - alignment: Alignment.bottomCenter, | ||
| 50 | - height: 100, | ||
| 51 | - color: Colors.black.withOpacity(0.4), | ||
| 52 | - child: Column( | ||
| 53 | - children: [ | ||
| 54 | - Padding( | 26 | + void initState() { |
| 27 | + super.initState(); | ||
| 28 | + controller.start(); | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + Widget _buildZoomScaleSlider() { | ||
| 32 | + return ValueListenableBuilder( | ||
| 33 | + valueListenable: controller, | ||
| 34 | + builder: (context, state, child) { | ||
| 35 | + if (!state.isInitialized || !state.isRunning) { | ||
| 36 | + return const SizedBox.shrink(); | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + final TextStyle labelStyle = Theme.of(context) | ||
| 40 | + .textTheme | ||
| 41 | + .headlineMedium! | ||
| 42 | + .copyWith(color: Colors.white); | ||
| 43 | + | ||
| 44 | + return Padding( | ||
| 55 | padding: const EdgeInsets.symmetric(horizontal: 8.0), | 45 | padding: const EdgeInsets.symmetric(horizontal: 8.0), |
| 56 | child: Row( | 46 | child: Row( |
| 57 | children: [ | 47 | children: [ |
| 58 | Text( | 48 | Text( |
| 59 | - "0%", | 49 | + '0%', |
| 60 | overflow: TextOverflow.fade, | 50 | overflow: TextOverflow.fade, |
| 61 | - style: Theme.of(context) | ||
| 62 | - .textTheme | ||
| 63 | - .headlineMedium! | ||
| 64 | - .copyWith(color: Colors.white), | 51 | + style: labelStyle, |
| 65 | ), | 52 | ), |
| 66 | Expanded( | 53 | Expanded( |
| 67 | child: Slider( | 54 | child: Slider( |
| 68 | - max: 100, | ||
| 69 | - divisions: 100, | ||
| 70 | value: _zoomFactor, | 55 | value: _zoomFactor, |
| 71 | - label: "${_zoomFactor.round()} %", | ||
| 72 | onChanged: (value) { | 56 | onChanged: (value) { |
| 73 | setState(() { | 57 | setState(() { |
| 74 | _zoomFactor = value; | 58 | _zoomFactor = value; |
| @@ -78,118 +62,54 @@ class _BarcodeScannerWithZoomState extends State<BarcodeScannerWithZoom> | @@ -78,118 +62,54 @@ class _BarcodeScannerWithZoomState extends State<BarcodeScannerWithZoom> | ||
| 78 | ), | 62 | ), |
| 79 | ), | 63 | ), |
| 80 | Text( | 64 | Text( |
| 81 | - "100%", | 65 | + '100%', |
| 82 | overflow: TextOverflow.fade, | 66 | overflow: TextOverflow.fade, |
| 83 | - style: Theme.of(context) | ||
| 84 | - .textTheme | ||
| 85 | - .headlineMedium! | ||
| 86 | - .copyWith(color: Colors.white), | 67 | + style: labelStyle, |
| 87 | ), | 68 | ), |
| 88 | ], | 69 | ], |
| 89 | ), | 70 | ), |
| 90 | - ), | ||
| 91 | - Row( | ||
| 92 | - mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||
| 93 | - children: [ | ||
| 94 | - IconButton( | ||
| 95 | - color: Colors.white, | ||
| 96 | - icon: ValueListenableBuilder<TorchState>( | ||
| 97 | - valueListenable: controller.torchState, | ||
| 98 | - builder: (context, state, child) { | ||
| 99 | - switch (state) { | ||
| 100 | - case TorchState.off: | ||
| 101 | - return const Icon( | ||
| 102 | - Icons.flash_off, | ||
| 103 | - color: Colors.grey, | ||
| 104 | - ); | ||
| 105 | - case TorchState.on: | ||
| 106 | - return const Icon( | ||
| 107 | - Icons.flash_on, | ||
| 108 | - color: Colors.yellow, | ||
| 109 | ); | 71 | ); |
| 110 | - } | ||
| 111 | }, | 72 | }, |
| 112 | - ), | ||
| 113 | - iconSize: 32.0, | ||
| 114 | - onPressed: () => controller.toggleTorch(), | ||
| 115 | - ), | ||
| 116 | - IconButton( | ||
| 117 | - color: Colors.white, | ||
| 118 | - icon: isStarted | ||
| 119 | - ? const Icon(Icons.stop) | ||
| 120 | - : const Icon(Icons.play_arrow), | ||
| 121 | - iconSize: 32.0, | ||
| 122 | - onPressed: () => setState(() { | ||
| 123 | - isStarted | ||
| 124 | - ? controller.stop() | ||
| 125 | - : controller.start(); | ||
| 126 | - isStarted = !isStarted; | ||
| 127 | - }), | ||
| 128 | - ), | ||
| 129 | - Center( | ||
| 130 | - child: SizedBox( | ||
| 131 | - width: MediaQuery.of(context).size.width - 200, | ||
| 132 | - height: 50, | ||
| 133 | - child: FittedBox( | ||
| 134 | - child: Text( | ||
| 135 | - barcode?.barcodes.first.rawValue ?? | ||
| 136 | - 'Scan something!', | ||
| 137 | - overflow: TextOverflow.fade, | ||
| 138 | - style: Theme.of(context) | ||
| 139 | - .textTheme | ||
| 140 | - .headlineMedium! | ||
| 141 | - .copyWith(color: Colors.white), | ||
| 142 | - ), | ||
| 143 | - ), | ||
| 144 | - ), | ||
| 145 | - ), | ||
| 146 | - IconButton( | ||
| 147 | - color: Colors.white, | ||
| 148 | - icon: ValueListenableBuilder<CameraFacing>( | ||
| 149 | - valueListenable: controller.cameraFacingState, | ||
| 150 | - builder: (context, state, child) { | ||
| 151 | - switch (state) { | ||
| 152 | - case CameraFacing.front: | ||
| 153 | - return const Icon(Icons.camera_front); | ||
| 154 | - case CameraFacing.back: | ||
| 155 | - return const Icon(Icons.camera_rear); | 73 | + ); |
| 156 | } | 74 | } |
| 75 | + | ||
| 76 | + @override | ||
| 77 | + Widget build(BuildContext context) { | ||
| 78 | + return Scaffold( | ||
| 79 | + appBar: AppBar(title: const Text('With zoom slider')), | ||
| 80 | + backgroundColor: Colors.black, | ||
| 81 | + body: Stack( | ||
| 82 | + children: [ | ||
| 83 | + MobileScanner( | ||
| 84 | + controller: controller, | ||
| 85 | + fit: BoxFit.contain, | ||
| 86 | + errorBuilder: (context, error, child) { | ||
| 87 | + return ScannerErrorWidget(error: error); | ||
| 157 | }, | 88 | }, |
| 158 | ), | 89 | ), |
| 159 | - iconSize: 32.0, | ||
| 160 | - onPressed: () => controller.switchCamera(), | ||
| 161 | - ), | ||
| 162 | - IconButton( | ||
| 163 | - color: Colors.white, | ||
| 164 | - icon: const Icon(Icons.image), | ||
| 165 | - iconSize: 32.0, | ||
| 166 | - onPressed: () async { | ||
| 167 | - final ImagePicker picker = ImagePicker(); | ||
| 168 | - // Pick an image | ||
| 169 | - final XFile? image = await picker.pickImage( | ||
| 170 | - source: ImageSource.gallery, | ||
| 171 | - ); | ||
| 172 | - if (image != null) { | ||
| 173 | - if (await controller.analyzeImage(image.path)) { | ||
| 174 | - if (!context.mounted) return; | ||
| 175 | - ScaffoldMessenger.of(context).showSnackBar( | ||
| 176 | - const SnackBar( | ||
| 177 | - content: Text('Barcode found!'), | ||
| 178 | - backgroundColor: Colors.green, | 90 | + Align( |
| 91 | + alignment: Alignment.bottomCenter, | ||
| 92 | + child: Container( | ||
| 93 | + alignment: Alignment.bottomCenter, | ||
| 94 | + height: 100, | ||
| 95 | + color: Colors.black.withOpacity(0.4), | ||
| 96 | + child: Column( | ||
| 97 | + children: [ | ||
| 98 | + if (!kIsWeb) _buildZoomScaleSlider(), | ||
| 99 | + Row( | ||
| 100 | + mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||
| 101 | + children: [ | ||
| 102 | + ToggleFlashlightButton(controller: controller), | ||
| 103 | + StartStopMobileScannerButton(controller: controller), | ||
| 104 | + Expanded( | ||
| 105 | + child: Center( | ||
| 106 | + child: ScannedBarcodeLabel( | ||
| 107 | + barcodes: controller.barcodes, | ||
| 179 | ), | 108 | ), |
| 180 | - ); | ||
| 181 | - } else { | ||
| 182 | - if (!context.mounted) return; | ||
| 183 | - ScaffoldMessenger.of(context).showSnackBar( | ||
| 184 | - const SnackBar( | ||
| 185 | - content: Text('No barcode found!'), | ||
| 186 | - backgroundColor: Colors.red, | ||
| 187 | ), | 109 | ), |
| 188 | - ); | ||
| 189 | - } | ||
| 190 | - } | ||
| 191 | - }, | ||
| 192 | ), | 110 | ), |
| 111 | + SwitchCameraButton(controller: controller), | ||
| 112 | + AnalyzeImageFromGalleryButton(controller: controller), | ||
| 193 | ], | 113 | ], |
| 194 | ), | 114 | ), |
| 195 | ], | 115 | ], |
| @@ -197,9 +117,13 @@ class _BarcodeScannerWithZoomState extends State<BarcodeScannerWithZoom> | @@ -197,9 +117,13 @@ class _BarcodeScannerWithZoomState extends State<BarcodeScannerWithZoom> | ||
| 197 | ), | 117 | ), |
| 198 | ), | 118 | ), |
| 199 | ], | 119 | ], |
| 200 | - ); | ||
| 201 | - }, | ||
| 202 | ), | 120 | ), |
| 203 | ); | 121 | ); |
| 204 | } | 122 | } |
| 123 | + | ||
| 124 | + @override | ||
| 125 | + Future<void> dispose() async { | ||
| 126 | + super.dispose(); | ||
| 127 | + await controller.dispose(); | ||
| 128 | + } | ||
| 205 | } | 129 | } |
| 1 | import 'package:flutter/material.dart'; | 1 | import 'package:flutter/material.dart'; |
| 2 | -import 'package:mobile_scanner_example/barcode_list_scanner_controller.dart'; | ||
| 3 | import 'package:mobile_scanner_example/barcode_scanner_controller.dart'; | 2 | import 'package:mobile_scanner_example/barcode_scanner_controller.dart'; |
| 3 | +import 'package:mobile_scanner_example/barcode_scanner_listview.dart'; | ||
| 4 | import 'package:mobile_scanner_example/barcode_scanner_pageview.dart'; | 4 | import 'package:mobile_scanner_example/barcode_scanner_pageview.dart'; |
| 5 | import 'package:mobile_scanner_example/barcode_scanner_returning_image.dart'; | 5 | import 'package:mobile_scanner_example/barcode_scanner_returning_image.dart'; |
| 6 | import 'package:mobile_scanner_example/barcode_scanner_window.dart'; | 6 | import 'package:mobile_scanner_example/barcode_scanner_window.dart'; |
| 7 | -import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart'; | ||
| 8 | import 'package:mobile_scanner_example/barcode_scanner_zoom.dart'; | 7 | import 'package:mobile_scanner_example/barcode_scanner_zoom.dart'; |
| 9 | import 'package:mobile_scanner_example/mobile_scanner_overlay.dart'; | 8 | import 'package:mobile_scanner_example/mobile_scanner_overlay.dart'; |
| 10 | 9 | ||
| 11 | -void main() => runApp(const MaterialApp(home: MyHome())); | 10 | +void main() { |
| 11 | + runApp( | ||
| 12 | + const MaterialApp( | ||
| 13 | + title: 'Mobile Scanner Example', | ||
| 14 | + home: MyHome(), | ||
| 15 | + ), | ||
| 16 | + ); | ||
| 17 | +} | ||
| 12 | 18 | ||
| 13 | class MyHome extends StatelessWidget { | 19 | class MyHome extends StatelessWidget { |
| 14 | const MyHome({super.key}); | 20 | const MyHome({super.key}); |
| @@ -16,23 +22,20 @@ class MyHome extends StatelessWidget { | @@ -16,23 +22,20 @@ class MyHome extends StatelessWidget { | ||
| 16 | @override | 22 | @override |
| 17 | Widget build(BuildContext context) { | 23 | Widget build(BuildContext context) { |
| 18 | return Scaffold( | 24 | return Scaffold( |
| 19 | - appBar: AppBar(title: const Text('Flutter Demo Home Page')), | ||
| 20 | - body: SizedBox( | ||
| 21 | - width: MediaQuery.of(context).size.width, | ||
| 22 | - height: MediaQuery.of(context).size.height, | 25 | + appBar: AppBar(title: const Text('Mobile Scanner Example')), |
| 26 | + body: Center( | ||
| 23 | child: Column( | 27 | child: Column( |
| 24 | - mainAxisAlignment: MainAxisAlignment.center, | 28 | + mainAxisAlignment: MainAxisAlignment.spaceAround, |
| 25 | children: [ | 29 | children: [ |
| 26 | ElevatedButton( | 30 | ElevatedButton( |
| 27 | onPressed: () { | 31 | onPressed: () { |
| 28 | Navigator.of(context).push( | 32 | Navigator.of(context).push( |
| 29 | MaterialPageRoute( | 33 | MaterialPageRoute( |
| 30 | - builder: (context) => | ||
| 31 | - const BarcodeListScannerWithController(), | 34 | + builder: (context) => const BarcodeScannerListView(), |
| 32 | ), | 35 | ), |
| 33 | ); | 36 | ); |
| 34 | }, | 37 | }, |
| 35 | - child: const Text('MobileScanner with List Controller'), | 38 | + child: const Text('MobileScanner with ListView'), |
| 36 | ), | 39 | ), |
| 37 | ElevatedButton( | 40 | ElevatedButton( |
| 38 | onPressed: () { | 41 | onPressed: () { |
| @@ -62,19 +65,9 @@ class MyHome extends StatelessWidget { | @@ -62,19 +65,9 @@ class MyHome extends StatelessWidget { | ||
| 62 | ), | 65 | ), |
| 63 | ); | 66 | ); |
| 64 | }, | 67 | }, |
| 65 | - child: | ||
| 66 | - const Text('MobileScanner with Controller (returning image)'), | ||
| 67 | - ), | ||
| 68 | - ElevatedButton( | ||
| 69 | - onPressed: () { | ||
| 70 | - Navigator.of(context).push( | ||
| 71 | - MaterialPageRoute( | ||
| 72 | - builder: (context) => | ||
| 73 | - const BarcodeScannerWithoutController(), | 68 | + child: const Text( |
| 69 | + 'MobileScanner with Controller (returning image)', | ||
| 74 | ), | 70 | ), |
| 75 | - ); | ||
| 76 | - }, | ||
| 77 | - child: const Text('MobileScanner without Controller'), | ||
| 78 | ), | 71 | ), |
| 79 | ElevatedButton( | 72 | ElevatedButton( |
| 80 | onPressed: () { | 73 | onPressed: () { |
| 1 | import 'package:flutter/material.dart'; | 1 | import 'package:flutter/material.dart'; |
| 2 | import 'package:mobile_scanner/mobile_scanner.dart'; | 2 | import 'package:mobile_scanner/mobile_scanner.dart'; |
| 3 | +import 'package:mobile_scanner_example/scanned_barcode_label.dart'; | ||
| 4 | +import 'package:mobile_scanner_example/scanner_button_widgets.dart'; | ||
| 3 | import 'package:mobile_scanner_example/scanner_error_widget.dart'; | 5 | import 'package:mobile_scanner_example/scanner_error_widget.dart'; |
| 4 | 6 | ||
| 5 | class BarcodeScannerWithOverlay extends StatefulWidget { | 7 | class BarcodeScannerWithOverlay extends StatefulWidget { |
| @@ -9,174 +11,105 @@ class BarcodeScannerWithOverlay extends StatefulWidget { | @@ -9,174 +11,105 @@ class BarcodeScannerWithOverlay extends StatefulWidget { | ||
| 9 | } | 11 | } |
| 10 | 12 | ||
| 11 | class _BarcodeScannerWithOverlayState extends State<BarcodeScannerWithOverlay> { | 13 | class _BarcodeScannerWithOverlayState extends State<BarcodeScannerWithOverlay> { |
| 12 | - String overlayText = "Please scan QR Code"; | ||
| 13 | - bool camStarted = false; | ||
| 14 | - | ||
| 15 | final MobileScannerController controller = MobileScannerController( | 14 | final MobileScannerController controller = MobileScannerController( |
| 16 | formats: const [BarcodeFormat.qrCode], | 15 | formats: const [BarcodeFormat.qrCode], |
| 17 | - autoStart: false, | ||
| 18 | ); | 16 | ); |
| 19 | 17 | ||
| 20 | @override | 18 | @override |
| 21 | - void dispose() { | ||
| 22 | - controller.dispose(); | ||
| 23 | - super.dispose(); | ||
| 24 | - } | ||
| 25 | - | ||
| 26 | - void startCamera() { | ||
| 27 | - if (camStarted) { | ||
| 28 | - return; | ||
| 29 | - } | ||
| 30 | - | ||
| 31 | - controller.start().then((_) { | ||
| 32 | - if (mounted) { | ||
| 33 | - setState(() { | ||
| 34 | - camStarted = true; | ||
| 35 | - }); | ||
| 36 | - } | ||
| 37 | - }).catchError((Object error, StackTrace stackTrace) { | ||
| 38 | - if (mounted) { | ||
| 39 | - ScaffoldMessenger.of(context).showSnackBar( | ||
| 40 | - SnackBar( | ||
| 41 | - content: Text('Something went wrong! $error'), | ||
| 42 | - backgroundColor: Colors.red, | ||
| 43 | - ), | ||
| 44 | - ); | ||
| 45 | - } | ||
| 46 | - }); | ||
| 47 | - } | ||
| 48 | - | ||
| 49 | - void onBarcodeDetect(BarcodeCapture barcodeCapture) { | ||
| 50 | - final barcode = barcodeCapture.barcodes.last; | ||
| 51 | - setState(() { | ||
| 52 | - overlayText = barcodeCapture.barcodes.last.displayValue ?? | ||
| 53 | - barcode.rawValue ?? | ||
| 54 | - 'Barcode has no displayable value'; | ||
| 55 | - }); | 19 | + void initState() { |
| 20 | + super.initState(); | ||
| 21 | + controller.start(); | ||
| 56 | } | 22 | } |
| 57 | 23 | ||
| 58 | @override | 24 | @override |
| 59 | Widget build(BuildContext context) { | 25 | Widget build(BuildContext context) { |
| 60 | final scanWindow = Rect.fromCenter( | 26 | final scanWindow = Rect.fromCenter( |
| 61 | - center: MediaQuery.of(context).size.center(Offset.zero), | 27 | + center: MediaQuery.sizeOf(context).center(Offset.zero), |
| 62 | width: 200, | 28 | width: 200, |
| 63 | height: 200, | 29 | height: 200, |
| 64 | ); | 30 | ); |
| 65 | 31 | ||
| 66 | return Scaffold( | 32 | return Scaffold( |
| 33 | + backgroundColor: Colors.black, | ||
| 67 | appBar: AppBar( | 34 | appBar: AppBar( |
| 68 | title: const Text('Scanner with Overlay Example app'), | 35 | title: const Text('Scanner with Overlay Example app'), |
| 69 | ), | 36 | ), |
| 70 | - body: Center( | ||
| 71 | - child: Column( | ||
| 72 | - mainAxisAlignment: MainAxisAlignment.center, | ||
| 73 | - children: <Widget>[ | ||
| 74 | - Expanded( | ||
| 75 | - child: camStarted | ||
| 76 | - ? Stack( | 37 | + body: Stack( |
| 77 | fit: StackFit.expand, | 38 | fit: StackFit.expand, |
| 78 | children: [ | 39 | children: [ |
| 79 | Center( | 40 | Center( |
| 80 | child: MobileScanner( | 41 | child: MobileScanner( |
| 81 | fit: BoxFit.contain, | 42 | fit: BoxFit.contain, |
| 82 | - onDetect: onBarcodeDetect, | ||
| 83 | - overlay: Padding( | ||
| 84 | - padding: const EdgeInsets.all(16.0), | ||
| 85 | - child: Align( | ||
| 86 | - alignment: Alignment.bottomCenter, | ||
| 87 | - child: Opacity( | ||
| 88 | - opacity: 0.7, | ||
| 89 | - child: Text( | ||
| 90 | - overlayText, | ||
| 91 | - style: const TextStyle( | ||
| 92 | - backgroundColor: Colors.black26, | ||
| 93 | - color: Colors.white, | ||
| 94 | - fontWeight: FontWeight.bold, | ||
| 95 | - fontSize: 24, | ||
| 96 | - overflow: TextOverflow.ellipsis, | ||
| 97 | - ), | ||
| 98 | - maxLines: 1, | ||
| 99 | - ), | ||
| 100 | - ), | ||
| 101 | - ), | ||
| 102 | - ), | ||
| 103 | controller: controller, | 43 | controller: controller, |
| 104 | scanWindow: scanWindow, | 44 | scanWindow: scanWindow, |
| 105 | errorBuilder: (context, error, child) { | 45 | errorBuilder: (context, error, child) { |
| 106 | return ScannerErrorWidget(error: error); | 46 | return ScannerErrorWidget(error: error); |
| 107 | }, | 47 | }, |
| 108 | - ), | ||
| 109 | - ), | ||
| 110 | - CustomPaint( | ||
| 111 | - painter: ScannerOverlay(scanWindow), | ||
| 112 | - ), | ||
| 113 | - Padding( | 48 | + overlayBuilder: (context, constraints) { |
| 49 | + return Padding( | ||
| 114 | padding: const EdgeInsets.all(16.0), | 50 | padding: const EdgeInsets.all(16.0), |
| 115 | child: Align( | 51 | child: Align( |
| 116 | alignment: Alignment.bottomCenter, | 52 | alignment: Alignment.bottomCenter, |
| 117 | - child: Row( | ||
| 118 | - mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||
| 119 | - children: [ | ||
| 120 | - ValueListenableBuilder<TorchState>( | ||
| 121 | - valueListenable: controller.torchState, | ||
| 122 | - builder: (context, value, child) { | ||
| 123 | - final Color iconColor; | ||
| 124 | - | ||
| 125 | - switch (value) { | ||
| 126 | - case TorchState.off: | ||
| 127 | - iconColor = Colors.black; | ||
| 128 | - case TorchState.on: | ||
| 129 | - iconColor = Colors.yellow; | ||
| 130 | - } | ||
| 131 | - | ||
| 132 | - return IconButton( | ||
| 133 | - onPressed: () => controller.toggleTorch(), | ||
| 134 | - icon: Icon( | ||
| 135 | - Icons.flashlight_on, | ||
| 136 | - color: iconColor, | 53 | + child: ScannedBarcodeLabel(barcodes: controller.barcodes), |
| 137 | ), | 54 | ), |
| 138 | ); | 55 | ); |
| 139 | }, | 56 | }, |
| 140 | ), | 57 | ), |
| 141 | - IconButton( | ||
| 142 | - onPressed: () => controller.switchCamera(), | ||
| 143 | - icon: const Icon( | ||
| 144 | - Icons.cameraswitch_rounded, | ||
| 145 | - color: Colors.white, | ||
| 146 | ), | 58 | ), |
| 59 | + ValueListenableBuilder( | ||
| 60 | + valueListenable: controller, | ||
| 61 | + builder: (context, value, child) { | ||
| 62 | + if (!value.isInitialized || | ||
| 63 | + !value.isRunning || | ||
| 64 | + value.error != null) { | ||
| 65 | + return const SizedBox(); | ||
| 66 | + } | ||
| 67 | + | ||
| 68 | + return CustomPaint( | ||
| 69 | + painter: ScannerOverlay(scanWindow: scanWindow), | ||
| 70 | + ); | ||
| 71 | + }, | ||
| 147 | ), | 72 | ), |
| 73 | + Align( | ||
| 74 | + alignment: Alignment.bottomCenter, | ||
| 75 | + child: Padding( | ||
| 76 | + padding: const EdgeInsets.all(16.0), | ||
| 77 | + child: Row( | ||
| 78 | + mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||
| 79 | + children: [ | ||
| 80 | + ToggleFlashlightButton(controller: controller), | ||
| 81 | + SwitchCameraButton(controller: controller), | ||
| 148 | ], | 82 | ], |
| 149 | ), | 83 | ), |
| 150 | ), | 84 | ), |
| 151 | ), | 85 | ), |
| 152 | ], | 86 | ], |
| 153 | - ) | ||
| 154 | - : const Center( | ||
| 155 | - child: Text("Tap on Camera to activate QR Scanner"), | ||
| 156 | - ), | ||
| 157 | - ), | ||
| 158 | - ], | ||
| 159 | - ), | ||
| 160 | - ), | ||
| 161 | - floatingActionButton: camStarted | ||
| 162 | - ? null | ||
| 163 | - : FloatingActionButton( | ||
| 164 | - onPressed: startCamera, | ||
| 165 | - child: const Icon(Icons.camera_alt), | ||
| 166 | ), | 87 | ), |
| 167 | ); | 88 | ); |
| 168 | } | 89 | } |
| 90 | + | ||
| 91 | + @override | ||
| 92 | + Future<void> dispose() async { | ||
| 93 | + super.dispose(); | ||
| 94 | + await controller.dispose(); | ||
| 95 | + } | ||
| 169 | } | 96 | } |
| 170 | 97 | ||
| 171 | class ScannerOverlay extends CustomPainter { | 98 | class ScannerOverlay extends CustomPainter { |
| 172 | - ScannerOverlay(this.scanWindow); | 99 | + const ScannerOverlay({ |
| 100 | + required this.scanWindow, | ||
| 101 | + this.borderRadius = 12.0, | ||
| 102 | + }); | ||
| 173 | 103 | ||
| 174 | final Rect scanWindow; | 104 | final Rect scanWindow; |
| 175 | - final double borderRadius = 12.0; | 105 | + final double borderRadius; |
| 176 | 106 | ||
| 177 | @override | 107 | @override |
| 178 | void paint(Canvas canvas, Size size) { | 108 | void paint(Canvas canvas, Size size) { |
| 109 | + // TODO: use `Offset.zero & size` instead of Rect.largest | ||
| 110 | + // we need to pass the size to the custom paint widget | ||
| 179 | final backgroundPath = Path()..addRect(Rect.largest); | 111 | final backgroundPath = Path()..addRect(Rect.largest); |
| 112 | + | ||
| 180 | final cutoutPath = Path() | 113 | final cutoutPath = Path() |
| 181 | ..addRRect( | 114 | ..addRRect( |
| 182 | RRect.fromRectAndCorners( | 115 | RRect.fromRectAndCorners( |
| @@ -199,14 +132,11 @@ class ScannerOverlay extends CustomPainter { | @@ -199,14 +132,11 @@ class ScannerOverlay extends CustomPainter { | ||
| 199 | cutoutPath, | 132 | cutoutPath, |
| 200 | ); | 133 | ); |
| 201 | 134 | ||
| 202 | - // Create a Paint object for the white border | ||
| 203 | final borderPaint = Paint() | 135 | final borderPaint = Paint() |
| 204 | ..color = Colors.white | 136 | ..color = Colors.white |
| 205 | ..style = PaintingStyle.stroke | 137 | ..style = PaintingStyle.stroke |
| 206 | - ..strokeWidth = 4.0; // Adjust the border width as needed | 138 | + ..strokeWidth = 4.0; |
| 207 | 139 | ||
| 208 | - // Calculate the border rectangle with rounded corners | ||
| 209 | -// Adjust the radius as needed | ||
| 210 | final borderRect = RRect.fromRectAndCorners( | 140 | final borderRect = RRect.fromRectAndCorners( |
| 211 | scanWindow, | 141 | scanWindow, |
| 212 | topLeft: Radius.circular(borderRadius), | 142 | topLeft: Radius.circular(borderRadius), |
| @@ -215,13 +145,16 @@ class ScannerOverlay extends CustomPainter { | @@ -215,13 +145,16 @@ class ScannerOverlay extends CustomPainter { | ||
| 215 | bottomRight: Radius.circular(borderRadius), | 145 | bottomRight: Radius.circular(borderRadius), |
| 216 | ); | 146 | ); |
| 217 | 147 | ||
| 218 | - // Draw the white border | 148 | + // First, draw the background, |
| 149 | + // with a cutout area that is a bit larger than the scan window. | ||
| 150 | + // Finally, draw the scan window itself. | ||
| 219 | canvas.drawPath(backgroundWithCutout, backgroundPaint); | 151 | canvas.drawPath(backgroundWithCutout, backgroundPaint); |
| 220 | canvas.drawRRect(borderRect, borderPaint); | 152 | canvas.drawRRect(borderRect, borderPaint); |
| 221 | } | 153 | } |
| 222 | 154 | ||
| 223 | @override | 155 | @override |
| 224 | - bool shouldRepaint(covariant CustomPainter oldDelegate) { | ||
| 225 | - return false; | 156 | + bool shouldRepaint(ScannerOverlay oldDelegate) { |
| 157 | + return scanWindow != oldDelegate.scanWindow || | ||
| 158 | + borderRadius != oldDelegate.borderRadius; | ||
| 226 | } | 159 | } |
| 227 | } | 160 | } |
example/lib/scanned_barcode_label.dart
0 → 100644
| 1 | +import 'package:flutter/material.dart'; | ||
| 2 | +import 'package:mobile_scanner/mobile_scanner.dart'; | ||
| 3 | + | ||
| 4 | +class ScannedBarcodeLabel extends StatelessWidget { | ||
| 5 | + const ScannedBarcodeLabel({ | ||
| 6 | + super.key, | ||
| 7 | + required this.barcodes, | ||
| 8 | + }); | ||
| 9 | + | ||
| 10 | + final Stream<BarcodeCapture> barcodes; | ||
| 11 | + | ||
| 12 | + @override | ||
| 13 | + Widget build(BuildContext context) { | ||
| 14 | + return StreamBuilder( | ||
| 15 | + stream: barcodes, | ||
| 16 | + builder: (context, snaphot) { | ||
| 17 | + final scannedBarcodes = snaphot.data?.barcodes ?? []; | ||
| 18 | + | ||
| 19 | + if (scannedBarcodes.isEmpty) { | ||
| 20 | + return const Text( | ||
| 21 | + 'Scan something!', | ||
| 22 | + overflow: TextOverflow.fade, | ||
| 23 | + style: TextStyle(color: Colors.white), | ||
| 24 | + ); | ||
| 25 | + } | ||
| 26 | + | ||
| 27 | + return Text( | ||
| 28 | + scannedBarcodes.first.displayValue ?? 'No display value.', | ||
| 29 | + overflow: TextOverflow.fade, | ||
| 30 | + style: const TextStyle(color: Colors.white), | ||
| 31 | + ); | ||
| 32 | + }, | ||
| 33 | + ); | ||
| 34 | + } | ||
| 35 | +} |
example/lib/scanner_button_widgets.dart
0 → 100644
| 1 | +import 'package:flutter/material.dart'; | ||
| 2 | +import 'package:image_picker/image_picker.dart'; | ||
| 3 | +import 'package:mobile_scanner/mobile_scanner.dart'; | ||
| 4 | + | ||
| 5 | +class AnalyzeImageFromGalleryButton extends StatelessWidget { | ||
| 6 | + const AnalyzeImageFromGalleryButton({required this.controller, super.key}); | ||
| 7 | + | ||
| 8 | + final MobileScannerController controller; | ||
| 9 | + | ||
| 10 | + @override | ||
| 11 | + Widget build(BuildContext context) { | ||
| 12 | + return IconButton( | ||
| 13 | + color: Colors.white, | ||
| 14 | + icon: const Icon(Icons.image), | ||
| 15 | + iconSize: 32.0, | ||
| 16 | + onPressed: () async { | ||
| 17 | + final ImagePicker picker = ImagePicker(); | ||
| 18 | + | ||
| 19 | + final XFile? image = await picker.pickImage( | ||
| 20 | + source: ImageSource.gallery, | ||
| 21 | + ); | ||
| 22 | + | ||
| 23 | + if (image == null) { | ||
| 24 | + return; | ||
| 25 | + } | ||
| 26 | + | ||
| 27 | + final BarcodeCapture? barcodes = await controller.analyzeImage( | ||
| 28 | + image.path, | ||
| 29 | + ); | ||
| 30 | + | ||
| 31 | + if (!context.mounted) { | ||
| 32 | + return; | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + final SnackBar snackbar = barcodes != null | ||
| 36 | + ? const SnackBar( | ||
| 37 | + content: Text('Barcode found!'), | ||
| 38 | + backgroundColor: Colors.green, | ||
| 39 | + ) | ||
| 40 | + : const SnackBar( | ||
| 41 | + content: Text('No barcode found!'), | ||
| 42 | + backgroundColor: Colors.red, | ||
| 43 | + ); | ||
| 44 | + | ||
| 45 | + ScaffoldMessenger.of(context).showSnackBar(snackbar); | ||
| 46 | + }, | ||
| 47 | + ); | ||
| 48 | + } | ||
| 49 | +} | ||
| 50 | + | ||
| 51 | +class StartStopMobileScannerButton extends StatelessWidget { | ||
| 52 | + const StartStopMobileScannerButton({required this.controller, super.key}); | ||
| 53 | + | ||
| 54 | + final MobileScannerController controller; | ||
| 55 | + | ||
| 56 | + @override | ||
| 57 | + Widget build(BuildContext context) { | ||
| 58 | + return ValueListenableBuilder( | ||
| 59 | + valueListenable: controller, | ||
| 60 | + builder: (context, state, child) { | ||
| 61 | + if (!state.isInitialized || !state.isRunning) { | ||
| 62 | + return IconButton( | ||
| 63 | + color: Colors.white, | ||
| 64 | + icon: const Icon(Icons.play_arrow), | ||
| 65 | + iconSize: 32.0, | ||
| 66 | + onPressed: () async { | ||
| 67 | + await controller.start(); | ||
| 68 | + }, | ||
| 69 | + ); | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + return IconButton( | ||
| 73 | + color: Colors.white, | ||
| 74 | + icon: const Icon(Icons.stop), | ||
| 75 | + iconSize: 32.0, | ||
| 76 | + onPressed: () async { | ||
| 77 | + await controller.stop(); | ||
| 78 | + }, | ||
| 79 | + ); | ||
| 80 | + }, | ||
| 81 | + ); | ||
| 82 | + } | ||
| 83 | +} | ||
| 84 | + | ||
| 85 | +class SwitchCameraButton extends StatelessWidget { | ||
| 86 | + const SwitchCameraButton({required this.controller, super.key}); | ||
| 87 | + | ||
| 88 | + final MobileScannerController controller; | ||
| 89 | + | ||
| 90 | + @override | ||
| 91 | + Widget build(BuildContext context) { | ||
| 92 | + return ValueListenableBuilder( | ||
| 93 | + valueListenable: controller, | ||
| 94 | + builder: (context, state, child) { | ||
| 95 | + if (!state.isInitialized || !state.isRunning) { | ||
| 96 | + return const SizedBox.shrink(); | ||
| 97 | + } | ||
| 98 | + | ||
| 99 | + final int? availableCameras = state.availableCameras; | ||
| 100 | + | ||
| 101 | + if (availableCameras != null && availableCameras < 2) { | ||
| 102 | + return const SizedBox.shrink(); | ||
| 103 | + } | ||
| 104 | + | ||
| 105 | + final Widget icon; | ||
| 106 | + | ||
| 107 | + switch (state.cameraDirection) { | ||
| 108 | + case CameraFacing.front: | ||
| 109 | + icon = const Icon(Icons.camera_front); | ||
| 110 | + case CameraFacing.back: | ||
| 111 | + icon = const Icon(Icons.camera_rear); | ||
| 112 | + } | ||
| 113 | + | ||
| 114 | + return IconButton( | ||
| 115 | + iconSize: 32.0, | ||
| 116 | + icon: icon, | ||
| 117 | + onPressed: () async { | ||
| 118 | + await controller.switchCamera(); | ||
| 119 | + }, | ||
| 120 | + ); | ||
| 121 | + }, | ||
| 122 | + ); | ||
| 123 | + } | ||
| 124 | +} | ||
| 125 | + | ||
| 126 | +class ToggleFlashlightButton extends StatelessWidget { | ||
| 127 | + const ToggleFlashlightButton({required this.controller, super.key}); | ||
| 128 | + | ||
| 129 | + final MobileScannerController controller; | ||
| 130 | + | ||
| 131 | + @override | ||
| 132 | + Widget build(BuildContext context) { | ||
| 133 | + return ValueListenableBuilder( | ||
| 134 | + valueListenable: controller, | ||
| 135 | + builder: (context, state, child) { | ||
| 136 | + if (!state.isInitialized || !state.isRunning) { | ||
| 137 | + return const SizedBox.shrink(); | ||
| 138 | + } | ||
| 139 | + | ||
| 140 | + switch (state.torchState) { | ||
| 141 | + case TorchState.off: | ||
| 142 | + return IconButton( | ||
| 143 | + color: Colors.white, | ||
| 144 | + iconSize: 32.0, | ||
| 145 | + icon: const Icon(Icons.flash_off), | ||
| 146 | + onPressed: () async { | ||
| 147 | + await controller.toggleTorch(); | ||
| 148 | + }, | ||
| 149 | + ); | ||
| 150 | + case TorchState.on: | ||
| 151 | + return IconButton( | ||
| 152 | + color: Colors.white, | ||
| 153 | + iconSize: 32.0, | ||
| 154 | + icon: const Icon(Icons.flash_on), | ||
| 155 | + onPressed: () async { | ||
| 156 | + await controller.toggleTorch(); | ||
| 157 | + }, | ||
| 158 | + ); | ||
| 159 | + case TorchState.unavailable: | ||
| 160 | + return const Icon( | ||
| 161 | + Icons.no_flash, | ||
| 162 | + color: Colors.grey, | ||
| 163 | + ); | ||
| 164 | + } | ||
| 165 | + }, | ||
| 166 | + ); | ||
| 167 | + } | ||
| 168 | +} |
| @@ -6,8 +6,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev | @@ -6,8 +6,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev | ||
| 6 | version: 0.0.1 | 6 | version: 0.0.1 |
| 7 | 7 | ||
| 8 | environment: | 8 | environment: |
| 9 | - sdk: ">=3.1.0 <4.0.0" | ||
| 10 | - flutter: ">=3.13.0" | 9 | + sdk: ">=3.3.0 <4.0.0" |
| 10 | + flutter: ">=3.19.0" | ||
| 11 | 11 | ||
| 12 | # Dependencies specify other packages that your package needs in order to work. | 12 | # Dependencies specify other packages that your package needs in order to work. |
| 13 | # To automatically upgrade your package dependencies to the latest versions | 13 | # To automatically upgrade your package dependencies to the latest versions |
| @@ -40,7 +40,6 @@ | @@ -40,7 +40,6 @@ | ||
| 40 | <script src="flutter.js" defer></script> | 40 | <script src="flutter.js" defer></script> |
| 41 | </head> | 41 | </head> |
| 42 | <body> | 42 | <body> |
| 43 | - <script src="https://unpkg.com/@zxing/library@0.19.1" type="application/javascript"></script> | ||
| 44 | <script> | 43 | <script> |
| 45 | window.addEventListener('load', function(ev) { | 44 | window.addEventListener('load', function(ev) { |
| 46 | // Download main.dart.js | 45 | // Download main.dart.js |
| @@ -245,12 +245,12 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | @@ -245,12 +245,12 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 245 | return | 245 | return |
| 246 | } | 246 | } |
| 247 | 247 | ||
| 248 | - mobileScanner.analyzeImage(image: uiImage!, position: AVCaptureDevice.Position.back, callback: { [self] barcodes, error in | 248 | + mobileScanner.analyzeImage(image: uiImage!, position: AVCaptureDevice.Position.back, callback: { barcodes, error in |
| 249 | if error != nil { | 249 | if error != nil { |
| 250 | - barcodeHandler.publishEvent(["name": "error", "message": error?.localizedDescription]) | ||
| 251 | - | ||
| 252 | DispatchQueue.main.async { | 250 | DispatchQueue.main.async { |
| 253 | - result(false) | 251 | + result(FlutterError(code: "MobileScanner", |
| 252 | + message: error?.localizedDescription, | ||
| 253 | + details: nil)) | ||
| 254 | } | 254 | } |
| 255 | 255 | ||
| 256 | return | 256 | return |
| @@ -258,15 +258,13 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | @@ -258,15 +258,13 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin { | ||
| 258 | 258 | ||
| 259 | if (barcodes == nil || barcodes!.isEmpty) { | 259 | if (barcodes == nil || barcodes!.isEmpty) { |
| 260 | DispatchQueue.main.async { | 260 | DispatchQueue.main.async { |
| 261 | - result(false) | 261 | + result(nil) |
| 262 | } | 262 | } |
| 263 | } else { | 263 | } else { |
| 264 | let barcodesMap: [Any?] = barcodes!.compactMap { barcode in barcode.data } | 264 | let barcodesMap: [Any?] = barcodes!.compactMap { barcode in barcode.data } |
| 265 | - let event: [String: Any?] = ["name": "barcode", "data": barcodesMap] | ||
| 266 | - barcodeHandler.publishEvent(event) | ||
| 267 | 265 | ||
| 268 | DispatchQueue.main.async { | 266 | DispatchQueue.main.async { |
| 269 | - result(true) | 267 | + result(["name": "barcode", "data": barcodesMap]) |
| 270 | } | 268 | } |
| 271 | } | 269 | } |
| 272 | }) | 270 | }) |
| @@ -5,13 +5,15 @@ export 'src/enums/camera_facing.dart'; | @@ -5,13 +5,15 @@ export 'src/enums/camera_facing.dart'; | ||
| 5 | export 'src/enums/detection_speed.dart'; | 5 | export 'src/enums/detection_speed.dart'; |
| 6 | export 'src/enums/email_type.dart'; | 6 | export 'src/enums/email_type.dart'; |
| 7 | export 'src/enums/encryption_type.dart'; | 7 | export 'src/enums/encryption_type.dart'; |
| 8 | +export 'src/enums/mobile_scanner_authorization_state.dart'; | ||
| 8 | export 'src/enums/mobile_scanner_error_code.dart'; | 9 | export 'src/enums/mobile_scanner_error_code.dart'; |
| 9 | -export 'src/enums/mobile_scanner_state.dart'; | ||
| 10 | export 'src/enums/phone_type.dart'; | 10 | export 'src/enums/phone_type.dart'; |
| 11 | export 'src/enums/torch_state.dart'; | 11 | export 'src/enums/torch_state.dart'; |
| 12 | export 'src/mobile_scanner.dart'; | 12 | export 'src/mobile_scanner.dart'; |
| 13 | export 'src/mobile_scanner_controller.dart'; | 13 | export 'src/mobile_scanner_controller.dart'; |
| 14 | -export 'src/mobile_scanner_exception.dart'; | 14 | +export 'src/mobile_scanner_exception.dart' |
| 15 | + hide PermissionRequestPendingException; | ||
| 16 | +export 'src/mobile_scanner_platform_interface.dart'; | ||
| 15 | export 'src/objects/address.dart'; | 17 | export 'src/objects/address.dart'; |
| 16 | export 'src/objects/barcode.dart'; | 18 | export 'src/objects/barcode.dart'; |
| 17 | export 'src/objects/barcode_capture.dart'; | 19 | export 'src/objects/barcode_capture.dart'; |
| @@ -20,7 +22,7 @@ export 'src/objects/contact_info.dart'; | @@ -20,7 +22,7 @@ export 'src/objects/contact_info.dart'; | ||
| 20 | export 'src/objects/driver_license.dart'; | 22 | export 'src/objects/driver_license.dart'; |
| 21 | export 'src/objects/email.dart'; | 23 | export 'src/objects/email.dart'; |
| 22 | export 'src/objects/geo_point.dart'; | 24 | export 'src/objects/geo_point.dart'; |
| 23 | -export 'src/objects/mobile_scanner_arguments.dart'; | 25 | +export 'src/objects/mobile_scanner_state.dart'; |
| 24 | export 'src/objects/person_name.dart'; | 26 | export 'src/objects/person_name.dart'; |
| 25 | export 'src/objects/phone.dart'; | 27 | export 'src/objects/phone.dart'; |
| 26 | export 'src/objects/sms.dart'; | 28 | export 'src/objects/sms.dart'; |
lib/mobile_scanner_web.dart
deleted
100644 → 0
lib/mobile_scanner_web_plugin.dart
deleted
100644 → 0
| 1 | -import 'dart:async'; | ||
| 2 | -import 'dart:html' as html; | ||
| 3 | -import 'dart:ui_web' as ui; | ||
| 4 | - | ||
| 5 | -import 'package:flutter/services.dart'; | ||
| 6 | -import 'package:flutter_web_plugins/flutter_web_plugins.dart'; | ||
| 7 | -import 'package:mobile_scanner/mobile_scanner_web.dart'; | ||
| 8 | -import 'package:mobile_scanner/src/enums/barcode_format.dart'; | ||
| 9 | -import 'package:mobile_scanner/src/enums/camera_facing.dart'; | ||
| 10 | - | ||
| 11 | -/// This plugin is the web implementation of mobile_scanner. | ||
| 12 | -/// It only supports QR codes. | ||
| 13 | -class MobileScannerWebPlugin { | ||
| 14 | - static void registerWith(Registrar registrar) { | ||
| 15 | - final PluginEventChannel event = PluginEventChannel( | ||
| 16 | - 'dev.steenbakker.mobile_scanner/scanner/event', | ||
| 17 | - const StandardMethodCodec(), | ||
| 18 | - registrar, | ||
| 19 | - ); | ||
| 20 | - final MethodChannel channel = MethodChannel( | ||
| 21 | - 'dev.steenbakker.mobile_scanner/scanner/method', | ||
| 22 | - const StandardMethodCodec(), | ||
| 23 | - registrar, | ||
| 24 | - ); | ||
| 25 | - final MobileScannerWebPlugin instance = MobileScannerWebPlugin(); | ||
| 26 | - | ||
| 27 | - channel.setMethodCallHandler(instance.handleMethodCall); | ||
| 28 | - event.setController(instance.controller); | ||
| 29 | - } | ||
| 30 | - | ||
| 31 | - // Controller to send events back to the framework | ||
| 32 | - StreamController controller = StreamController.broadcast(); | ||
| 33 | - | ||
| 34 | - // ID of the video feed | ||
| 35 | - String viewID = 'WebScanner-${DateTime.now().millisecondsSinceEpoch}'; | ||
| 36 | - | ||
| 37 | - static final html.DivElement vidDiv = html.DivElement(); | ||
| 38 | - | ||
| 39 | - /// Represents barcode reader library. | ||
| 40 | - /// Change this property if you want to use a custom implementation. | ||
| 41 | - /// | ||
| 42 | - /// Example of using the jsQR library: | ||
| 43 | - /// void main() { | ||
| 44 | - /// if (kIsWeb) { | ||
| 45 | - /// MobileScannerWebPlugin.barCodeReader = | ||
| 46 | - /// JsQrCodeReader(videoContainer: MobileScannerWebPlugin.vidDiv); | ||
| 47 | - /// } | ||
| 48 | - /// runApp(const MaterialApp(home: MyHome())); | ||
| 49 | - /// } | ||
| 50 | - static WebBarcodeReaderBase barCodeReader = | ||
| 51 | - ZXingBarcodeReader(videoContainer: vidDiv); | ||
| 52 | - StreamSubscription? _barCodeStreamSubscription; | ||
| 53 | - | ||
| 54 | - /// Handle incomming messages | ||
| 55 | - Future<dynamic> handleMethodCall(MethodCall call) async { | ||
| 56 | - switch (call.method) { | ||
| 57 | - case 'start': | ||
| 58 | - return _start(call.arguments as Map); | ||
| 59 | - case 'torch': | ||
| 60 | - return _torch(call.arguments); | ||
| 61 | - case 'stop': | ||
| 62 | - return cancel(); | ||
| 63 | - case 'updateScanWindow': | ||
| 64 | - return Future<void>.value(); | ||
| 65 | - default: | ||
| 66 | - throw PlatformException( | ||
| 67 | - code: 'Unimplemented', | ||
| 68 | - details: "The mobile_scanner plugin for web doesn't implement " | ||
| 69 | - "the method '${call.method}'", | ||
| 70 | - ); | ||
| 71 | - } | ||
| 72 | - } | ||
| 73 | - | ||
| 74 | - /// Can enable or disable the flash if available | ||
| 75 | - Future<void> _torch(arguments) async { | ||
| 76 | - barCodeReader.toggleTorch(enabled: arguments == 1); | ||
| 77 | - } | ||
| 78 | - | ||
| 79 | - /// Starts the video stream and the scanner | ||
| 80 | - Future<Map> _start(Map arguments) async { | ||
| 81 | - var cameraFacing = CameraFacing.front; | ||
| 82 | - if (arguments.containsKey('facing')) { | ||
| 83 | - cameraFacing = CameraFacing.values[arguments['facing'] as int]; | ||
| 84 | - } | ||
| 85 | - | ||
| 86 | - ui.platformViewRegistry.registerViewFactory( | ||
| 87 | - viewID, | ||
| 88 | - (int id) { | ||
| 89 | - return vidDiv | ||
| 90 | - ..style.width = '100%' | ||
| 91 | - ..style.height = '100%'; | ||
| 92 | - }, | ||
| 93 | - ); | ||
| 94 | - | ||
| 95 | - // Check if stream is running | ||
| 96 | - if (barCodeReader.isStarted) { | ||
| 97 | - final hasTorch = await barCodeReader.hasTorch(); | ||
| 98 | - return { | ||
| 99 | - 'ViewID': viewID, | ||
| 100 | - 'videoWidth': barCodeReader.videoWidth, | ||
| 101 | - 'videoHeight': barCodeReader.videoHeight, | ||
| 102 | - 'torchable': hasTorch, | ||
| 103 | - }; | ||
| 104 | - } | ||
| 105 | - try { | ||
| 106 | - List<BarcodeFormat>? formats; | ||
| 107 | - if (arguments.containsKey('formats')) { | ||
| 108 | - formats = (arguments['formats'] as List) | ||
| 109 | - .cast<int>() | ||
| 110 | - .map(BarcodeFormat.fromRawValue) | ||
| 111 | - .toList(); | ||
| 112 | - } | ||
| 113 | - | ||
| 114 | - final Duration? detectionTimeout; | ||
| 115 | - if (arguments.containsKey('timeout')) { | ||
| 116 | - detectionTimeout = Duration(milliseconds: arguments['timeout'] as int); | ||
| 117 | - } else { | ||
| 118 | - detectionTimeout = null; | ||
| 119 | - } | ||
| 120 | - | ||
| 121 | - await barCodeReader.start( | ||
| 122 | - cameraFacing: cameraFacing, | ||
| 123 | - formats: formats, | ||
| 124 | - detectionTimeout: detectionTimeout, | ||
| 125 | - ); | ||
| 126 | - | ||
| 127 | - _barCodeStreamSubscription = | ||
| 128 | - barCodeReader.detectBarcodeContinuously().listen((code) { | ||
| 129 | - if (code != null) { | ||
| 130 | - controller.add({ | ||
| 131 | - 'name': 'barcodeWeb', | ||
| 132 | - 'data': { | ||
| 133 | - 'rawValue': code.rawValue, | ||
| 134 | - 'rawBytes': code.rawBytes, | ||
| 135 | - 'format': code.format.rawValue, | ||
| 136 | - 'displayValue': code.displayValue, | ||
| 137 | - 'type': code.type.rawValue, | ||
| 138 | - if (code.corners.isNotEmpty) | ||
| 139 | - 'corners': code.corners | ||
| 140 | - .map( | ||
| 141 | - (Offset c) => <Object?, Object?>{'x': c.dx, 'y': c.dy}, | ||
| 142 | - ) | ||
| 143 | - .toList(), | ||
| 144 | - }, | ||
| 145 | - }); | ||
| 146 | - } | ||
| 147 | - }); | ||
| 148 | - | ||
| 149 | - final hasTorch = await barCodeReader.hasTorch(); | ||
| 150 | - final bool? enableTorch = arguments['torch'] as bool?; | ||
| 151 | - | ||
| 152 | - if (hasTorch && enableTorch != null) { | ||
| 153 | - await barCodeReader.toggleTorch(enabled: enableTorch); | ||
| 154 | - } | ||
| 155 | - | ||
| 156 | - return { | ||
| 157 | - 'ViewID': viewID, | ||
| 158 | - 'videoWidth': barCodeReader.videoWidth, | ||
| 159 | - 'videoHeight': barCodeReader.videoHeight, | ||
| 160 | - 'torchable': hasTorch, | ||
| 161 | - }; | ||
| 162 | - } catch (e, stackTrace) { | ||
| 163 | - throw PlatformException( | ||
| 164 | - code: 'MobileScannerWeb', | ||
| 165 | - message: '$e', | ||
| 166 | - details: stackTrace.toString(), | ||
| 167 | - ); | ||
| 168 | - } | ||
| 169 | - } | ||
| 170 | - | ||
| 171 | - /// Check if any camera's are available | ||
| 172 | - static Future<bool> cameraAvailable() async { | ||
| 173 | - final sources = | ||
| 174 | - await html.window.navigator.mediaDevices!.enumerateDevices(); | ||
| 175 | - for (final e in sources) { | ||
| 176 | - // TODO: | ||
| 177 | - // ignore: avoid_dynamic_calls | ||
| 178 | - if (e.kind == 'videoinput') { | ||
| 179 | - return true; | ||
| 180 | - } | ||
| 181 | - } | ||
| 182 | - return false; | ||
| 183 | - } | ||
| 184 | - | ||
| 185 | - /// Stops the video feed and analyzer | ||
| 186 | - Future<void> cancel() async { | ||
| 187 | - await barCodeReader.stop(); | ||
| 188 | - await barCodeReader.stopDetectBarcodeContinuously(); | ||
| 189 | - await _barCodeStreamSubscription?.cancel(); | ||
| 190 | - _barCodeStreamSubscription = null; | ||
| 191 | - } | ||
| 192 | -} |
| 1 | /// The authorization state of the scanner. | 1 | /// The authorization state of the scanner. |
| 2 | -enum MobileScannerState { | 2 | +enum MobileScannerAuthorizationState { |
| 3 | /// The scanner has not yet requested the required permissions. | 3 | /// The scanner has not yet requested the required permissions. |
| 4 | undetermined(0), | 4 | undetermined(0), |
| 5 | 5 | ||
| @@ -9,16 +9,16 @@ enum MobileScannerState { | @@ -9,16 +9,16 @@ enum MobileScannerState { | ||
| 9 | /// The user denied the required permissions. | 9 | /// The user denied the required permissions. |
| 10 | denied(2); | 10 | denied(2); |
| 11 | 11 | ||
| 12 | - const MobileScannerState(this.rawValue); | 12 | + const MobileScannerAuthorizationState(this.rawValue); |
| 13 | 13 | ||
| 14 | - factory MobileScannerState.fromRawValue(int value) { | 14 | + factory MobileScannerAuthorizationState.fromRawValue(int value) { |
| 15 | switch (value) { | 15 | switch (value) { |
| 16 | case 0: | 16 | case 0: |
| 17 | - return MobileScannerState.undetermined; | 17 | + return MobileScannerAuthorizationState.undetermined; |
| 18 | case 1: | 18 | case 1: |
| 19 | - return MobileScannerState.authorized; | 19 | + return MobileScannerAuthorizationState.authorized; |
| 20 | case 2: | 20 | case 2: |
| 21 | - return MobileScannerState.denied; | 21 | + return MobileScannerAuthorizationState.denied; |
| 22 | default: | 22 | default: |
| 23 | throw ArgumentError.value(value, 'value', 'Invalid raw value.'); | 23 | throw ArgumentError.value(value, 'value', 'Invalid raw value.'); |
| 24 | } | 24 | } |
| 1 | +import 'package:mobile_scanner/src/mobile_scanner_controller.dart'; | ||
| 2 | + | ||
| 1 | /// This enum defines the different error codes for the mobile scanner. | 3 | /// This enum defines the different error codes for the mobile scanner. |
| 2 | enum MobileScannerErrorCode { | 4 | enum MobileScannerErrorCode { |
| 5 | + /// The controller was already started. | ||
| 6 | + /// | ||
| 7 | + /// The controller should be stopped using [MobileScannerController.stop], | ||
| 8 | + /// before restarting it. | ||
| 9 | + controllerAlreadyInitialized, | ||
| 10 | + | ||
| 11 | + /// The controller was used after being disposed. | ||
| 12 | + controllerDisposed, | ||
| 13 | + | ||
| 3 | /// The controller was used | 14 | /// The controller was used |
| 4 | /// while it was not yet initialized using [MobileScannerController.start]. | 15 | /// while it was not yet initialized using [MobileScannerController.start]. |
| 5 | controllerUninitialized, | 16 | controllerUninitialized, |
| @@ -4,7 +4,10 @@ enum TorchState { | @@ -4,7 +4,10 @@ enum TorchState { | ||
| 4 | off(0), | 4 | off(0), |
| 5 | 5 | ||
| 6 | /// The flashlight is on. | 6 | /// The flashlight is on. |
| 7 | - on(1); | 7 | + on(1), |
| 8 | + | ||
| 9 | + /// The flashlight is unavailable. | ||
| 10 | + unavailable(2); | ||
| 8 | 11 | ||
| 9 | const TorchState(this.rawValue); | 12 | const TorchState(this.rawValue); |
| 10 | 13 | ||
| @@ -14,6 +17,8 @@ enum TorchState { | @@ -14,6 +17,8 @@ enum TorchState { | ||
| 14 | return TorchState.off; | 17 | return TorchState.off; |
| 15 | case 1: | 18 | case 1: |
| 16 | return TorchState.on; | 19 | return TorchState.on; |
| 20 | + case 2: | ||
| 21 | + return TorchState.unavailable; | ||
| 17 | default: | 22 | default: |
| 18 | throw ArgumentError.value(value, 'value', 'Invalid raw value.'); | 23 | throw ArgumentError.value(value, 'value', 'Invalid raw value.'); |
| 19 | } | 24 | } |
| 1 | +import 'dart:async'; | ||
| 2 | +import 'dart:io'; | ||
| 3 | + | ||
| 4 | +import 'package:flutter/services.dart'; | ||
| 5 | +import 'package:flutter/widgets.dart'; | ||
| 6 | +import 'package:mobile_scanner/src/enums/barcode_format.dart'; | ||
| 7 | +import 'package:mobile_scanner/src/enums/mobile_scanner_authorization_state.dart'; | ||
| 8 | +import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; | ||
| 9 | +import 'package:mobile_scanner/src/enums/torch_state.dart'; | ||
| 10 | +import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; | ||
| 11 | +import 'package:mobile_scanner/src/mobile_scanner_platform_interface.dart'; | ||
| 12 | +import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart'; | ||
| 13 | +import 'package:mobile_scanner/src/objects/barcode.dart'; | ||
| 14 | +import 'package:mobile_scanner/src/objects/barcode_capture.dart'; | ||
| 15 | +import 'package:mobile_scanner/src/objects/start_options.dart'; | ||
| 16 | + | ||
| 17 | +/// An implementation of [MobileScannerPlatform] that uses method channels. | ||
| 18 | +class MethodChannelMobileScanner extends MobileScannerPlatform { | ||
| 19 | + /// The method channel used to interact with the native platform. | ||
| 20 | + @visibleForTesting | ||
| 21 | + final methodChannel = const MethodChannel( | ||
| 22 | + 'dev.steenbakker.mobile_scanner/scanner/method', | ||
| 23 | + ); | ||
| 24 | + | ||
| 25 | + /// The event channel that sends back scanned barcode events. | ||
| 26 | + @visibleForTesting | ||
| 27 | + final eventChannel = const EventChannel( | ||
| 28 | + 'dev.steenbakker.mobile_scanner/scanner/event', | ||
| 29 | + ); | ||
| 30 | + | ||
| 31 | + Stream<Map<Object?, Object?>>? _eventsStream; | ||
| 32 | + | ||
| 33 | + Stream<Map<Object?, Object?>> get eventsStream { | ||
| 34 | + _eventsStream ??= | ||
| 35 | + eventChannel.receiveBroadcastStream().cast<Map<Object?, Object?>>(); | ||
| 36 | + | ||
| 37 | + return _eventsStream!; | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + int? _textureId; | ||
| 41 | + | ||
| 42 | + /// Parse a [BarcodeCapture] from the given [event]. | ||
| 43 | + BarcodeCapture? _parseBarcode(Map<Object?, Object?>? event) { | ||
| 44 | + if (event == null) { | ||
| 45 | + return null; | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + final Object? data = event['data']; | ||
| 49 | + | ||
| 50 | + if (data == null || data is! List<Object?>) { | ||
| 51 | + return null; | ||
| 52 | + } | ||
| 53 | + | ||
| 54 | + final List<Map<Object?, Object?>> barcodes = | ||
| 55 | + data.cast<Map<Object?, Object?>>(); | ||
| 56 | + | ||
| 57 | + if (Platform.isMacOS) { | ||
| 58 | + return BarcodeCapture( | ||
| 59 | + raw: event, | ||
| 60 | + barcodes: barcodes | ||
| 61 | + .map( | ||
| 62 | + (barcode) => Barcode( | ||
| 63 | + rawValue: barcode['payload'] as String?, | ||
| 64 | + format: BarcodeFormat.fromRawValue( | ||
| 65 | + barcode['symbology'] as int? ?? -1, | ||
| 66 | + ), | ||
| 67 | + ), | ||
| 68 | + ) | ||
| 69 | + .toList(), | ||
| 70 | + ); | ||
| 71 | + } | ||
| 72 | + | ||
| 73 | + if (Platform.isAndroid || Platform.isIOS) { | ||
| 74 | + final double? width = event['width'] as double?; | ||
| 75 | + final double? height = event['height'] as double?; | ||
| 76 | + | ||
| 77 | + return BarcodeCapture( | ||
| 78 | + raw: data, | ||
| 79 | + barcodes: barcodes.map(Barcode.fromNative).toList(), | ||
| 80 | + image: event['image'] as Uint8List?, | ||
| 81 | + size: width == null || height == null ? Size.zero : Size(width, height), | ||
| 82 | + ); | ||
| 83 | + } | ||
| 84 | + | ||
| 85 | + throw const MobileScannerException( | ||
| 86 | + errorCode: MobileScannerErrorCode.genericError, | ||
| 87 | + errorDetails: MobileScannerErrorDetails( | ||
| 88 | + message: 'Only Android, iOS and macOS are supported.', | ||
| 89 | + ), | ||
| 90 | + ); | ||
| 91 | + } | ||
| 92 | + | ||
| 93 | + /// Request permission to access the camera. | ||
| 94 | + /// | ||
| 95 | + /// Throws a [MobileScannerException] if the permission is not granted. | ||
| 96 | + Future<void> _requestCameraPermission() async { | ||
| 97 | + final MobileScannerAuthorizationState authorizationState; | ||
| 98 | + | ||
| 99 | + try { | ||
| 100 | + authorizationState = MobileScannerAuthorizationState.fromRawValue( | ||
| 101 | + await methodChannel.invokeMethod<int>('state') ?? 0, | ||
| 102 | + ); | ||
| 103 | + } on PlatformException catch (error) { | ||
| 104 | + // If the permission state is invalid, that is an error. | ||
| 105 | + throw MobileScannerException( | ||
| 106 | + errorCode: MobileScannerErrorCode.genericError, | ||
| 107 | + errorDetails: MobileScannerErrorDetails( | ||
| 108 | + code: error.code, | ||
| 109 | + details: error.details as Object?, | ||
| 110 | + message: error.message, | ||
| 111 | + ), | ||
| 112 | + ); | ||
| 113 | + } | ||
| 114 | + | ||
| 115 | + switch (authorizationState) { | ||
| 116 | + case MobileScannerAuthorizationState.authorized: | ||
| 117 | + return; // Already authorized. | ||
| 118 | + // Android does not have an undetermined authorization state. | ||
| 119 | + // So if the permission was denied, request it again. | ||
| 120 | + case MobileScannerAuthorizationState.denied: | ||
| 121 | + case MobileScannerAuthorizationState.undetermined: | ||
| 122 | + try { | ||
| 123 | + final bool granted = | ||
| 124 | + await methodChannel.invokeMethod<bool>('request') ?? false; | ||
| 125 | + | ||
| 126 | + if (granted) { | ||
| 127 | + return; // Authorization was granted. | ||
| 128 | + } | ||
| 129 | + | ||
| 130 | + throw const MobileScannerException( | ||
| 131 | + errorCode: MobileScannerErrorCode.permissionDenied, | ||
| 132 | + ); | ||
| 133 | + } on PlatformException catch (error) { | ||
| 134 | + // If the permission state is invalid, that is an error. | ||
| 135 | + throw MobileScannerException( | ||
| 136 | + errorCode: MobileScannerErrorCode.genericError, | ||
| 137 | + errorDetails: MobileScannerErrorDetails( | ||
| 138 | + code: error.code, | ||
| 139 | + details: error.details as Object?, | ||
| 140 | + message: error.message, | ||
| 141 | + ), | ||
| 142 | + ); | ||
| 143 | + } | ||
| 144 | + } | ||
| 145 | + } | ||
| 146 | + | ||
| 147 | + @override | ||
| 148 | + Stream<BarcodeCapture?> get barcodesStream { | ||
| 149 | + return eventsStream | ||
| 150 | + .where((event) => event['name'] == 'barcode') | ||
| 151 | + .map((event) => _parseBarcode(event)); | ||
| 152 | + } | ||
| 153 | + | ||
| 154 | + @override | ||
| 155 | + Stream<TorchState> get torchStateStream { | ||
| 156 | + return eventsStream | ||
| 157 | + .where((event) => event['name'] == 'torchState') | ||
| 158 | + .map((event) => TorchState.fromRawValue(event['data'] as int? ?? 0)); | ||
| 159 | + } | ||
| 160 | + | ||
| 161 | + @override | ||
| 162 | + Stream<double> get zoomScaleStateStream { | ||
| 163 | + return eventsStream | ||
| 164 | + .where((event) => event['name'] == 'zoomScaleState') | ||
| 165 | + .map((event) => event['data'] as double? ?? 0.0); | ||
| 166 | + } | ||
| 167 | + | ||
| 168 | + @override | ||
| 169 | + Future<BarcodeCapture?> analyzeImage(String path) async { | ||
| 170 | + final Map<String, Object?>? result = | ||
| 171 | + await methodChannel.invokeMapMethod<String, Object?>( | ||
| 172 | + 'analyzeImage', | ||
| 173 | + path, | ||
| 174 | + ); | ||
| 175 | + | ||
| 176 | + return _parseBarcode(result); | ||
| 177 | + } | ||
| 178 | + | ||
| 179 | + @override | ||
| 180 | + Widget buildCameraView() { | ||
| 181 | + if (_textureId == null) { | ||
| 182 | + return const SizedBox(); | ||
| 183 | + } | ||
| 184 | + | ||
| 185 | + return Texture(textureId: _textureId!); | ||
| 186 | + } | ||
| 187 | + | ||
| 188 | + @override | ||
| 189 | + Future<void> resetZoomScale() async { | ||
| 190 | + await methodChannel.invokeMethod<void>('resetScale'); | ||
| 191 | + } | ||
| 192 | + | ||
| 193 | + @override | ||
| 194 | + Future<void> setTorchState(TorchState torchState) async { | ||
| 195 | + if (torchState == TorchState.unavailable) { | ||
| 196 | + return; | ||
| 197 | + } | ||
| 198 | + | ||
| 199 | + await methodChannel.invokeMethod<void>('torch', torchState.rawValue); | ||
| 200 | + } | ||
| 201 | + | ||
| 202 | + @override | ||
| 203 | + Future<void> setZoomScale(double zoomScale) async { | ||
| 204 | + await methodChannel.invokeMethod<void>('setScale', zoomScale); | ||
| 205 | + } | ||
| 206 | + | ||
| 207 | + @override | ||
| 208 | + Future<MobileScannerViewAttributes> start(StartOptions startOptions) async { | ||
| 209 | + if (_textureId != null) { | ||
| 210 | + throw const MobileScannerException( | ||
| 211 | + errorCode: MobileScannerErrorCode.controllerAlreadyInitialized, | ||
| 212 | + errorDetails: MobileScannerErrorDetails( | ||
| 213 | + message: | ||
| 214 | + 'The scanner was already started. Call stop() before calling start() again.', | ||
| 215 | + ), | ||
| 216 | + ); | ||
| 217 | + } | ||
| 218 | + | ||
| 219 | + await _requestCameraPermission(); | ||
| 220 | + | ||
| 221 | + Map<String, Object?>? startResult; | ||
| 222 | + | ||
| 223 | + try { | ||
| 224 | + startResult = await methodChannel.invokeMapMethod<String, Object?>( | ||
| 225 | + 'start', | ||
| 226 | + startOptions.toMap(), | ||
| 227 | + ); | ||
| 228 | + } on PlatformException catch (error) { | ||
| 229 | + throw MobileScannerException( | ||
| 230 | + errorCode: MobileScannerErrorCode.genericError, | ||
| 231 | + errorDetails: MobileScannerErrorDetails( | ||
| 232 | + code: error.code, | ||
| 233 | + details: error.details as Object?, | ||
| 234 | + message: error.message, | ||
| 235 | + ), | ||
| 236 | + ); | ||
| 237 | + } | ||
| 238 | + | ||
| 239 | + if (startResult == null) { | ||
| 240 | + throw const MobileScannerException( | ||
| 241 | + errorCode: MobileScannerErrorCode.genericError, | ||
| 242 | + errorDetails: MobileScannerErrorDetails( | ||
| 243 | + message: 'The start method did not return a view configuration.', | ||
| 244 | + ), | ||
| 245 | + ); | ||
| 246 | + } | ||
| 247 | + | ||
| 248 | + final int? textureId = startResult['textureId'] as int?; | ||
| 249 | + | ||
| 250 | + if (textureId == null) { | ||
| 251 | + throw const MobileScannerException( | ||
| 252 | + errorCode: MobileScannerErrorCode.genericError, | ||
| 253 | + errorDetails: MobileScannerErrorDetails( | ||
| 254 | + message: 'The start method did not return a texture id.', | ||
| 255 | + ), | ||
| 256 | + ); | ||
| 257 | + } | ||
| 258 | + | ||
| 259 | + _textureId = textureId; | ||
| 260 | + | ||
| 261 | + final int? numberOfCameras = startResult['numberOfCameras'] as int?; | ||
| 262 | + final bool hasTorch = startResult['torchable'] as bool? ?? false; | ||
| 263 | + | ||
| 264 | + final Map<Object?, Object?>? sizeInfo = | ||
| 265 | + startResult['size'] as Map<Object?, Object?>?; | ||
| 266 | + final double? width = sizeInfo?['width'] as double?; | ||
| 267 | + final double? height = sizeInfo?['height'] as double?; | ||
| 268 | + | ||
| 269 | + final Size size; | ||
| 270 | + | ||
| 271 | + if (width == null || height == null) { | ||
| 272 | + size = Size.zero; | ||
| 273 | + } else { | ||
| 274 | + size = Size(width, height); | ||
| 275 | + } | ||
| 276 | + | ||
| 277 | + return MobileScannerViewAttributes( | ||
| 278 | + hasTorch: hasTorch, | ||
| 279 | + numberOfCameras: numberOfCameras, | ||
| 280 | + size: size, | ||
| 281 | + ); | ||
| 282 | + } | ||
| 283 | + | ||
| 284 | + @override | ||
| 285 | + Future<void> stop() async { | ||
| 286 | + if (_textureId == null) { | ||
| 287 | + return; | ||
| 288 | + } | ||
| 289 | + | ||
| 290 | + _textureId = null; | ||
| 291 | + | ||
| 292 | + await methodChannel.invokeMethod<void>('stop'); | ||
| 293 | + } | ||
| 294 | + | ||
| 295 | + @override | ||
| 296 | + Future<void> updateScanWindow(Rect? window) async { | ||
| 297 | + if (_textureId == null) { | ||
| 298 | + return; | ||
| 299 | + } | ||
| 300 | + | ||
| 301 | + List<double>? points; | ||
| 302 | + | ||
| 303 | + if (window != null) { | ||
| 304 | + points = [window.left, window.top, window.right, window.bottom]; | ||
| 305 | + } | ||
| 306 | + | ||
| 307 | + await methodChannel.invokeMethod<void>( | ||
| 308 | + 'updateScanWindow', | ||
| 309 | + {'rect': points}, | ||
| 310 | + ); | ||
| 311 | + } | ||
| 312 | + | ||
| 313 | + @override | ||
| 314 | + Future<void> dispose() async { | ||
| 315 | + await stop(); | ||
| 316 | + } | ||
| 317 | +} |
| 1 | import 'dart:async'; | 1 | import 'dart:async'; |
| 2 | 2 | ||
| 3 | -import 'package:flutter/foundation.dart'; | ||
| 4 | import 'package:flutter/material.dart'; | 3 | import 'package:flutter/material.dart'; |
| 5 | -import 'package:flutter/services.dart'; | ||
| 6 | -import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; | ||
| 7 | import 'package:mobile_scanner/src/mobile_scanner_controller.dart'; | 4 | import 'package:mobile_scanner/src/mobile_scanner_controller.dart'; |
| 8 | import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; | 5 | import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; |
| 9 | -import 'package:mobile_scanner/src/objects/barcode_capture.dart'; | ||
| 10 | -import 'package:mobile_scanner/src/objects/mobile_scanner_arguments.dart'; | 6 | +import 'package:mobile_scanner/src/mobile_scanner_platform_interface.dart'; |
| 7 | +import 'package:mobile_scanner/src/objects/mobile_scanner_state.dart'; | ||
| 11 | import 'package:mobile_scanner/src/scan_window_calculation.dart'; | 8 | import 'package:mobile_scanner/src/scan_window_calculation.dart'; |
| 12 | 9 | ||
| 13 | /// The function signature for the error builder. | 10 | /// The function signature for the error builder. |
| @@ -17,18 +14,26 @@ typedef MobileScannerErrorBuilder = Widget Function( | @@ -17,18 +14,26 @@ typedef MobileScannerErrorBuilder = Widget Function( | ||
| 17 | Widget?, | 14 | Widget?, |
| 18 | ); | 15 | ); |
| 19 | 16 | ||
| 20 | -/// The [MobileScanner] widget displays a live camera preview. | 17 | +/// This widget displays a live camera preview for the barcode scanner. |
| 21 | class MobileScanner extends StatefulWidget { | 18 | class MobileScanner extends StatefulWidget { |
| 22 | - /// The controller that manages the barcode scanner. | ||
| 23 | - /// | ||
| 24 | - /// If this is null, the scanner will manage its own controller. | ||
| 25 | - final MobileScannerController? controller; | 19 | + /// Create a new [MobileScanner] using the provided [controller]. |
| 20 | + const MobileScanner({ | ||
| 21 | + required this.controller, | ||
| 22 | + this.fit = BoxFit.cover, | ||
| 23 | + this.errorBuilder, | ||
| 24 | + this.overlayBuilder, | ||
| 25 | + this.placeholderBuilder, | ||
| 26 | + this.scanWindow, | ||
| 27 | + super.key, | ||
| 28 | + }); | ||
| 29 | + | ||
| 30 | + /// The controller for the camera preview. | ||
| 31 | + final MobileScannerController controller; | ||
| 26 | 32 | ||
| 27 | - /// The function that builds an error widget when the scanner | ||
| 28 | - /// could not be started. | 33 | + /// The error builder for the camera preview. |
| 29 | /// | 34 | /// |
| 30 | - /// If this is null, defaults to a black [ColoredBox] | ||
| 31 | - /// with a centered white [Icons.error] icon. | 35 | + /// If this is null, a black [ColoredBox], |
| 36 | + /// with a centered white [Icons.error] icon is used as error widget. | ||
| 32 | final MobileScannerErrorBuilder? errorBuilder; | 37 | final MobileScannerErrorBuilder? errorBuilder; |
| 33 | 38 | ||
| 34 | /// The [BoxFit] for the camera preview. | 39 | /// The [BoxFit] for the camera preview. |
| @@ -36,250 +41,153 @@ class MobileScanner extends StatefulWidget { | @@ -36,250 +41,153 @@ class MobileScanner extends StatefulWidget { | ||
| 36 | /// Defaults to [BoxFit.cover]. | 41 | /// Defaults to [BoxFit.cover]. |
| 37 | final BoxFit fit; | 42 | final BoxFit fit; |
| 38 | 43 | ||
| 39 | - /// The function that signals when new codes were detected by the [controller]. | ||
| 40 | - final void Function(BarcodeCapture barcodes) onDetect; | ||
| 41 | - | ||
| 42 | - /// The function that signals when the barcode scanner is started. | ||
| 43 | - @Deprecated('Use onScannerStarted() instead.') | ||
| 44 | - final void Function(MobileScannerArguments? arguments)? onStart; | ||
| 45 | - | ||
| 46 | - /// The function that signals when the barcode scanner is started. | ||
| 47 | - final void Function(MobileScannerArguments? arguments)? onScannerStarted; | 44 | + /// The builder for the overlay above the camera preview. |
| 45 | + /// | ||
| 46 | + /// The resulting widget can be combined with the [scanWindow] rectangle | ||
| 47 | + /// to create a cutout for the camera preview. | ||
| 48 | + /// | ||
| 49 | + /// The [BoxConstraints] for this builder | ||
| 50 | + /// are the same constraints that are used to compute the effective [scanWindow]. | ||
| 51 | + /// | ||
| 52 | + /// The overlay is only displayed when the camera preview is visible. | ||
| 53 | + final LayoutWidgetBuilder? overlayBuilder; | ||
| 48 | 54 | ||
| 49 | - /// The function that builds a placeholder widget when the scanner | ||
| 50 | - /// is not yet displaying its camera preview. | 55 | + /// The placeholder builder for the camera preview. |
| 51 | /// | 56 | /// |
| 52 | /// If this is null, a black [ColoredBox] is used as placeholder. | 57 | /// If this is null, a black [ColoredBox] is used as placeholder. |
| 58 | + /// | ||
| 59 | + /// The placeholder is displayed when the camera preview is being initialized. | ||
| 53 | final Widget Function(BuildContext, Widget?)? placeholderBuilder; | 60 | final Widget Function(BuildContext, Widget?)? placeholderBuilder; |
| 54 | 61 | ||
| 55 | - /// if set barcodes will only be scanned if they fall within this [Rect] | ||
| 56 | - /// useful for having a cut-out overlay for example. these [Rect] | ||
| 57 | - /// coordinates are relative to the widget size, so by how much your | ||
| 58 | - /// rectangle overlays the actual image can depend on things like the | ||
| 59 | - /// [BoxFit] | ||
| 60 | - final Rect? scanWindow; | ||
| 61 | - | ||
| 62 | - /// Only set this to true if you are starting another instance of mobile_scanner | ||
| 63 | - /// right after disposing the first one, like in a PageView. | 62 | + /// The scan window rectangle for the barcode scanner. |
| 64 | /// | 63 | /// |
| 65 | - /// Default: false | ||
| 66 | - final bool startDelay; | ||
| 67 | - | ||
| 68 | - /// The overlay which will be painted above the scanner when has started successful. | ||
| 69 | - /// Will no be pointed when an error occurs or the scanner hasn't been started yet. | ||
| 70 | - final Widget? overlay; | ||
| 71 | - | ||
| 72 | - /// Create a new [MobileScanner] using the provided [controller] | ||
| 73 | - /// and [onBarcodeDetected] callback. | ||
| 74 | - const MobileScanner({ | ||
| 75 | - this.controller, | ||
| 76 | - this.errorBuilder, | ||
| 77 | - this.fit = BoxFit.cover, | ||
| 78 | - required this.onDetect, | ||
| 79 | - @Deprecated('Use onScannerStarted() instead.') this.onStart, | ||
| 80 | - this.onScannerStarted, | ||
| 81 | - this.placeholderBuilder, | ||
| 82 | - this.scanWindow, | ||
| 83 | - this.startDelay = false, | ||
| 84 | - this.overlay, | ||
| 85 | - super.key, | ||
| 86 | - }); | 64 | + /// If this is not null, the barcode scanner will only scan barcodes |
| 65 | + /// which intersect this rectangle. | ||
| 66 | + /// | ||
| 67 | + /// This rectangle is relative to the layout size | ||
| 68 | + /// of the *camera preview widget* in the widget tree, | ||
| 69 | + /// rather than the actual size of the camera preview output. | ||
| 70 | + /// This is because the size of the camera preview widget | ||
| 71 | + /// might not be the same as the size of the camera output. | ||
| 72 | + /// | ||
| 73 | + /// For example, the applied [fit] has an effect on the size of the camera preview widget, | ||
| 74 | + /// while the camera preview size remains the same. | ||
| 75 | + /// | ||
| 76 | + /// The following example shows a scan window that is centered, | ||
| 77 | + /// fills half the height and one third of the width of the layout: | ||
| 78 | + /// | ||
| 79 | + /// ```dart | ||
| 80 | + /// LayoutBuider( | ||
| 81 | + /// builder: (BuildContext context, BoxConstraints constraints) { | ||
| 82 | + /// final Size layoutSize = constraints.biggest; | ||
| 83 | + /// | ||
| 84 | + /// final double scanWindowWidth = layoutSize.width / 3; | ||
| 85 | + /// final double scanWindowHeight = layoutSize.height / 2; | ||
| 86 | + /// | ||
| 87 | + /// final Rect scanWindow = Rect.fromCenter( | ||
| 88 | + /// center: layoutSize.center(Offset.zero), | ||
| 89 | + /// width: scanWindowWidth, | ||
| 90 | + /// height: scanWindowHeight, | ||
| 91 | + /// ); | ||
| 92 | + /// } | ||
| 93 | + /// ); | ||
| 94 | + /// ``` | ||
| 95 | + final Rect? scanWindow; | ||
| 87 | 96 | ||
| 88 | @override | 97 | @override |
| 89 | State<MobileScanner> createState() => _MobileScannerState(); | 98 | State<MobileScanner> createState() => _MobileScannerState(); |
| 90 | } | 99 | } |
| 91 | 100 | ||
| 92 | -class _MobileScannerState extends State<MobileScanner> | ||
| 93 | - with WidgetsBindingObserver { | ||
| 94 | - /// The subscription that listens to barcode detection. | ||
| 95 | - StreamSubscription<BarcodeCapture>? _barcodesSubscription; | ||
| 96 | - | ||
| 97 | - /// The internally managed controller. | ||
| 98 | - late MobileScannerController _controller; | ||
| 99 | - | ||
| 100 | - /// Whether the controller should resume | ||
| 101 | - /// when the application comes back to the foreground. | ||
| 102 | - bool _resumeFromBackground = false; | ||
| 103 | - | ||
| 104 | - MobileScannerException? _startException; | ||
| 105 | - | ||
| 106 | - Widget _buildPlaceholderOrError(BuildContext context, Widget? child) { | ||
| 107 | - final error = _startException; | 101 | +class _MobileScannerState extends State<MobileScanner> { |
| 102 | + /// The current scan window. | ||
| 103 | + Rect? scanWindow; | ||
| 108 | 104 | ||
| 109 | - if (error != null) { | ||
| 110 | - return widget.errorBuilder?.call(context, error, child) ?? | ||
| 111 | - const ColoredBox( | ||
| 112 | - color: Colors.black, | ||
| 113 | - child: Center(child: Icon(Icons.error, color: Colors.white)), | 105 | + /// Calculate the scan window based on the given [constraints]. |
| 106 | + /// | ||
| 107 | + /// If the [scanWindow] is already set, this method does nothing. | ||
| 108 | + void _maybeUpdateScanWindow( | ||
| 109 | + MobileScannerState scannerState, | ||
| 110 | + BoxConstraints constraints, | ||
| 111 | + ) { | ||
| 112 | + if (widget.scanWindow != null && scanWindow == null) { | ||
| 113 | + scanWindow = calculateScanWindowRelativeToTextureInPercentage( | ||
| 114 | + widget.fit, | ||
| 115 | + widget.scanWindow!, | ||
| 116 | + textureSize: scannerState.size, | ||
| 117 | + widgetSize: constraints.biggest, | ||
| 114 | ); | 118 | ); |
| 115 | - } | ||
| 116 | 119 | ||
| 117 | - return widget.placeholderBuilder?.call(context, child) ?? | ||
| 118 | - const ColoredBox(color: Colors.black); | 120 | + unawaited(widget.controller.updateScanWindow(scanWindow)); |
| 119 | } | 121 | } |
| 120 | - | ||
| 121 | - /// Start the given [scanner]. | ||
| 122 | - Future<void> _startScanner() async { | ||
| 123 | - if (widget.startDelay) { | ||
| 124 | - await Future.delayed(const Duration(seconds: 1, milliseconds: 500)); | ||
| 125 | } | 122 | } |
| 126 | 123 | ||
| 127 | - _barcodesSubscription ??= _controller.barcodes.listen( | ||
| 128 | - widget.onDetect, | ||
| 129 | - ); | 124 | + @override |
| 125 | + Widget build(BuildContext context) { | ||
| 126 | + return ValueListenableBuilder<MobileScannerState>( | ||
| 127 | + valueListenable: widget.controller, | ||
| 128 | + builder: (BuildContext context, MobileScannerState value, Widget? child) { | ||
| 129 | + if (!value.isInitialized) { | ||
| 130 | + const Widget defaultPlaceholder = ColoredBox(color: Colors.black); | ||
| 130 | 131 | ||
| 131 | - if (!_controller.autoStart) { | ||
| 132 | - debugPrint( | ||
| 133 | - 'mobile_scanner: not starting automatically because autoStart is set to false in the controller.', | ||
| 134 | - ); | ||
| 135 | - return; | 132 | + return widget.placeholderBuilder?.call(context, child) ?? |
| 133 | + defaultPlaceholder; | ||
| 136 | } | 134 | } |
| 137 | 135 | ||
| 138 | - _controller.start().then((arguments) { | ||
| 139 | - // ignore: deprecated_member_use_from_same_package | ||
| 140 | - widget.onStart?.call(arguments); | ||
| 141 | - widget.onScannerStarted?.call(arguments); | ||
| 142 | - }).catchError((error) { | ||
| 143 | - if (!mounted) { | ||
| 144 | - return; | ||
| 145 | - } | 136 | + final MobileScannerException? error = value.error; |
| 146 | 137 | ||
| 147 | - if (error is MobileScannerException) { | ||
| 148 | - _startException = error; | ||
| 149 | - } else if (error is PlatformException) { | ||
| 150 | - _startException = MobileScannerException( | ||
| 151 | - errorCode: MobileScannerErrorCode.genericError, | ||
| 152 | - errorDetails: MobileScannerErrorDetails( | ||
| 153 | - code: error.code, | ||
| 154 | - message: error.message, | ||
| 155 | - details: error.details, | ||
| 156 | - ), | ||
| 157 | - ); | ||
| 158 | - } else { | ||
| 159 | - _startException = MobileScannerException( | ||
| 160 | - errorCode: MobileScannerErrorCode.genericError, | ||
| 161 | - errorDetails: MobileScannerErrorDetails( | ||
| 162 | - details: error, | ||
| 163 | - ), | 138 | + if (error != null) { |
| 139 | + const Widget defaultError = ColoredBox( | ||
| 140 | + color: Colors.black, | ||
| 141 | + child: Center(child: Icon(Icons.error, color: Colors.white)), | ||
| 164 | ); | 142 | ); |
| 165 | - } | ||
| 166 | - | ||
| 167 | - setState(() {}); | ||
| 168 | - }); | ||
| 169 | - } | ||
| 170 | - | ||
| 171 | - @override | ||
| 172 | - void initState() { | ||
| 173 | - super.initState(); | ||
| 174 | - WidgetsBinding.instance.addObserver(this); | ||
| 175 | - _controller = widget.controller ?? MobileScannerController(); | ||
| 176 | - _startScanner(); | ||
| 177 | - } | ||
| 178 | - | ||
| 179 | - @override | ||
| 180 | - void didChangeAppLifecycleState(AppLifecycleState state) { | ||
| 181 | - // App state changed before the controller was initialized. | ||
| 182 | - if (_controller.isStarting) { | ||
| 183 | - return; | ||
| 184 | - } | ||
| 185 | 143 | ||
| 186 | - switch (state) { | ||
| 187 | - case AppLifecycleState.resumed: | ||
| 188 | - if (_resumeFromBackground) { | ||
| 189 | - _startScanner(); | ||
| 190 | - } | ||
| 191 | - case AppLifecycleState.inactive: | ||
| 192 | - _resumeFromBackground = true; | ||
| 193 | - _controller.stop(); | ||
| 194 | - default: | ||
| 195 | - break; | ||
| 196 | - } | 144 | + return widget.errorBuilder?.call(context, error, child) ?? |
| 145 | + defaultError; | ||
| 197 | } | 146 | } |
| 198 | 147 | ||
| 199 | - Rect? scanWindow; | ||
| 200 | - | ||
| 201 | - @override | ||
| 202 | - Widget build(BuildContext context) { | ||
| 203 | return LayoutBuilder( | 148 | return LayoutBuilder( |
| 204 | builder: (context, constraints) { | 149 | builder: (context, constraints) { |
| 205 | - return ValueListenableBuilder<MobileScannerArguments?>( | ||
| 206 | - valueListenable: _controller.startArguments, | ||
| 207 | - builder: (context, value, child) { | ||
| 208 | - if (value == null) { | ||
| 209 | - return _buildPlaceholderOrError(context, child); | ||
| 210 | - } | 150 | + _maybeUpdateScanWindow(value, constraints); |
| 211 | 151 | ||
| 212 | - if (widget.scanWindow != null && scanWindow == null) { | ||
| 213 | - scanWindow = calculateScanWindowRelativeToTextureInPercentage( | ||
| 214 | - widget.fit, | ||
| 215 | - widget.scanWindow!, | ||
| 216 | - textureSize: value.size, | ||
| 217 | - widgetSize: constraints.biggest, | 152 | + final Widget? overlay = |
| 153 | + widget.overlayBuilder?.call(context, constraints); | ||
| 154 | + final Size cameraPreviewSize = value.size; | ||
| 155 | + | ||
| 156 | + final Widget scannerWidget = ClipRect( | ||
| 157 | + child: SizedBox.fromSize( | ||
| 158 | + size: constraints.biggest, | ||
| 159 | + child: FittedBox( | ||
| 160 | + fit: widget.fit, | ||
| 161 | + child: SizedBox( | ||
| 162 | + width: cameraPreviewSize.width, | ||
| 163 | + height: cameraPreviewSize.height, | ||
| 164 | + child: MobileScannerPlatform.instance.buildCameraView(), | ||
| 165 | + ), | ||
| 166 | + ), | ||
| 167 | + ), | ||
| 218 | ); | 168 | ); |
| 219 | 169 | ||
| 220 | - _controller.updateScanWindow(scanWindow); | 170 | + if (overlay == null) { |
| 171 | + return scannerWidget; | ||
| 221 | } | 172 | } |
| 222 | - if (widget.overlay != null) { | 173 | + |
| 223 | return Stack( | 174 | return Stack( |
| 224 | alignment: Alignment.center, | 175 | alignment: Alignment.center, |
| 225 | - children: [ | ||
| 226 | - _scanner( | ||
| 227 | - value.size, | ||
| 228 | - value.webId, | ||
| 229 | - value.textureId, | ||
| 230 | - value.numberOfCameras, | ||
| 231 | - ), | ||
| 232 | - widget.overlay!, | 176 | + children: <Widget>[ |
| 177 | + scannerWidget, | ||
| 178 | + overlay, | ||
| 233 | ], | 179 | ], |
| 234 | ); | 180 | ); |
| 235 | - } else { | ||
| 236 | - return _scanner( | ||
| 237 | - value.size, | ||
| 238 | - value.webId, | ||
| 239 | - value.textureId, | ||
| 240 | - value.numberOfCameras, | ||
| 241 | - ); | ||
| 242 | - } | ||
| 243 | - }, | ||
| 244 | - ); | ||
| 245 | }, | 181 | }, |
| 246 | ); | 182 | ); |
| 247 | - } | ||
| 248 | - | ||
| 249 | - Widget _scanner( | ||
| 250 | - Size size, | ||
| 251 | - String? webId, | ||
| 252 | - int? textureId, | ||
| 253 | - int? numberOfCameras, | ||
| 254 | - ) { | ||
| 255 | - return ClipRect( | ||
| 256 | - child: LayoutBuilder( | ||
| 257 | - builder: (_, constraints) { | ||
| 258 | - return SizedBox.fromSize( | ||
| 259 | - size: constraints.biggest, | ||
| 260 | - child: FittedBox( | ||
| 261 | - fit: widget.fit, | ||
| 262 | - child: SizedBox( | ||
| 263 | - width: size.width, | ||
| 264 | - height: size.height, | ||
| 265 | - child: kIsWeb | ||
| 266 | - ? HtmlElementView(viewType: webId!) | ||
| 267 | - : Texture(textureId: textureId!), | ||
| 268 | - ), | ||
| 269 | - ), | ||
| 270 | - ); | ||
| 271 | }, | 183 | }, |
| 272 | - ), | ||
| 273 | ); | 184 | ); |
| 274 | } | 185 | } |
| 275 | 186 | ||
| 276 | @override | 187 | @override |
| 277 | void dispose() { | 188 | void dispose() { |
| 278 | - _controller.updateScanWindow(null); | ||
| 279 | - WidgetsBinding.instance.removeObserver(this); | ||
| 280 | - _barcodesSubscription?.cancel(); | ||
| 281 | - _barcodesSubscription = null; | ||
| 282 | - _controller.dispose(); | ||
| 283 | super.dispose(); | 189 | super.dispose(); |
| 190 | + // When this widget is unmounted, reset the scan window. | ||
| 191 | + unawaited(widget.controller.updateScanWindow(null)); | ||
| 284 | } | 192 | } |
| 285 | } | 193 | } |
| 1 | import 'dart:async'; | 1 | import 'dart:async'; |
| 2 | -import 'dart:io'; | ||
| 3 | -// ignore: unnecessary_import | ||
| 4 | -import 'dart:typed_data'; | ||
| 5 | 2 | ||
| 6 | -import 'package:flutter/foundation.dart'; | ||
| 7 | -import 'package:flutter/services.dart'; | 3 | +import 'package:flutter/widgets.dart'; |
| 8 | import 'package:mobile_scanner/src/enums/barcode_format.dart'; | 4 | import 'package:mobile_scanner/src/enums/barcode_format.dart'; |
| 9 | import 'package:mobile_scanner/src/enums/camera_facing.dart'; | 5 | import 'package:mobile_scanner/src/enums/camera_facing.dart'; |
| 10 | import 'package:mobile_scanner/src/enums/detection_speed.dart'; | 6 | import 'package:mobile_scanner/src/enums/detection_speed.dart'; |
| 11 | import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; | 7 | import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; |
| 12 | -import 'package:mobile_scanner/src/enums/mobile_scanner_state.dart'; | ||
| 13 | import 'package:mobile_scanner/src/enums/torch_state.dart'; | 8 | import 'package:mobile_scanner/src/enums/torch_state.dart'; |
| 14 | import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; | 9 | import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; |
| 15 | -import 'package:mobile_scanner/src/objects/barcode.dart'; | 10 | +import 'package:mobile_scanner/src/mobile_scanner_platform_interface.dart'; |
| 11 | +import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart'; | ||
| 16 | import 'package:mobile_scanner/src/objects/barcode_capture.dart'; | 12 | import 'package:mobile_scanner/src/objects/barcode_capture.dart'; |
| 17 | -import 'package:mobile_scanner/src/objects/mobile_scanner_arguments.dart'; | 13 | +import 'package:mobile_scanner/src/objects/mobile_scanner_state.dart'; |
| 14 | +import 'package:mobile_scanner/src/objects/start_options.dart'; | ||
| 18 | 15 | ||
| 19 | -/// The [MobileScannerController] holds all the logic of this plugin, | ||
| 20 | -/// where as the [MobileScanner] class is the frontend of this plugin. | ||
| 21 | -class MobileScannerController { | 16 | +/// The controller for the [MobileScanner] widget. |
| 17 | +class MobileScannerController extends ValueNotifier<MobileScannerState> { | ||
| 18 | + /// Construct a new [MobileScannerController] instance. | ||
| 22 | MobileScannerController({ | 19 | MobileScannerController({ |
| 23 | - this.facing = CameraFacing.back, | 20 | + this.cameraResolution, |
| 24 | this.detectionSpeed = DetectionSpeed.normal, | 21 | this.detectionSpeed = DetectionSpeed.normal, |
| 25 | - this.detectionTimeoutMs = 250, | ||
| 26 | - this.torchEnabled = false, | ||
| 27 | - this.formats, | 22 | + int detectionTimeoutMs = 250, |
| 23 | + this.facing = CameraFacing.back, | ||
| 24 | + this.formats = const <BarcodeFormat>[], | ||
| 28 | this.returnImage = false, | 25 | this.returnImage = false, |
| 29 | - @Deprecated( | ||
| 30 | - 'Instead, use the result of calling `start()` to determine if permissions were granted.', | ||
| 31 | - ) | ||
| 32 | - this.onPermissionSet, | ||
| 33 | - this.autoStart = true, | ||
| 34 | - this.cameraResolution, | 26 | + this.torchEnabled = false, |
| 35 | this.useNewCameraSelector = false, | 27 | this.useNewCameraSelector = false, |
| 36 | - }); | 28 | + }) : detectionTimeoutMs = |
| 29 | + detectionSpeed == DetectionSpeed.normal ? detectionTimeoutMs : 0, | ||
| 30 | + assert( | ||
| 31 | + detectionTimeoutMs >= 0, | ||
| 32 | + 'The detection timeout must be greater than or equal to 0.', | ||
| 33 | + ), | ||
| 34 | + super(MobileScannerState.uninitialized(facing)); | ||
| 37 | 35 | ||
| 38 | - /// Select which camera should be used. | 36 | + /// The desired resolution for the camera. |
| 39 | /// | 37 | /// |
| 40 | - /// Default: CameraFacing.back | ||
| 41 | - final CameraFacing facing; | ||
| 42 | - | ||
| 43 | - /// Enable or disable the torch (Flash) on start | 38 | + /// When this value is provided, the camera will try to match this resolution, |
| 39 | + /// or fallback to the closest available resolution. | ||
| 40 | + /// When this is null, Android defaults to a resolution of 640x480. | ||
| 44 | /// | 41 | /// |
| 45 | - /// Default: disabled | ||
| 46 | - final bool torchEnabled; | ||
| 47 | - | ||
| 48 | - /// Set to true if you want to return the image buffer with the Barcode event | 42 | + /// Bear in mind that changing the resolution has an effect on the aspect ratio. |
| 49 | /// | 43 | /// |
| 50 | - /// Only supported on iOS and Android | ||
| 51 | - final bool returnImage; | ||
| 52 | - | ||
| 53 | - /// If provided, the scanner will only detect those specific formats | ||
| 54 | - final List<BarcodeFormat>? formats; | 44 | + /// When the camera orientation changes, |
| 45 | + /// the resolution will be flipped to match the new dimensions of the display. | ||
| 46 | + /// | ||
| 47 | + /// Currently only supported on Android. | ||
| 48 | + final Size? cameraResolution; | ||
| 55 | 49 | ||
| 56 | - /// Sets the speed of detections. | 50 | + /// The detection speed for the scanner. |
| 57 | /// | 51 | /// |
| 58 | - /// WARNING: DetectionSpeed.unrestricted can cause memory issues on some devices | 52 | + /// Defaults to [DetectionSpeed.normal]. |
| 59 | final DetectionSpeed detectionSpeed; | 53 | final DetectionSpeed detectionSpeed; |
| 60 | 54 | ||
| 61 | - /// Sets the timeout, in milliseconds, of the scanner. | 55 | + /// The detection timeout, in milliseconds, for the scanner. |
| 62 | /// | 56 | /// |
| 63 | /// This timeout is ignored if the [detectionSpeed] | 57 | /// This timeout is ignored if the [detectionSpeed] |
| 64 | /// is not set to [DetectionSpeed.normal]. | 58 | /// is not set to [DetectionSpeed.normal]. |
| @@ -67,439 +61,342 @@ class MobileScannerController { | @@ -67,439 +61,342 @@ class MobileScannerController { | ||
| 67 | /// which prevents memory issues on older devices. | 61 | /// which prevents memory issues on older devices. |
| 68 | final int detectionTimeoutMs; | 62 | final int detectionTimeoutMs; |
| 69 | 63 | ||
| 70 | - /// Automatically start the mobileScanner on initialization. | ||
| 71 | - final bool autoStart; | 64 | + /// The facing direction for the camera. |
| 65 | + /// | ||
| 66 | + /// Defaults to the back-facing camera. | ||
| 67 | + final CameraFacing facing; | ||
| 72 | 68 | ||
| 73 | - /// The desired resolution for the camera. | 69 | + /// The formats that the scanner should detect. |
| 74 | /// | 70 | /// |
| 75 | - /// When this value is provided, the camera will try to match this resolution, | ||
| 76 | - /// or fallback to the closest available resolution. | ||
| 77 | - /// When this is null, Android defaults to a resolution of 640x480. | 71 | + /// If this is empty, all supported formats are detected. |
| 72 | + final List<BarcodeFormat> formats; | ||
| 73 | + | ||
| 74 | + /// Whether scanned barcodes should contain the image | ||
| 75 | + /// that is embedded into the barcode. | ||
| 78 | /// | 76 | /// |
| 79 | - /// Bear in mind that changing the resolution has an effect on the aspect ratio. | 77 | + /// If this is false, [BarcodeCapture.image] will always be null. |
| 80 | /// | 78 | /// |
| 81 | - /// When the camera orientation changes, | ||
| 82 | - /// the resolution will be flipped to match the new dimensions of the display. | 79 | + /// Defaults to false, and is only supported on iOS and Android. |
| 80 | + final bool returnImage; | ||
| 81 | + | ||
| 82 | + /// Whether the flashlight should be turned on when the camera is started. | ||
| 83 | /// | 83 | /// |
| 84 | - /// Currently only supported on Android. | ||
| 85 | - final Size? cameraResolution; | 84 | + /// Defaults to false. |
| 85 | + final bool torchEnabled; | ||
| 86 | 86 | ||
| 87 | - /// Use the new resolution selector. Warning: not fully tested, may produce | ||
| 88 | - /// unwanted/zoomed images. | 87 | + /// Use the new resolution selector. |
| 88 | + /// | ||
| 89 | + /// This feature is experimental and not fully tested yet. | ||
| 90 | + /// Use caution when using this flag, | ||
| 91 | + /// as the new resolution selector may produce unwanted or zoomed images. | ||
| 89 | /// | 92 | /// |
| 90 | - /// Only supported on Android | 93 | + /// Only supported on Android. |
| 91 | final bool useNewCameraSelector; | 94 | final bool useNewCameraSelector; |
| 92 | 95 | ||
| 93 | - /// Sets the barcode stream | 96 | + /// The internal barcode controller, that listens for detected barcodes. |
| 94 | final StreamController<BarcodeCapture> _barcodesController = | 97 | final StreamController<BarcodeCapture> _barcodesController = |
| 95 | StreamController.broadcast(); | 98 | StreamController.broadcast(); |
| 96 | 99 | ||
| 100 | + /// Get the stream of scanned barcodes. | ||
| 97 | Stream<BarcodeCapture> get barcodes => _barcodesController.stream; | 101 | Stream<BarcodeCapture> get barcodes => _barcodesController.stream; |
| 98 | 102 | ||
| 99 | - static const MethodChannel _methodChannel = | ||
| 100 | - MethodChannel('dev.steenbakker.mobile_scanner/scanner/method'); | ||
| 101 | - static const EventChannel _eventChannel = | ||
| 102 | - EventChannel('dev.steenbakker.mobile_scanner/scanner/event'); | 103 | + StreamSubscription<BarcodeCapture?>? _barcodesSubscription; |
| 104 | + StreamSubscription<TorchState>? _torchStateSubscription; | ||
| 105 | + StreamSubscription<double>? _zoomScaleSubscription; | ||
| 103 | 106 | ||
| 104 | - @Deprecated( | ||
| 105 | - 'Instead, use the result of calling `start()` to determine if permissions were granted.', | ||
| 106 | - ) | ||
| 107 | - Function(bool permissionGranted)? onPermissionSet; | 107 | + bool _isDisposed = false; |
| 108 | 108 | ||
| 109 | - /// Listen to events from the platform specific code | ||
| 110 | - StreamSubscription? events; | 109 | + void _disposeListeners() { |
| 110 | + _barcodesSubscription?.cancel(); | ||
| 111 | + _torchStateSubscription?.cancel(); | ||
| 112 | + _zoomScaleSubscription?.cancel(); | ||
| 111 | 113 | ||
| 112 | - /// A notifier that provides several arguments about the MobileScanner | ||
| 113 | - final ValueNotifier<MobileScannerArguments?> startArguments = | ||
| 114 | - ValueNotifier(null); | ||
| 115 | - | ||
| 116 | - /// A notifier that provides the state of the Torch (Flash) | ||
| 117 | - final ValueNotifier<TorchState> torchState = ValueNotifier(TorchState.off); | 114 | + _barcodesSubscription = null; |
| 115 | + _torchStateSubscription = null; | ||
| 116 | + _zoomScaleSubscription = null; | ||
| 117 | + } | ||
| 118 | 118 | ||
| 119 | - /// A notifier that provides the state of which camera is being used | ||
| 120 | - late final ValueNotifier<CameraFacing> cameraFacingState = | ||
| 121 | - ValueNotifier(facing); | 119 | + void _setupListeners() { |
| 120 | + _barcodesSubscription = MobileScannerPlatform.instance.barcodesStream | ||
| 121 | + .listen((BarcodeCapture? barcode) { | ||
| 122 | + if (_barcodesController.isClosed || barcode == null) { | ||
| 123 | + return; | ||
| 124 | + } | ||
| 122 | 125 | ||
| 123 | - /// A notifier that provides zoomScale. | ||
| 124 | - final ValueNotifier<double> zoomScaleState = ValueNotifier(0.0); | 126 | + _barcodesController.add(barcode); |
| 127 | + }); | ||
| 125 | 128 | ||
| 126 | - bool isStarting = false; | 129 | + _torchStateSubscription = MobileScannerPlatform.instance.torchStateStream |
| 130 | + .listen((TorchState torchState) { | ||
| 131 | + if (_isDisposed) { | ||
| 132 | + return; | ||
| 133 | + } | ||
| 127 | 134 | ||
| 128 | - /// A notifier that provides availability of the Torch (Flash) | ||
| 129 | - final ValueNotifier<bool?> hasTorchState = ValueNotifier(false); | 135 | + value = value.copyWith(torchState: torchState); |
| 136 | + }); | ||
| 130 | 137 | ||
| 131 | - /// Returns whether the device has a torch. | ||
| 132 | - /// | ||
| 133 | - /// Throws an error if the controller is not initialized. | ||
| 134 | - bool get hasTorch { | ||
| 135 | - final hasTorch = hasTorchState.value; | ||
| 136 | - if (hasTorch == null) { | ||
| 137 | - throw const MobileScannerException( | ||
| 138 | - errorCode: MobileScannerErrorCode.controllerUninitialized, | ||
| 139 | - ); | 138 | + _zoomScaleSubscription = MobileScannerPlatform.instance.zoomScaleStateStream |
| 139 | + .listen((double zoomScale) { | ||
| 140 | + if (_isDisposed) { | ||
| 141 | + return; | ||
| 140 | } | 142 | } |
| 141 | 143 | ||
| 142 | - return hasTorch; | 144 | + value = value.copyWith(zoomScale: zoomScale); |
| 145 | + }); | ||
| 143 | } | 146 | } |
| 144 | 147 | ||
| 145 | - /// Set the starting arguments for the camera | ||
| 146 | - Map<String, dynamic> _argumentsToMap({CameraFacing? cameraFacingOverride}) { | ||
| 147 | - final Map<String, dynamic> arguments = {}; | ||
| 148 | - | ||
| 149 | - cameraFacingState.value = cameraFacingOverride ?? facing; | ||
| 150 | - arguments['facing'] = cameraFacingState.value.rawValue; | ||
| 151 | - arguments['torch'] = torchEnabled; | ||
| 152 | - arguments['speed'] = detectionSpeed.rawValue; | ||
| 153 | - arguments['timeout'] = detectionTimeoutMs; | ||
| 154 | - arguments['returnImage'] = returnImage; | ||
| 155 | - arguments['useNewCameraSelector'] = useNewCameraSelector; | ||
| 156 | - | ||
| 157 | - /* if (scanWindow != null) { | ||
| 158 | - arguments['scanWindow'] = [ | ||
| 159 | - scanWindow!.left, | ||
| 160 | - scanWindow!.top, | ||
| 161 | - scanWindow!.right, | ||
| 162 | - scanWindow!.bottom, | ||
| 163 | - ]; | ||
| 164 | - } */ | ||
| 165 | - | ||
| 166 | - if (formats != null) { | ||
| 167 | - if (kIsWeb || Platform.isIOS || Platform.isMacOS || Platform.isAndroid) { | ||
| 168 | - arguments['formats'] = formats!.map((e) => e.rawValue).toList(); | ||
| 169 | - } | 148 | + void _throwIfNotInitialized() { |
| 149 | + if (!value.isInitialized) { | ||
| 150 | + throw const MobileScannerException( | ||
| 151 | + errorCode: MobileScannerErrorCode.controllerUninitialized, | ||
| 152 | + errorDetails: MobileScannerErrorDetails( | ||
| 153 | + message: 'The MobileScannerController has not been initialized.', | ||
| 154 | + ), | ||
| 155 | + ); | ||
| 170 | } | 156 | } |
| 171 | 157 | ||
| 172 | - if (cameraResolution != null) { | ||
| 173 | - arguments['cameraResolution'] = <int>[ | ||
| 174 | - cameraResolution!.width.toInt(), | ||
| 175 | - cameraResolution!.height.toInt(), | ||
| 176 | - ]; | 158 | + if (_isDisposed) { |
| 159 | + throw const MobileScannerException( | ||
| 160 | + errorCode: MobileScannerErrorCode.controllerDisposed, | ||
| 161 | + errorDetails: MobileScannerErrorDetails( | ||
| 162 | + message: | ||
| 163 | + 'The MobileScannerController was used after it has been disposed.', | ||
| 164 | + ), | ||
| 165 | + ); | ||
| 177 | } | 166 | } |
| 178 | - | ||
| 179 | - return arguments; | ||
| 180 | } | 167 | } |
| 181 | 168 | ||
| 182 | - /// Start scanning for barcodes. | ||
| 183 | - /// Upon calling this method, the necessary camera permission will be requested. | 169 | + /// Analyze an image file. |
| 170 | + /// | ||
| 171 | + /// The [path] points to a file on the device. | ||
| 184 | /// | 172 | /// |
| 185 | - /// Returns an instance of [MobileScannerArguments] | ||
| 186 | - /// when the scanner was successfully started. | ||
| 187 | - /// Returns null if the scanner is currently starting. | 173 | + /// This is only supported on Android and iOS. |
| 188 | /// | 174 | /// |
| 189 | - /// Throws a [MobileScannerException] if starting the scanner failed. | ||
| 190 | - Future<MobileScannerArguments?> start({ | ||
| 191 | - CameraFacing? cameraFacingOverride, | ||
| 192 | - }) async { | ||
| 193 | - if (isStarting) { | ||
| 194 | - debugPrint("Called start() while starting."); | ||
| 195 | - return null; | 175 | + /// Returns the [BarcodeCapture] that was found in the image. |
| 176 | + Future<BarcodeCapture?> analyzeImage(String path) { | ||
| 177 | + return MobileScannerPlatform.instance.analyzeImage(path); | ||
| 196 | } | 178 | } |
| 197 | 179 | ||
| 198 | - events ??= _eventChannel | ||
| 199 | - .receiveBroadcastStream() | ||
| 200 | - .listen((data) => _handleEvent(data as Map)); | 180 | + /// Build a camera preview widget. |
| 181 | + Widget buildCameraView() { | ||
| 182 | + _throwIfNotInitialized(); | ||
| 201 | 183 | ||
| 202 | - isStarting = true; | 184 | + return MobileScannerPlatform.instance.buildCameraView(); |
| 185 | + } | ||
| 203 | 186 | ||
| 204 | - // Check authorization status | ||
| 205 | - if (!kIsWeb) { | ||
| 206 | - final MobileScannerState state; | 187 | + /// Reset the zoom scale of the camera. |
| 188 | + /// | ||
| 189 | + /// Does nothing if the camera is not running. | ||
| 190 | + Future<void> resetZoomScale() async { | ||
| 191 | + _throwIfNotInitialized(); | ||
| 207 | 192 | ||
| 208 | - try { | ||
| 209 | - state = MobileScannerState.fromRawValue( | ||
| 210 | - await _methodChannel.invokeMethod('state') as int? ?? 0, | ||
| 211 | - ); | ||
| 212 | - } on PlatformException catch (error) { | ||
| 213 | - isStarting = false; | 193 | + if (!value.isRunning) { |
| 194 | + return; | ||
| 195 | + } | ||
| 214 | 196 | ||
| 215 | - throw MobileScannerException( | ||
| 216 | - errorCode: MobileScannerErrorCode.genericError, | ||
| 217 | - errorDetails: MobileScannerErrorDetails( | ||
| 218 | - code: error.code, | ||
| 219 | - details: error.details as Object?, | ||
| 220 | - message: error.message, | ||
| 221 | - ), | ||
| 222 | - ); | 197 | + // When the platform has updated the zoom scale, |
| 198 | + // it will send an update through the zoom scale state event stream. | ||
| 199 | + await MobileScannerPlatform.instance.resetZoomScale(); | ||
| 223 | } | 200 | } |
| 224 | 201 | ||
| 225 | - switch (state) { | ||
| 226 | - // Android does not have an undetermined permission state. | ||
| 227 | - // So if the permission state is denied, just request it now. | ||
| 228 | - case MobileScannerState.undetermined: | ||
| 229 | - case MobileScannerState.denied: | ||
| 230 | - try { | ||
| 231 | - final bool granted = | ||
| 232 | - await _methodChannel.invokeMethod('request') as bool? ?? false; | 202 | + /// Set the zoom scale of the camera. |
| 203 | + /// | ||
| 204 | + /// The [zoomScale] must be between 0.0 and 1.0 (both inclusive). | ||
| 205 | + /// | ||
| 206 | + /// If the [zoomScale] is out of range, | ||
| 207 | + /// it is adjusted to fit within the allowed range. | ||
| 208 | + /// | ||
| 209 | + /// Does nothing if the camera is not running. | ||
| 210 | + Future<void> setZoomScale(double zoomScale) async { | ||
| 211 | + _throwIfNotInitialized(); | ||
| 233 | 212 | ||
| 234 | - if (!granted) { | ||
| 235 | - isStarting = false; | ||
| 236 | - throw const MobileScannerException( | ||
| 237 | - errorCode: MobileScannerErrorCode.permissionDenied, | ||
| 238 | - ); | ||
| 239 | - } | ||
| 240 | - } on PlatformException catch (error) { | ||
| 241 | - isStarting = false; | ||
| 242 | - throw MobileScannerException( | ||
| 243 | - errorCode: MobileScannerErrorCode.genericError, | ||
| 244 | - errorDetails: MobileScannerErrorDetails( | ||
| 245 | - code: error.code, | ||
| 246 | - details: error.details as Object?, | ||
| 247 | - message: error.message, | ||
| 248 | - ), | ||
| 249 | - ); | 213 | + if (!value.isRunning) { |
| 214 | + return; | ||
| 250 | } | 215 | } |
| 251 | 216 | ||
| 252 | - case MobileScannerState.authorized: | ||
| 253 | - break; | ||
| 254 | - } | ||
| 255 | - } | 217 | + final double clampedZoomScale = zoomScale.clamp(0.0, 1.0); |
| 256 | 218 | ||
| 257 | - // Start the camera with arguments | ||
| 258 | - Map<String, dynamic>? startResult = {}; | ||
| 259 | - try { | ||
| 260 | - startResult = await _methodChannel.invokeMapMethod<String, dynamic>( | ||
| 261 | - 'start', | ||
| 262 | - _argumentsToMap(cameraFacingOverride: cameraFacingOverride), | ||
| 263 | - ); | ||
| 264 | - } on PlatformException catch (error) { | ||
| 265 | - MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError; | ||
| 266 | - | ||
| 267 | - final String? errorMessage = error.message; | ||
| 268 | - | ||
| 269 | - if (kIsWeb) { | ||
| 270 | - if (errorMessage == null) { | ||
| 271 | - errorCode = MobileScannerErrorCode.genericError; | ||
| 272 | - } else if (errorMessage.contains('NotFoundError') || | ||
| 273 | - errorMessage.contains('NotSupportedError')) { | ||
| 274 | - errorCode = MobileScannerErrorCode.unsupported; | ||
| 275 | - } else if (errorMessage.contains('NotAllowedError')) { | ||
| 276 | - errorCode = MobileScannerErrorCode.permissionDenied; | ||
| 277 | - } else { | ||
| 278 | - errorCode = MobileScannerErrorCode.genericError; | 219 | + // Update the zoom scale state to the new state. |
| 220 | + // When the platform has updated the zoom scale, | ||
| 221 | + // it will send an update through the zoom scale state event stream. | ||
| 222 | + await MobileScannerPlatform.instance.setZoomScale(clampedZoomScale); | ||
| 279 | } | 223 | } |
| 280 | - } | ||
| 281 | - | ||
| 282 | - isStarting = false; | ||
| 283 | 224 | ||
| 284 | - throw MobileScannerException( | ||
| 285 | - errorCode: errorCode, | 225 | + /// Start scanning for barcodes. |
| 226 | + /// Upon calling this method, the necessary camera permission will be requested. | ||
| 227 | + /// | ||
| 228 | + /// The [cameraDirection] can be used to specify the camera direction. | ||
| 229 | + /// If this is null, this defaults to the [facing] value. | ||
| 230 | + /// | ||
| 231 | + /// Does nothing if the camera is already running. | ||
| 232 | + Future<void> start({CameraFacing? cameraDirection}) async { | ||
| 233 | + if (_isDisposed) { | ||
| 234 | + throw const MobileScannerException( | ||
| 235 | + errorCode: MobileScannerErrorCode.controllerDisposed, | ||
| 286 | errorDetails: MobileScannerErrorDetails( | 236 | errorDetails: MobileScannerErrorDetails( |
| 287 | - code: error.code, | ||
| 288 | - details: error.details as Object?, | ||
| 289 | - message: error.message, | 237 | + message: |
| 238 | + 'The MobileScannerController was used after it has been disposed.', | ||
| 290 | ), | 239 | ), |
| 291 | ); | 240 | ); |
| 292 | } | 241 | } |
| 293 | 242 | ||
| 294 | - if (startResult == null) { | ||
| 295 | - isStarting = false; | ||
| 296 | - throw const MobileScannerException( | ||
| 297 | - errorCode: MobileScannerErrorCode.genericError, | ||
| 298 | - ); | 243 | + // Do nothing if the camera is already running. |
| 244 | + if (value.isRunning) { | ||
| 245 | + return; | ||
| 299 | } | 246 | } |
| 300 | 247 | ||
| 301 | - final hasTorch = startResult['torchable'] as bool? ?? false; | ||
| 302 | - hasTorchState.value = hasTorch; | 248 | + final CameraFacing effectiveDirection = cameraDirection ?? facing; |
| 303 | 249 | ||
| 304 | - final Size size; | ||
| 305 | - | ||
| 306 | - if (kIsWeb) { | ||
| 307 | - size = Size( | ||
| 308 | - startResult['videoWidth'] as double? ?? 0, | ||
| 309 | - startResult['videoHeight'] as double? ?? 0, | 250 | + final StartOptions options = StartOptions( |
| 251 | + cameraDirection: effectiveDirection, | ||
| 252 | + cameraResolution: cameraResolution, | ||
| 253 | + detectionSpeed: detectionSpeed, | ||
| 254 | + detectionTimeoutMs: detectionTimeoutMs, | ||
| 255 | + formats: formats, | ||
| 256 | + returnImage: returnImage, | ||
| 257 | + torchEnabled: torchEnabled, | ||
| 310 | ); | 258 | ); |
| 311 | - } else { | ||
| 312 | - final Map<Object?, Object?>? sizeInfo = | ||
| 313 | - startResult['size'] as Map<Object?, Object?>?; | ||
| 314 | 259 | ||
| 315 | - size = Size( | ||
| 316 | - sizeInfo?['width'] as double? ?? 0, | ||
| 317 | - sizeInfo?['height'] as double? ?? 0, | 260 | + try { |
| 261 | + _setupListeners(); | ||
| 262 | + | ||
| 263 | + final MobileScannerViewAttributes viewAttributes = | ||
| 264 | + await MobileScannerPlatform.instance.start( | ||
| 265 | + options, | ||
| 318 | ); | 266 | ); |
| 319 | - } | ||
| 320 | 267 | ||
| 321 | - isStarting = false; | ||
| 322 | - return startArguments.value = MobileScannerArguments( | ||
| 323 | - numberOfCameras: startResult['numberOfCameras'] as int?, | ||
| 324 | - size: size, | ||
| 325 | - hasTorch: hasTorch, | ||
| 326 | - textureId: kIsWeb ? null : startResult['textureId'] as int?, | ||
| 327 | - webId: kIsWeb ? startResult['ViewID'] as String? : null, | 268 | + value = value.copyWith( |
| 269 | + availableCameras: viewAttributes.numberOfCameras, | ||
| 270 | + cameraDirection: effectiveDirection, | ||
| 271 | + isInitialized: true, | ||
| 272 | + isRunning: true, | ||
| 273 | + size: viewAttributes.size, | ||
| 274 | + // If the device has a flashlight, let the platform update the torch state. | ||
| 275 | + // If it does not have one, provide the unavailable state directly. | ||
| 276 | + torchState: viewAttributes.hasTorch ? null : TorchState.unavailable, | ||
| 277 | + ); | ||
| 278 | + } on MobileScannerException catch (error) { | ||
| 279 | + // The initialization finished with an error. | ||
| 280 | + // To avoid stale values, reset the output size, | ||
| 281 | + // torch state and zoom scale to the defaults. | ||
| 282 | + if (!_isDisposed) { | ||
| 283 | + value = value.copyWith( | ||
| 284 | + cameraDirection: facing, | ||
| 285 | + isInitialized: true, | ||
| 286 | + isRunning: false, | ||
| 287 | + error: error, | ||
| 288 | + size: Size.zero, | ||
| 289 | + torchState: TorchState.unavailable, | ||
| 290 | + zoomScale: 1.0, | ||
| 328 | ); | 291 | ); |
| 329 | } | 292 | } |
| 293 | + } on PermissionRequestPendingException catch (_) { | ||
| 294 | + // If a permission request was already pending, do nothing. | ||
| 295 | + } | ||
| 296 | + } | ||
| 330 | 297 | ||
| 331 | - /// Stops the camera, but does not dispose this controller. | 298 | + /// Stop the camera. |
| 299 | + /// | ||
| 300 | + /// After calling this method, the camera can be restarted using [start]. | ||
| 301 | + /// | ||
| 302 | + /// Does nothing if the camera is already stopped. | ||
| 332 | Future<void> stop() async { | 303 | Future<void> stop() async { |
| 333 | - await _methodChannel.invokeMethod('stop'); | 304 | + // Do nothing if not initialized or already stopped. |
| 305 | + // On the web, the permission popup triggers a lifecycle change from resumed to inactive, | ||
| 306 | + // due to the permission popup gaining focus. | ||
| 307 | + // This would 'stop' the camera while it is not ready yet. | ||
| 308 | + if (!value.isInitialized || !value.isRunning || _isDisposed) { | ||
| 309 | + return; | ||
| 310 | + } | ||
| 311 | + | ||
| 312 | + _disposeListeners(); | ||
| 334 | 313 | ||
| 335 | // After the camera stopped, set the torch state to off, | 314 | // After the camera stopped, set the torch state to off, |
| 336 | // as the torch state callback is never called when the camera is stopped. | 315 | // as the torch state callback is never called when the camera is stopped. |
| 337 | - torchState.value = TorchState.off; | 316 | + value = value.copyWith( |
| 317 | + isRunning: false, | ||
| 318 | + torchState: TorchState.off, | ||
| 319 | + ); | ||
| 320 | + | ||
| 321 | + await MobileScannerPlatform.instance.stop(); | ||
| 338 | } | 322 | } |
| 339 | 323 | ||
| 340 | - /// Switches the torch on or off. | ||
| 341 | - /// | ||
| 342 | - /// Does nothing if the device has no torch. | 324 | + /// Switch between the front and back camera. |
| 343 | /// | 325 | /// |
| 344 | - /// Throws if the controller was not initialized. | ||
| 345 | - Future<void> toggleTorch() async { | ||
| 346 | - final hasTorch = hasTorchState.value; | 326 | + /// Does nothing if the device has less than 2 cameras. |
| 327 | + Future<void> switchCamera() async { | ||
| 328 | + _throwIfNotInitialized(); | ||
| 347 | 329 | ||
| 348 | - if (hasTorch == null) { | ||
| 349 | - throw const MobileScannerException( | ||
| 350 | - errorCode: MobileScannerErrorCode.controllerUninitialized, | ||
| 351 | - ); | ||
| 352 | - } | 330 | + final int? availableCameras = value.availableCameras; |
| 353 | 331 | ||
| 354 | - if (!hasTorch) { | 332 | + // Do nothing if the amount of cameras is less than 2 cameras. |
| 333 | + // If the the current platform does not provide the amount of cameras, | ||
| 334 | + // continue anyway. | ||
| 335 | + if (availableCameras != null && availableCameras < 2) { | ||
| 355 | return; | 336 | return; |
| 356 | } | 337 | } |
| 357 | 338 | ||
| 358 | - final TorchState newState = | ||
| 359 | - torchState.value == TorchState.off ? TorchState.on : TorchState.off; | 339 | + await stop(); |
| 360 | 340 | ||
| 361 | - await _methodChannel.invokeMethod('torch', newState.rawValue); | ||
| 362 | - } | 341 | + final CameraFacing cameraDirection = value.cameraDirection; |
| 363 | 342 | ||
| 364 | - /// Changes the state of the camera (front or back). | ||
| 365 | - /// | ||
| 366 | - /// Does nothing if the device has no front camera. | ||
| 367 | - Future<void> switchCamera() async { | ||
| 368 | - await _methodChannel.invokeMethod('stop'); | ||
| 369 | - final CameraFacing facingToUse = | ||
| 370 | - cameraFacingState.value == CameraFacing.back | ||
| 371 | - ? CameraFacing.front | ||
| 372 | - : CameraFacing.back; | ||
| 373 | - await start(cameraFacingOverride: facingToUse); | 343 | + await start( |
| 344 | + cameraDirection: cameraDirection == CameraFacing.front | ||
| 345 | + ? CameraFacing.back | ||
| 346 | + : CameraFacing.front, | ||
| 347 | + ); | ||
| 374 | } | 348 | } |
| 375 | 349 | ||
| 376 | - /// Handles a local image file. | ||
| 377 | - /// Returns true if a barcode or QR code is found. | ||
| 378 | - /// Returns false if nothing is found. | 350 | + /// Switches the flashlight on or off. |
| 379 | /// | 351 | /// |
| 380 | - /// [path] The path of the image on the devices | ||
| 381 | - Future<bool> analyzeImage(String path) async { | ||
| 382 | - events ??= _eventChannel | ||
| 383 | - .receiveBroadcastStream() | ||
| 384 | - .listen((data) => _handleEvent(data as Map)); | ||
| 385 | - | ||
| 386 | - return _methodChannel | ||
| 387 | - .invokeMethod<bool>('analyzeImage', path) | ||
| 388 | - .then<bool>((bool? value) => value ?? false); | ||
| 389 | - } | 352 | + /// Does nothing if the device has no torch, |
| 353 | + /// or if the camera is not running. | ||
| 354 | + Future<void> toggleTorch() async { | ||
| 355 | + _throwIfNotInitialized(); | ||
| 390 | 356 | ||
| 391 | - /// Set the zoomScale of the camera. | ||
| 392 | - /// | ||
| 393 | - /// [zoomScale] must be within 0.0 and 1.0, where 1.0 is the max zoom, and 0.0 | ||
| 394 | - /// is zoomed out. | ||
| 395 | - Future<void> setZoomScale(double zoomScale) async { | ||
| 396 | - if (zoomScale < 0 || zoomScale > 1) { | ||
| 397 | - throw const MobileScannerException( | ||
| 398 | - errorCode: MobileScannerErrorCode.genericError, | ||
| 399 | - errorDetails: MobileScannerErrorDetails( | ||
| 400 | - message: 'The zoomScale must be between 0 and 1.', | ||
| 401 | - ), | ||
| 402 | - ); | 357 | + if (!value.isRunning) { |
| 358 | + return; | ||
| 403 | } | 359 | } |
| 404 | - await _methodChannel.invokeMethod('setScale', zoomScale); | 360 | + |
| 361 | + final TorchState torchState = value.torchState; | ||
| 362 | + | ||
| 363 | + if (torchState == TorchState.unavailable) { | ||
| 364 | + return; | ||
| 405 | } | 365 | } |
| 406 | 366 | ||
| 407 | - /// Reset the zoomScale of the camera to use standard scale 1x. | ||
| 408 | - Future<void> resetZoomScale() async { | ||
| 409 | - await _methodChannel.invokeMethod('resetScale'); | 367 | + final TorchState newState = |
| 368 | + torchState == TorchState.off ? TorchState.on : TorchState.off; | ||
| 369 | + | ||
| 370 | + // Update the torch state to the new state. | ||
| 371 | + // When the platform has updated the torch state, | ||
| 372 | + // it will send an update through the torch state event stream. | ||
| 373 | + await MobileScannerPlatform.instance.setTorchState(newState); | ||
| 410 | } | 374 | } |
| 411 | 375 | ||
| 412 | - /// Disposes the MobileScannerController and closes all listeners. | 376 | + /// Update the scan window with the given [window] rectangle. |
| 413 | /// | 377 | /// |
| 414 | - /// If you call this, you cannot use this controller object anymore. | ||
| 415 | - void dispose() { | ||
| 416 | - stop(); | ||
| 417 | - events?.cancel(); | ||
| 418 | - _barcodesController.close(); | 378 | + /// If [window] is null, the scan window will be reset to the full camera preview. |
| 379 | + Future<void> updateScanWindow(Rect? window) async { | ||
| 380 | + if (_isDisposed || !value.isInitialized) { | ||
| 381 | + return; | ||
| 419 | } | 382 | } |
| 420 | 383 | ||
| 421 | - /// Handles a returning event from the platform side | ||
| 422 | - void _handleEvent(Map event) { | ||
| 423 | - final name = event['name']; | ||
| 424 | - final data = event['data']; | ||
| 425 | - | ||
| 426 | - switch (name) { | ||
| 427 | - case 'torchState': | ||
| 428 | - final state = TorchState.values[data as int? ?? 0]; | ||
| 429 | - torchState.value = state; | ||
| 430 | - case 'zoomScaleState': | ||
| 431 | - zoomScaleState.value = data as double? ?? 0.0; | ||
| 432 | - case 'barcode': | ||
| 433 | - if (data == null) return; | ||
| 434 | - final parsed = (data as List) | ||
| 435 | - .map((value) => Barcode.fromNative(value as Map)) | ||
| 436 | - .toList(); | ||
| 437 | - _barcodesController.add( | ||
| 438 | - BarcodeCapture( | ||
| 439 | - raw: data, | ||
| 440 | - barcodes: parsed, | ||
| 441 | - image: event['image'] as Uint8List?, | ||
| 442 | - width: event['width'] as double?, | ||
| 443 | - height: event['height'] as double?, | ||
| 444 | - ), | ||
| 445 | - ); | ||
| 446 | - case 'barcodeMac': | ||
| 447 | - _barcodesController.add( | ||
| 448 | - BarcodeCapture( | ||
| 449 | - raw: data, | ||
| 450 | - barcodes: [ | ||
| 451 | - Barcode( | ||
| 452 | - rawValue: (data as Map)['payload'] as String?, | ||
| 453 | - format: BarcodeFormat.fromRawValue( | ||
| 454 | - data['symbology'] as int? ?? -1, | ||
| 455 | - ), | ||
| 456 | - ), | ||
| 457 | - ], | ||
| 458 | - ), | ||
| 459 | - ); | ||
| 460 | - case 'barcodeWeb': | ||
| 461 | - final barcode = data as Map?; | ||
| 462 | - final corners = barcode?['corners'] as List<Object?>? ?? <Object?>[]; | ||
| 463 | - | ||
| 464 | - _barcodesController.add( | ||
| 465 | - BarcodeCapture( | ||
| 466 | - raw: data, | ||
| 467 | - barcodes: [ | ||
| 468 | - if (barcode != null) | ||
| 469 | - Barcode( | ||
| 470 | - rawValue: barcode['rawValue'] as String?, | ||
| 471 | - rawBytes: barcode['rawBytes'] as Uint8List?, | ||
| 472 | - format: BarcodeFormat.fromRawValue( | ||
| 473 | - barcode['format'] as int? ?? -1, | ||
| 474 | - ), | ||
| 475 | - corners: List.unmodifiable( | ||
| 476 | - corners.cast<Map<Object?, Object?>>().map( | ||
| 477 | - (Map<Object?, Object?> e) { | ||
| 478 | - return Offset(e['x']! as double, e['y']! as double); | ||
| 479 | - }, | ||
| 480 | - ), | ||
| 481 | - ), | ||
| 482 | - ), | ||
| 483 | - ], | ||
| 484 | - ), | ||
| 485 | - ); | ||
| 486 | - case 'error': | ||
| 487 | - throw MobileScannerException( | ||
| 488 | - errorCode: MobileScannerErrorCode.genericError, | ||
| 489 | - errorDetails: MobileScannerErrorDetails(message: data as String?), | ||
| 490 | - ); | ||
| 491 | - default: | ||
| 492 | - throw UnimplementedError(name as String?); | ||
| 493 | - } | 384 | + await MobileScannerPlatform.instance.updateScanWindow(window); |
| 494 | } | 385 | } |
| 495 | 386 | ||
| 496 | - /// updates the native ScanWindow | ||
| 497 | - Future<void> updateScanWindow(Rect? window) async { | ||
| 498 | - List? data; | ||
| 499 | - if (window != null) { | ||
| 500 | - data = [window.left, window.top, window.right, window.bottom]; | 387 | + /// Dispose the controller. |
| 388 | + /// | ||
| 389 | + /// Once the controller is disposed, it cannot be used anymore. | ||
| 390 | + @override | ||
| 391 | + Future<void> dispose() async { | ||
| 392 | + if (_isDisposed) { | ||
| 393 | + return; | ||
| 501 | } | 394 | } |
| 502 | 395 | ||
| 503 | - await _methodChannel.invokeMethod('updateScanWindow', {'rect': data}); | 396 | + _isDisposed = true; |
| 397 | + unawaited(_barcodesController.close()); | ||
| 398 | + super.dispose(); | ||
| 399 | + | ||
| 400 | + await MobileScannerPlatform.instance.dispose(); | ||
| 504 | } | 401 | } |
| 505 | } | 402 | } |
| @@ -39,3 +39,10 @@ class MobileScannerErrorDetails { | @@ -39,3 +39,10 @@ class MobileScannerErrorDetails { | ||
| 39 | /// The error message from the [PlatformException]. | 39 | /// The error message from the [PlatformException]. |
| 40 | final String? message; | 40 | final String? message; |
| 41 | } | 41 | } |
| 42 | + | ||
| 43 | +/// This class represents an exception that is thrown | ||
| 44 | +/// when the scanner was (re)started while a permission request was pending. | ||
| 45 | +/// | ||
| 46 | +/// This exception type is only used internally, | ||
| 47 | +/// and is not part of the public API. | ||
| 48 | +class PermissionRequestPendingException implements Exception {} |
| 1 | +import 'package:flutter/widgets.dart'; | ||
| 2 | +import 'package:mobile_scanner/src/enums/torch_state.dart'; | ||
| 3 | +import 'package:mobile_scanner/src/method_channel/mobile_scanner_method_channel.dart'; | ||
| 4 | +import 'package:mobile_scanner/src/mobile_scanner_view_attributes.dart'; | ||
| 5 | +import 'package:mobile_scanner/src/objects/barcode_capture.dart'; | ||
| 6 | +import 'package:mobile_scanner/src/objects/start_options.dart'; | ||
| 7 | +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; | ||
| 8 | + | ||
| 9 | +/// The platform interface for the `mobile_scanner` plugin. | ||
| 10 | +abstract class MobileScannerPlatform extends PlatformInterface { | ||
| 11 | + /// Constructs a MobileScannerPlatform. | ||
| 12 | + MobileScannerPlatform() : super(token: _token); | ||
| 13 | + | ||
| 14 | + static final Object _token = Object(); | ||
| 15 | + | ||
| 16 | + static MobileScannerPlatform _instance = MethodChannelMobileScanner(); | ||
| 17 | + | ||
| 18 | + /// The default instance of [MobileScannerPlatform] to use. | ||
| 19 | + /// | ||
| 20 | + /// Defaults to [MethodChannelMobileScanner]. | ||
| 21 | + static MobileScannerPlatform get instance => _instance; | ||
| 22 | + | ||
| 23 | + /// Platform-specific implementations should set this with their own | ||
| 24 | + /// platform-specific class that extends [MobileScannerPlatform] when | ||
| 25 | + /// they register themselves. | ||
| 26 | + static set instance(MobileScannerPlatform instance) { | ||
| 27 | + PlatformInterface.verifyToken(instance, _token); | ||
| 28 | + _instance = instance; | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + /// Get the stream of barcode captures. | ||
| 32 | + Stream<BarcodeCapture?> get barcodesStream { | ||
| 33 | + throw UnimplementedError('barcodesStream has not been implemented.'); | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + /// Get the stream of torch state changes. | ||
| 37 | + Stream<TorchState> get torchStateStream { | ||
| 38 | + throw UnimplementedError('torchStateStream has not been implemented.'); | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + /// Get the stream of zoom scale changes. | ||
| 42 | + Stream<double> get zoomScaleStateStream { | ||
| 43 | + throw UnimplementedError('zoomScaleStateStream has not been implemented.'); | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + /// Analyze a local image file for barcodes. | ||
| 47 | + /// | ||
| 48 | + /// The [path] is the path to the file on disk. | ||
| 49 | + /// | ||
| 50 | + /// Returns the barcodes that were found in the image. | ||
| 51 | + Future<BarcodeCapture?> analyzeImage(String path) { | ||
| 52 | + throw UnimplementedError('analyzeImage() has not been implemented.'); | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + /// Build the camera view for the barcode scanner. | ||
| 56 | + Widget buildCameraView() { | ||
| 57 | + throw UnimplementedError('buildCameraView() has not been implemented.'); | ||
| 58 | + } | ||
| 59 | + | ||
| 60 | + /// Reset the zoom scale, so that the camera is fully zoomed out. | ||
| 61 | + Future<void> resetZoomScale() { | ||
| 62 | + throw UnimplementedError('resetZoomScale() has not been implemented.'); | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + /// Set the source url for the barcode library. | ||
| 66 | + /// | ||
| 67 | + /// This is only supported on the web. | ||
| 68 | + void setBarcodeLibraryScriptUrl(String scriptUrl) {} | ||
| 69 | + | ||
| 70 | + /// Set the torch state of the active camera. | ||
| 71 | + Future<void> setTorchState(TorchState torchState) { | ||
| 72 | + throw UnimplementedError('setTorchState() has not been implemented.'); | ||
| 73 | + } | ||
| 74 | + | ||
| 75 | + /// Set the zoom scale of the camera. | ||
| 76 | + /// | ||
| 77 | + /// The [zoomScale] must be between `0.0` and `1.0` (both inclusive). | ||
| 78 | + /// A value of `0.0` indicates that the camera is fully zoomed out, | ||
| 79 | + /// while `1.0` indicates that the camera is fully zoomed in. | ||
| 80 | + Future<void> setZoomScale(double zoomScale) { | ||
| 81 | + throw UnimplementedError('setZoomScale() has not been implemented.'); | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + /// Start the barcode scanner and prepare a scanner view. | ||
| 85 | + /// | ||
| 86 | + /// Upon calling this method, the necessary camera permission will be requested. | ||
| 87 | + /// | ||
| 88 | + /// The given [StartOptions.cameraDirection] is used as the direction for the camera that needs to be set up. | ||
| 89 | + Future<MobileScannerViewAttributes> start(StartOptions startOptions) { | ||
| 90 | + throw UnimplementedError('start() has not been implemented.'); | ||
| 91 | + } | ||
| 92 | + | ||
| 93 | + /// Stop the camera. | ||
| 94 | + Future<void> stop() { | ||
| 95 | + throw UnimplementedError('stop() has not been implemented.'); | ||
| 96 | + } | ||
| 97 | + | ||
| 98 | + /// Update the scan window to the given [window] rectangle. | ||
| 99 | + /// | ||
| 100 | + /// Any barcodes that do not intersect with the given [window] will be ignored. | ||
| 101 | + /// | ||
| 102 | + /// If [window] is `null`, the scan window will be reset to the full screen. | ||
| 103 | + Future<void> updateScanWindow(Rect? window) { | ||
| 104 | + throw UnimplementedError('updateScanWindow() has not been implemented.'); | ||
| 105 | + } | ||
| 106 | + | ||
| 107 | + /// Dispose of this [MobileScannerPlatform] instance. | ||
| 108 | + Future<void> dispose() { | ||
| 109 | + throw UnimplementedError('dispose() has not been implemented.'); | ||
| 110 | + } | ||
| 111 | +} |
lib/src/mobile_scanner_view_attributes.dart
0 → 100644
| 1 | +import 'dart:ui'; | ||
| 2 | + | ||
| 3 | +/// This class defines the attributes for the mobile scanner view. | ||
| 4 | +class MobileScannerViewAttributes { | ||
| 5 | + const MobileScannerViewAttributes({ | ||
| 6 | + required this.hasTorch, | ||
| 7 | + this.numberOfCameras, | ||
| 8 | + required this.size, | ||
| 9 | + }); | ||
| 10 | + | ||
| 11 | + /// Whether the current active camera has a torch. | ||
| 12 | + final bool hasTorch; | ||
| 13 | + | ||
| 14 | + /// The number of available cameras. | ||
| 15 | + final int? numberOfCameras; | ||
| 16 | + | ||
| 17 | + /// The size of the camera output. | ||
| 18 | + final Size size; | ||
| 19 | +} |
| @@ -147,7 +147,9 @@ class Barcode { | @@ -147,7 +147,9 @@ class Barcode { | ||
| 147 | /// The SMS message that is embedded in the barcode. | 147 | /// The SMS message that is embedded in the barcode. |
| 148 | final SMS? sms; | 148 | final SMS? sms; |
| 149 | 149 | ||
| 150 | - /// The type of the [format] of the barcode. | 150 | + /// The contextual type of the [format] of the barcode. |
| 151 | + /// | ||
| 152 | + /// For example: TYPE_TEXT, TYPE_PRODUCT, TYPE_URL, etc. | ||
| 151 | /// | 153 | /// |
| 152 | /// For types that are recognized, | 154 | /// For types that are recognized, |
| 153 | /// but could not be parsed correctly, [BarcodeType.text] will be returned. | 155 | /// but could not be parsed correctly, [BarcodeType.text] will be returned. |
| @@ -6,38 +6,25 @@ import 'package:mobile_scanner/src/objects/barcode.dart'; | @@ -6,38 +6,25 @@ import 'package:mobile_scanner/src/objects/barcode.dart'; | ||
| 6 | /// This class represents a scanned barcode. | 6 | /// This class represents a scanned barcode. |
| 7 | class BarcodeCapture { | 7 | class BarcodeCapture { |
| 8 | /// Create a new [BarcodeCapture] instance. | 8 | /// Create a new [BarcodeCapture] instance. |
| 9 | - BarcodeCapture({ | 9 | + const BarcodeCapture({ |
| 10 | this.barcodes = const <Barcode>[], | 10 | this.barcodes = const <Barcode>[], |
| 11 | - double? height, | ||
| 12 | this.image, | 11 | this.image, |
| 13 | this.raw, | 12 | this.raw, |
| 14 | - double? width, | ||
| 15 | - }) : size = | ||
| 16 | - width == null && height == null ? Size.zero : Size(width!, height!); | 13 | + this.size = Size.zero, |
| 14 | + }); | ||
| 17 | 15 | ||
| 18 | /// The list of scanned barcodes. | 16 | /// The list of scanned barcodes. |
| 19 | final List<Barcode> barcodes; | 17 | final List<Barcode> barcodes; |
| 20 | 18 | ||
| 21 | /// The bytes of the image that is embedded in the barcode. | 19 | /// The bytes of the image that is embedded in the barcode. |
| 22 | /// | 20 | /// |
| 23 | - /// This null if [MobileScannerController.returnImage] is false. | 21 | + /// This null if [MobileScannerController.returnImage] is false, |
| 22 | + /// or if there is no available image. | ||
| 24 | final Uint8List? image; | 23 | final Uint8List? image; |
| 25 | 24 | ||
| 26 | /// The raw data of the scanned barcode. | 25 | /// The raw data of the scanned barcode. |
| 27 | - final dynamic raw; // TODO: this should be `Object?` instead of dynamic | 26 | + final Object? raw; |
| 28 | 27 | ||
| 29 | /// The size of the scanned barcode. | 28 | /// The size of the scanned barcode. |
| 30 | final Size size; | 29 | final Size size; |
| 31 | - | ||
| 32 | - /// The width of the scanned barcode. | ||
| 33 | - /// | ||
| 34 | - /// Prefer using `size.width` instead, | ||
| 35 | - /// as this getter will be removed in the future. | ||
| 36 | - double get width => size.width; | ||
| 37 | - | ||
| 38 | - /// The height of the scanned barcode. | ||
| 39 | - /// | ||
| 40 | - /// Prefer using `size.height` instead, | ||
| 41 | - /// as this getter will be removed in the future. | ||
| 42 | - double get height => size.height; | ||
| 43 | } | 30 | } |
| 1 | -import 'package:flutter/material.dart'; | ||
| 2 | - | ||
| 3 | -/// The start arguments of the scanner. | ||
| 4 | -class MobileScannerArguments { | ||
| 5 | - /// The output size of the camera. | ||
| 6 | - /// This value can be used to draw a box in the image. | ||
| 7 | - final Size size; | ||
| 8 | - | ||
| 9 | - /// A bool which is true if the device has a torch. | ||
| 10 | - final bool hasTorch; | ||
| 11 | - | ||
| 12 | - /// The texture id of the capture used internally. | ||
| 13 | - final int? textureId; | ||
| 14 | - | ||
| 15 | - /// The texture id of the capture used internally if device is web. | ||
| 16 | - final String? webId; | ||
| 17 | - | ||
| 18 | - /// Indicates how many cameras are available. | ||
| 19 | - /// | ||
| 20 | - /// Currently only supported on Android. | ||
| 21 | - final int? numberOfCameras; | ||
| 22 | - | ||
| 23 | - MobileScannerArguments({ | ||
| 24 | - required this.size, | ||
| 25 | - required this.hasTorch, | ||
| 26 | - this.textureId, | ||
| 27 | - this.webId, | ||
| 28 | - this.numberOfCameras, | ||
| 29 | - }); | ||
| 30 | -} |
lib/src/objects/mobile_scanner_state.dart
0 → 100644
| 1 | +import 'dart:ui'; | ||
| 2 | + | ||
| 3 | +import 'package:mobile_scanner/src/enums/camera_facing.dart'; | ||
| 4 | +import 'package:mobile_scanner/src/enums/torch_state.dart'; | ||
| 5 | +import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; | ||
| 6 | + | ||
| 7 | +/// This class represents the current state of a [MobileScannerController]. | ||
| 8 | +class MobileScannerState { | ||
| 9 | + /// Create a new [MobileScannerState] instance. | ||
| 10 | + const MobileScannerState({ | ||
| 11 | + required this.availableCameras, | ||
| 12 | + required this.cameraDirection, | ||
| 13 | + required this.isInitialized, | ||
| 14 | + required this.isRunning, | ||
| 15 | + required this.size, | ||
| 16 | + required this.torchState, | ||
| 17 | + required this.zoomScale, | ||
| 18 | + this.error, | ||
| 19 | + }); | ||
| 20 | + | ||
| 21 | + /// Create a new [MobileScannerState] instance that is uninitialized. | ||
| 22 | + const MobileScannerState.uninitialized(CameraFacing facing) | ||
| 23 | + : this( | ||
| 24 | + availableCameras: null, | ||
| 25 | + cameraDirection: facing, | ||
| 26 | + isInitialized: false, | ||
| 27 | + isRunning: false, | ||
| 28 | + size: Size.zero, | ||
| 29 | + torchState: TorchState.unavailable, | ||
| 30 | + zoomScale: 1.0, | ||
| 31 | + ); | ||
| 32 | + | ||
| 33 | + /// The number of available cameras. | ||
| 34 | + /// | ||
| 35 | + /// This is null if the number of cameras is unknown. | ||
| 36 | + final int? availableCameras; | ||
| 37 | + | ||
| 38 | + /// The facing direction of the camera. | ||
| 39 | + final CameraFacing cameraDirection; | ||
| 40 | + | ||
| 41 | + /// The error that occurred while setting up or using the canera. | ||
| 42 | + final MobileScannerException? error; | ||
| 43 | + | ||
| 44 | + /// Whether the mobile scanner has initialized successfully. | ||
| 45 | + /// | ||
| 46 | + /// This is `true` if the camera is ready to be used. | ||
| 47 | + final bool isInitialized; | ||
| 48 | + | ||
| 49 | + /// Whether the mobile scanner is currently running. | ||
| 50 | + /// | ||
| 51 | + /// This is `true` if the camera is active. | ||
| 52 | + final bool isRunning; | ||
| 53 | + | ||
| 54 | + /// The size of the camera output. | ||
| 55 | + final Size size; | ||
| 56 | + | ||
| 57 | + /// The current state of the flashlight of the camera. | ||
| 58 | + final TorchState torchState; | ||
| 59 | + | ||
| 60 | + /// The current zoom scale of the camera. | ||
| 61 | + final double zoomScale; | ||
| 62 | + | ||
| 63 | + /// Create a copy of this state with the given parameters. | ||
| 64 | + MobileScannerState copyWith({ | ||
| 65 | + int? availableCameras, | ||
| 66 | + CameraFacing? cameraDirection, | ||
| 67 | + MobileScannerException? error, | ||
| 68 | + bool? isInitialized, | ||
| 69 | + bool? isRunning, | ||
| 70 | + Size? size, | ||
| 71 | + TorchState? torchState, | ||
| 72 | + double? zoomScale, | ||
| 73 | + }) { | ||
| 74 | + return MobileScannerState( | ||
| 75 | + availableCameras: availableCameras ?? this.availableCameras, | ||
| 76 | + cameraDirection: cameraDirection ?? this.cameraDirection, | ||
| 77 | + error: error, | ||
| 78 | + isInitialized: isInitialized ?? this.isInitialized, | ||
| 79 | + isRunning: isRunning ?? this.isRunning, | ||
| 80 | + size: size ?? this.size, | ||
| 81 | + torchState: torchState ?? this.torchState, | ||
| 82 | + zoomScale: zoomScale ?? this.zoomScale, | ||
| 83 | + ); | ||
| 84 | + } | ||
| 85 | +} |
lib/src/objects/start_options.dart
0 → 100644
| 1 | +import 'dart:ui'; | ||
| 2 | + | ||
| 3 | +import 'package:mobile_scanner/src/enums/barcode_format.dart'; | ||
| 4 | +import 'package:mobile_scanner/src/enums/camera_facing.dart'; | ||
| 5 | +import 'package:mobile_scanner/src/enums/detection_speed.dart'; | ||
| 6 | + | ||
| 7 | +/// This class defines the different start options for the mobile scanner. | ||
| 8 | +class StartOptions { | ||
| 9 | + const StartOptions({ | ||
| 10 | + required this.cameraDirection, | ||
| 11 | + required this.cameraResolution, | ||
| 12 | + required this.detectionSpeed, | ||
| 13 | + required this.detectionTimeoutMs, | ||
| 14 | + required this.formats, | ||
| 15 | + required this.returnImage, | ||
| 16 | + required this.torchEnabled, | ||
| 17 | + }); | ||
| 18 | + | ||
| 19 | + /// The direction for the camera. | ||
| 20 | + final CameraFacing cameraDirection; | ||
| 21 | + | ||
| 22 | + /// The desired camera resolution for the scanner. | ||
| 23 | + final Size? cameraResolution; | ||
| 24 | + | ||
| 25 | + /// The detection speed for the scanner. | ||
| 26 | + final DetectionSpeed detectionSpeed; | ||
| 27 | + | ||
| 28 | + /// The detection timeout for the scanner, in milliseconds. | ||
| 29 | + final int detectionTimeoutMs; | ||
| 30 | + | ||
| 31 | + /// The barcode formats to detect. | ||
| 32 | + final List<BarcodeFormat> formats; | ||
| 33 | + | ||
| 34 | + /// Whether the detected barcodes should provide their image data. | ||
| 35 | + final bool returnImage; | ||
| 36 | + | ||
| 37 | + /// Whether the torch should be turned on when the scanner starts. | ||
| 38 | + final bool torchEnabled; | ||
| 39 | + | ||
| 40 | + Map<String, Object?> toMap() { | ||
| 41 | + return <String, Object?>{ | ||
| 42 | + if (cameraResolution != null) | ||
| 43 | + 'cameraResolution': <int>[ | ||
| 44 | + cameraResolution!.width.toInt(), | ||
| 45 | + cameraResolution!.height.toInt(), | ||
| 46 | + ], | ||
| 47 | + 'facing': cameraDirection.rawValue, | ||
| 48 | + if (formats.isNotEmpty) | ||
| 49 | + 'formats': formats.map((f) => f.rawValue).toList(), | ||
| 50 | + 'returnImage': returnImage, | ||
| 51 | + 'speed': detectionSpeed.rawValue, | ||
| 52 | + 'timeout': detectionTimeoutMs, | ||
| 53 | + 'torch': torchEnabled, | ||
| 54 | + }; | ||
| 55 | + } | ||
| 56 | +} |
-
Please register or login to post a comment