diff --git a/.github/workflows/flutter.yml b/.github/workflows/flutter.yml index 748295bfb..0331a4375 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.22' + flutter-version: '3.24' channel: 'stable' - name: Version run: flutter doctor -v diff --git a/CHANGELOG.md b/CHANGELOG.md index f91bdd079..8fd449106 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,34 +1,51 @@ ## NEXT -- Fixed an issue which caused the scanWindow to always be present if provided, even when scanWindow is updated to null -- Integrated basic barcode overlay and scanner overlay into the package. -- Made updateScanWindow private, because logic within the MobileScanner widget is needed in order to pass a correct scanWindow. -The scanWindow can be updated by directly changing the scanWindow in the MobileScanner widget. + +**BREAKING CHANGES:** + +* The `updateScanWindow` method is now private. Instead, update the scan window in the `MobileScanner` widget directly. +* The deprecated `EncryptionType.none` constant has been removed. Use `EncryptionType.unknown` instead. + +Bugs fixed: +* [Apple] Fixed an issue which caused the scanWindow to always be present, even when reset to no value. +* [Apple] Fixed an issue that caused the barcode size to report the wrong height. +* [Apple] Fixed a bug that caused the corner points to not be returned in clockwise orientation. + +Improvements: +* Added a basic barcode overlay widget, for use with the camera preview. +* Added a basic scan window overlay widget, for use with the camera preview. +* Update the bundled MLKit model for Android to version `17.3.0`. +* Added documentation in places where it was missing. +* Added `color` and `style` properties to the `BarcodePainter` widget. ## 7.0.0-beta.3 -Fix build issues on macOS + +* Fixed a build issue on macOS. ## 7.0.0-beta.2 +Bugs fixed: +* [Apple] Fixed an issue with the zoom slider being non-functional. +* [Apple] Fixed an issue where the flash would briefly show when the camera is turned on. +* [Apple] Fixed an issue that prevented the scan window from working. +* [Apple] Fixed an issue that caused the barcode overlay to use the wrong dimensions. + Improvements: * [iOS] Adds support for Swift Package Manager. -* [Apple] Fixes zoom slider -* Fixed torch at start not working -* Fixed scanWindow not being correct -* Fixed barcode overlay not being correct Known issues: -* BoxFit.cover & BoxFit.fitHeight produces wrong width in barcodeOverlay +* BoxFit.cover & BoxFit.fitHeight produce the wrong width in the barcode overlay. ## 7.0.0-beta.1 -This version replaces MLKit on iOS with Apple's Vision API and merges the iOS and MacOS sources. -The requirement for the minimum iOS version has been relaxed back down to iOS 12.0. +Improvements: +* [iOS] Migrate to the Vision API. +* [iOS] Updated the minimum iOS version back down to 12.0. +* [Apple] Merged the iOS and MacOS sources. -There are still some problems with this build. -* Zoom slider not working -* scanWindow not working -* Flash shows briefly when starting scanner. -* Other issues, not fully tested yet. +Known issues: +* [Apple] The zoom slider does not work correctly. +* [Apple] The scan window does not work correctly. +* [Apple] The camera flash briefly shows when the camera is started. ## 6.0.2 diff --git a/analysis_options.yaml b/analysis_options.yaml index abd9f6a85..3e6b29b12 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -3,6 +3,8 @@ include: package:lint/analysis_options_package.yaml linter: rules: - combinators_ordering + - comment_references - require_trailing_commas - unnecessary_library_directive - - prefer_single_quotes \ No newline at end of file + - prefer_single_quotes + - public_member_api_docs \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 6b7c31872..d9332f267 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -70,7 +70,7 @@ dependencies { implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.3.1' } else { // Bundled model in app - implementation 'com.google.mlkit:barcode-scanning:17.2.0' + implementation 'com.google.mlkit:barcode-scanning:17.3.0' } // org.jetbrains.kotlin:kotlin-bom artifact purpose is to align kotlin stdlib and related code versions. diff --git a/darwin/mobile_scanner/Sources/mobile_scanner/MobileScannerPlugin.swift b/darwin/mobile_scanner/Sources/mobile_scanner/MobileScannerPlugin.swift index 8cf687521..e8d7c0bae 100644 --- a/darwin/mobile_scanner/Sources/mobile_scanner/MobileScannerPlugin.swift +++ b/darwin/mobile_scanner/Sources/mobile_scanner/MobileScannerPlugin.swift @@ -177,11 +177,11 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, }) DispatchQueue.main.async { + // If the image is nil, use zero as the size. guard let image = cgImage else { - // Image not known, default image size to 1 self?.sink?([ "name": "barcode", - "data": barcodes.map({ $0.toMap(width: 1, height: 1) }), + "data": barcodes.map({ $0.toMap(imageWidth: 0, imageHeight: 0) }), ]) return } @@ -194,10 +194,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, "height": Double(image.height), ] - self?.sink?([ "name": "barcode", - "data": barcodes.map({ $0.toMap(width: image.width, height: image.height) }), + "data": barcodes.map({ $0.toMap(imageWidth: image.width, imageHeight: image.height) }), "image": imageData, ]) } @@ -473,7 +472,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, device.torchMode = .on device.unlockForConfiguration() } catch(_) { - + // Do nothing. } } @@ -526,7 +525,6 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, /// Set the zoom factor of the camera func setScaleInternal(_ scale: CGFloat) throws { - if (device == nil) { throw MobileScannerError.zoomWhenStopped } @@ -545,10 +543,8 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, actualScale = min(maxZoomFactor, actualScale) // Limit to 1.0 scale - device.videoZoomFactor = actualScale - device.unlockForConfiguration() #endif } catch { @@ -620,7 +616,9 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, try device.lockForConfiguration() device.torchMode = newTorchMode device.unlockForConfiguration() - } catch(_) {} + } catch(_) { + // Do nothing. + } result(nil) } @@ -690,7 +688,7 @@ public class MobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, result([ "name": "barcode", - "data": barcodes.map({ $0.toMap(width: 1, height: 1) }), + "data": barcodes.map({ $0.toMap(imageWidth: Int(ciImage.extent.width), imageHeight: Int(ciImage.extent.height)) }), ]) }) @@ -803,28 +801,32 @@ extension VNBarcodeObservation { return sqrt(pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2)) } - public func toMap(width: Int, height: Int) -> [String: Any?] { - let topLeftX = topLeft.x * CGFloat(width) - let topRightX = topRight.x * CGFloat(width) - let bottomRightX = bottomRight.x * CGFloat(width) - let bottomLeftX = bottomLeft.x * CGFloat(width) - let topLeftY = (1 - topLeft.y) * CGFloat(height) - let topRightY = (1 - topRight.y) * CGFloat(height) - let bottomRightY = (1 - bottomRight.y) * CGFloat(height) - let bottomLeftY = (1 - bottomLeft.y) * CGFloat(height) + /// Map this `VNBarcodeObservation` to a dictionary. + /// + /// The `imageWidth` and `imageHeight` indicate the width and height of the input image that contains this observation. + public func toMap(imageWidth: Int, imageHeight: Int) -> [String: Any?] { + let topLeftX = topLeft.x * CGFloat(imageWidth) + let topRightX = topRight.x * CGFloat(imageWidth) + let bottomRightX = bottomRight.x * CGFloat(imageWidth) + let bottomLeftX = bottomLeft.x * CGFloat(imageWidth) + let topLeftY = (1 - topLeft.y) * CGFloat(imageHeight) + let topRightY = (1 - topRight.y) * CGFloat(imageHeight) + let bottomRightY = (1 - bottomRight.y) * CGFloat(imageHeight) + let bottomLeftY = (1 - bottomLeft.y) * CGFloat(imageHeight) let data = [ + // Clockwise, starting from the top-left corner. "corners": [ - ["x": bottomLeftX, "y": bottomLeftY], ["x": topLeftX, "y": topLeftY], ["x": topRightX, "y": topRightY], ["x": bottomRightX, "y": bottomRightY], + ["x": bottomLeftX, "y": bottomLeftY], ], "format": symbology.toInt ?? -1, "rawValue": payloadStringValue ?? "", "displayValue": payloadStringValue ?? "", "size": [ - "width": distanceBetween(topLeft, topRight) * CGFloat(width), - "height": distanceBetween(topLeft, bottomLeft) * CGFloat(width), + "width": distanceBetween(topLeft, topRight) * CGFloat(imageWidth), + "height": distanceBetween(topLeft, bottomLeft) * CGFloat(imageHeight), ], ] as [String : Any] return data diff --git a/example/lib/barcode_scanner_controller.dart b/example/lib/barcode_scanner_controller.dart index 51643a45b..7f906c6ca 100644 --- a/example/lib/barcode_scanner_controller.dart +++ b/example/lib/barcode_scanner_controller.dart @@ -21,8 +21,6 @@ class _BarcodeScannerWithControllerState torchEnabled: true, ); - StreamSubscription? _subscription; - @override void initState() { super.initState(); @@ -44,8 +42,6 @@ class _BarcodeScannerWithControllerState case AppLifecycleState.resumed: unawaited(controller.start()); case AppLifecycleState.inactive: - unawaited(_subscription?.cancel()); - _subscription = null; unawaited(controller.stop()); } } @@ -96,8 +92,6 @@ class _BarcodeScannerWithControllerState @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_window.dart b/example/lib/barcode_scanner_window.dart index 0f9002228..9ff493adb 100644 --- a/example/lib/barcode_scanner_window.dart +++ b/example/lib/barcode_scanner_window.dart @@ -44,7 +44,7 @@ class _BarcodeScannerWithScanWindowState }, ), BarcodeOverlay(controller: controller, boxFit: boxFit), - ScannerOverlay( + ScanWindowOverlay( scanWindow: scanWindow, controller: controller, ), diff --git a/example/lib/mobile_scanner_overlay.dart b/example/lib/mobile_scanner_overlay.dart index ae188cb35..402736785 100644 --- a/example/lib/mobile_scanner_overlay.dart +++ b/example/lib/mobile_scanner_overlay.dart @@ -57,12 +57,14 @@ class _BarcodeScannerWithOverlayState extends State { builder: (context, value, child) { if (!value.isInitialized || !value.isRunning || - value.error != null) { + value.error != null || + scanWindow.isEmpty) { return const SizedBox(); } - return CustomPaint( - painter: ScannerOverlay(scanWindow: scanWindow), + return ScanWindowOverlay( + controller: controller, + scanWindow: scanWindow, ); }, ), @@ -90,67 +92,3 @@ class _BarcodeScannerWithOverlayState extends State { await controller.dispose(); } } - -class ScannerOverlay extends CustomPainter { - const ScannerOverlay({ - required this.scanWindow, - this.borderRadius = 12.0, - }); - - final Rect scanWindow; - 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( - scanWindow, - topLeft: Radius.circular(borderRadius), - topRight: Radius.circular(borderRadius), - bottomLeft: Radius.circular(borderRadius), - bottomRight: Radius.circular(borderRadius), - ), - ); - - final backgroundPaint = Paint() - ..color = Colors.black.withOpacity(0.5) - ..style = PaintingStyle.fill - ..blendMode = BlendMode.dstOut; - - final backgroundWithCutout = Path.combine( - PathOperation.difference, - backgroundPath, - cutoutPath, - ); - - final borderPaint = Paint() - ..color = Colors.white - ..style = PaintingStyle.stroke - ..strokeWidth = 4.0; - - final borderRect = RRect.fromRectAndCorners( - scanWindow, - topLeft: Radius.circular(borderRadius), - topRight: Radius.circular(borderRadius), - bottomLeft: Radius.circular(borderRadius), - bottomRight: Radius.circular(borderRadius), - ); - - // 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(ScannerOverlay oldDelegate) { - return scanWindow != oldDelegate.scanWindow || - borderRadius != oldDelegate.borderRadius; - } -} diff --git a/lib/mobile_scanner.dart b/lib/mobile_scanner.dart index 706984ef9..0473e4c45 100644 --- a/lib/mobile_scanner.dart +++ b/lib/mobile_scanner.dart @@ -29,5 +29,5 @@ export 'src/objects/url_bookmark.dart'; export 'src/objects/wifi.dart'; export 'src/overlay/barcode_overlay.dart'; export 'src/overlay/barcode_painter.dart'; -export 'src/overlay/scanner_overlay.dart'; -export 'src/overlay/scanner_painter.dart'; +export 'src/overlay/scan_window_overlay.dart'; +export 'src/overlay/scan_window_painter.dart'; diff --git a/lib/src/enums/encryption_type.dart b/lib/src/enums/encryption_type.dart index 65624591b..05db6755c 100644 --- a/lib/src/enums/encryption_type.dart +++ b/lib/src/enums/encryption_type.dart @@ -14,11 +14,6 @@ enum EncryptionType { const EncryptionType(this.rawValue); - @Deprecated( - 'EncryptionType.none is deprecated. Use EncryptionType.unknown instead.', - ) - static const EncryptionType none = EncryptionType.unknown; - factory EncryptionType.fromRawValue(int value) { switch (value) { case 0: diff --git a/lib/src/method_channel/mobile_scanner_method_channel.dart b/lib/src/method_channel/mobile_scanner_method_channel.dart index 793492bf3..2807f54f1 100644 --- a/lib/src/method_channel/mobile_scanner_method_channel.dart +++ b/lib/src/method_channel/mobile_scanner_method_channel.dart @@ -38,6 +38,7 @@ class MethodChannelMobileScanner extends MobileScannerPlatform { Stream>? _eventsStream; + /// Get the event stream of barcode events that come from the [eventChannel]. Stream> get eventsStream { _eventsStream ??= eventChannel.receiveBroadcastStream().cast>(); diff --git a/lib/src/mobile_scanner_controller.dart b/lib/src/mobile_scanner_controller.dart index 8ea6a7b47..b8d5fdc15 100644 --- a/lib/src/mobile_scanner_controller.dart +++ b/lib/src/mobile_scanner_controller.dart @@ -1,3 +1,6 @@ +/// @docImport 'package:mobile_scanner/src/mobile_scanner.dart'; +library; + import 'dart:async'; import 'package:flutter/widgets.dart'; diff --git a/lib/src/mobile_scanner_exception.dart b/lib/src/mobile_scanner_exception.dart index 52fac113f..6d93bdb3d 100644 --- a/lib/src/mobile_scanner_exception.dart +++ b/lib/src/mobile_scanner_exception.dart @@ -1,7 +1,12 @@ +/// @docImport 'package:flutter/services.dart'; +/// @docImport 'package:mobile_scanner/src/mobile_scanner_controller.dart'; +library; + import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; /// This class represents an exception thrown by the [MobileScannerController]. class MobileScannerException implements Exception { + /// Construct a new [MobileScannerException] instance. const MobileScannerException({ required this.errorCode, this.errorDetails, @@ -24,6 +29,7 @@ class MobileScannerException implements Exception { /// The raw error details for a [MobileScannerException]. class MobileScannerErrorDetails { + /// Construct a new [MobileScannerErrorDetails] instance. const MobileScannerErrorDetails({ this.code, this.details, diff --git a/lib/src/mobile_scanner_view_attributes.dart b/lib/src/mobile_scanner_view_attributes.dart index d203fd2ba..d5211be24 100644 --- a/lib/src/mobile_scanner_view_attributes.dart +++ b/lib/src/mobile_scanner_view_attributes.dart @@ -4,6 +4,7 @@ import 'package:mobile_scanner/src/enums/torch_state.dart'; /// This class defines the attributes for the mobile scanner view. class MobileScannerViewAttributes { + /// Construct a new [MobileScannerViewAttributes] instance. const MobileScannerViewAttributes({ required this.currentTorchMode, this.numberOfCameras, diff --git a/lib/src/objects/barcode.dart b/lib/src/objects/barcode.dart index 907756be7..ea9aa03dc 100644 --- a/lib/src/objects/barcode.dart +++ b/lib/src/objects/barcode.dart @@ -102,10 +102,16 @@ class Barcode { /// The contact information that is embedded in the barcode. final ContactInfo? contactInfo; - /// The four corner points of the barcode, - /// in clockwise order, starting with the top-left point. + /// The corner points of the barcode. /// - /// Due to the possible perspective distortions, this is not necessarily a rectangle. + /// On Android, iOS and MacOS, this is a list of four points, + /// in clockwise direction, starting with the top left. + /// + /// On the web, the amount of points and their order + /// is dependent on the type of barcode that was detected. + /// + /// Due to the possible perspective distortions, + /// the points do not necessarily form a rectangle. /// /// This list is empty if the corners can not be determined. final List corners; diff --git a/lib/src/objects/mobile_scanner_state.dart b/lib/src/objects/mobile_scanner_state.dart index f097ebb9c..eef6e18e9 100644 --- a/lib/src/objects/mobile_scanner_state.dart +++ b/lib/src/objects/mobile_scanner_state.dart @@ -1,3 +1,6 @@ +/// @docImport 'package:mobile_scanner/src/mobile_scanner_controller.dart'; +library; + import 'dart:ui'; import 'package:mobile_scanner/src/enums/camera_facing.dart'; diff --git a/lib/src/objects/start_options.dart b/lib/src/objects/start_options.dart index 71f368d69..6a6321d68 100644 --- a/lib/src/objects/start_options.dart +++ b/lib/src/objects/start_options.dart @@ -6,6 +6,7 @@ import 'package:mobile_scanner/src/enums/detection_speed.dart'; /// This class defines the different start options for the mobile scanner. class StartOptions { + /// Construct a new [StartOptions] instance. const StartOptions({ required this.cameraDirection, required this.cameraResolution, @@ -37,6 +38,7 @@ class StartOptions { /// Whether the torch should be turned on when the scanner starts. final bool torchEnabled; + /// Converts this object to a map. Map toMap() { return { if (cameraResolution != null) diff --git a/lib/src/overlay/barcode_overlay.dart b/lib/src/overlay/barcode_overlay.dart index 9f8d4edcd..dd4e9ba16 100644 --- a/lib/src/overlay/barcode_overlay.dart +++ b/lib/src/overlay/barcode_overlay.dart @@ -1,16 +1,33 @@ import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; +/// This widget represents an overlay that paints the bounding boxes of detected barcodes. class BarcodeOverlay extends StatelessWidget { + /// Construct a new [BarcodeOverlay] instance. const BarcodeOverlay({ super.key, - required this.controller, required this.boxFit, + required this.controller, + this.color = const Color(0x4DF44336), + this.style = PaintingStyle.fill, }); - final MobileScannerController controller; + /// The [BoxFit] to use when painting the barcode box. final BoxFit boxFit; + /// The controller that provides the barcodes to display. + final MobileScannerController controller; + + /// The color to use when painting the barcode box. + /// + /// Defaults to [Colors.red], with an opacity of 30%. + final Color color; + + /// The style to use when painting the barcode box. + /// + /// Defaults to [PaintingStyle.fill]. + final PaintingStyle style; + @override Widget build(BuildContext context) { return ValueListenableBuilder( @@ -26,32 +43,27 @@ class BarcodeOverlay extends StatelessWidget { builder: (context, snapshot) { final BarcodeCapture? barcodeCapture = snapshot.data; - // No barcode. - if (barcodeCapture == null || barcodeCapture.barcodes.isEmpty) { + // No barcode or preview size. + if (barcodeCapture == null || + barcodeCapture.size.isEmpty || + barcodeCapture.barcodes.isEmpty) { return const SizedBox(); } - final overlays = []; - - for (final scannedBarcode in barcodeCapture.barcodes) { - // No barcode corners, or size, or no camera preview size. - if (value.size.isEmpty || - scannedBarcode.size.isEmpty || - scannedBarcode.corners.isEmpty) { - continue; - } - - overlays.add( - CustomPaint( - painter: BarcodePainter( - barcodeCorners: scannedBarcode.corners, - barcodeSize: scannedBarcode.size, - boxFit: boxFit, - cameraPreviewSize: barcodeCapture.size, + final overlays = [ + for (final Barcode barcode in barcodeCapture.barcodes) + if (!barcode.size.isEmpty && barcode.corners.isNotEmpty) + CustomPaint( + painter: BarcodePainter( + barcodeCorners: barcode.corners, + barcodeSize: barcode.size, + boxFit: boxFit, + cameraPreviewSize: barcodeCapture.size, + color: color, + style: style, + ), ), - ), - ); - } + ]; return Stack( fit: StackFit.expand, diff --git a/lib/src/overlay/barcode_painter.dart b/lib/src/overlay/barcode_painter.dart index 9b24c3d5f..58d3636fa 100644 --- a/lib/src/overlay/barcode_painter.dart +++ b/lib/src/overlay/barcode_painter.dart @@ -1,18 +1,37 @@ +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +/// This class represents a [CustomPainter] that draws the [barcodeCorners] of a single barcode. class BarcodePainter extends CustomPainter { - BarcodePainter({ + /// Construct a new [BarcodePainter] instance. + const BarcodePainter({ required this.barcodeCorners, required this.barcodeSize, required this.boxFit, required this.cameraPreviewSize, + required this.color, + required this.style, }); + /// The corners of the barcode. final List barcodeCorners; + + /// The size of the barcode. final Size barcodeSize; + + /// The [BoxFit] to use when painting the barcode box. final BoxFit boxFit; + + /// The size of the camera preview, + /// relative to which the [barcodeSize] and [barcodeCorners] are positioned. final Size cameraPreviewSize; + /// The color to use when painting the barcode box. + final Color color; + + /// The style to use when painting the barcode box. + final PaintingStyle style; + @override void paint(Canvas canvas, Size size) { if (barcodeCorners.isEmpty || @@ -37,11 +56,10 @@ class BarcodePainter extends CustomPainter { horizontalPadding = 0; } - final double ratioWidth; - final double ratioHeight; - - ratioWidth = cameraPreviewSize.width / adjustedSize.destination.width; - ratioHeight = cameraPreviewSize.height / adjustedSize.destination.height; + final double ratioWidth = + cameraPreviewSize.width / adjustedSize.destination.width; + final double ratioHeight = + cameraPreviewSize.height / adjustedSize.destination.height; final List adjustedOffset = [ for (final offset in barcodeCorners) @@ -54,14 +72,21 @@ class BarcodePainter extends CustomPainter { final cutoutPath = Path()..addPolygon(adjustedOffset, true); final backgroundPaint = Paint() - ..color = Colors.red.withOpacity(0.3) - ..style = PaintingStyle.fill; + ..color = color + ..style = style; canvas.drawPath(cutoutPath, backgroundPaint); } @override - bool shouldRepaint(covariant CustomPainter oldDelegate) { - return true; + bool shouldRepaint(BarcodePainter oldDelegate) { + const ListEquality listEquality = ListEquality(); + + return listEquality.equals(oldDelegate.barcodeCorners, barcodeCorners) || + oldDelegate.barcodeSize != barcodeSize || + oldDelegate.boxFit != boxFit || + oldDelegate.cameraPreviewSize != cameraPreviewSize || + oldDelegate.color != color || + oldDelegate.style != style; } } diff --git a/lib/src/overlay/scan_window_overlay.dart b/lib/src/overlay/scan_window_overlay.dart new file mode 100644 index 000000000..13b2de5fb --- /dev/null +++ b/lib/src/overlay/scan_window_overlay.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:mobile_scanner/src/mobile_scanner_controller.dart'; +import 'package:mobile_scanner/src/overlay/scan_window_painter.dart'; + +/// This widget represents an overlay that paints a scan window cutout. +class ScanWindowOverlay extends StatelessWidget { + /// Construct a new [ScanWindowOverlay] instance. + const ScanWindowOverlay({ + super.key, + required this.controller, + required this.scanWindow, + this.borderColor = Colors.white, + this.borderRadius = BorderRadius.zero, + this.borderStrokeCap = StrokeCap.butt, + this.borderStrokeJoin = StrokeJoin.miter, + this.borderStyle = PaintingStyle.stroke, + this.borderWidth = 2.0, + this.color = const Color(0x80000000), + }); + + /// The color for the scan window border. + /// + /// Defaults to [Colors.white]. + final Color borderColor; + + /// The border radius for the scan window and its border. + /// + /// Defaults to [BorderRadius.zero]. + final BorderRadius borderRadius; + + /// The stroke cap for the border around the scan window. + /// + /// Defaults to [StrokeCap.butt]. + final StrokeCap borderStrokeCap; + + /// The stroke join for the border around the scan window. + /// + /// Defaults to [StrokeJoin.miter]. + final StrokeJoin borderStrokeJoin; + + /// The style for the border around the scan window. + /// + /// Defaults to [PaintingStyle.stroke]. + final PaintingStyle borderStyle; + + /// The width for the border around the scan window. + /// + /// Defaults to 2.0. + final double borderWidth; + + /// The color for the scan window box. + /// + /// Defaults to [Colors.black] with 50% opacity. + final Color color; + + /// The controller that manages the camera preview. + final MobileScannerController controller; + + /// The scan window for the overlay. + final Rect scanWindow; + + @override + Widget build(BuildContext context) { + if (scanWindow.isEmpty || scanWindow.isInfinite) { + return const SizedBox(); + } + + 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( + size: value.size, + painter: ScanWindowPainter( + borderColor: borderColor, + borderRadius: borderRadius, + borderStrokeCap: borderStrokeCap, + borderStrokeJoin: borderStrokeJoin, + borderStyle: borderStyle, + borderWidth: borderWidth, + scanWindow: scanWindow, + color: color, + ), + ); + }, + ); + } +} diff --git a/lib/src/overlay/scan_window_painter.dart b/lib/src/overlay/scan_window_painter.dart new file mode 100644 index 000000000..bbae884e0 --- /dev/null +++ b/lib/src/overlay/scan_window_painter.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; + +/// This class represents a [CustomPainter] that draws a [scanWindow] rectangle. +class ScanWindowPainter extends CustomPainter { + /// Construct a new [ScanWindowPainter] instance. + const ScanWindowPainter({ + required this.borderColor, + required this.borderRadius, + required this.borderStrokeCap, + required this.borderStrokeJoin, + required this.borderStyle, + required this.borderWidth, + required this.color, + required this.scanWindow, + }); + + /// The color for the scan window border. + final Color borderColor; + + /// The border radius for the scan window and its border. + final BorderRadius borderRadius; + + /// The stroke cap for the border around the scan window. + final StrokeCap borderStrokeCap; + + /// The stroke join for the border around the scan window. + final StrokeJoin borderStrokeJoin; + + /// The style for the border around the scan window. + final PaintingStyle borderStyle; + + /// The width for the border around the scan window. + final double borderWidth; + + /// The color for the scan window box. + final Color color; + + /// The rectangle that defines the scan window. + final Rect scanWindow; + + @override + void paint(Canvas canvas, Size size) { + if (scanWindow.isEmpty || scanWindow.isInfinite) { + return; + } + + // Define the main overlay path covering the entire screen. + final backgroundPath = Path()..addRect(Offset.zero & size); + + // The cutout rect depends on the border radius. + final RRect cutoutRect = borderRadius == BorderRadius.zero + ? RRect.fromRectAndCorners(scanWindow) + : RRect.fromRectAndCorners( + scanWindow, + topLeft: borderRadius.topLeft, + topRight: borderRadius.topRight, + bottomLeft: borderRadius.bottomLeft, + bottomRight: borderRadius.bottomRight, + ); + + // The cutout path is always in the center. + final Path cutoutPath = Path()..addRRect(cutoutRect); + + // Combine the two paths: overlay minus the cutout area + final Path overlayWithCutoutPath = Path.combine( + PathOperation.difference, + backgroundPath, + cutoutPath, + ); + + final Paint overlayWithCutoutPaint = Paint() + ..color = color + ..style = PaintingStyle.fill + ..blendMode = BlendMode.dstOver; + + final Paint borderPaint = Paint() + ..color = borderColor + ..style = borderStyle + ..strokeWidth = borderWidth + ..strokeCap = borderStrokeCap + ..strokeJoin = borderStrokeJoin; + + // Paint the overlay with the cutout. + canvas.drawPath(overlayWithCutoutPath, overlayWithCutoutPaint); + + // Then, draw the border around the cutout area. + canvas.drawRRect(cutoutRect, borderPaint); + } + + @override + bool shouldRepaint(ScanWindowPainter oldDelegate) { + return oldDelegate.scanWindow != scanWindow || + oldDelegate.color != color || + oldDelegate.borderRadius != borderRadius; + } +} diff --git a/lib/src/overlay/scanner_overlay.dart b/lib/src/overlay/scanner_overlay.dart deleted file mode 100644 index 9a725a749..000000000 --- a/lib/src/overlay/scanner_overlay.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:mobile_scanner/src/mobile_scanner_controller.dart'; -import 'package:mobile_scanner/src/overlay/scanner_painter.dart'; - -class ScannerOverlay extends StatelessWidget { - final MobileScannerController controller; - final Rect scanWindow; - - const ScannerOverlay({ - super.key, - required this.controller, - required this.scanWindow, - }); - - @override - Widget build(BuildContext context) { - 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( - size: value.size, - painter: ScannerPainter(scanWindow), - ); - }, - ); - } -} diff --git a/lib/src/overlay/scanner_painter.dart b/lib/src/overlay/scanner_painter.dart deleted file mode 100644 index b753f4a1d..000000000 --- a/lib/src/overlay/scanner_painter.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter/material.dart'; - -class ScannerPainter extends CustomPainter { - ScannerPainter(this.scanWindow); - - final Rect scanWindow; - - @override - void paint(Canvas canvas, Size size) { - // Define the main overlay path covering the entire screen - final backgroundPath = Path() - ..addRect(Rect.fromLTWH(0, 0, size.width, size.height)); - - // Define the cutout path in the center - final cutoutPath = Path()..addRect(scanWindow); - - // Combine the two paths: overlay minus the cutout area - final overlayWithCutoutPath = - Path.combine(PathOperation.difference, backgroundPath, cutoutPath); - - // Paint the overlay with the cutout - final paint = Paint() - ..color = Colors.black.withOpacity(0.5); // Semi-transparent black - canvas.drawPath(overlayWithCutoutPath, paint); - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) { - return false; - } -} diff --git a/lib/src/web/barcode_reader.dart b/lib/src/web/barcode_reader.dart index 42fc894ba..ed53e4ae8 100644 --- a/lib/src/web/barcode_reader.dart +++ b/lib/src/web/barcode_reader.dart @@ -11,6 +11,9 @@ import 'package:web/web.dart'; /// This class represents the base interface for a barcode reader implementation. abstract class BarcodeReader { + /// Construct a new [BarcodeReader] instance. + /// + /// This constructor is const, for subclasses. const BarcodeReader(); /// Whether the scanner is currently scanning for barcodes. diff --git a/lib/src/web/javascript_map.dart b/lib/src/web/javascript_map.dart index 5b4457641..40179d521 100644 --- a/lib/src/web/javascript_map.dart +++ b/lib/src/web/javascript_map.dart @@ -11,8 +11,12 @@ import 'dart:js_interop'; @JS('Map') extension type JSMap._(JSObject _) implements JSObject { + /// Construct a new Javascript `Map`. external factory JSMap(); + /// Get the value for the given [key]. external V? get(K key); + + /// Set the [value] for the given [key]. external JSVoid set(K key, V? value); } diff --git a/lib/src/web/mobile_scanner_web.dart b/lib/src/web/mobile_scanner_web.dart index ba6867e34..348e40468 100644 --- a/lib/src/web/mobile_scanner_web.dart +++ b/lib/src/web/mobile_scanner_web.dart @@ -57,6 +57,7 @@ class MobileScannerWeb extends MobileScannerPlatform { /// Get the view type for the platform view factory. String _getViewType(int textureId) => 'mobile-scanner-view-$textureId'; + /// Registers this class as the default instance of [MobileScannerPlatform]. static void registerWith(Registrar registrar) { MobileScannerPlatform.instance = MobileScannerWeb(); } diff --git a/lib/src/web/zxing/result.dart b/lib/src/web/zxing/result.dart index dd43b09f1..903b6214c 100644 --- a/lib/src/web/zxing/result.dart +++ b/lib/src/web/zxing/result.dart @@ -75,6 +75,8 @@ extension type Result(JSObject _) implements JSObject { /// Convert this result to a [Barcode]. Barcode get toBarcode { + // The order of the points is dependent on the type of barcode. + // Don't do a manual correction here, but leave it up to the reader implementation. final List corners = resultPoints; return Barcode( diff --git a/lib/src/web/zxing/zxing_barcode_reader.dart b/lib/src/web/zxing/zxing_barcode_reader.dart index 96c04a70f..5cc71a3a5 100644 --- a/lib/src/web/zxing/zxing_barcode_reader.dart +++ b/lib/src/web/zxing/zxing_barcode_reader.dart @@ -17,6 +17,7 @@ import 'package:web/web.dart' as web; /// A barcode reader implementation that uses the ZXing library. final class ZXingBarcodeReader extends BarcodeReader { + /// Construct a new [ZXingBarcodeReader] instance. ZXingBarcodeReader(); /// ZXing reports an error with this message if the code could not be detected. diff --git a/lib/src/web/zxing/zxing_browser_multi_format_reader.dart b/lib/src/web/zxing/zxing_browser_multi_format_reader.dart index 292cb6bb8..544a125dd 100644 --- a/lib/src/web/zxing/zxing_browser_multi_format_reader.dart +++ b/lib/src/web/zxing/zxing_browser_multi_format_reader.dart @@ -1,3 +1,6 @@ +/// @docImport 'package:mobile_scanner/src/web/zxing/result.dart'; +library; + import 'dart:js_interop'; import 'package:mobile_scanner/src/web/javascript_map.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 00648e91c..e7982c2e4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,6 +20,7 @@ environment: flutter: ">=3.22.0" dependencies: + collection: ">=1.15.0" flutter: sdk: flutter flutter_web_plugins: