diff --git a/.github/workflows/flutter.yml b/.github/workflows/flutter.yml index 2a88003ba..48f055a7c 100644 --- a/.github/workflows/flutter.yml +++ b/.github/workflows/flutter.yml @@ -36,7 +36,7 @@ jobs: - uses: subosito/flutter-action@v2.12.0 with: cache: true - flutter-version: '3.13' + flutter-version: '3.19' channel: 'stable' - name: Version run: flutter doctor -v diff --git a/CHANGELOG.md b/CHANGELOG.md index 40082b8e9..27562c9fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,39 @@ +## 5.0.0-beta.1 + +**BREAKING CHANGES:** + +* Flutter 3.19.0 is now required. +* The `width` and `height` of `BarcodeCapture` have been removed, in favor of `size`. +* The `raw` attribute is now `Object?` instead of `dynamic`, so that it participates in type promotion. +* The `MobileScannerArguments` class has been removed from the public API, as it is an internal type. +* The `cameraFacingOverride` named argument for the `start()` method has been renamed to `cameraDirection`. +* The `analyzeImage` function now correctly returns a `BarcodeCapture?` instead of a boolean. +* The `formats` attribute of the `MobileScannerController` is now non-null. +* The `MobileScannerState` enum has been renamed to `MobileScannerAuthorizationState`. +* The various `ValueNotifier`s for the camera state have been removed. Use the `value` of the `MobileScannerController` instead. +* The `hasTorch` getter has been removed. Instead, use the torch state of the controller's value. + The `TorchState` enum now provides a new value for unavailable flashlights. +* The `autoStart` attribute has been removed from the `MobileScannerController`. The controller should be manually started on-demand. +* A controller is now required for the `MobileScanner` widget. +* The `onPermissionSet`, `onStart` and `onScannerStarted` methods have been removed from the `MobileScanner` widget. Instead, await `MobileScannerController.start()`. +* The `startDelay` has been removed from the `MobileScanner` widget. Instead, use a delay between manual starts of one or more controllers. +* The `onDetect` method has been removed from the `MobileScanner` widget. Instead, listen to `MobileScannerController.barcodes` directly. +* The `overlay` widget of the `MobileScanner` has been replaced by a new property, `overlayBuilder`, which provides the constraints for the overlay. +* The torch can no longer be toggled on the web, as this is only available for image tracks and not video tracks. As a result the torch state for the web will always be `TorchState.unavailable`. +* The zoom scale can no longer be modified on the web, as this is only available for image tracks and not video tracks. As a result, the zoom scale will always be `1.0`. + +Improvements: +* The `MobileScannerController` is now a ChangeNotifier, with `MobileScannerState` as its model. +* The web implementation now supports alternate URLs for loading the barcode library. + ## 4.0.1 Bugs fixed: * [iOS] Fixed a crash with a nil capture session when starting the camera. (thanks @navaronbracke !) ## 4.0.0 -BREAKING CHANGES: + +**BREAKING CHANGES:** + * [Android] compileSdk has been upgraded to version 34. * [Android] Java version has been upgraded to version 17. @@ -186,7 +216,8 @@ Deprecated: * The `onStart` method has been renamed to `onScannerStarted`. * The `onPermissionSet` argument of the `MobileScannerController` is now deprecated. -Breaking changes: +**BREAKING CHANGES:** + * `MobileScannerException` now uses an `errorCode` instead of a `message`. * `MobileScannerException` now contains additional details from the original error. * Refactored `MobileScannerController.start()` to throw `MobileScannerException`s @@ -223,7 +254,9 @@ Fixes: * [iOS] Fix crash when changing torch state ## 3.0.0-beta.2 -Breaking changes: + +**BREAKING CHANGES:** + * The arguments parameter of onDetect is removed. The data is now returned by the onStart callback in the MobileScanner widget. * onDetect now returns the object BarcodeCapture, which contains a List of barcodes and, if enabled, an image. @@ -243,7 +276,9 @@ Other improvements: * [iOS] Updated POD dependencies ## 3.0.0-beta.1 -Breaking changes: + +**BREAKING CHANGES:** + * [Android] SDK updated to SDK 33. Features: @@ -259,7 +294,9 @@ Other changes: * Several minor code improvements ## 2.0.0 -Breaking changes: + +**BREAKING CHANGES:** + This version is only compatible with flutter 3.0.0 and later. ## 1.1.2-play-services @@ -293,7 +330,9 @@ Bugfixes: * Upgraded several dependencies. ## 1.0.0 -BREAKING CHANGES: + +**BREAKING CHANGES:** + This version adds a new allowDuplicates option which now defaults to FALSE. this means that it will only call onDetect once after a scan. If you still want duplicates, you can set allowDuplicates to true. This also means that you don't have to check for duplicates yourself anymore. diff --git a/README.md b/README.md index bfb33dc22..109fc04ea 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,15 @@ A universal scanner for Flutter based on MLKit. Uses CameraX on Android and AVFoundation on iOS. - ## Features Supported See the example app for detailed implementation information. -| Features | Android | iOS | macOS | Web | -|------------------------|--------------------|--------------------|-------|-----| -| analyzeImage (Gallery) | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | -| returnImage | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | -| scanWindow | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | -| barcodeOverlay | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | +| Features | Android | iOS | macOS | Web | +|------------------------|--------------------|--------------------|----------------------|-----| +| analyzeImage (Gallery) | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | +| returnImage | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | +| scanWindow | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: | ## Platform Support @@ -26,6 +24,7 @@ See the example app for detailed implementation information. | ✔ | ✔ | ✔ | ✔ | :x: | :x: | ## Platform specific setup + ### Android This package uses by default the **bundled version** of MLKit Barcode-scanning for Android. This version is immediately available to the device. But it will increase the size of the app by approximately 3 to 10 MB. @@ -61,194 +60,110 @@ Ensure that you granted camera permission in XCode -> Signing & Capabilities: Screenshot of XCode where Camera is checked ## Web -This package uses ZXing on web to read barcodes so it needs to be included in `index.html` as script. -```html - -``` -## Usage +As of version 5.0.0 adding the library to the `index.html` is no longer required, +as the library is automatically loaded on first use. -Import `package:mobile_scanner/mobile_scanner.dart`, and use the widget with or without the controller. +### Providing a mirror for the barcode scanning library -If you don't provide a controller, you can't control functions like the torch(flash) or switching camera. - -If you don't set `detectionSpeed` to `DetectionSpeed.noDuplicates`, you can get multiple scans in a very short time, causing things like pop() to fire lots of times. - -Example without controller: +If a different mirror is needed to load the barcode scanning library, +the source URL can be set beforehand. ```dart +import 'package:flutter/foundation.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Mobile Scanner')), - body: MobileScanner( - // fit: BoxFit.contain, - onDetect: (capture) { - final List barcodes = capture.barcodes; - final Uint8List? image = capture.image; - for (final barcode in barcodes) { - debugPrint('Barcode found! ${barcode.rawValue}'); - } - }, - ), - ); - } +final String scriptUrl = // ... + +if (kIsWeb) { + MobileScannerPlatform.instance.setBarcodeLibraryScriptUrl(scriptUrl); +} ``` -Example with controller and initial values: +## Usage + +Import the package with `package:mobile_scanner/mobile_scanner.dart`. + +Create a new `MobileScannerController` controller, using the required options. +Provide a `StreamSubscription` for the barcode events. ```dart -import 'package:mobile_scanner/mobile_scanner.dart'; +final MobileScannerController controller = MobileScannerController( + // required options for the scanner +); - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Mobile Scanner')), - body: MobileScanner( - // fit: BoxFit.contain, - controller: MobileScannerController( - detectionSpeed: DetectionSpeed.normal, - facing: CameraFacing.front, - torchEnabled: true, - ), - onDetect: (capture) { - final List barcodes = capture.barcodes; - final Uint8List? image = capture.image; - for (final barcode in barcodes) { - debugPrint('Barcode found! ${barcode.rawValue}'); - } - }, - ), - ); - } +StreamSubscription? _subscription; ``` -Example with controller and torch & camera controls: +Ensure that your `State` class mixes in `WidgetsBindingObserver`, to handle lifecyle changes: ```dart -import 'package:mobile_scanner/mobile_scanner.dart'; - - MobileScannerController cameraController = MobileScannerController(); +class MyState extends State with WidgetsBindingObserver { + // ... @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Mobile Scanner'), - actions: [ - IconButton( - color: Colors.white, - icon: ValueListenableBuilder( - valueListenable: cameraController.torchState, - builder: (context, state, child) { - switch (state as TorchState) { - case TorchState.off: - return const Icon(Icons.flash_off, color: Colors.grey); - case TorchState.on: - return const Icon(Icons.flash_on, color: Colors.yellow); - } - }, - ), - iconSize: 32.0, - onPressed: () => cameraController.toggleTorch(), - ), - IconButton( - color: Colors.white, - icon: ValueListenableBuilder( - valueListenable: cameraController.cameraFacingState, - builder: (context, state, child) { - switch (state as CameraFacing) { - case CameraFacing.front: - return const Icon(Icons.camera_front); - case CameraFacing.back: - return const Icon(Icons.camera_rear); - } - }, - ), - iconSize: 32.0, - onPressed: () => cameraController.switchCamera(), - ), - ], - ), - body: MobileScanner( - // fit: BoxFit.contain, - controller: cameraController, - onDetect: (capture) { - final List barcodes = capture.barcodes; - final Uint8List? image = capture.image; - for (final barcode in barcodes) { - debugPrint('Barcode found! ${barcode.rawValue}'); - } - }, - ), - ); + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + + switch (state) { + case AppLifecycleState.detached: + case AppLifecycleState.hidden: + case AppLifecycleState.paused: + return; + case AppLifecycleState.resumed: + // Restart the scanner when the app is resumed. + // Don't forget to resume listening to the barcode events. + _subscription = controller.barcodes.listen(_handleBarcode); + + unawaited(controller.start()); + case AppLifecycleState.inactive: + // Stop the scanner when the app is paused. + // Also stop the barcode events subscription. + unawaited(_subscription?.cancel()); + _subscription = null; + unawaited(controller.stop()); + } } + + // ... +} ``` -Example with controller and returning images +Then, start the scanner in `void initState()`: ```dart -import 'package:mobile_scanner/mobile_scanner.dart'; +@override +void initState() { + super.initState(); + // Start listening to lifecycle changes. + WidgetsBinding.instance.addObserver(this); + + // Start listening to the barcode events. + _subscription = controller.barcodes.listen(_handleBarcode); + + // Finally, start the scanner itself. + unawaited(controller.start()); +} +``` - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Mobile Scanner')), - body: MobileScanner( - fit: BoxFit.contain, - controller: MobileScannerController( - // facing: CameraFacing.back, - // torchEnabled: false, - returnImage: true, - ), - onDetect: (capture) { - final List barcodes = capture.barcodes; - final Uint8List? image = capture.image; - for (final barcode in barcodes) { - debugPrint('Barcode found! ${barcode.rawValue}'); - } - if (image != null) { - showDialog( - context: context, - builder: (context) => - Image(image: MemoryImage(image)), - ); - Future.delayed(const Duration(seconds: 5), () { - Navigator.pop(context); - }); - } - }, - ), - ); - } +Finally, dispose of the the `MobileScannerController` when you are done with it. + +```dart +@override +Future dispose() async { + // Stop listening to lifecycle changes. + WidgetsBinding.instance.removeObserver(this); + // Stop listening to the barcode events. + unawaited(_subscription?.cancel()); + _subscription = null; + // Dispose the widget itself. + super.dispose(); + // Finally, dispose of the controller. + await controller.dispose(); +} ``` -### BarcodeCapture - -The onDetect function returns a BarcodeCapture objects which contains the following items. - -| Property name | Type | Description | -|---------------|---------------|-----------------------------------| -| barcodes | List | A list with scanned barcodes. | -| image | Uint8List? | If enabled, an image of the scan. | - -You can use the following properties of the Barcode object. - -| Property name | Type | Description | -|---------------|----------------|-------------------------------------| -| format | BarcodeFormat | | -| rawBytes | Uint8List? | binary scan result | -| rawValue | String? | Value if barcode is in UTF-8 format | -| displayValue | String? | | -| type | BarcodeType | | -| calendarEvent | CalendarEvent? | | -| contactInfo | ContactInfo? | | -| driverLicense | DriverLicense? | | -| email | Email? | | -| geoPoint | GeoPoint? | | -| phone | Phone? | | -| sms | SMS? | | -| url | UrlBookmark? | | -| wifi | WiFi? | WiFi Access-Point details | +To display the camera preview, pass the controller to a `MobileScanner` widget. + +See the examples for runnable examples of various usages, +such as the basic usage, applying a scan window, or retrieving images from the barcodes. diff --git a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt index cbd1b1127..7cd091132 100644 --- a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt +++ b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt @@ -78,7 +78,7 @@ class MobileScanner( scanner.process(inputImage) .addOnSuccessListener { barcodes -> if (detectionSpeed == DetectionSpeed.NO_DUPLICATES) { - val newScannedBarcodes = barcodes.mapNotNull({ barcode -> barcode.rawValue }).sorted() + val newScannedBarcodes = barcodes.mapNotNull { barcode -> barcode.rawValue }.sorted() if (newScannedBarcodes == lastScanned) { // New scanned is duplicate, returning return@addOnSuccessListener @@ -424,7 +424,7 @@ class MobileScanner( /** * Analyze a single image. */ - fun analyzeImage(image: Uri, analyzerCallback: AnalyzerCallback) { + fun analyzeImage(image: Uri, onSuccess: AnalyzerSuccessCallback, onError: AnalyzerErrorCallback) { val inputImage = InputImage.fromFilePath(activity, image) scanner.process(inputImage) @@ -432,15 +432,13 @@ class MobileScanner( val barcodeMap = barcodes.map { barcode -> barcode.data } if (barcodeMap.isNotEmpty()) { - analyzerCallback(barcodeMap) + onSuccess(barcodeMap) } else { - analyzerCallback(null) + onSuccess(null) } } .addOnFailureListener { e -> - mobileScannerErrorCallback( - e.localizedMessage ?: e.toString() - ) + onError(e.localizedMessage ?: e.toString()) } } diff --git a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerCallbacks.kt b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerCallbacks.kt index f8549b3a2..5732a28ed 100644 --- a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerCallbacks.kt +++ b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerCallbacks.kt @@ -3,7 +3,8 @@ package dev.steenbakker.mobile_scanner import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters typealias MobileScannerCallback = (barcodes: List>, image: ByteArray?, width: Int?, height: Int?) -> Unit -typealias AnalyzerCallback = (barcodes: List>?) -> Unit +typealias AnalyzerErrorCallback = (message: String) -> Unit +typealias AnalyzerSuccessCallback = (barcodes: List>?) -> Unit typealias MobileScannerErrorCallback = (error: String) -> Unit typealias TorchStateCallback = (state: Int) -> Unit typealias ZoomScaleStateCallback = (zoomScale: Double) -> Unit diff --git a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt index b23a2d200..17299ebd3 100644 --- a/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt +++ b/android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScannerHandler.kt @@ -26,16 +26,19 @@ class MobileScannerHandler( private val addPermissionListener: (RequestPermissionsResultListener) -> Unit, textureRegistry: TextureRegistry): MethodChannel.MethodCallHandler { - private val analyzerCallback: AnalyzerCallback = { barcodes: List>?-> - if (barcodes != null) { - barcodeHandler.publishEvent(mapOf( - "name" to "barcode", - "data" to barcodes - )) + private val analyzeImageErrorCallback: AnalyzerErrorCallback = { + Handler(Looper.getMainLooper()).post { + analyzerResult?.error("MobileScanner", it, null) + analyzerResult = null } + } + private val analyzeImageSuccessCallback: AnalyzerSuccessCallback = { Handler(Looper.getMainLooper()).post { - analyzerResult?.success(barcodes != null) + analyzerResult?.success(mapOf( + "name" to "barcode", + "data" to it + )) analyzerResult = null } } @@ -236,7 +239,8 @@ class MobileScannerHandler( private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) { analyzerResult = result val uri = Uri.fromFile(File(call.arguments.toString())) - mobileScanner!!.analyzeImage(uri, analyzerCallback) + + mobileScanner!!.analyzeImage(uri, analyzeImageSuccessCallback, analyzeImageErrorCallback) } private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) { @@ -265,7 +269,7 @@ class MobileScannerHandler( } private fun updateScanWindow(call: MethodCall, result: MethodChannel.Result) { - mobileScanner!!.scanWindow = call.argument?>("rect") + mobileScanner?.scanWindow = call.argument?>("rect") result.success(null) } diff --git a/example/lib/barcode_list_scanner_controller.dart b/example/lib/barcode_list_scanner_controller.dart deleted file mode 100644 index e1a4f39aa..000000000 --- a/example/lib/barcode_list_scanner_controller.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:mobile_scanner/mobile_scanner.dart'; -import 'package:mobile_scanner_example/scanner_error_widget.dart'; - -class BarcodeListScannerWithController extends StatefulWidget { - const BarcodeListScannerWithController({super.key}); - - @override - State createState() => - _BarcodeListScannerWithControllerState(); -} - -class _BarcodeListScannerWithControllerState - extends State - with SingleTickerProviderStateMixin { - BarcodeCapture? barcodeCapture; - - final MobileScannerController controller = MobileScannerController( - torchEnabled: true, - // formats: [BarcodeFormat.qrCode] - // facing: CameraFacing.front, - // detectionSpeed: DetectionSpeed.normal - // detectionTimeoutMs: 1000, - // returnImage: false, - ); - - bool isStarted = true; - - void _startOrStop() { - try { - if (isStarted) { - controller.stop(); - } else { - controller.start(); - } - setState(() { - isStarted = !isStarted; - }); - } on Exception catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Something went wrong! $e'), - backgroundColor: Colors.red, - ), - ); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('With ValueListenableBuilder')), - backgroundColor: Colors.black, - body: Builder( - builder: (context) { - return Stack( - children: [ - MobileScanner( - controller: controller, - errorBuilder: (context, error, child) { - return ScannerErrorWidget(error: error); - }, - fit: BoxFit.contain, - onDetect: (barcodeCapture) { - setState(() { - this.barcodeCapture = barcodeCapture; - }); - }, - onScannerStarted: (arguments) { - // Do something with arguments. - }, - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - alignment: Alignment.bottomCenter, - height: 100, - color: Colors.black.withOpacity(0.4), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - IconButton( - color: Colors.white, - icon: ValueListenableBuilder( - valueListenable: controller.torchState, - builder: (context, state, child) { - switch (state) { - case TorchState.off: - return const Icon( - Icons.flash_off, - color: Colors.grey, - ); - case TorchState.on: - return const Icon( - Icons.flash_on, - color: Colors.yellow, - ); - } - }, - ), - iconSize: 32.0, - onPressed: () => controller.toggleTorch(), - ), - IconButton( - color: Colors.white, - icon: isStarted - ? const Icon(Icons.stop) - : const Icon(Icons.play_arrow), - iconSize: 32.0, - onPressed: _startOrStop, - ), - Center( - child: SizedBox( - width: MediaQuery.of(context).size.width - 200, - height: 50, - child: FittedBox( - child: Text( - '${barcodeCapture?.barcodes.map((e) => e.rawValue) ?? 'Scan something!'}', - overflow: TextOverflow.fade, - style: Theme.of(context) - .textTheme - .headlineMedium! - .copyWith(color: Colors.white), - ), - ), - ), - ), - IconButton( - color: Colors.white, - icon: ValueListenableBuilder( - valueListenable: controller.cameraFacingState, - builder: (context, state, child) { - switch (state) { - case CameraFacing.front: - return const Icon(Icons.camera_front); - case CameraFacing.back: - return const Icon(Icons.camera_rear); - } - }, - ), - iconSize: 32.0, - onPressed: () => controller.switchCamera(), - ), - IconButton( - color: Colors.white, - icon: const Icon(Icons.image), - iconSize: 32.0, - onPressed: () async { - final ImagePicker picker = ImagePicker(); - // Pick an image - final XFile? image = await picker.pickImage( - source: ImageSource.gallery, - ); - if (image != null) { - if (await controller.analyzeImage(image.path)) { - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Barcode found!'), - backgroundColor: Colors.green, - ), - ); - } else { - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('No barcode found!'), - backgroundColor: Colors.red, - ), - ); - } - } - }, - ), - ], - ), - ), - ), - ], - ); - }, - ), - ); - } - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } -} diff --git a/example/lib/barcode_scanner_controller.dart b/example/lib/barcode_scanner_controller.dart index 8aac24bbf..126297bd0 100644 --- a/example/lib/barcode_scanner_controller.dart +++ b/example/lib/barcode_scanner_controller.dart @@ -1,6 +1,8 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; -import 'package:image_picker/image_picker.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:mobile_scanner_example/scanner_button_widgets.dart'; import 'package:mobile_scanner_example/scanner_error_widget.dart'; class BarcodeScannerWithController extends StatefulWidget { @@ -12,10 +14,7 @@ class BarcodeScannerWithController extends StatefulWidget { } class _BarcodeScannerWithControllerState - extends State - with SingleTickerProviderStateMixin { - BarcodeCapture? barcode; - + extends State with WidgetsBindingObserver { final MobileScannerController controller = MobileScannerController( torchEnabled: true, useNewCameraSelector: true, // formats: [BarcodeFormat.qrCode] @@ -25,178 +24,106 @@ class _BarcodeScannerWithControllerState // returnImage: false, ); - bool isStarted = true; + Barcode? _barcode; + StreamSubscription? _subscription; + + Widget _buildBarcode(Barcode? value) { + if (value == null) { + return const Text( + 'Scan something!', + overflow: TextOverflow.fade, + style: TextStyle(color: Colors.white), + ); + } + + return Text( + value.displayValue ?? 'No display value.', + overflow: TextOverflow.fade, + style: const TextStyle(color: Colors.white), + ); + } - void _startOrStop() { - try { - if (isStarted) { - controller.stop(); - } else { - controller.start(); - } + void _handleBarcode(BarcodeCapture barcodes) { + if (mounted) { setState(() { - isStarted = !isStarted; + _barcode = barcodes.barcodes.firstOrNull; }); - } on Exception catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Something went wrong! $e'), - backgroundColor: Colors.red, - ), - ); } } - int? numberOfCameras; + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + + _subscription = controller.barcodes.listen(_handleBarcode); + + unawaited(controller.start()); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + + switch (state) { + case AppLifecycleState.detached: + case AppLifecycleState.hidden: + case AppLifecycleState.paused: + return; + case AppLifecycleState.resumed: + _subscription = controller.barcodes.listen(_handleBarcode); + + unawaited(controller.start()); + case AppLifecycleState.inactive: + unawaited(_subscription?.cancel()); + _subscription = null; + unawaited(controller.stop()); + } + } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('With controller')), backgroundColor: Colors.black, - body: Builder( - builder: (context) { - return Stack( - children: [ - MobileScanner( - onScannerStarted: (arguments) { - if (mounted && arguments?.numberOfCameras != null) { - numberOfCameras = arguments!.numberOfCameras; - setState(() {}); - } - }, - controller: controller, - errorBuilder: (context, error, child) { - return ScannerErrorWidget(error: error); - }, - fit: BoxFit.contain, - onDetect: (barcode) { - setState(() { - this.barcode = barcode; - }); - }, - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - alignment: Alignment.bottomCenter, - height: 100, - color: Colors.black.withOpacity(0.4), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - ValueListenableBuilder( - valueListenable: controller.hasTorchState, - builder: (context, state, child) { - if (state != true) { - return const SizedBox.shrink(); - } - return IconButton( - color: Colors.white, - icon: ValueListenableBuilder( - valueListenable: controller.torchState, - builder: (context, state, child) { - switch (state) { - case TorchState.off: - return const Icon( - Icons.flash_off, - color: Colors.grey, - ); - case TorchState.on: - return const Icon( - Icons.flash_on, - color: Colors.yellow, - ); - } - }, - ), - iconSize: 32.0, - onPressed: () => controller.toggleTorch(), - ); - }, - ), - IconButton( - color: Colors.white, - icon: isStarted - ? const Icon(Icons.stop) - : const Icon(Icons.play_arrow), - iconSize: 32.0, - onPressed: _startOrStop, - ), - Center( - child: SizedBox( - width: MediaQuery.of(context).size.width - 200, - height: 50, - child: FittedBox( - child: Text( - barcode?.barcodes.first.rawValue ?? - 'Scan something!', - overflow: TextOverflow.fade, - style: Theme.of(context) - .textTheme - .headlineMedium! - .copyWith(color: Colors.white), - ), - ), - ), - ), - IconButton( - color: Colors.white, - icon: ValueListenableBuilder( - valueListenable: controller.cameraFacingState, - builder: (context, state, child) { - switch (state) { - case CameraFacing.front: - return const Icon(Icons.camera_front); - case CameraFacing.back: - return const Icon(Icons.camera_rear); - } - }, - ), - iconSize: 32.0, - onPressed: (numberOfCameras ?? 0) < 2 - ? null - : () => controller.switchCamera(), - ), - IconButton( - color: Colors.white, - icon: const Icon(Icons.image), - iconSize: 32.0, - onPressed: () async { - final ImagePicker picker = ImagePicker(); - // Pick an image - final XFile? image = await picker.pickImage( - source: ImageSource.gallery, - ); - if (image != null) { - if (await controller.analyzeImage(image.path)) { - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Barcode found!'), - backgroundColor: Colors.green, - ), - ); - } else { - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('No barcode found!'), - backgroundColor: Colors.red, - ), - ); - } - } - }, - ), - ], - ), - ), + body: Stack( + children: [ + MobileScanner( + controller: controller, + errorBuilder: (context, error, child) { + return ScannerErrorWidget(error: error); + }, + fit: BoxFit.contain, + ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + alignment: Alignment.bottomCenter, + height: 100, + color: Colors.black.withOpacity(0.4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ToggleFlashlightButton(controller: controller), + StartStopMobileScannerButton(controller: controller), + Expanded(child: Center(child: _buildBarcode(_barcode))), + SwitchCameraButton(controller: controller), + AnalyzeImageFromGalleryButton(controller: controller), + ], ), - ], - ); - }, + ), + ), + ], ), ); } + + @override + Future dispose() async { + WidgetsBinding.instance.removeObserver(this); + unawaited(_subscription?.cancel()); + _subscription = null; + super.dispose(); + await controller.dispose(); + } } diff --git a/example/lib/barcode_scanner_listview.dart b/example/lib/barcode_scanner_listview.dart new file mode 100644 index 000000000..ca0de42ba --- /dev/null +++ b/example/lib/barcode_scanner_listview.dart @@ -0,0 +1,113 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:mobile_scanner_example/scanner_button_widgets.dart'; +import 'package:mobile_scanner_example/scanner_error_widget.dart'; + +class BarcodeScannerListView extends StatefulWidget { + const BarcodeScannerListView({super.key}); + + @override + State createState() => _BarcodeScannerListViewState(); +} + +class _BarcodeScannerListViewState extends State { + final MobileScannerController controller = MobileScannerController( + torchEnabled: true, + // formats: [BarcodeFormat.qrCode] + // facing: CameraFacing.front, + // detectionSpeed: DetectionSpeed.normal + // detectionTimeoutMs: 1000, + // returnImage: false, + ); + + @override + void initState() { + super.initState(); + + controller.start(); + } + + Widget _buildBarcodesListView() { + return StreamBuilder( + stream: controller.barcodes, + builder: (context, snapshot) { + final barcodes = snapshot.data?.barcodes; + + if (barcodes == null || barcodes.isEmpty) { + return const Center( + child: Text( + 'Scan Something!', + style: TextStyle(color: Colors.white, fontSize: 20), + ), + ); + } + + return ListView.builder( + itemCount: barcodes.length, + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + barcodes[index].rawValue ?? 'No raw value', + overflow: TextOverflow.fade, + style: const TextStyle(color: Colors.white), + ), + ); + }, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('With ListView')), + backgroundColor: Colors.black, + body: Stack( + children: [ + MobileScanner( + controller: controller, + errorBuilder: (context, error, child) { + return ScannerErrorWidget(error: error); + }, + fit: BoxFit.contain, + ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + alignment: Alignment.bottomCenter, + height: 100, + color: Colors.black.withOpacity(0.4), + child: Column( + children: [ + Expanded( + child: _buildBarcodesListView(), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ToggleFlashlightButton(controller: controller), + StartStopMobileScannerButton(controller: controller), + const Spacer(), + SwitchCameraButton(controller: controller), + AnalyzeImageFromGalleryButton(controller: controller), + ], + ), + ], + ), + ), + ), + ], + ), + ); + } + + @override + Future dispose() async { + super.dispose(); + await controller.dispose(); + } +} diff --git a/example/lib/barcode_scanner_pageview.dart b/example/lib/barcode_scanner_pageview.dart index b06cf755e..c81865b15 100644 --- a/example/lib/barcode_scanner_pageview.dart +++ b/example/lib/barcode_scanner_pageview.dart @@ -1,5 +1,8 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:mobile_scanner_example/scanned_barcode_label.dart'; import 'package:mobile_scanner_example/scanner_error_widget.dart'; class BarcodeScannerPageView extends StatefulWidget { @@ -9,62 +12,15 @@ class BarcodeScannerPageView extends StatefulWidget { State createState() => _BarcodeScannerPageViewState(); } -class _BarcodeScannerPageViewState extends State - with SingleTickerProviderStateMixin { - BarcodeCapture? capture; +class _BarcodeScannerPageViewState extends State { + final MobileScannerController controller = MobileScannerController(); - Widget cameraView() { - return Builder( - builder: (context) { - return Stack( - children: [ - MobileScanner( - startDelay: true, - controller: MobileScannerController(torchEnabled: true), - fit: BoxFit.contain, - errorBuilder: (context, error, child) { - return ScannerErrorWidget(error: error); - }, - onDetect: (capture) { - setState(() { - this.capture = capture; - }); - }, - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - alignment: Alignment.bottomCenter, - height: 100, - color: Colors.black.withOpacity(0.4), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Center( - child: SizedBox( - width: MediaQuery.of(context).size.width - 120, - height: 50, - child: FittedBox( - child: Text( - capture?.barcodes.first.rawValue ?? - 'Scan something!', - overflow: TextOverflow.fade, - style: Theme.of(context) - .textTheme - .headlineMedium! - .copyWith(color: Colors.white), - ), - ), - ), - ), - ], - ), - ), - ), - ], - ); - }, - ); + final PageController pageController = PageController(); + + @override + void initState() { + super.initState(); + unawaited(controller.start()); } @override @@ -73,13 +29,68 @@ class _BarcodeScannerPageViewState extends State appBar: AppBar(title: const Text('With PageView')), backgroundColor: Colors.black, body: PageView( + controller: pageController, + onPageChanged: (index) async { + // Stop the camera view for the current page, + // and then restart the camera for the new page. + await controller.stop(); + + // When switching pages, add a delay to the next start call. + // Otherwise the camera will start before the next page is displayed. + await Future.delayed(const Duration(seconds: 1, milliseconds: 500)); + + if (!mounted) { + return; + } + + unawaited(controller.start()); + }, children: [ - cameraView(), - Container(), - cameraView(), - cameraView(), + _BarcodeScannerPage(controller: controller), + const SizedBox(), + _BarcodeScannerPage(controller: controller), + _BarcodeScannerPage(controller: controller), ], ), ); } + + @override + Future dispose() async { + pageController.dispose(); + super.dispose(); + await controller.dispose(); + } +} + +class _BarcodeScannerPage extends StatelessWidget { + const _BarcodeScannerPage({required this.controller}); + + final MobileScannerController controller; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + MobileScanner( + controller: controller, + fit: BoxFit.contain, + errorBuilder: (context, error, child) { + return ScannerErrorWidget(error: error); + }, + ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + alignment: Alignment.bottomCenter, + height: 100, + color: Colors.black.withOpacity(0.4), + child: Center( + child: ScannedBarcodeLabel(barcodes: controller.barcodes), + ), + ), + ), + ], + ); + } } diff --git a/example/lib/barcode_scanner_returning_image.dart b/example/lib/barcode_scanner_returning_image.dart index 15c1912a4..5f2943c0e 100644 --- a/example/lib/barcode_scanner_returning_image.dart +++ b/example/lib/barcode_scanner_returning_image.dart @@ -2,6 +2,8 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:mobile_scanner_example/scanned_barcode_label.dart'; +import 'package:mobile_scanner_example/scanner_button_widgets.dart'; import 'package:mobile_scanner_example/scanner_error_widget.dart'; class BarcodeScannerReturningImage extends StatefulWidget { @@ -13,11 +15,7 @@ class BarcodeScannerReturningImage extends StatefulWidget { } class _BarcodeScannerReturningImageState - extends State - with SingleTickerProviderStateMixin { - BarcodeCapture? barcode; - // MobileScannerArguments? arguments; - + extends State { final MobileScannerController controller = MobileScannerController( torchEnabled: true, // formats: [BarcodeFormat.qrCode] @@ -27,26 +25,10 @@ class _BarcodeScannerReturningImageState returnImage: true, ); - bool isStarted = true; - - void _startOrStop() { - try { - if (isStarted) { - controller.stop(); - } else { - controller.start(); - } - setState(() { - isStarted = !isStarted; - }); - } on Exception catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Something went wrong! $e'), - backgroundColor: Colors.red, - ), - ); - } + @override + void initState() { + super.initState(); + controller.start(); } @override @@ -57,20 +39,55 @@ class _BarcodeScannerReturningImageState child: Column( children: [ Expanded( - child: barcode?.image != null - ? Transform.rotate( - angle: 90 * pi / 180, - child: Image( - gaplessPlayback: true, - image: MemoryImage(barcode!.image!), - fit: BoxFit.contain, - ), - ) - : const Center( + child: StreamBuilder( + stream: controller.barcodes, + builder: (context, snapshot) { + final barcode = snapshot.data; + + if (barcode == null) { + return const Center( child: Text( 'Your scanned barcode will appear here!', ), - ), + ); + } + + final barcodeImage = barcode.image; + + if (barcodeImage == null) { + return const Center( + child: Text('No image for this barcode.'), + ); + } + + return Image.memory( + barcodeImage, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return Center( + child: Text('Could not decode image bytes. $error'), + ); + }, + frameBuilder: ( + BuildContext context, + Widget child, + int? frame, + bool? wasSynchronouslyLoaded, + ) { + if (wasSynchronouslyLoaded == true || frame != null) { + return Transform.rotate( + angle: 90 * pi / 180, + child: child, + ); + } + + return const Center( + child: CircularProgressIndicator(), + ); + }, + ); + }, + ), ), Expanded( flex: 2, @@ -84,11 +101,6 @@ class _BarcodeScannerReturningImageState return ScannerErrorWidget(error: error); }, fit: BoxFit.contain, - onDetect: (barcode) { - setState(() { - this.barcode = barcode; - }); - }, ), Align( alignment: Alignment.bottomCenter, @@ -99,69 +111,18 @@ class _BarcodeScannerReturningImageState child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - IconButton( - color: Colors.white, - icon: ValueListenableBuilder( - valueListenable: controller.torchState, - builder: (context, state, child) { - switch (state) { - case TorchState.off: - return const Icon( - Icons.flash_off, - color: Colors.grey, - ); - case TorchState.on: - return const Icon( - Icons.flash_on, - color: Colors.yellow, - ); - } - }, - ), - iconSize: 32.0, - onPressed: () => controller.toggleTorch(), - ), - IconButton( - color: Colors.white, - icon: isStarted - ? const Icon(Icons.stop) - : const Icon(Icons.play_arrow), - iconSize: 32.0, - onPressed: _startOrStop, + ToggleFlashlightButton(controller: controller), + StartStopMobileScannerButton( + controller: controller, ), - Center( - child: SizedBox( - width: MediaQuery.of(context).size.width - 200, - height: 50, - child: FittedBox( - child: Text( - barcode?.barcodes.first.rawValue ?? - 'Scan something!', - overflow: TextOverflow.fade, - style: Theme.of(context) - .textTheme - .headlineMedium! - .copyWith(color: Colors.white), - ), + Expanded( + child: Center( + child: ScannedBarcodeLabel( + barcodes: controller.barcodes, ), ), ), - IconButton( - color: Colors.white, - icon: ValueListenableBuilder( - valueListenable: controller.cameraFacingState, - builder: (context, state, child) { - switch (state) { - case CameraFacing.front: - return const Icon(Icons.camera_front); - case CameraFacing.back: - return const Icon(Icons.camera_rear); - } - }, - ), - iconSize: 32.0, - onPressed: () => controller.switchCamera(), - ), + SwitchCameraButton(controller: controller), ], ), ), @@ -177,8 +138,8 @@ class _BarcodeScannerReturningImageState } @override - void dispose() { - controller.dispose(); + Future dispose() async { super.dispose(); + await controller.dispose(); } } diff --git a/example/lib/barcode_scanner_window.dart b/example/lib/barcode_scanner_window.dart index 974ff5053..dd52f618e 100644 --- a/example/lib/barcode_scanner_window.dart +++ b/example/lib/barcode_scanner_window.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:mobile_scanner_example/scanned_barcode_label.dart'; import 'package:mobile_scanner_example/scanner_error_widget.dart'; @@ -16,95 +17,120 @@ class BarcodeScannerWithScanWindow extends StatefulWidget { class _BarcodeScannerWithScanWindowState extends State { - late MobileScannerController controller = MobileScannerController(); - Barcode? barcode; - BarcodeCapture? capture; + final MobileScannerController controller = MobileScannerController(); - Future onDetect(BarcodeCapture barcode) async { - capture = barcode; - setState(() => this.barcode = barcode.barcodes.first); + @override + void initState() { + super.initState(); + + controller.start(); } - MobileScannerArguments? arguments; + Widget _buildBarcodeOverlay() { + return ValueListenableBuilder( + valueListenable: controller, + builder: (context, value, child) { + // Not ready. + if (!value.isInitialized || !value.isRunning || value.error != null) { + return const SizedBox(); + } + + return StreamBuilder( + stream: controller.barcodes, + builder: (context, snapshot) { + final BarcodeCapture? barcodeCapture = snapshot.data; + + // No barcode. + if (barcodeCapture == null || barcodeCapture.barcodes.isEmpty) { + return const SizedBox(); + } + + final scannedBarcode = barcodeCapture.barcodes.first; + + // No barcode corners, or size, or no camera preview size. + if (scannedBarcode.corners.isEmpty || + value.size.isEmpty || + barcodeCapture.size.isEmpty) { + return const SizedBox(); + } + + return CustomPaint( + painter: BarcodeOverlay( + barcodeCorners: scannedBarcode.corners, + barcodeSize: barcodeCapture.size, + boxFit: BoxFit.contain, + cameraPreviewSize: value.size, + ), + ); + }, + ); + }, + ); + } + + Widget _buildScanWindow(Rect scanWindowRect) { + return ValueListenableBuilder( + valueListenable: controller, + builder: (context, value, child) { + // Not ready. + if (!value.isInitialized || + !value.isRunning || + value.error != null || + value.size.isEmpty) { + return const SizedBox(); + } + + return CustomPaint( + painter: ScannerOverlay(scanWindowRect), + ); + }, + ); + } @override Widget build(BuildContext context) { final scanWindow = Rect.fromCenter( - center: MediaQuery.of(context).size.center(Offset.zero), + center: MediaQuery.sizeOf(context).center(Offset.zero), width: 200, height: 200, ); + return Scaffold( appBar: AppBar(title: const Text('With Scan window')), backgroundColor: Colors.black, - body: Builder( - builder: (context) { - return Stack( - fit: StackFit.expand, - children: [ - MobileScanner( - fit: BoxFit.contain, - scanWindow: scanWindow, - controller: controller, - onScannerStarted: (arguments) { - setState(() { - this.arguments = arguments; - }); - }, - errorBuilder: (context, error, child) { - return ScannerErrorWidget(error: error); - }, - onDetect: onDetect, - ), - if (barcode != null && - barcode?.corners != null && - arguments != null) - CustomPaint( - painter: BarcodeOverlay( - barcode: barcode!, - arguments: arguments!, - boxFit: BoxFit.contain, - capture: capture!, - ), - ), - CustomPaint( - painter: ScannerOverlay(scanWindow), - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - alignment: Alignment.bottomCenter, - height: 100, - color: Colors.black.withOpacity(0.4), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Center( - child: SizedBox( - width: MediaQuery.of(context).size.width - 120, - height: 50, - child: FittedBox( - child: Text( - barcode?.displayValue ?? 'Scan something!', - overflow: TextOverflow.fade, - style: Theme.of(context) - .textTheme - .headlineMedium! - .copyWith(color: Colors.white), - ), - ), - ), - ), - ], - ), - ), - ), - ], - ); - }, + body: Stack( + fit: StackFit.expand, + children: [ + MobileScanner( + fit: BoxFit.contain, + scanWindow: scanWindow, + controller: controller, + errorBuilder: (context, error, child) { + return ScannerErrorWidget(error: error); + }, + ), + _buildBarcodeOverlay(), + _buildScanWindow(scanWindow), + Align( + alignment: Alignment.bottomCenter, + child: Container( + alignment: Alignment.center, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + height: 100, + color: Colors.black.withOpacity(0.4), + child: ScannedBarcodeLabel(barcodes: controller.barcodes), + ), + ), + ], ), ); } + + @override + Future dispose() async { + super.dispose(); + await controller.dispose(); + } } class ScannerOverlay extends CustomPainter { @@ -114,6 +140,8 @@ class ScannerOverlay extends CustomPainter { @override void paint(Canvas canvas, Size size) { + // TODO: use `Offset.zero & size` instead of Rect.largest + // we need to pass the size to the custom paint widget final backgroundPath = Path()..addRect(Rect.largest); final cutoutPath = Path()..addRect(scanWindow); @@ -138,24 +166,26 @@ class ScannerOverlay extends CustomPainter { class BarcodeOverlay extends CustomPainter { BarcodeOverlay({ - required this.barcode, - required this.arguments, + required this.barcodeCorners, + required this.barcodeSize, required this.boxFit, - required this.capture, + required this.cameraPreviewSize, }); - final BarcodeCapture capture; - final Barcode barcode; - final MobileScannerArguments arguments; + final List barcodeCorners; + final Size barcodeSize; final BoxFit boxFit; + final Size cameraPreviewSize; @override void paint(Canvas canvas, Size size) { - if (barcode.corners.isEmpty) { + if (barcodeCorners.isEmpty || + barcodeSize.isEmpty || + cameraPreviewSize.isEmpty) { return; } - final adjustedSize = applyBoxFit(boxFit, arguments.size, size); + final adjustedSize = applyBoxFit(boxFit, cameraPreviewSize, size); double verticalPadding = size.height - adjustedSize.destination.height; double horizontalPadding = size.width - adjustedSize.destination.width; @@ -175,22 +205,21 @@ class BarcodeOverlay extends CustomPainter { final double ratioHeight; if (!kIsWeb && Platform.isIOS) { - ratioWidth = capture.size.width / adjustedSize.destination.width; - ratioHeight = capture.size.height / adjustedSize.destination.height; + ratioWidth = barcodeSize.width / adjustedSize.destination.width; + ratioHeight = barcodeSize.height / adjustedSize.destination.height; } else { - ratioWidth = arguments.size.width / adjustedSize.destination.width; - ratioHeight = arguments.size.height / adjustedSize.destination.height; + ratioWidth = cameraPreviewSize.width / adjustedSize.destination.width; + ratioHeight = cameraPreviewSize.height / adjustedSize.destination.height; } - final List adjustedOffset = []; - for (final offset in barcode.corners) { - adjustedOffset.add( + final List adjustedOffset = [ + for (final offset in barcodeCorners) Offset( offset.dx / ratioWidth + horizontalPadding, offset.dy / ratioHeight + verticalPadding, ), - ); - } + ]; + final cutoutPath = Path()..addPolygon(adjustedOffset, true); final backgroundPaint = Paint() diff --git a/example/lib/barcode_scanner_without_controller.dart b/example/lib/barcode_scanner_without_controller.dart deleted file mode 100644 index fe4c84252..000000000 --- a/example/lib/barcode_scanner_without_controller.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:mobile_scanner/mobile_scanner.dart'; -import 'package:mobile_scanner_example/scanner_error_widget.dart'; - -class BarcodeScannerWithoutController extends StatefulWidget { - const BarcodeScannerWithoutController({super.key}); - - @override - State createState() => - _BarcodeScannerWithoutControllerState(); -} - -class _BarcodeScannerWithoutControllerState - extends State - with SingleTickerProviderStateMixin { - BarcodeCapture? capture; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Without controller')), - backgroundColor: Colors.black, - body: Builder( - builder: (context) { - return Stack( - children: [ - MobileScanner( - fit: BoxFit.contain, - errorBuilder: (context, error, child) { - return ScannerErrorWidget(error: error); - }, - onDetect: (capture) { - setState(() { - this.capture = capture; - }); - }, - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - alignment: Alignment.bottomCenter, - height: 100, - color: Colors.black.withOpacity(0.4), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Center( - child: SizedBox( - width: MediaQuery.of(context).size.width - 120, - height: 50, - child: FittedBox( - child: Text( - capture?.barcodes.first.rawValue ?? - 'Scan something!', - overflow: TextOverflow.fade, - style: Theme.of(context) - .textTheme - .headlineMedium! - .copyWith(color: Colors.white), - ), - ), - ), - ), - ], - ), - ), - ), - ], - ); - }, - ), - ); - } -} diff --git a/example/lib/barcode_scanner_zoom.dart b/example/lib/barcode_scanner_zoom.dart index 665968b2d..4cd36ecb6 100644 --- a/example/lib/barcode_scanner_zoom.dart +++ b/example/lib/barcode_scanner_zoom.dart @@ -1,7 +1,11 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:image_picker/image_picker.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:mobile_scanner_example/scanned_barcode_label.dart'; +import 'package:mobile_scanner_example/scanner_button_widgets.dart'; import 'package:mobile_scanner_example/scanner_error_widget.dart'; class BarcodeScannerWithZoom extends StatefulWidget { @@ -11,195 +15,115 @@ class BarcodeScannerWithZoom extends StatefulWidget { State createState() => _BarcodeScannerWithZoomState(); } -class _BarcodeScannerWithZoomState extends State - with SingleTickerProviderStateMixin { - BarcodeCapture? barcode; - - MobileScannerController controller = MobileScannerController( +class _BarcodeScannerWithZoomState extends State { + final MobileScannerController controller = MobileScannerController( torchEnabled: true, ); - bool isStarted = true; double _zoomFactor = 0.0; + @override + void initState() { + super.initState(); + controller.start(); + } + + Widget _buildZoomScaleSlider() { + return ValueListenableBuilder( + valueListenable: controller, + builder: (context, state, child) { + if (!state.isInitialized || !state.isRunning) { + return const SizedBox.shrink(); + } + + final TextStyle labelStyle = Theme.of(context) + .textTheme + .headlineMedium! + .copyWith(color: Colors.white); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + children: [ + Text( + '0%', + overflow: TextOverflow.fade, + style: labelStyle, + ), + Expanded( + child: Slider( + value: _zoomFactor, + onChanged: (value) { + setState(() { + _zoomFactor = value; + controller.setZoomScale(value); + }); + }, + ), + ), + Text( + '100%', + overflow: TextOverflow.fade, + style: labelStyle, + ), + ], + ), + ); + }, + ); + } + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('With zoom slider')), backgroundColor: Colors.black, - body: Builder( - builder: (context) { - return Stack( - children: [ - MobileScanner( - controller: controller, - fit: BoxFit.contain, - errorBuilder: (context, error, child) { - return ScannerErrorWidget(error: error); - }, - onDetect: (barcode) { - setState(() { - this.barcode = barcode; - }); - }, - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - alignment: Alignment.bottomCenter, - height: 100, - color: Colors.black.withOpacity(0.4), - child: Column( + body: Stack( + children: [ + MobileScanner( + controller: controller, + fit: BoxFit.contain, + errorBuilder: (context, error, child) { + return ScannerErrorWidget(error: error); + }, + ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + alignment: Alignment.bottomCenter, + height: 100, + color: Colors.black.withOpacity(0.4), + child: Column( + children: [ + if (!kIsWeb) _buildZoomScaleSlider(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Row( - children: [ - Text( - "0%", - overflow: TextOverflow.fade, - style: Theme.of(context) - .textTheme - .headlineMedium! - .copyWith(color: Colors.white), - ), - Expanded( - child: Slider( - max: 100, - divisions: 100, - value: _zoomFactor, - label: "${_zoomFactor.round()} %", - onChanged: (value) { - setState(() { - _zoomFactor = value; - controller.setZoomScale(value); - }); - }, - ), - ), - Text( - "100%", - overflow: TextOverflow.fade, - style: Theme.of(context) - .textTheme - .headlineMedium! - .copyWith(color: Colors.white), - ), - ], - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - IconButton( - color: Colors.white, - icon: ValueListenableBuilder( - valueListenable: controller.torchState, - builder: (context, state, child) { - switch (state) { - case TorchState.off: - return const Icon( - Icons.flash_off, - color: Colors.grey, - ); - case TorchState.on: - return const Icon( - Icons.flash_on, - color: Colors.yellow, - ); - } - }, - ), - iconSize: 32.0, - onPressed: () => controller.toggleTorch(), - ), - IconButton( - color: Colors.white, - icon: isStarted - ? const Icon(Icons.stop) - : const Icon(Icons.play_arrow), - iconSize: 32.0, - onPressed: () => setState(() { - isStarted - ? controller.stop() - : controller.start(); - isStarted = !isStarted; - }), - ), - Center( - child: SizedBox( - width: MediaQuery.of(context).size.width - 200, - height: 50, - child: FittedBox( - child: Text( - barcode?.barcodes.first.rawValue ?? - 'Scan something!', - overflow: TextOverflow.fade, - style: Theme.of(context) - .textTheme - .headlineMedium! - .copyWith(color: Colors.white), - ), - ), - ), - ), - IconButton( - color: Colors.white, - icon: ValueListenableBuilder( - valueListenable: controller.cameraFacingState, - builder: (context, state, child) { - switch (state) { - case CameraFacing.front: - return const Icon(Icons.camera_front); - case CameraFacing.back: - return const Icon(Icons.camera_rear); - } - }, - ), - iconSize: 32.0, - onPressed: () => controller.switchCamera(), + ToggleFlashlightButton(controller: controller), + StartStopMobileScannerButton(controller: controller), + Expanded( + child: Center( + child: ScannedBarcodeLabel( + barcodes: controller.barcodes, ), - IconButton( - color: Colors.white, - icon: const Icon(Icons.image), - iconSize: 32.0, - onPressed: () async { - final ImagePicker picker = ImagePicker(); - // Pick an image - final XFile? image = await picker.pickImage( - source: ImageSource.gallery, - ); - if (image != null) { - if (await controller.analyzeImage(image.path)) { - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Barcode found!'), - backgroundColor: Colors.green, - ), - ); - } else { - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('No barcode found!'), - backgroundColor: Colors.red, - ), - ); - } - } - }, - ), - ], + ), ), + SwitchCameraButton(controller: controller), + AnalyzeImageFromGalleryButton(controller: controller), ], ), - ), + ], ), - ], - ); - }, + ), + ), + ], ), ); } + + @override + Future dispose() async { + super.dispose(); + await controller.dispose(); + } } diff --git a/example/lib/main.dart b/example/lib/main.dart index 5c0fdf6e9..082e3395e 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,14 +1,20 @@ import 'package:flutter/material.dart'; -import 'package:mobile_scanner_example/barcode_list_scanner_controller.dart'; import 'package:mobile_scanner_example/barcode_scanner_controller.dart'; +import 'package:mobile_scanner_example/barcode_scanner_listview.dart'; import 'package:mobile_scanner_example/barcode_scanner_pageview.dart'; import 'package:mobile_scanner_example/barcode_scanner_returning_image.dart'; import 'package:mobile_scanner_example/barcode_scanner_window.dart'; -import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart'; import 'package:mobile_scanner_example/barcode_scanner_zoom.dart'; import 'package:mobile_scanner_example/mobile_scanner_overlay.dart'; -void main() => runApp(const MaterialApp(home: MyHome())); +void main() { + runApp( + const MaterialApp( + title: 'Mobile Scanner Example', + home: MyHome(), + ), + ); +} class MyHome extends StatelessWidget { const MyHome({super.key}); @@ -16,23 +22,20 @@ class MyHome extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Flutter Demo Home Page')), - body: SizedBox( - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height, + appBar: AppBar(title: const Text('Mobile Scanner Example')), + body: Center( child: Column( - mainAxisAlignment: MainAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ ElevatedButton( onPressed: () { Navigator.of(context).push( MaterialPageRoute( - builder: (context) => - const BarcodeListScannerWithController(), + builder: (context) => const BarcodeScannerListView(), ), ); }, - child: const Text('MobileScanner with List Controller'), + child: const Text('MobileScanner with ListView'), ), ElevatedButton( onPressed: () { @@ -62,19 +65,9 @@ class MyHome extends StatelessWidget { ), ); }, - child: - const Text('MobileScanner with Controller (returning image)'), - ), - ElevatedButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => - const BarcodeScannerWithoutController(), - ), - ); - }, - child: const Text('MobileScanner without Controller'), + child: const Text( + 'MobileScanner with Controller (returning image)', + ), ), ElevatedButton( onPressed: () { diff --git a/example/lib/mobile_scanner_overlay.dart b/example/lib/mobile_scanner_overlay.dart index cff099b38..922a63409 100644 --- a/example/lib/mobile_scanner_overlay.dart +++ b/example/lib/mobile_scanner_overlay.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:mobile_scanner_example/scanned_barcode_label.dart'; +import 'package:mobile_scanner_example/scanner_button_widgets.dart'; import 'package:mobile_scanner_example/scanner_error_widget.dart'; class BarcodeScannerWithOverlay extends StatefulWidget { @@ -9,174 +11,105 @@ class BarcodeScannerWithOverlay extends StatefulWidget { } class _BarcodeScannerWithOverlayState extends State { - String overlayText = "Please scan QR Code"; - bool camStarted = false; - final MobileScannerController controller = MobileScannerController( formats: const [BarcodeFormat.qrCode], - autoStart: false, ); @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - void startCamera() { - if (camStarted) { - return; - } - - controller.start().then((_) { - if (mounted) { - setState(() { - camStarted = true; - }); - } - }).catchError((Object error, StackTrace stackTrace) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Something went wrong! $error'), - backgroundColor: Colors.red, - ), - ); - } - }); - } - - void onBarcodeDetect(BarcodeCapture barcodeCapture) { - final barcode = barcodeCapture.barcodes.last; - setState(() { - overlayText = barcodeCapture.barcodes.last.displayValue ?? - barcode.rawValue ?? - 'Barcode has no displayable value'; - }); + void initState() { + super.initState(); + controller.start(); } @override Widget build(BuildContext context) { final scanWindow = Rect.fromCenter( - center: MediaQuery.of(context).size.center(Offset.zero), + center: MediaQuery.sizeOf(context).center(Offset.zero), width: 200, height: 200, ); return Scaffold( + backgroundColor: Colors.black, appBar: AppBar( title: const Text('Scanner with Overlay Example app'), ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: camStarted - ? Stack( - fit: StackFit.expand, - children: [ - Center( - child: MobileScanner( - fit: BoxFit.contain, - onDetect: onBarcodeDetect, - overlay: Padding( - padding: const EdgeInsets.all(16.0), - child: Align( - alignment: Alignment.bottomCenter, - child: Opacity( - opacity: 0.7, - child: Text( - overlayText, - style: const TextStyle( - backgroundColor: Colors.black26, - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 24, - overflow: TextOverflow.ellipsis, - ), - maxLines: 1, - ), - ), - ), - ), - controller: controller, - scanWindow: scanWindow, - errorBuilder: (context, error, child) { - return ScannerErrorWidget(error: error); - }, - ), - ), - CustomPaint( - painter: ScannerOverlay(scanWindow), - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: Align( - alignment: Alignment.bottomCenter, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - ValueListenableBuilder( - valueListenable: controller.torchState, - builder: (context, value, child) { - final Color iconColor; - - switch (value) { - case TorchState.off: - iconColor = Colors.black; - case TorchState.on: - iconColor = Colors.yellow; - } - - return IconButton( - onPressed: () => controller.toggleTorch(), - icon: Icon( - Icons.flashlight_on, - color: iconColor, - ), - ); - }, - ), - IconButton( - onPressed: () => controller.switchCamera(), - icon: const Icon( - Icons.cameraswitch_rounded, - color: Colors.white, - ), - ), - ], - ), - ), - ), - ], - ) - : const Center( - child: Text("Tap on Camera to activate QR Scanner"), - ), + body: Stack( + fit: StackFit.expand, + children: [ + Center( + child: MobileScanner( + fit: BoxFit.contain, + controller: controller, + scanWindow: scanWindow, + errorBuilder: (context, error, child) { + return ScannerErrorWidget(error: error); + }, + overlayBuilder: (context, constraints) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Align( + alignment: Alignment.bottomCenter, + child: ScannedBarcodeLabel(barcodes: controller.barcodes), + ), + ); + }, ), - ], - ), - ), - floatingActionButton: camStarted - ? null - : FloatingActionButton( - onPressed: startCamera, - child: const Icon(Icons.camera_alt), + ), + ValueListenableBuilder( + valueListenable: controller, + builder: (context, value, child) { + if (!value.isInitialized || + !value.isRunning || + value.error != null) { + return const SizedBox(); + } + + return CustomPaint( + painter: ScannerOverlay(scanWindow: scanWindow), + ); + }, + ), + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ToggleFlashlightButton(controller: controller), + SwitchCameraButton(controller: controller), + ], + ), ), + ), + ], + ), ); } + + @override + Future dispose() async { + super.dispose(); + await controller.dispose(); + } } class ScannerOverlay extends CustomPainter { - ScannerOverlay(this.scanWindow); + const ScannerOverlay({ + required this.scanWindow, + this.borderRadius = 12.0, + }); final Rect scanWindow; - final double borderRadius = 12.0; + final double borderRadius; @override void paint(Canvas canvas, Size size) { + // TODO: use `Offset.zero & size` instead of Rect.largest + // we need to pass the size to the custom paint widget final backgroundPath = Path()..addRect(Rect.largest); + final cutoutPath = Path() ..addRRect( RRect.fromRectAndCorners( @@ -199,14 +132,11 @@ class ScannerOverlay extends CustomPainter { cutoutPath, ); - // Create a Paint object for the white border final borderPaint = Paint() ..color = Colors.white ..style = PaintingStyle.stroke - ..strokeWidth = 4.0; // Adjust the border width as needed + ..strokeWidth = 4.0; - // Calculate the border rectangle with rounded corners -// Adjust the radius as needed final borderRect = RRect.fromRectAndCorners( scanWindow, topLeft: Radius.circular(borderRadius), @@ -215,13 +145,16 @@ class ScannerOverlay extends CustomPainter { bottomRight: Radius.circular(borderRadius), ); - // Draw the white border + // First, draw the background, + // with a cutout area that is a bit larger than the scan window. + // Finally, draw the scan window itself. canvas.drawPath(backgroundWithCutout, backgroundPaint); canvas.drawRRect(borderRect, borderPaint); } @override - bool shouldRepaint(covariant CustomPainter oldDelegate) { - return false; + bool shouldRepaint(ScannerOverlay oldDelegate) { + return scanWindow != oldDelegate.scanWindow || + borderRadius != oldDelegate.borderRadius; } } diff --git a/example/lib/scanned_barcode_label.dart b/example/lib/scanned_barcode_label.dart new file mode 100644 index 000000000..c7f168f6d --- /dev/null +++ b/example/lib/scanned_barcode_label.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; + +class ScannedBarcodeLabel extends StatelessWidget { + const ScannedBarcodeLabel({ + super.key, + required this.barcodes, + }); + + final Stream barcodes; + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: barcodes, + builder: (context, snaphot) { + final scannedBarcodes = snaphot.data?.barcodes ?? []; + + if (scannedBarcodes.isEmpty) { + return const Text( + 'Scan something!', + overflow: TextOverflow.fade, + style: TextStyle(color: Colors.white), + ); + } + + return Text( + scannedBarcodes.first.displayValue ?? 'No display value.', + overflow: TextOverflow.fade, + style: const TextStyle(color: Colors.white), + ); + }, + ); + } +} diff --git a/example/lib/scanner_button_widgets.dart b/example/lib/scanner_button_widgets.dart new file mode 100644 index 000000000..427441cdc --- /dev/null +++ b/example/lib/scanner_button_widgets.dart @@ -0,0 +1,168 @@ +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; + +class AnalyzeImageFromGalleryButton extends StatelessWidget { + const AnalyzeImageFromGalleryButton({required this.controller, super.key}); + + final MobileScannerController controller; + + @override + Widget build(BuildContext context) { + return IconButton( + color: Colors.white, + icon: const Icon(Icons.image), + iconSize: 32.0, + onPressed: () async { + final ImagePicker picker = ImagePicker(); + + final XFile? image = await picker.pickImage( + source: ImageSource.gallery, + ); + + if (image == null) { + return; + } + + final BarcodeCapture? barcodes = await controller.analyzeImage( + image.path, + ); + + if (!context.mounted) { + return; + } + + final SnackBar snackbar = barcodes != null + ? const SnackBar( + content: Text('Barcode found!'), + backgroundColor: Colors.green, + ) + : const SnackBar( + content: Text('No barcode found!'), + backgroundColor: Colors.red, + ); + + ScaffoldMessenger.of(context).showSnackBar(snackbar); + }, + ); + } +} + +class StartStopMobileScannerButton extends StatelessWidget { + const StartStopMobileScannerButton({required this.controller, super.key}); + + final MobileScannerController controller; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: controller, + builder: (context, state, child) { + if (!state.isInitialized || !state.isRunning) { + return IconButton( + color: Colors.white, + icon: const Icon(Icons.play_arrow), + iconSize: 32.0, + onPressed: () async { + await controller.start(); + }, + ); + } + + return IconButton( + color: Colors.white, + icon: const Icon(Icons.stop), + iconSize: 32.0, + onPressed: () async { + await controller.stop(); + }, + ); + }, + ); + } +} + +class SwitchCameraButton extends StatelessWidget { + const SwitchCameraButton({required this.controller, super.key}); + + final MobileScannerController controller; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: controller, + builder: (context, state, child) { + if (!state.isInitialized || !state.isRunning) { + return const SizedBox.shrink(); + } + + final int? availableCameras = state.availableCameras; + + if (availableCameras != null && availableCameras < 2) { + return const SizedBox.shrink(); + } + + final Widget icon; + + switch (state.cameraDirection) { + case CameraFacing.front: + icon = const Icon(Icons.camera_front); + case CameraFacing.back: + icon = const Icon(Icons.camera_rear); + } + + return IconButton( + iconSize: 32.0, + icon: icon, + onPressed: () async { + await controller.switchCamera(); + }, + ); + }, + ); + } +} + +class ToggleFlashlightButton extends StatelessWidget { + const ToggleFlashlightButton({required this.controller, super.key}); + + final MobileScannerController controller; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: controller, + builder: (context, state, child) { + if (!state.isInitialized || !state.isRunning) { + return const SizedBox.shrink(); + } + + switch (state.torchState) { + case TorchState.off: + return IconButton( + color: Colors.white, + iconSize: 32.0, + icon: const Icon(Icons.flash_off), + onPressed: () async { + await controller.toggleTorch(); + }, + ); + case TorchState.on: + return IconButton( + color: Colors.white, + iconSize: 32.0, + icon: const Icon(Icons.flash_on), + onPressed: () async { + await controller.toggleTorch(); + }, + ); + case TorchState.unavailable: + return const Icon( + Icons.no_flash, + color: Colors.grey, + ); + } + }, + ); + } +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 1faac68b9..cffa3350c 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -6,8 +6,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 0.0.1 environment: - sdk: ">=3.1.0 <4.0.0" - flutter: ">=3.13.0" + sdk: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions diff --git a/example/web/index.html b/example/web/index.html index bc900fb63..5beb6ff8e 100644 --- a/example/web/index.html +++ b/example/web/index.html @@ -40,7 +40,6 @@ -