Merge branch 'master' into feature/scan-window
# Conflicts: # android/src/main/kotlin/dev/steenbakker/mobile_scanner/MobileScanner.kt # example/ios/Runner.xcodeproj/project.pbxproj # example/lib/main.dart # ios/Classes/SwiftMobileScannerPlugin.swift # lib/src/mobile_scanner.dart # lib/src/mobile_scanner_controller.dart
Showing
61 changed files
with
2570 additions
and
1237 deletions
Too many changes to show.
To preserve performance only 61 of 61+ files are displayed.
.github/CODEOWNERS
0 → 100644
| 1 | +# These owners will be the default owners for everything in | ||
| 2 | +# the repo. Unless a later match takes precedence, | ||
| 3 | +# review when someone opens a pull request. | ||
| 4 | +# For more on how to customize the CODEOWNERS file - https://help.github.com/en/articles/about-code-owners | ||
| 5 | + | ||
| 6 | +* @juliansteenbakker |
| @@ -6,15 +6,33 @@ updates: | @@ -6,15 +6,33 @@ updates: | ||
| 6 | interval: "weekly" | 6 | interval: "weekly" |
| 7 | reviewers: | 7 | reviewers: |
| 8 | - "juliansteenbakker" | 8 | - "juliansteenbakker" |
| 9 | + commit-message: | ||
| 10 | + prefix: "chore" | ||
| 11 | + include: "scope" | ||
| 9 | - package-ecosystem: gradle | 12 | - package-ecosystem: gradle |
| 10 | directory: "/android" | 13 | directory: "/android" |
| 11 | schedule: | 14 | schedule: |
| 12 | interval: "weekly" | 15 | interval: "weekly" |
| 13 | reviewers: | 16 | reviewers: |
| 14 | - "juliansteenbakker" | 17 | - "juliansteenbakker" |
| 18 | + commit-message: | ||
| 19 | + prefix: "chore" | ||
| 20 | + include: "scope" | ||
| 15 | - package-ecosystem: gradle | 21 | - package-ecosystem: gradle |
| 16 | directory: "/example/android" | 22 | directory: "/example/android" |
| 17 | schedule: | 23 | schedule: |
| 18 | interval: "weekly" | 24 | interval: "weekly" |
| 19 | reviewers: | 25 | reviewers: |
| 20 | - - "juliansteenbakker" | ||
| 26 | + - "juliansteenbakker" | ||
| 27 | + commit-message: | ||
| 28 | + prefix: "chore" | ||
| 29 | + include: "scope" | ||
| 30 | + - package-ecosystem: "pub" | ||
| 31 | + directory: "/" | ||
| 32 | + schedule: | ||
| 33 | + interval: "weekly" | ||
| 34 | + commit-message: | ||
| 35 | + prefix: "chore" | ||
| 36 | + include: "scope" | ||
| 37 | + reviewers: | ||
| 38 | + - "juliansteenbakker" |
.github/workflows/auto-assign-pr.yml
0 → 100644
| 1 | +# .github/workflows/auto-author-assign.yml | ||
| 2 | +name: 'Auto Author Assign' | ||
| 3 | + | ||
| 4 | +on: | ||
| 5 | + pull_request_target: | ||
| 6 | + types: [opened, reopened] | ||
| 7 | + | ||
| 8 | +permissions: | ||
| 9 | + pull-requests: write | ||
| 10 | + | ||
| 11 | +jobs: | ||
| 12 | + assign-author: | ||
| 13 | + runs-on: ubuntu-latest | ||
| 14 | + steps: | ||
| 15 | + - uses: toshimaru/auto-author-assign@v1.6.1 |
| @@ -11,12 +11,12 @@ jobs: | @@ -11,12 +11,12 @@ jobs: | ||
| 11 | analysis: | 11 | analysis: |
| 12 | runs-on: ubuntu-latest | 12 | runs-on: ubuntu-latest |
| 13 | steps: | 13 | steps: |
| 14 | - - uses: actions/checkout@v3.0.2 | ||
| 15 | - - uses: actions/setup-java@v3.4.1 | 14 | + - uses: actions/checkout@v3.1.0 |
| 15 | + - uses: actions/setup-java@v3.6.0 | ||
| 16 | with: | 16 | with: |
| 17 | java-version: 11 | 17 | java-version: 11 |
| 18 | distribution: temurin | 18 | distribution: temurin |
| 19 | - - uses: subosito/flutter-action@v2.6.1 | 19 | + - uses: subosito/flutter-action@v2.8.0 |
| 20 | with: | 20 | with: |
| 21 | cache: true | 21 | cache: true |
| 22 | - name: Version | 22 | - name: Version |
| @@ -28,12 +28,12 @@ jobs: | @@ -28,12 +28,12 @@ jobs: | ||
| 28 | formatting: | 28 | formatting: |
| 29 | runs-on: ubuntu-latest | 29 | runs-on: ubuntu-latest |
| 30 | steps: | 30 | steps: |
| 31 | - - uses: actions/checkout@v3.0.2 | ||
| 32 | - - uses: actions/setup-java@v3.4.1 | 31 | + - uses: actions/checkout@v3.1.0 |
| 32 | + - uses: actions/setup-java@v3.6.0 | ||
| 33 | with: | 33 | with: |
| 34 | java-version: 11 | 34 | java-version: 11 |
| 35 | distribution: temurin | 35 | distribution: temurin |
| 36 | - - uses: subosito/flutter-action@v2.6.1 | 36 | + - uses: subosito/flutter-action@v2.8.0 |
| 37 | with: | 37 | with: |
| 38 | cache: true | 38 | cache: true |
| 39 | - name: Format | 39 | - name: Format |
.github/workflows/release-please.yml
0 → 100644
.github/workflows/semantic-pr.yml
0 → 100644
| 1 | +name: "Semantic PRs" | ||
| 2 | + | ||
| 3 | +on: | ||
| 4 | + pull_request_target: | ||
| 5 | + types: | ||
| 6 | + - opened | ||
| 7 | + - edited | ||
| 8 | + - synchronize | ||
| 9 | + | ||
| 10 | +jobs: | ||
| 11 | + main: | ||
| 12 | + name: Validate PR title | ||
| 13 | + runs-on: ubuntu-latest | ||
| 14 | + steps: | ||
| 15 | + - uses: amannn/action-semantic-pull-request@v5 | ||
| 16 | + env: | ||
| 17 | + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 1 | +## 3.0.0-beta.3 | ||
| 2 | +Deprecated: | ||
| 3 | +* The `onStart` method has been renamed to `onScannerStarted`. | ||
| 4 | +* The `onPermissionSet` argument of the `MobileScannerController` is now deprecated. | ||
| 5 | + | ||
| 6 | +Breaking changes: | ||
| 7 | +* `MobileScannerException` now uses an `errorCode` instead of a `message`. | ||
| 8 | +* `MobileScannerException` now contains additional details from the original error. | ||
| 9 | +* Refactored `MobileScannerController.start()` to throw `MobileScannerException`s | ||
| 10 | + with consistent error codes, rather than string messages. | ||
| 11 | + To handle permission errors, consider catching the result of `MobileScannerController.start()`. | ||
| 12 | +* The `autoResume` attribute has been removed from the `MobileScanner` widget. | ||
| 13 | + The controller already automatically resumes, so it had no effect. | ||
| 14 | +* Removed `MobileScannerCallback` and `MobileScannerArgumentsCallback` typedef. | ||
| 15 | + | ||
| 16 | +Improvements: | ||
| 17 | +* Toggling the device torch now does nothing if the device has no torch, rather than throwing an error. | ||
| 18 | +* Removed `called stop while already stopped` messages. | ||
| 19 | + | ||
| 20 | +Features: | ||
| 21 | +* Added a new `placeholderBuilder` function to the `MobileScanner` widget to customize the preview placeholder. | ||
| 22 | +* Added `autoStart` parameter to MobileScannerController(). If set to false, controller won't start automatically. | ||
| 23 | +* Added `hasTorch` function on MobileScannerController(). After starting the controller, you can check if the device has a torch. | ||
| 24 | + | ||
| 25 | +Fixes: | ||
| 26 | +* Fixes the missing gradle setup for the Android project, which prevented gradle sync from working. | ||
| 27 | +* Fixes `MobileScannerController.stop()` throwing when already stopped. | ||
| 28 | +* Fixes `MobileScannerController.toggleTorch()` throwing if the device has no torch. | ||
| 29 | + Now it does nothing if the torch is not available. | ||
| 30 | +* Fixes a memory leak where the `MobileScanner` would keep listening to the barcode events. | ||
| 31 | +* Fixes the `MobileScanner` preview depending on all attributes of `MediaQueryData`. | ||
| 32 | + Now it only depends on its layout constraints. | ||
| 33 | +* Fixed a potential crash when the scanner is restarted due to the app being resumed. | ||
| 34 | + | ||
| 35 | +## 3.0.0-beta.2 | ||
| 36 | +Breaking changes: | ||
| 37 | +* The arguments parameter of onDetect is removed. The data is now returned by the onStart callback | ||
| 38 | +in the MobileScanner widget. | ||
| 39 | +* onDetect now returns the object BarcodeCapture, which contains a List of barcodes and, if enabled, an image. | ||
| 40 | +* allowDuplicates is removed and replaced by MobileScannerSpeed enum. | ||
| 41 | +* onPermissionSet in MobileScanner widget is deprecated and will be removed. Use the onPermissionSet | ||
| 42 | +onPermissionSet callback in MobileScannerController instead. | ||
| 43 | +* [iOS] The minimum deployment target is now 11.0 or higher. | ||
| 44 | + | ||
| 45 | +Features: | ||
| 46 | +* The returnImage is working for both iOS and Android. You can enable it in the MobileScannerController. | ||
| 47 | +The image will be returned in the BarcodeCapture object provided by onDetect. | ||
| 48 | +* You can now control the DetectionSpeed, as well as the timeout of the DetectionSpeed. For more | ||
| 49 | +info see the DetectionSpeed documentation. This replaces the allowDuplicates function. | ||
| 50 | + | ||
| 51 | +Other improvements: | ||
| 52 | +* Both the [iOS] and [Android] codebases have been refactored completely. | ||
| 53 | +* [iOS] Updated POD dependencies | ||
| 54 | + | ||
| 55 | +## 3.0.0-beta.1 | ||
| 56 | +Breaking changes: | ||
| 57 | +* [Android] SDK updated to SDK 33. | ||
| 58 | + | ||
| 59 | +Features: | ||
| 60 | +* [Web] Add binaryData for raw value. | ||
| 61 | +* [iOS] Captures the last scanned barcode with Barcode.image. | ||
| 62 | +* [iOS] Add support for multiple formats on iOS with BarcodeScannerOptions. | ||
| 63 | +* Add displayValue which returns barcode value in a user-friendly format. | ||
| 64 | +* Add autoResume option to MobileScannerController which automatically resumes the camera when the application is resumed | ||
| 65 | + | ||
| 66 | +Other changes: | ||
| 67 | +* [Android] Revert camera2 dependency to stable release | ||
| 68 | +* [iOS] Update barcode scanning library to latest version | ||
| 69 | +* Several minor code improvements | ||
| 70 | + | ||
| 1 | ## 2.0.0 | 71 | ## 2.0.0 |
| 2 | Breaking changes: | 72 | Breaking changes: |
| 3 | This version is only compatible with flutter 3.0.0 and later. | 73 | This version is only compatible with flutter 3.0.0 and later. |
| @@ -36,22 +36,23 @@ NSPhotoLibraryUsageDescription - describe why your app needs permission for the | @@ -36,22 +36,23 @@ NSPhotoLibraryUsageDescription - describe why your app needs permission for the | ||
| 36 | 36 | ||
| 37 | ### macOS | 37 | ### macOS |
| 38 | macOS 10.13 or newer. Reason: Apple Vision library. | 38 | macOS 10.13 or newer. Reason: Apple Vision library. |
| 39 | + | ||
| 40 | +Ensure that you granted camera permission in XCode -> Signing & Capabilities: | ||
| 41 | + | ||
| 42 | +<img width="696" alt="Screenshot of XCode where Camera is checked" src="https://user-images.githubusercontent.com/24459435/193464115-d76f81d0-6355-4cb2-8bee-538e413a3ad0.png"> | ||
| 39 | 43 | ||
| 40 | ### Web | 44 | ### Web |
| 41 | Add this to `web/index.html`: | 45 | Add this to `web/index.html`: |
| 42 | 46 | ||
| 43 | ```html | 47 | ```html |
| 44 | -<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script> | 48 | +<script type="text/javascript" src="https://unpkg.com/@zxing/library@0.19.1"></script> |
| 45 | ``` | 49 | ``` |
| 46 | 50 | ||
| 47 | -Web only supports QR codes for now. | ||
| 48 | -Do you have experience with Flutter Web development? [Help me with migrating from jsQR to qr-scanner for full barcode support!](https://github.com/juliansteenbakker/mobile_scanner/issues/54) | ||
| 49 | - | ||
| 50 | ## Features Supported | 51 | ## Features Supported |
| 51 | 52 | ||
| 52 | | Features | Android | iOS | macOS | Web | | 53 | | Features | Android | iOS | macOS | Web | |
| 53 | |------------------------|--------------------|--------------------|-------|-----| | 54 | |------------------------|--------------------|--------------------|-------|-----| |
| 54 | -| analyzeImage (Gallery) | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | | 55 | +| analyzeImage (Gallery) | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | |
| 55 | 56 | ||
| 56 | ## Usage | 57 | ## Usage |
| 57 | 58 | ||
| @@ -71,15 +72,15 @@ import 'package:mobile_scanner/mobile_scanner.dart'; | @@ -71,15 +72,15 @@ import 'package:mobile_scanner/mobile_scanner.dart'; | ||
| 71 | return Scaffold( | 72 | return Scaffold( |
| 72 | appBar: AppBar(title: const Text('Mobile Scanner')), | 73 | appBar: AppBar(title: const Text('Mobile Scanner')), |
| 73 | body: MobileScanner( | 74 | body: MobileScanner( |
| 74 | - allowDuplicates: false, | ||
| 75 | - onDetect: (barcode, args) { | ||
| 76 | - if (barcode.rawValue == null) { | ||
| 77 | - debugPrint('Failed to scan Barcode'); | ||
| 78 | - } else { | ||
| 79 | - final String code = barcode.rawValue!; | ||
| 80 | - debugPrint('Barcode found! $code'); | ||
| 81 | - } | ||
| 82 | - }), | 75 | + // fit: BoxFit.contain, |
| 76 | + onDetect: (capture) { | ||
| 77 | + final List<Barcode> barcodes = capture.barcodes; | ||
| 78 | + final Uint8List? image = capture.image; | ||
| 79 | + for (final barcode in barcodes) { | ||
| 80 | + debugPrint('Barcode found! ${barcode.rawValue}'); | ||
| 81 | + } | ||
| 82 | + }, | ||
| 83 | + ), | ||
| 83 | ); | 84 | ); |
| 84 | } | 85 | } |
| 85 | ``` | 86 | ``` |
| @@ -94,17 +95,18 @@ import 'package:mobile_scanner/mobile_scanner.dart'; | @@ -94,17 +95,18 @@ import 'package:mobile_scanner/mobile_scanner.dart'; | ||
| 94 | return Scaffold( | 95 | return Scaffold( |
| 95 | appBar: AppBar(title: const Text('Mobile Scanner')), | 96 | appBar: AppBar(title: const Text('Mobile Scanner')), |
| 96 | body: MobileScanner( | 97 | body: MobileScanner( |
| 97 | - allowDuplicates: false, | ||
| 98 | - controller: MobileScannerController( | ||
| 99 | - facing: CameraFacing.front, torchEnabled: true), | ||
| 100 | - onDetect: (barcode, args) { | ||
| 101 | - if (barcode.rawValue == null) { | ||
| 102 | - debugPrint('Failed to scan Barcode'); | ||
| 103 | - } else { | ||
| 104 | - final String code = barcode.rawValue!; | ||
| 105 | - debugPrint('Barcode found! $code'); | ||
| 106 | - } | ||
| 107 | - }), | 98 | + // fit: BoxFit.contain, |
| 99 | + controller: MobileScannerController( | ||
| 100 | + facing: CameraFacing.front, torchEnabled: true, | ||
| 101 | + ), | ||
| 102 | + onDetect: (capture) { | ||
| 103 | + final List<Barcode> barcodes = capture.barcodes; | ||
| 104 | + final Uint8List? image = capture.image; | ||
| 105 | + for (final barcode in barcodes) { | ||
| 106 | + debugPrint('Barcode found! ${barcode.rawValue}'); | ||
| 107 | + } | ||
| 108 | + }, | ||
| 109 | + ), | ||
| 108 | ); | 110 | ); |
| 109 | } | 111 | } |
| 110 | ``` | 112 | ``` |
| @@ -157,15 +159,82 @@ import 'package:mobile_scanner/mobile_scanner.dart'; | @@ -157,15 +159,82 @@ import 'package:mobile_scanner/mobile_scanner.dart'; | ||
| 157 | ], | 159 | ], |
| 158 | ), | 160 | ), |
| 159 | body: MobileScanner( | 161 | body: MobileScanner( |
| 160 | - allowDuplicates: false, | ||
| 161 | - controller: cameraController, | ||
| 162 | - onDetect: (barcode, args) { | ||
| 163 | - if (barcode.rawValue == null) { | ||
| 164 | - debugPrint('Failed to scan Barcode'); | ||
| 165 | - } else { | ||
| 166 | - final String code = barcode.rawValue!; | ||
| 167 | - debugPrint('Barcode found! $code'); | ||
| 168 | - } | ||
| 169 | - })); | 162 | + // fit: BoxFit.contain, |
| 163 | + controller: cameraController, | ||
| 164 | + onDetect: (capture) { | ||
| 165 | + final List<Barcode> barcodes = capture.barcodes; | ||
| 166 | + final Uint8List? image = capture.image; | ||
| 167 | + for (final barcode in barcodes) { | ||
| 168 | + debugPrint('Barcode found! ${barcode.rawValue}'); | ||
| 169 | + } | ||
| 170 | + }, | ||
| 171 | + ), | ||
| 172 | + ); | ||
| 173 | + } | ||
| 174 | +``` | ||
| 175 | + | ||
| 176 | +Example with controller and returning images | ||
| 177 | + | ||
| 178 | +```dart | ||
| 179 | +import 'package:mobile_scanner/mobile_scanner.dart'; | ||
| 180 | + | ||
| 181 | + @override | ||
| 182 | + Widget build(BuildContext context) { | ||
| 183 | + return Scaffold( | ||
| 184 | + appBar: AppBar(title: const Text('Mobile Scanner')), | ||
| 185 | + body: MobileScanner( | ||
| 186 | + fit: BoxFit.contain, | ||
| 187 | + controller: MobileScannerController( | ||
| 188 | + // facing: CameraFacing.back, | ||
| 189 | + // torchEnabled: false, | ||
| 190 | + returnImage: true, | ||
| 191 | + ), | ||
| 192 | + onDetect: (capture) { | ||
| 193 | + final List<Barcode> barcodes = capture.barcodes; | ||
| 194 | + final Uint8List? image = capture.image; | ||
| 195 | + for (final barcode in barcodes) { | ||
| 196 | + debugPrint('Barcode found! ${barcode.rawValue}'); | ||
| 197 | + } | ||
| 198 | + if (image != null) { | ||
| 199 | + showDialog( | ||
| 200 | + context: context, | ||
| 201 | + builder: (context) => | ||
| 202 | + Image(image: MemoryImage(image)), | ||
| 203 | + ); | ||
| 204 | + Future.delayed(const Duration(seconds: 5), () { | ||
| 205 | + Navigator.pop(context); | ||
| 206 | + }); | ||
| 207 | + } | ||
| 208 | + }, | ||
| 209 | + ), | ||
| 210 | + ); | ||
| 170 | } | 211 | } |
| 171 | ``` | 212 | ``` |
| 213 | + | ||
| 214 | +### BarcodeCapture | ||
| 215 | + | ||
| 216 | +The onDetect function returns a BarcodeCapture objects which contains the following items. | ||
| 217 | + | ||
| 218 | +| Property name | Type | Description | | ||
| 219 | +|---------------|---------------|-----------------------------------| | ||
| 220 | +| barcodes | List<Barcode> | A list with scanned barcodes. | | ||
| 221 | +| image | Uint8List? | If enabled, an image of the scan. | | ||
| 222 | + | ||
| 223 | +You can use the following properties of the Barcode object. | ||
| 224 | + | ||
| 225 | +| Property name | Type | Description | | ||
| 226 | +|---------------|----------------|-------------------------------------| | ||
| 227 | +| format | BarcodeFormat | | | ||
| 228 | +| rawBytes | Uint8List? | binary scan result | | ||
| 229 | +| rawValue | String? | Value if barcode is in UTF-8 format | | ||
| 230 | +| displayValue | String? | | | ||
| 231 | +| type | BarcodeType | | | ||
| 232 | +| calendarEvent | CalendarEvent? | | | ||
| 233 | +| contactInfo | ContactInfo? | | | ||
| 234 | +| driverLicense | DriverLicense? | | | ||
| 235 | +| email | Email? | | | ||
| 236 | +| geoPoint | GeoPoint? | | | ||
| 237 | +| phone | Phone? | | | ||
| 238 | +| sms | SMS? | | | ||
| 239 | +| url | UrlBookmark? | | | ||
| 240 | +| wifi | WiFi? | WiFi Access-Point details | |
| @@ -2,14 +2,15 @@ group 'dev.steenbakker.mobile_scanner' | @@ -2,14 +2,15 @@ group 'dev.steenbakker.mobile_scanner' | ||
| 2 | version '1.0-SNAPSHOT' | 2 | version '1.0-SNAPSHOT' |
| 3 | 3 | ||
| 4 | buildscript { | 4 | buildscript { |
| 5 | - ext.kotlin_version = '1.7.10' | 5 | + ext.kotlin_version = '1.7.22' |
| 6 | repositories { | 6 | repositories { |
| 7 | google() | 7 | google() |
| 8 | mavenCentral() | 8 | mavenCentral() |
| 9 | } | 9 | } |
| 10 | 10 | ||
| 11 | dependencies { | 11 | dependencies { |
| 12 | - classpath 'com.android.tools.build:gradle:7.2.1' | 12 | + classpath 'com.android.tools.build:gradle:7.3.1' |
| 13 | + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" | ||
| 13 | } | 14 | } |
| 14 | } | 15 | } |
| 15 | 16 | ||
| @@ -24,7 +25,7 @@ apply plugin: 'com.android.library' | @@ -24,7 +25,7 @@ apply plugin: 'com.android.library' | ||
| 24 | apply plugin: 'kotlin-android' | 25 | apply plugin: 'kotlin-android' |
| 25 | 26 | ||
| 26 | android { | 27 | android { |
| 27 | - compileSdkVersion 32 | 28 | + compileSdkVersion 33 |
| 28 | 29 | ||
| 29 | compileOptions { | 30 | compileOptions { |
| 30 | sourceCompatibility JavaVersion.VERSION_1_8 | 31 | sourceCompatibility JavaVersion.VERSION_1_8 |
| @@ -50,18 +51,8 @@ dependencies { | @@ -50,18 +51,8 @@ dependencies { | ||
| 50 | // Use this dependency to bundle the model with your app | 51 | // Use this dependency to bundle the model with your app |
| 51 | implementation 'com.google.mlkit:barcode-scanning:17.0.2' | 52 | implementation 'com.google.mlkit:barcode-scanning:17.0.2' |
| 52 | // Use this dependency to use the dynamically downloaded model in Google Play Services | 53 | // Use this dependency to use the dynamically downloaded model in Google Play Services |
| 53 | -// implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.0.0' | 54 | +// implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.1.0' |
| 54 | 55 | ||
| 55 | - implementation "androidx.camera:camera-camera2:1.2.0-alpha04" | ||
| 56 | - implementation 'androidx.camera:camera-lifecycle:1.2.0-alpha04' | ||
| 57 | - | ||
| 58 | -// // The following line is optional, as the core library is included indirectly by camera-camera2 | ||
| 59 | -// implementation "androidx.camera:camera-core:1.1.0-alpha11" | ||
| 60 | -// implementation "androidx.camera:camera-camera2:1.1.0-alpha11" | ||
| 61 | -// // If you want to additionally use the CameraX Lifecycle library | ||
| 62 | -// implementation "androidx.camera:camera-lifecycle:1.1.0-alpha11" | ||
| 63 | -// // If you want to additionally use the CameraX View class | ||
| 64 | -// implementation "androidx.camera:camera-view:1.0.0-alpha31" | ||
| 65 | -// // If you want to additionally use the CameraX Extensions library | ||
| 66 | -// implementation "androidx.camera:camera-extensions:1.0.0-alpha31" | 56 | + implementation 'androidx.camera:camera-camera2:1.1.0' |
| 57 | + implementation 'androidx.camera:camera-lifecycle:1.1.0' | ||
| 67 | } | 58 | } |
| 1 | -package dev.steenbakker.mobile_scanner | ||
| 2 | - | ||
| 3 | -import androidx.annotation.IntDef | ||
| 4 | - | ||
| 5 | -@IntDef(AnalyzeMode.NONE, AnalyzeMode.BARCODE) | ||
| 6 | -@Target(AnnotationTarget.FIELD) | ||
| 7 | -@Retention(AnnotationRetention.SOURCE) | ||
| 8 | -annotation class AnalyzeMode { | ||
| 9 | - companion object { | ||
| 10 | - const val NONE = 0 | ||
| 11 | - const val BARCODE = 1 | ||
| 12 | - } | ||
| 13 | -} |
| 1 | +package dev.steenbakker.mobile_scanner | ||
| 2 | + | ||
| 3 | +import android.os.Handler | ||
| 4 | +import android.os.Looper | ||
| 5 | +import io.flutter.embedding.engine.plugins.FlutterPlugin | ||
| 6 | +import io.flutter.plugin.common.EventChannel | ||
| 7 | + | ||
| 8 | +class BarcodeHandler(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) : EventChannel.StreamHandler { | ||
| 9 | + | ||
| 10 | + private var eventSink: EventChannel.EventSink? = null | ||
| 11 | + | ||
| 12 | + private val eventChannel = EventChannel( | ||
| 13 | + flutterPluginBinding.binaryMessenger, | ||
| 14 | + "dev.steenbakker.mobile_scanner/scanner/event" | ||
| 15 | + ) | ||
| 16 | + | ||
| 17 | + init { | ||
| 18 | + eventChannel.setStreamHandler(this) | ||
| 19 | + } | ||
| 20 | + | ||
| 21 | + fun publishEvent(event: Map<String, Any>) { | ||
| 22 | + Handler(Looper.getMainLooper()).post { | ||
| 23 | + eventSink?.success(event) | ||
| 24 | + } | ||
| 25 | + } | ||
| 26 | + | ||
| 27 | + override fun onListen(event: Any?, eventSink: EventChannel.EventSink?) { | ||
| 28 | + this.eventSink = eventSink | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + override fun onCancel(event: Any?) { | ||
| 32 | + this.eventSink = null | ||
| 33 | + } | ||
| 34 | +} |
| @@ -7,12 +7,9 @@ import android.graphics.Point | @@ -7,12 +7,9 @@ import android.graphics.Point | ||
| 7 | import android.graphics.Rect | 7 | import android.graphics.Rect |
| 8 | import android.graphics.RectF | 8 | import android.graphics.RectF |
| 9 | import android.net.Uri | 9 | import android.net.Uri |
| 10 | -import android.util.Log | ||
| 11 | -import android.util.Size | ||
| 12 | -import android.util.Rational | ||
| 13 | -import android.media.Image | 10 | +import android.os.Handler |
| 11 | +import android.os.Looper | ||
| 14 | import android.view.Surface | 12 | import android.view.Surface |
| 15 | -import androidx.annotation.NonNull | ||
| 16 | import androidx.camera.core.* | 13 | import androidx.camera.core.* |
| 17 | import androidx.camera.lifecycle.ProcessCameraProvider | 14 | import androidx.camera.lifecycle.ProcessCameraProvider |
| 18 | import androidx.core.app.ActivityCompat | 15 | import androidx.core.app.ActivityCompat |
| @@ -20,117 +17,160 @@ import androidx.core.content.ContextCompat | @@ -20,117 +17,160 @@ import androidx.core.content.ContextCompat | ||
| 20 | import androidx.lifecycle.LifecycleOwner | 17 | import androidx.lifecycle.LifecycleOwner |
| 21 | import com.google.mlkit.vision.barcode.BarcodeScannerOptions | 18 | import com.google.mlkit.vision.barcode.BarcodeScannerOptions |
| 22 | import com.google.mlkit.vision.barcode.BarcodeScanning | 19 | import com.google.mlkit.vision.barcode.BarcodeScanning |
| 23 | -import com.google.mlkit.vision.barcode.common.Barcode | ||
| 24 | import com.google.mlkit.vision.common.InputImage | 20 | import com.google.mlkit.vision.common.InputImage |
| 25 | -import com.google.mlkit.vision.common.InputImage.IMAGE_FORMAT_NV21 | ||
| 26 | -import io.flutter.plugin.common.EventChannel | ||
| 27 | -import io.flutter.plugin.common.MethodCall | 21 | +import dev.steenbakker.mobile_scanner.objects.DetectionSpeed |
| 22 | +import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters | ||
| 28 | import io.flutter.plugin.common.MethodChannel | 23 | import io.flutter.plugin.common.MethodChannel |
| 29 | import io.flutter.plugin.common.PluginRegistry | 24 | import io.flutter.plugin.common.PluginRegistry |
| 30 | import io.flutter.view.TextureRegistry | 25 | import io.flutter.view.TextureRegistry |
| 26 | +typealias MobileScannerCallback = (barcodes: List<Map<String, Any?>>, image: ByteArray?) -> Unit | ||
| 27 | +typealias AnalyzerCallback = (barcodes: List<Map<String, Any?>>?) -> Unit | ||
| 28 | +typealias MobileScannerErrorCallback = (error: String) -> Unit | ||
| 29 | +typealias TorchStateCallback = (state: Int) -> Unit | ||
| 30 | +typealias MobileScannerStartedCallback = (parameters: MobileScannerStartParameters) -> Unit | ||
| 31 | import java.io.File | 31 | import java.io.File |
| 32 | import kotlin.math.roundToInt | 32 | import kotlin.math.roundToInt |
| 33 | 33 | ||
| 34 | - | ||
| 35 | -class MobileScanner(private val activity: Activity, private val textureRegistry: TextureRegistry) | ||
| 36 | - : MethodChannel.MethodCallHandler, EventChannel.StreamHandler, PluginRegistry.RequestPermissionsResultListener { | 34 | +class NoCamera : Exception() |
| 35 | +class AlreadyStarted : Exception() | ||
| 36 | +class AlreadyStopped : Exception() | ||
| 37 | +class TorchError : Exception() | ||
| 38 | +class CameraError : Exception() | ||
| 39 | +class TorchWhenStopped : Exception() | ||
| 40 | + | ||
| 41 | +class MobileScanner( | ||
| 42 | + private val activity: Activity, | ||
| 43 | + private val textureRegistry: TextureRegistry, | ||
| 44 | + private val mobileScannerCallback: MobileScannerCallback, | ||
| 45 | + private val mobileScannerErrorCallback: MobileScannerErrorCallback | ||
| 46 | +) : | ||
| 47 | + PluginRegistry.RequestPermissionsResultListener { | ||
| 37 | companion object { | 48 | companion object { |
| 38 | - private const val REQUEST_CODE = 22022022 | ||
| 39 | - private val TAG = MobileScanner::class.java.simpleName | 49 | + /** |
| 50 | + * When the application's activity is [androidx.fragment.app.FragmentActivity], requestCode can only use the lower 16 bits. | ||
| 51 | + * @see androidx.fragment.app.FragmentActivity.validateRequestPermissionsRequestCode | ||
| 52 | + */ | ||
| 53 | + private const val REQUEST_CODE = 0x0786 | ||
| 40 | } | 54 | } |
| 41 | 55 | ||
| 42 | - private var sink: EventChannel.EventSink? = null | ||
| 43 | - private var listener: PluginRegistry.RequestPermissionsResultListener? = null | ||
| 44 | - | ||
| 45 | private var cameraProvider: ProcessCameraProvider? = null | 56 | private var cameraProvider: ProcessCameraProvider? = null |
| 46 | private var camera: Camera? = null | 57 | private var camera: Camera? = null |
| 58 | + private var pendingPermissionResult: MethodChannel.Result? = null | ||
| 47 | private var preview: Preview? = null | 59 | private var preview: Preview? = null |
| 48 | private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null | 60 | private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null |
| 49 | private var scanWindow: List<Float>? = null; | 61 | private var scanWindow: List<Float>? = null; |
| 50 | 62 | ||
| 51 | -// @AnalyzeMode | ||
| 52 | -// private var analyzeMode: Int = AnalyzeMode.NONE | ||
| 53 | - | ||
| 54 | - @ExperimentalGetImage | ||
| 55 | - override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: MethodChannel.Result) { | ||
| 56 | - when (call.method) { | ||
| 57 | - "state" -> checkPermission(result) | ||
| 58 | - "request" -> requestPermission(result) | ||
| 59 | - "start" -> start(call, result) | ||
| 60 | - "torch" -> toggleTorch(call, result) | ||
| 61 | -// "analyze" -> switchAnalyzeMode(call, result) | ||
| 62 | - "stop" -> stop(result) | ||
| 63 | - "analyzeImage" -> analyzeImage(call, result) | ||
| 64 | - "updateScanWindow" -> updateScanWindow(call) | ||
| 65 | - else -> result.notImplemented() | ||
| 66 | - } | ||
| 67 | - } | 63 | + private var detectionSpeed: DetectionSpeed = DetectionSpeed.NO_DUPLICATES |
| 64 | + private var detectionTimeout: Long = 250 | ||
| 65 | + private var lastScanned: List<String?>? = null | ||
| 68 | 66 | ||
| 69 | - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { | ||
| 70 | - this.sink = events | ||
| 71 | - } | 67 | + private var scannerTimeout = false |
| 72 | 68 | ||
| 73 | - override fun onCancel(arguments: Any?) { | ||
| 74 | - sink = null | ||
| 75 | - } | 69 | + private var returnImage = false |
| 76 | 70 | ||
| 77 | - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray): Boolean { | ||
| 78 | - return listener?.onRequestPermissionsResult(requestCode, permissions, grantResults) ?: false | ||
| 79 | - } | 71 | + private var scanner = BarcodeScanning.getClient() |
| 80 | 72 | ||
| 81 | - private fun checkPermission(result: MethodChannel.Result) { | 73 | + /** |
| 74 | + * Check if we already have camera permission. | ||
| 75 | + */ | ||
| 76 | + fun hasCameraPermission(): Int { | ||
| 82 | // Can't get exact denied or not_determined state without request. Just return not_determined when state isn't authorized | 77 | // Can't get exact denied or not_determined state without request. Just return not_determined when state isn't authorized |
| 83 | - val state = | ||
| 84 | - if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) 1 | ||
| 85 | - else 0 | ||
| 86 | - result.success(state) | 78 | + val hasPermission = ContextCompat.checkSelfPermission( |
| 79 | + activity, | ||
| 80 | + Manifest.permission.CAMERA | ||
| 81 | + ) == PackageManager.PERMISSION_GRANTED | ||
| 82 | + | ||
| 83 | + return if (hasPermission) { | ||
| 84 | + 1 | ||
| 85 | + } else { | ||
| 86 | + 0 | ||
| 87 | + } | ||
| 87 | } | 88 | } |
| 88 | 89 | ||
| 89 | - private fun requestPermission(result: MethodChannel.Result) { | ||
| 90 | - listener = PluginRegistry.RequestPermissionsResultListener { requestCode, _, grantResults -> | ||
| 91 | - if (requestCode != REQUEST_CODE) { | ||
| 92 | - false | ||
| 93 | - } else { | ||
| 94 | - val authorized = grantResults[0] == PackageManager.PERMISSION_GRANTED | ||
| 95 | - result.success(authorized) | ||
| 96 | - listener = null | ||
| 97 | - true | ||
| 98 | - } | 90 | + /** |
| 91 | + * Request camera permissions. | ||
| 92 | + */ | ||
| 93 | + fun requestPermission(result: MethodChannel.Result) { | ||
| 94 | + if(pendingPermissionResult != null) { | ||
| 95 | + return | ||
| 99 | } | 96 | } |
| 97 | + | ||
| 98 | + pendingPermissionResult = result | ||
| 100 | val permissions = arrayOf(Manifest.permission.CAMERA) | 99 | val permissions = arrayOf(Manifest.permission.CAMERA) |
| 101 | ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE) | 100 | ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE) |
| 102 | } | 101 | } |
| 103 | 102 | ||
| 103 | + /** | ||
| 104 | + * Calls the callback after permissions are requested. | ||
| 105 | + */ | ||
| 106 | + override fun onRequestPermissionsResult( | ||
| 107 | + requestCode: Int, | ||
| 108 | + permissions: Array<out String>, | ||
| 109 | + grantResults: IntArray | ||
| 110 | + ): Boolean { | ||
| 111 | + if (requestCode != REQUEST_CODE) { | ||
| 112 | + return false | ||
| 113 | + } | ||
| 114 | + | ||
| 115 | + pendingPermissionResult?.success(grantResults[0] == PackageManager.PERMISSION_GRANTED) | ||
| 116 | + pendingPermissionResult = null | ||
| 104 | 117 | ||
| 118 | + return true | ||
| 119 | + } | ||
| 120 | + | ||
| 121 | + /** | ||
| 122 | + * callback for the camera. Every frame is passed through this function. | ||
| 123 | + */ | ||
| 105 | @ExperimentalGetImage | 124 | @ExperimentalGetImage |
| 106 | - val analyzer = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format | 125 | + val captureOutput = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format |
| 107 | val mediaImage = imageProxy.image ?: return@Analyzer | 126 | val mediaImage = imageProxy.image ?: return@Analyzer |
| 108 | - var inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) | 127 | + val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) |
| 128 | + | ||
| 129 | + if (detectionSpeed == DetectionSpeed.NORMAL && scannerTimeout) { | ||
| 130 | + imageProxy.close() | ||
| 131 | + return@Analyzer | ||
| 132 | + } else if (detectionSpeed == DetectionSpeed.NORMAL) { | ||
| 133 | + scannerTimeout = true | ||
| 134 | + } | ||
| 109 | 135 | ||
| 110 | scanner.process(inputImage) | 136 | scanner.process(inputImage) |
| 111 | .addOnSuccessListener { barcodes -> | 137 | .addOnSuccessListener { barcodes -> |
| 112 | - for (barcode in barcodes) { | ||
| 113 | - print("image: ") | ||
| 114 | - println(inputImage.getWidth()); | ||
| 115 | - println(inputImage.getHeight()); | ||
| 116 | - | ||
| 117 | - print("barcode: ") | ||
| 118 | - println(barcode.getBoundingBox()); | ||
| 119 | - | ||
| 120 | - if(scanWindow != null) { | ||
| 121 | - val match = isbarCodeInScanWindow(scanWindow!!, barcode, imageProxy) | ||
| 122 | - if(!match) continue | 138 | + if (detectionSpeed == DetectionSpeed.NO_DUPLICATES) { |
| 139 | + val newScannedBarcodes = barcodes.map { barcode -> barcode.rawValue } | ||
| 140 | + if (newScannedBarcodes == lastScanned) { | ||
| 141 | + // New scanned is duplicate, returning | ||
| 142 | + return@addOnSuccessListener | ||
| 123 | } | 143 | } |
| 144 | + lastScanned = newScannedBarcodes | ||
| 145 | + } | ||
| 146 | + | ||
| 147 | + val barcodeMap = barcodes.map { barcode -> barcode.data } | ||
| 124 | 148 | ||
| 125 | - val event = mapOf("name" to "barcode", "data" to barcode.data) | ||
| 126 | - sink?.success(event) | 149 | + if(scanWindow != null) { |
| 150 | + val match = isbarCodeInScanWindow(scanWindow!!, barcode, imageProxy) | ||
| 151 | + if(!match) continue | ||
| 127 | } | 152 | } |
| 153 | + if (barcodeMap.isNotEmpty()) { | ||
| 154 | + mobileScannerCallback( | ||
| 155 | + barcodeMap, | ||
| 156 | + if (returnImage) mediaImage.toByteArray() else null | ||
| 157 | + ) | ||
| 158 | + } | ||
| 159 | + } | ||
| 160 | + .addOnFailureListener { e -> | ||
| 161 | + mobileScannerErrorCallback( | ||
| 162 | + e.localizedMessage ?: e.toString() | ||
| 163 | + ) | ||
| 128 | } | 164 | } |
| 129 | - .addOnFailureListener { e -> Log.e(TAG, e.message, e) } | ||
| 130 | .addOnCompleteListener { imageProxy.close() } | 165 | .addOnCompleteListener { imageProxy.close() } |
| 131 | - } | ||
| 132 | 166 | ||
| 133 | - private var scanner = BarcodeScanning.getClient() | 167 | + if (detectionSpeed == DetectionSpeed.NORMAL) { |
| 168 | + // Set timer and continue | ||
| 169 | + Handler(Looper.getMainLooper()).postDelayed({ | ||
| 170 | + scannerTimeout = false | ||
| 171 | + }, detectionTimeout) | ||
| 172 | + } | ||
| 173 | + } | ||
| 134 | 174 | ||
| 135 | private fun updateScanWindow(call: MethodCall) { | 175 | private fun updateScanWindow(call: MethodCall) { |
| 136 | scanWindow = call.argument<List<Float>>("rect") | 176 | scanWindow = call.argument<List<Float>>("rect") |
| @@ -138,7 +178,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -138,7 +178,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 138 | 178 | ||
| 139 | // scales the scanWindow to the provided inputImage and checks if that scaled | 179 | // scales the scanWindow to the provided inputImage and checks if that scaled |
| 140 | // scanWindow contains the barcode | 180 | // scanWindow contains the barcode |
| 141 | - private fun isbarCodeInScanWindow(scanWindow: List<Float>, barcode: Barcode, inputImage: ImageProxy): Boolean { | 181 | + private fun isbarCodeInScanWindow(scanWindow: List<Float>, barcode: Barcode, inputImage: ImageProxy): Boolean { |
| 142 | val barcodeBoundingBox = barcode.getBoundingBox() | 182 | val barcodeBoundingBox = barcode.getBoundingBox() |
| 143 | if(barcodeBoundingBox == null) return false | 183 | if(barcodeBoundingBox == null) return false |
| 144 | 184 | ||
| @@ -150,157 +190,118 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -150,157 +190,118 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 150 | val right = (scanWindow[2] * imageWidth).roundToInt() | 190 | val right = (scanWindow[2] * imageWidth).roundToInt() |
| 151 | val bottom = (scanWindow[3] * imageHeight).roundToInt() | 191 | val bottom = (scanWindow[3] * imageHeight).roundToInt() |
| 152 | 192 | ||
| 153 | - val scaledScanWindow = Rect(left, top, right, bottom) | 193 | + val scaledScanWindow = Rect(left, top, right, bottom) |
| 154 | 194 | ||
| 155 | - print("scanWindow: ") | ||
| 156 | - println(scaledScanWindow) | 195 | + print("scanWindow: ") |
| 196 | + println(scaledScanWindow) | ||
| 157 | return scaledScanWindow.contains(barcodeBoundingBox) | 197 | return scaledScanWindow.contains(barcodeBoundingBox) |
| 158 | } | 198 | } |
| 159 | 199 | ||
| 160 | - | ||
| 161 | - | 200 | + /** |
| 201 | + * Start barcode scanning by initializing the camera and barcode scanner. | ||
| 202 | + */ | ||
| 162 | @ExperimentalGetImage | 203 | @ExperimentalGetImage |
| 163 | - private fun start(call: MethodCall, result: MethodChannel.Result) { | ||
| 164 | - if (camera?.cameraInfo != null && preview != null && textureEntry != null) { | ||
| 165 | - val resolution = preview!!.resolutionInfo!!.resolution | ||
| 166 | - val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0 | ||
| 167 | - val width = resolution.width.toDouble() | ||
| 168 | - val height = resolution.height.toDouble() | ||
| 169 | - val size = if (portrait) mapOf("width" to width, "height" to height) else mapOf("width" to height, "height" to width) | ||
| 170 | - val answer = mapOf("textureId" to textureEntry!!.id(), "size" to size, "torchable" to camera!!.cameraInfo.hasFlashUnit()) | ||
| 171 | - result.success(answer) | ||
| 172 | - } else { | ||
| 173 | - val facing: Int = call.argument<Int>("facing") ?: 0 | ||
| 174 | - val ratio: Int = call.argument<Int>("ratio") ?: 1 | ||
| 175 | - val torch: Boolean = call.argument<Boolean>("torch") ?: false | ||
| 176 | - val formats: List<Int>? = call.argument<List<Int>>("formats") | ||
| 177 | - | ||
| 178 | - if (formats != null) { | ||
| 179 | - val formatsList: MutableList<Int> = mutableListOf() | ||
| 180 | - for (index in formats) { | ||
| 181 | - formatsList.add(BarcodeFormats.values()[index].intValue) | ||
| 182 | - } | ||
| 183 | - scanner = if (formatsList.size == 1) { | ||
| 184 | - BarcodeScanning.getClient(BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first()).build()) | ||
| 185 | - } else { | ||
| 186 | - BarcodeScanning.getClient(BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first(), *formatsList.subList(1, formatsList.size).toIntArray()).build()) | ||
| 187 | - } | ||
| 188 | - } | 204 | + fun start( |
| 205 | + barcodeScannerOptions: BarcodeScannerOptions?, | ||
| 206 | + returnImage: Boolean, | ||
| 207 | + cameraPosition: CameraSelector, | ||
| 208 | + torch: Boolean, | ||
| 209 | + detectionSpeed: DetectionSpeed, | ||
| 210 | + torchStateCallback: TorchStateCallback, | ||
| 211 | + mobileScannerStartedCallback: MobileScannerStartedCallback, | ||
| 212 | + detectionTimeout: Long | ||
| 213 | + ) { | ||
| 214 | + this.detectionSpeed = detectionSpeed | ||
| 215 | + this.detectionTimeout = detectionTimeout | ||
| 216 | + this.returnImage = returnImage | ||
| 189 | 217 | ||
| 190 | - val future = ProcessCameraProvider.getInstance(activity) | ||
| 191 | - val executor = ContextCompat.getMainExecutor(activity) | 218 | + if (camera?.cameraInfo != null && preview != null && textureEntry != null) { |
| 219 | + throw AlreadyStarted() | ||
| 220 | + } | ||
| 192 | 221 | ||
| 193 | - future.addListener({ | ||
| 194 | - cameraProvider = future.get() | ||
| 195 | - if (cameraProvider == null) { | ||
| 196 | - result.error("cameraProvider", "cameraProvider is null", null) | ||
| 197 | - return@addListener | ||
| 198 | - } | ||
| 199 | - cameraProvider!!.unbindAll() | ||
| 200 | - textureEntry = textureRegistry.createSurfaceTexture() | ||
| 201 | - if (textureEntry == null) { | ||
| 202 | - result.error("textureEntry", "textureEntry is null", null) | ||
| 203 | - return@addListener | ||
| 204 | - } | ||
| 205 | - | ||
| 206 | - // Select the correct camera | ||
| 207 | - val selector = if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA | ||
| 208 | - | ||
| 209 | - // Preview | ||
| 210 | - val surfaceProvider = Preview.SurfaceProvider { request -> | ||
| 211 | - val texture = textureEntry!!.surfaceTexture() | ||
| 212 | - texture.setDefaultBufferSize(request.resolution.width, request.resolution.height) | ||
| 213 | - val surface = Surface(texture) | ||
| 214 | - request.provideSurface(surface, executor) { } | ||
| 215 | - } | 222 | + scanner = if (barcodeScannerOptions != null) { |
| 223 | + BarcodeScanning.getClient(barcodeScannerOptions) | ||
| 224 | + } else { | ||
| 225 | + BarcodeScanning.getClient() | ||
| 226 | + } | ||
| 216 | 227 | ||
| 217 | - // Build the preview to be shown on the Flutter texture | ||
| 218 | - val previewBuilder = Preview.Builder().setTargetAspectRatio(ratio) | ||
| 219 | - preview = previewBuilder.build().apply { setSurfaceProvider(surfaceProvider) } | ||
| 220 | - | ||
| 221 | - // bind to lifecycle temporarily to fetch dimensions. | ||
| 222 | - cameraProvider!!.bindToLifecycle(activity as LifecycleOwner, selector, preview) | ||
| 223 | - val previewResolution = preview!!.resolutionInfo!!.resolution | ||
| 224 | - val previewRotation = preview!!.getTargetRotation() | ||
| 225 | - | ||
| 226 | - // Build the analyzer to be passed on to MLKit | ||
| 227 | - val analysisBuilder = ImageAnalysis.Builder() | ||
| 228 | - .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) | ||
| 229 | - .setTargetAspectRatio(ratio) | ||
| 230 | - .setTargetRotation(previewRotation) | ||
| 231 | - val analysis = analysisBuilder.build().apply { setAnalyzer(executor, analyzer) } | ||
| 232 | - | ||
| 233 | - val viewPort = ViewPort.Builder(Rational(previewResolution.width, previewResolution.height), previewRotation).build() | ||
| 234 | - val useCaseGroup = UseCaseGroup.Builder() | ||
| 235 | - .setViewPort(viewPort) | ||
| 236 | - .addUseCase(preview!!) | ||
| 237 | - .addUseCase(analysis) | ||
| 238 | - .build() | ||
| 239 | - | ||
| 240 | - cameraProvider!!.unbindAll() | ||
| 241 | - camera = cameraProvider!!.bindToLifecycle(activity as LifecycleOwner, selector, useCaseGroup) | ||
| 242 | - | ||
| 243 | - if (camera == null) { | ||
| 244 | - result.error("camera", "camera is null", null) | ||
| 245 | - return@addListener | ||
| 246 | - } | 228 | + val cameraProviderFuture = ProcessCameraProvider.getInstance(activity) |
| 229 | + val executor = ContextCompat.getMainExecutor(activity) | ||
| 247 | 230 | ||
| 248 | - // Register the torch listener | ||
| 249 | - camera!!.cameraInfo.torchState.observe(activity) { state -> | ||
| 250 | - // TorchState.OFF = 0; TorchState.ON = 1 | ||
| 251 | - sink?.success(mapOf("name" to "torchState", "data" to state)) | ||
| 252 | - } | 231 | + cameraProviderFuture.addListener({ |
| 232 | + cameraProvider = cameraProviderFuture.get() | ||
| 233 | + if (cameraProvider == null) { | ||
| 234 | + throw CameraError() | ||
| 235 | + } | ||
| 236 | + cameraProvider!!.unbindAll() | ||
| 237 | + textureEntry = textureRegistry.createSurfaceTexture() | ||
| 238 | + | ||
| 239 | + // Preview | ||
| 240 | + val surfaceProvider = Preview.SurfaceProvider { request -> | ||
| 241 | + val texture = textureEntry!!.surfaceTexture() | ||
| 242 | + texture.setDefaultBufferSize( | ||
| 243 | + request.resolution.width, | ||
| 244 | + request.resolution.height | ||
| 245 | + ) | ||
| 246 | + | ||
| 247 | + val surface = Surface(texture) | ||
| 248 | + request.provideSurface(surface, executor) { } | ||
| 249 | + } | ||
| 253 | 250 | ||
| 254 | - // Enable torch if provided | ||
| 255 | - camera!!.cameraControl.enableTorch(torch) | ||
| 256 | - | ||
| 257 | - val resolution = preview!!.resolutionInfo!!.resolution | ||
| 258 | - val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0 | ||
| 259 | - val width = resolution.width.toDouble() | ||
| 260 | - val height = resolution.height.toDouble() | ||
| 261 | - val size = if (portrait) mapOf("width" to width, "height" to height) else mapOf("width" to height, "height" to width) | ||
| 262 | - val answer = mapOf("textureId" to textureEntry!!.id(), "size" to size, "torchable" to camera!!.cameraInfo.hasFlashUnit()) | ||
| 263 | - result.success(answer) | ||
| 264 | - }, executor) | ||
| 265 | - } | ||
| 266 | - } | 251 | + // Build the preview to be shown on the Flutter texture |
| 252 | + val previewBuilder = Preview.Builder() | ||
| 253 | + preview = previewBuilder.build().apply { setSurfaceProvider(surfaceProvider) } | ||
| 254 | + | ||
| 255 | + // Build the analyzer to be passed on to MLKit | ||
| 256 | + val analysisBuilder = ImageAnalysis.Builder() | ||
| 257 | + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) | ||
| 258 | +// analysisBuilder.setTargetResolution(Size(1440, 1920)) | ||
| 259 | + val analysis = analysisBuilder.build().apply { setAnalyzer(executor, captureOutput) } | ||
| 260 | + | ||
| 261 | + camera = cameraProvider!!.bindToLifecycle( | ||
| 262 | + activity as LifecycleOwner, | ||
| 263 | + cameraPosition, | ||
| 264 | + preview, | ||
| 265 | + analysis | ||
| 266 | + ) | ||
| 267 | + | ||
| 268 | + // Register the torch listener | ||
| 269 | + camera!!.cameraInfo.torchState.observe(activity) { state -> | ||
| 270 | + // TorchState.OFF = 0; TorchState.ON = 1 | ||
| 271 | + torchStateCallback(state) | ||
| 272 | + } | ||
| 267 | 273 | ||
| 268 | - private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) { | ||
| 269 | - if (camera == null) { | ||
| 270 | - result.error(TAG,"Called toggleTorch() while stopped!", null) | ||
| 271 | - return | ||
| 272 | - } | ||
| 273 | - camera!!.cameraControl.enableTorch(call.arguments == 1) | ||
| 274 | - result.success(null) | ||
| 275 | - } | 274 | +// val analysisSize = analysis.resolutionInfo?.resolution ?: Size(0, 0) |
| 275 | +// val previewSize = preview!!.resolutionInfo?.resolution ?: Size(0, 0) | ||
| 276 | +// Log.i("LOG", "Analyzer: $analysisSize") | ||
| 277 | +// Log.i("LOG", "Preview: $previewSize") | ||
| 276 | 278 | ||
| 277 | -// private fun switchAnalyzeMode(call: MethodCall, result: MethodChannel.Result) { | ||
| 278 | -// analyzeMode = call.arguments as Int | ||
| 279 | -// result.success(null) | ||
| 280 | -// } | 279 | + // Enable torch if provided |
| 280 | + camera!!.cameraControl.enableTorch(torch) | ||
| 281 | 281 | ||
| 282 | - private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) { | ||
| 283 | - val uri = Uri.fromFile( File(call.arguments.toString())) | ||
| 284 | - val inputImage = InputImage.fromFilePath(activity, uri) | 282 | + val resolution = preview!!.resolutionInfo!!.resolution |
| 283 | + val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0 | ||
| 284 | + val width = resolution.width.toDouble() | ||
| 285 | + val height = resolution.height.toDouble() | ||
| 285 | 286 | ||
| 286 | - var barcodeFound = false | ||
| 287 | - scanner.process(inputImage) | ||
| 288 | - .addOnSuccessListener { barcodes -> | ||
| 289 | - for (barcode in barcodes) { | ||
| 290 | - barcodeFound = true | ||
| 291 | - sink?.success(mapOf("name" to "barcode", "data" to barcode.data)) | ||
| 292 | - } | ||
| 293 | - } | ||
| 294 | - .addOnFailureListener { e -> Log.e(TAG, e.message, e) | ||
| 295 | - result.error(TAG, e.message, e)} | ||
| 296 | - .addOnCompleteListener { result.success(barcodeFound) } | 287 | + mobileScannerStartedCallback( |
| 288 | + MobileScannerStartParameters( | ||
| 289 | + if (portrait) width else height, | ||
| 290 | + if (portrait) height else width, | ||
| 291 | + camera!!.cameraInfo.hasFlashUnit(), | ||
| 292 | + textureEntry!!.id() | ||
| 293 | + ) | ||
| 294 | + ) | ||
| 295 | + }, executor) | ||
| 297 | 296 | ||
| 298 | } | 297 | } |
| 299 | 298 | ||
| 300 | - private fun stop(result: MethodChannel.Result) { | 299 | + /** |
| 300 | + * Stop barcode scanning. | ||
| 301 | + */ | ||
| 302 | + fun stop() { | ||
| 301 | if (camera == null && preview == null) { | 303 | if (camera == null && preview == null) { |
| 302 | - result.error(TAG,"Called stop() while already stopped!", null) | ||
| 303 | - return | 304 | + throw AlreadyStopped() |
| 304 | } | 305 | } |
| 305 | 306 | ||
| 306 | val owner = activity as LifecycleOwner | 307 | val owner = activity as LifecycleOwner |
| @@ -308,68 +309,43 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -308,68 +309,43 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 308 | cameraProvider?.unbindAll() | 309 | cameraProvider?.unbindAll() |
| 309 | textureEntry?.release() | 310 | textureEntry?.release() |
| 310 | 311 | ||
| 311 | -// analyzeMode = AnalyzeMode.NONE | ||
| 312 | camera = null | 312 | camera = null |
| 313 | preview = null | 313 | preview = null |
| 314 | textureEntry = null | 314 | textureEntry = null |
| 315 | cameraProvider = null | 315 | cameraProvider = null |
| 316 | - | ||
| 317 | - result.success(null) | ||
| 318 | } | 316 | } |
| 319 | 317 | ||
| 318 | + /** | ||
| 319 | + * Toggles the flash light on or off. | ||
| 320 | + */ | ||
| 321 | + fun toggleTorch(enableTorch: Boolean) { | ||
| 322 | + if (camera == null) { | ||
| 323 | + throw TorchWhenStopped() | ||
| 324 | + } | ||
| 325 | + camera!!.cameraControl.enableTorch(enableTorch) | ||
| 326 | + } | ||
| 320 | 327 | ||
| 321 | - private val Barcode.data: Map<String, Any?> | ||
| 322 | - get() = mapOf("corners" to cornerPoints?.map { corner -> corner.data }, "format" to format, | ||
| 323 | - "rawBytes" to rawBytes, "rawValue" to rawValue, "type" to valueType, | ||
| 324 | - "calendarEvent" to calendarEvent?.data, "contactInfo" to contactInfo?.data, | ||
| 325 | - "driverLicense" to driverLicense?.data, "email" to email?.data, | ||
| 326 | - "geoPoint" to geoPoint?.data, "phone" to phone?.data, "sms" to sms?.data, | ||
| 327 | - "url" to url?.data, "wifi" to wifi?.data, "displayValue" to displayValue) | ||
| 328 | - | ||
| 329 | - private val Point.data: Map<String, Double> | ||
| 330 | - get() = mapOf("x" to x.toDouble(), "y" to y.toDouble()) | ||
| 331 | - | ||
| 332 | - private val Barcode.CalendarEvent.data: Map<String, Any?> | ||
| 333 | - get() = mapOf("description" to description, "end" to end?.rawValue, "location" to location, | ||
| 334 | - "organizer" to organizer, "start" to start?.rawValue, "status" to status, | ||
| 335 | - "summary" to summary) | ||
| 336 | - | ||
| 337 | - private val Barcode.ContactInfo.data: Map<String, Any?> | ||
| 338 | - get() = mapOf("addresses" to addresses.map { address -> address.data }, | ||
| 339 | - "emails" to emails.map { email -> email.data }, "name" to name?.data, | ||
| 340 | - "organization" to organization, "phones" to phones.map { phone -> phone.data }, | ||
| 341 | - "title" to title, "urls" to urls) | ||
| 342 | - | ||
| 343 | - private val Barcode.Address.data: Map<String, Any?> | ||
| 344 | - get() = mapOf("addressLines" to addressLines.map { addressLine -> addressLine.toString() }, "type" to type) | ||
| 345 | - | ||
| 346 | - private val Barcode.PersonName.data: Map<String, Any?> | ||
| 347 | - get() = mapOf("first" to first, "formattedName" to formattedName, "last" to last, | ||
| 348 | - "middle" to middle, "prefix" to prefix, "pronunciation" to pronunciation, | ||
| 349 | - "suffix" to suffix) | ||
| 350 | - | ||
| 351 | - private val Barcode.DriverLicense.data: Map<String, Any?> | ||
| 352 | - get() = mapOf("addressCity" to addressCity, "addressState" to addressState, | ||
| 353 | - "addressStreet" to addressStreet, "addressZip" to addressZip, "birthDate" to birthDate, | ||
| 354 | - "documentType" to documentType, "expiryDate" to expiryDate, "firstName" to firstName, | ||
| 355 | - "gender" to gender, "issueDate" to issueDate, "issuingCountry" to issuingCountry, | ||
| 356 | - "lastName" to lastName, "licenseNumber" to licenseNumber, "middleName" to middleName) | ||
| 357 | - | ||
| 358 | - private val Barcode.Email.data: Map<String, Any?> | ||
| 359 | - get() = mapOf("address" to address, "body" to body, "subject" to subject, "type" to type) | ||
| 360 | - | ||
| 361 | - private val Barcode.GeoPoint.data: Map<String, Any?> | ||
| 362 | - get() = mapOf("latitude" to lat, "longitude" to lng) | ||
| 363 | - | ||
| 364 | - private val Barcode.Phone.data: Map<String, Any?> | ||
| 365 | - get() = mapOf("number" to number, "type" to type) | 328 | + /** |
| 329 | + * Analyze a single image. | ||
| 330 | + */ | ||
| 331 | + fun analyzeImage(image: Uri, analyzerCallback: AnalyzerCallback) { | ||
| 332 | + val inputImage = InputImage.fromFilePath(activity, image) | ||
| 366 | 333 | ||
| 367 | - private val Barcode.Sms.data: Map<String, Any?> | ||
| 368 | - get() = mapOf("message" to message, "phoneNumber" to phoneNumber) | 334 | + scanner.process(inputImage) |
| 335 | + .addOnSuccessListener { barcodes -> | ||
| 336 | + val barcodeMap = barcodes.map { barcode -> barcode.data } | ||
| 369 | 337 | ||
| 370 | - private val Barcode.UrlBookmark.data: Map<String, Any?> | ||
| 371 | - get() = mapOf("title" to title, "url" to url) | 338 | + if (barcodeMap.isNotEmpty()) { |
| 339 | + analyzerCallback(barcodeMap) | ||
| 340 | + } else { | ||
| 341 | + analyzerCallback(null) | ||
| 342 | + } | ||
| 343 | + } | ||
| 344 | + .addOnFailureListener { e -> | ||
| 345 | + mobileScannerErrorCallback( | ||
| 346 | + e.localizedMessage ?: e.toString() | ||
| 347 | + ) | ||
| 348 | + } | ||
| 349 | + } | ||
| 372 | 350 | ||
| 373 | - private val Barcode.WiFi.data: Map<String, Any?> | ||
| 374 | - get() = mapOf("encryptionType" to encryptionType, "password" to password, "ssid" to ssid) | ||
| 375 | } | 351 | } |
| 1 | package dev.steenbakker.mobile_scanner | 1 | package dev.steenbakker.mobile_scanner |
| 2 | 2 | ||
| 3 | -import androidx.annotation.NonNull | 3 | +import android.net.Uri |
| 4 | +import androidx.camera.core.CameraSelector | ||
| 5 | +import androidx.camera.core.ExperimentalGetImage | ||
| 6 | +import com.google.mlkit.vision.barcode.BarcodeScannerOptions | ||
| 7 | +import dev.steenbakker.mobile_scanner.objects.BarcodeFormats | ||
| 8 | +import dev.steenbakker.mobile_scanner.objects.DetectionSpeed | ||
| 4 | import io.flutter.embedding.engine.plugins.FlutterPlugin | 9 | import io.flutter.embedding.engine.plugins.FlutterPlugin |
| 5 | import io.flutter.embedding.engine.plugins.activity.ActivityAware | 10 | import io.flutter.embedding.engine.plugins.activity.ActivityAware |
| 6 | import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding | 11 | import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding |
| 7 | -import io.flutter.plugin.common.EventChannel | 12 | +import io.flutter.plugin.common.MethodCall |
| 8 | import io.flutter.plugin.common.MethodChannel | 13 | import io.flutter.plugin.common.MethodChannel |
| 14 | +import java.io.File | ||
| 9 | 15 | ||
| 10 | /** MobileScannerPlugin */ | 16 | /** MobileScannerPlugin */ |
| 11 | -class MobileScannerPlugin : FlutterPlugin, ActivityAware { | ||
| 12 | - private var flutter: FlutterPlugin.FlutterPluginBinding? = null | ||
| 13 | - private var activity: ActivityPluginBinding? = null | 17 | +class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCallHandler { |
| 18 | + | ||
| 19 | + private var flutterPluginBinding: FlutterPlugin.FlutterPluginBinding? = null | ||
| 20 | + private var activityPluginBinding: ActivityPluginBinding? = null | ||
| 14 | private var handler: MobileScanner? = null | 21 | private var handler: MobileScanner? = null |
| 15 | private var method: MethodChannel? = null | 22 | private var method: MethodChannel? = null |
| 16 | - private var event: EventChannel? = null | ||
| 17 | 23 | ||
| 18 | - override fun onAttachedToEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { | ||
| 19 | - this.flutter = binding | 24 | + private lateinit var barcodeHandler: BarcodeHandler |
| 25 | + | ||
| 26 | + private var analyzerResult: MethodChannel.Result? = null | ||
| 27 | + | ||
| 28 | + private val callback: MobileScannerCallback = { barcodes: List<Map<String, Any?>>, image: ByteArray? -> | ||
| 29 | + if (image != null) { | ||
| 30 | + barcodeHandler.publishEvent(mapOf( | ||
| 31 | + "name" to "barcode", | ||
| 32 | + "data" to barcodes, | ||
| 33 | + "image" to image | ||
| 34 | + )) | ||
| 35 | + } else { | ||
| 36 | + barcodeHandler.publishEvent(mapOf( | ||
| 37 | + "name" to "barcode", | ||
| 38 | + "data" to barcodes | ||
| 39 | + )) | ||
| 40 | + } | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + private val analyzerCallback: AnalyzerCallback = { barcodes: List<Map<String, Any?>>?-> | ||
| 44 | + if (barcodes != null) { | ||
| 45 | + barcodeHandler.publishEvent(mapOf( | ||
| 46 | + "name" to "barcode", | ||
| 47 | + "data" to barcodes | ||
| 48 | + )) | ||
| 49 | + analyzerResult?.success(true) | ||
| 50 | + } else { | ||
| 51 | + analyzerResult?.success(false) | ||
| 52 | + } | ||
| 53 | + analyzerResult = null | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + private val errorCallback: MobileScannerErrorCallback = {error: String -> | ||
| 57 | + barcodeHandler.publishEvent(mapOf( | ||
| 58 | + "name" to "error", | ||
| 59 | + "data" to error, | ||
| 60 | + )) | ||
| 20 | } | 61 | } |
| 21 | 62 | ||
| 22 | - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { | ||
| 23 | - this.flutter = null | 63 | + private val torchStateCallback: TorchStateCallback = {state: Int -> |
| 64 | + barcodeHandler.publishEvent(mapOf("name" to "torchState", "data" to state)) | ||
| 24 | } | 65 | } |
| 25 | 66 | ||
| 26 | - override fun onAttachedToActivity(binding: ActivityPluginBinding) { | ||
| 27 | - activity = binding | ||
| 28 | - handler = MobileScanner(activity!!.activity, flutter!!.textureRegistry) | ||
| 29 | - method = MethodChannel(flutter!!.binaryMessenger, "dev.steenbakker.mobile_scanner/scanner/method") | ||
| 30 | - event = EventChannel(flutter!!.binaryMessenger, "dev.steenbakker.mobile_scanner/scanner/event") | ||
| 31 | - method!!.setMethodCallHandler(handler) | ||
| 32 | - event!!.setStreamHandler(handler) | ||
| 33 | - activity!!.addRequestPermissionsResultListener(handler!!) | 67 | + @ExperimentalGetImage |
| 68 | + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { | ||
| 69 | + if (handler == null) { | ||
| 70 | + result.error("MobileScanner", "Called ${call.method} before initializing.", null) | ||
| 71 | + return | ||
| 72 | + } | ||
| 73 | + when (call.method) { | ||
| 74 | + "state" -> result.success(handler!!.hasCameraPermission()) | ||
| 75 | + "request" -> handler!!.requestPermission(result) | ||
| 76 | + "start" -> start(call, result) | ||
| 77 | + "torch" -> toggleTorch(call, result) | ||
| 78 | + "stop" -> stop(result) | ||
| 79 | + "analyzeImage" -> analyzeImage(call, result) | ||
| 80 | + else -> result.notImplemented() | ||
| 81 | + } | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { | ||
| 85 | + method = MethodChannel(binding.binaryMessenger, "dev.steenbakker.mobile_scanner/scanner/method") | ||
| 86 | + method!!.setMethodCallHandler(this) | ||
| 87 | + | ||
| 88 | + barcodeHandler = BarcodeHandler(binding) | ||
| 89 | + | ||
| 90 | + this.flutterPluginBinding = binding | ||
| 91 | + } | ||
| 92 | + | ||
| 93 | + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { | ||
| 94 | + this.flutterPluginBinding = null | ||
| 95 | + } | ||
| 96 | + | ||
| 97 | + override fun onAttachedToActivity(activityPluginBinding: ActivityPluginBinding) { | ||
| 98 | + handler = MobileScanner(activityPluginBinding.activity, flutterPluginBinding!!.textureRegistry, callback, errorCallback | ||
| 99 | + ) | ||
| 100 | + activityPluginBinding.addRequestPermissionsResultListener(handler!!) | ||
| 101 | + | ||
| 102 | + this.activityPluginBinding = activityPluginBinding | ||
| 34 | } | 103 | } |
| 35 | 104 | ||
| 36 | override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { | 105 | override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { |
| @@ -38,16 +107,112 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware { | @@ -38,16 +107,112 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware { | ||
| 38 | } | 107 | } |
| 39 | 108 | ||
| 40 | override fun onDetachedFromActivity() { | 109 | override fun onDetachedFromActivity() { |
| 41 | - activity!!.removeRequestPermissionsResultListener(handler!!) | ||
| 42 | - event!!.setStreamHandler(null) | 110 | + activityPluginBinding!!.removeRequestPermissionsResultListener(handler!!) |
| 43 | method!!.setMethodCallHandler(null) | 111 | method!!.setMethodCallHandler(null) |
| 44 | - event = null | ||
| 45 | method = null | 112 | method = null |
| 46 | handler = null | 113 | handler = null |
| 47 | - activity = null | 114 | + activityPluginBinding = null |
| 48 | } | 115 | } |
| 49 | 116 | ||
| 50 | override fun onDetachedFromActivityForConfigChanges() { | 117 | override fun onDetachedFromActivityForConfigChanges() { |
| 51 | onDetachedFromActivity() | 118 | onDetachedFromActivity() |
| 52 | } | 119 | } |
| 120 | + | ||
| 121 | + @ExperimentalGetImage | ||
| 122 | + private fun start(call: MethodCall, result: MethodChannel.Result) { | ||
| 123 | + val torch: Boolean = call.argument<Boolean>("torch") ?: false | ||
| 124 | + val facing: Int = call.argument<Int>("facing") ?: 0 | ||
| 125 | + val formats: List<Int>? = call.argument<List<Int>>("formats") | ||
| 126 | + val returnImage: Boolean = call.argument<Boolean>("returnImage") ?: false | ||
| 127 | + val speed: Int = call.argument<Int>("speed") ?: 1 | ||
| 128 | + val timeout: Int = call.argument<Int>("timeout") ?: 250 | ||
| 129 | + | ||
| 130 | + var barcodeScannerOptions: BarcodeScannerOptions? = null | ||
| 131 | + if (formats != null) { | ||
| 132 | + val formatsList: MutableList<Int> = mutableListOf() | ||
| 133 | + for (index in formats) { | ||
| 134 | + formatsList.add(BarcodeFormats.values()[index].intValue) | ||
| 135 | + } | ||
| 136 | + barcodeScannerOptions = if (formatsList.size == 1) { | ||
| 137 | + BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first()) | ||
| 138 | + .build() | ||
| 139 | + } else { | ||
| 140 | + BarcodeScannerOptions.Builder().setBarcodeFormats( | ||
| 141 | + formatsList.first(), | ||
| 142 | + *formatsList.subList(1, formatsList.size).toIntArray() | ||
| 143 | + ).build() | ||
| 144 | + } | ||
| 145 | + } | ||
| 146 | + | ||
| 147 | + val position = | ||
| 148 | + if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA | ||
| 149 | + | ||
| 150 | + val detectionSpeed: DetectionSpeed = DetectionSpeed.values().first { it.intValue == speed} | ||
| 151 | + | ||
| 152 | + try { | ||
| 153 | + handler!!.start(barcodeScannerOptions, returnImage, position, torch, detectionSpeed, torchStateCallback, mobileScannerStartedCallback = { | ||
| 154 | + result.success(mapOf( | ||
| 155 | + "textureId" to it.id, | ||
| 156 | + "size" to mapOf("width" to it.width, "height" to it.height), | ||
| 157 | + "torchable" to it.hasFlashUnit | ||
| 158 | + )) | ||
| 159 | + }, | ||
| 160 | + timeout.toLong()) | ||
| 161 | + | ||
| 162 | + } catch (e: AlreadyStarted) { | ||
| 163 | + result.error( | ||
| 164 | + "MobileScanner", | ||
| 165 | + "Called start() while already started", | ||
| 166 | + null | ||
| 167 | + ) | ||
| 168 | + } catch (e: NoCamera) { | ||
| 169 | + result.error( | ||
| 170 | + "MobileScanner", | ||
| 171 | + "No camera found or failed to open camera!", | ||
| 172 | + null | ||
| 173 | + ) | ||
| 174 | + } catch (e: TorchError) { | ||
| 175 | + result.error( | ||
| 176 | + "MobileScanner", | ||
| 177 | + "Error occurred when setting torch!", | ||
| 178 | + null | ||
| 179 | + ) | ||
| 180 | + } catch (e: CameraError) { | ||
| 181 | + result.error( | ||
| 182 | + "MobileScanner", | ||
| 183 | + "Error occurred when setting up camera!", | ||
| 184 | + null | ||
| 185 | + ) | ||
| 186 | + } catch (e: Exception) { | ||
| 187 | + result.error( | ||
| 188 | + "MobileScanner", | ||
| 189 | + "Unknown error occurred..", | ||
| 190 | + null | ||
| 191 | + ) | ||
| 192 | + } | ||
| 193 | + } | ||
| 194 | + | ||
| 195 | + private fun stop(result: MethodChannel.Result) { | ||
| 196 | + try { | ||
| 197 | + handler!!.stop() | ||
| 198 | + result.success(null) | ||
| 199 | + } catch (e: AlreadyStopped) { | ||
| 200 | + result.success(null) | ||
| 201 | + } | ||
| 202 | + } | ||
| 203 | + | ||
| 204 | + private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) { | ||
| 205 | + analyzerResult = result | ||
| 206 | + val uri = Uri.fromFile(File(call.arguments.toString())) | ||
| 207 | + handler!!.analyzeImage(uri, analyzerCallback) | ||
| 208 | + } | ||
| 209 | + | ||
| 210 | + private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) { | ||
| 211 | + try { | ||
| 212 | + handler!!.toggleTorch(call.arguments == 1) | ||
| 213 | + result.success(null) | ||
| 214 | + } catch (e: AlreadyStopped) { | ||
| 215 | + result.error("MobileScanner", "Called toggleTorch() while stopped!", null) | ||
| 216 | + } | ||
| 217 | + } | ||
| 53 | } | 218 | } |
| 1 | +package dev.steenbakker.mobile_scanner | ||
| 2 | + | ||
| 3 | +import android.graphics.ImageFormat | ||
| 4 | +import android.graphics.Point | ||
| 5 | +import android.graphics.Rect | ||
| 6 | +import android.graphics.YuvImage | ||
| 7 | +import android.media.Image | ||
| 8 | +import com.google.mlkit.vision.barcode.common.Barcode | ||
| 9 | +import java.io.ByteArrayOutputStream | ||
| 10 | + | ||
| 11 | +fun Image.toByteArray(): ByteArray { | ||
| 12 | + val yBuffer = planes[0].buffer // Y | ||
| 13 | + val vuBuffer = planes[2].buffer // VU | ||
| 14 | + | ||
| 15 | + val ySize = yBuffer.remaining() | ||
| 16 | + val vuSize = vuBuffer.remaining() | ||
| 17 | + | ||
| 18 | + val nv21 = ByteArray(ySize + vuSize) | ||
| 19 | + | ||
| 20 | + yBuffer.get(nv21, 0, ySize) | ||
| 21 | + vuBuffer.get(nv21, ySize, vuSize) | ||
| 22 | + | ||
| 23 | + val yuvImage = YuvImage(nv21, ImageFormat.NV21, this.width, this.height, null) | ||
| 24 | + val out = ByteArrayOutputStream() | ||
| 25 | + yuvImage.compressToJpeg(Rect(0, 0, yuvImage.width, yuvImage.height), 50, out) | ||
| 26 | + return out.toByteArray() | ||
| 27 | +} | ||
| 28 | + | ||
| 29 | +val Barcode.data: Map<String, Any?> | ||
| 30 | + get() = mapOf( | ||
| 31 | + "corners" to cornerPoints?.map { corner -> corner.data }, "format" to format, | ||
| 32 | + "rawBytes" to rawBytes, "rawValue" to rawValue, "type" to valueType, | ||
| 33 | + "calendarEvent" to calendarEvent?.data, "contactInfo" to contactInfo?.data, | ||
| 34 | + "driverLicense" to driverLicense?.data, "email" to email?.data, | ||
| 35 | + "geoPoint" to geoPoint?.data, "phone" to phone?.data, "sms" to sms?.data, | ||
| 36 | + "url" to url?.data, "wifi" to wifi?.data, "displayValue" to displayValue | ||
| 37 | + ) | ||
| 38 | + | ||
| 39 | +private val Point.data: Map<String, Double> | ||
| 40 | + get() = mapOf("x" to x.toDouble(), "y" to y.toDouble()) | ||
| 41 | + | ||
| 42 | +private val Barcode.CalendarEvent.data: Map<String, Any?> | ||
| 43 | + get() = mapOf( | ||
| 44 | + "description" to description, "end" to end?.rawValue, "location" to location, | ||
| 45 | + "organizer" to organizer, "start" to start?.rawValue, "status" to status, | ||
| 46 | + "summary" to summary | ||
| 47 | + ) | ||
| 48 | + | ||
| 49 | +private val Barcode.ContactInfo.data: Map<String, Any?> | ||
| 50 | + get() = mapOf( | ||
| 51 | + "addresses" to addresses.map { address -> address.data }, | ||
| 52 | + "emails" to emails.map { email -> email.data }, "name" to name?.data, | ||
| 53 | + "organization" to organization, "phones" to phones.map { phone -> phone.data }, | ||
| 54 | + "title" to title, "urls" to urls | ||
| 55 | + ) | ||
| 56 | + | ||
| 57 | +private val Barcode.Address.data: Map<String, Any?> | ||
| 58 | + get() = mapOf( | ||
| 59 | + "addressLines" to addressLines.map { addressLine -> addressLine.toString() }, | ||
| 60 | + "type" to type | ||
| 61 | + ) | ||
| 62 | + | ||
| 63 | +private val Barcode.PersonName.data: Map<String, Any?> | ||
| 64 | + get() = mapOf( | ||
| 65 | + "first" to first, "formattedName" to formattedName, "last" to last, | ||
| 66 | + "middle" to middle, "prefix" to prefix, "pronunciation" to pronunciation, | ||
| 67 | + "suffix" to suffix | ||
| 68 | + ) | ||
| 69 | + | ||
| 70 | +private val Barcode.DriverLicense.data: Map<String, Any?> | ||
| 71 | + get() = mapOf( | ||
| 72 | + "addressCity" to addressCity, "addressState" to addressState, | ||
| 73 | + "addressStreet" to addressStreet, "addressZip" to addressZip, "birthDate" to birthDate, | ||
| 74 | + "documentType" to documentType, "expiryDate" to expiryDate, "firstName" to firstName, | ||
| 75 | + "gender" to gender, "issueDate" to issueDate, "issuingCountry" to issuingCountry, | ||
| 76 | + "lastName" to lastName, "licenseNumber" to licenseNumber, "middleName" to middleName | ||
| 77 | + ) | ||
| 78 | + | ||
| 79 | +private val Barcode.Email.data: Map<String, Any?> | ||
| 80 | + get() = mapOf("address" to address, "body" to body, "subject" to subject, "type" to type) | ||
| 81 | + | ||
| 82 | +private val Barcode.GeoPoint.data: Map<String, Any?> | ||
| 83 | + get() = mapOf("latitude" to lat, "longitude" to lng) | ||
| 84 | + | ||
| 85 | +private val Barcode.Phone.data: Map<String, Any?> | ||
| 86 | + get() = mapOf("number" to number, "type" to type) | ||
| 87 | + | ||
| 88 | +private val Barcode.Sms.data: Map<String, Any?> | ||
| 89 | + get() = mapOf("message" to message, "phoneNumber" to phoneNumber) | ||
| 90 | + | ||
| 91 | +private val Barcode.UrlBookmark.data: Map<String, Any?> | ||
| 92 | + get() = mapOf("title" to title, "url" to url) | ||
| 93 | + | ||
| 94 | +private val Barcode.WiFi.data: Map<String, Any?> | ||
| 95 | + get() = mapOf("encryptionType" to encryptionType, "password" to password, "ssid" to ssid) |
android/src/main/kotlin/dev/steenbakker/mobile_scanner/exceptions/NoPermissionException.kt
deleted
100644 → 0
| 1 | -package dev.steenbakker.mobile_scanner.exceptions | ||
| 2 | - | ||
| 3 | -internal class NoPermissionException : RuntimeException() | ||
| 4 | - | ||
| 5 | -//internal class Exception(val reason: Reason) : | ||
| 6 | -// java.lang.Exception("Mobile Scanner failed because $reason") { | ||
| 7 | -// | ||
| 8 | -// internal enum class Reason { | ||
| 9 | -// noHardware, noPermissions, noBackCamera | ||
| 10 | -// } | ||
| 11 | -//} |
| 1 | -package dev.steenbakker.mobile_scanner | ||
| 2 | - | ||
| 3 | -import com.google.mlkit.vision.barcode.BarcodeScannerOptions | 1 | +package dev.steenbakker.mobile_scanner.objects |
| 4 | 2 | ||
| 5 | enum class BarcodeFormats(val intValue: Int) { | 3 | enum class BarcodeFormats(val intValue: Int) { |
| 6 | UNKNOWN(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_UNKNOWN), | 4 | UNKNOWN(com.google.mlkit.vision.barcode.common.Barcode.FORMAT_UNKNOWN), |
| @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' | @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' | ||
| 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" | 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" |
| 27 | 27 | ||
| 28 | android { | 28 | android { |
| 29 | - compileSdkVersion 32 | 29 | + compileSdkVersion 33 |
| 30 | 30 | ||
| 31 | compileOptions { | 31 | compileOptions { |
| 32 | sourceCompatibility JavaVersion.VERSION_1_8 | 32 | sourceCompatibility JavaVersion.VERSION_1_8 |
| @@ -45,7 +45,7 @@ android { | @@ -45,7 +45,7 @@ android { | ||
| 45 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). | 45 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). |
| 46 | applicationId "dev.steenbakker.mobile_scanner_example" | 46 | applicationId "dev.steenbakker.mobile_scanner_example" |
| 47 | minSdkVersion 21 | 47 | minSdkVersion 21 |
| 48 | - targetSdkVersion 32 | 48 | + targetSdkVersion 33 |
| 49 | versionCode flutterVersionCode.toInteger() | 49 | versionCode flutterVersionCode.toInteger() |
| 50 | versionName flutterVersionName | 50 | versionName flutterVersionName |
| 51 | } | 51 | } |
| @@ -5,7 +5,7 @@ | @@ -5,7 +5,7 @@ | ||
| 5 | android:label="mobile_scanner_example" | 5 | android:label="mobile_scanner_example" |
| 6 | android:icon="@mipmap/ic_launcher"> | 6 | android:icon="@mipmap/ic_launcher"> |
| 7 | <activity | 7 | <activity |
| 8 | - android:name="io.flutter.embedding.android.FlutterActivity" | 8 | + android:name=".MainActivity" |
| 9 | android:exported="true" | 9 | android:exported="true" |
| 10 | android:launchMode="singleTop" | 10 | android:launchMode="singleTop" |
| 11 | android:theme="@style/LaunchTheme" | 11 | android:theme="@style/LaunchTheme" |
| 1 | package dev.steenbakker.mobile_scanner_example | 1 | package dev.steenbakker.mobile_scanner_example |
| 2 | 2 | ||
| 3 | -import io.flutter.embedding.android.FlutterActivity | 3 | +import io.flutter.embedding.android.FlutterFragmentActivity |
| 4 | 4 | ||
| 5 | -class MainActivity: FlutterActivity() { | 5 | +class MainActivity : FlutterFragmentActivity() { |
| 6 | } | 6 | } |
| 1 | buildscript { | 1 | buildscript { |
| 2 | - ext.kotlin_version = '1.7.10' | 2 | + ext.kotlin_version = '1.7.22' |
| 3 | repositories { | 3 | repositories { |
| 4 | google() | 4 | google() |
| 5 | mavenCentral() | 5 | mavenCentral() |
| 6 | } | 6 | } |
| 7 | 7 | ||
| 8 | dependencies { | 8 | dependencies { |
| 9 | - classpath 'com.android.tools.build:gradle:7.2.1' | 9 | + classpath 'com.android.tools.build:gradle:7.3.1' |
| 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" | 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" |
| 11 | } | 11 | } |
| 12 | } | 12 | } |
| 1 | -#Tue May 31 10:34:01 CEST 2022 | 1 | +#Tue Aug 23 15:51:00 CEST 2022 |
| 2 | distributionBase=GRADLE_USER_HOME | 2 | distributionBase=GRADLE_USER_HOME |
| 3 | -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip | 3 | +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip |
| 4 | distributionPath=wrapper/dists | 4 | distributionPath=wrapper/dists |
| 5 | zipStorePath=wrapper/dists | 5 | zipStorePath=wrapper/dists |
| 6 | zipStoreBase=GRADLE_USER_HOME | 6 | zipStoreBase=GRADLE_USER_HOME |
| @@ -21,6 +21,6 @@ | @@ -21,6 +21,6 @@ | ||
| 21 | <key>CFBundleVersion</key> | 21 | <key>CFBundleVersion</key> |
| 22 | <string>1.0</string> | 22 | <string>1.0</string> |
| 23 | <key>MinimumOSVersion</key> | 23 | <key>MinimumOSVersion</key> |
| 24 | - <string>9.0</string> | 24 | + <string>11.0</string> |
| 25 | </dict> | 25 | </dict> |
| 26 | </plist> | 26 | </plist> |
| 1 | # Uncomment this line to define a global platform for your project | 1 | # Uncomment this line to define a global platform for your project |
| 2 | -platform :ios, '10.0' | 2 | +platform :ios, '11.0' |
| 3 | 3 | ||
| 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. |
| 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' | 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' |
| @@ -37,5 +37,9 @@ end | @@ -37,5 +37,9 @@ end | ||
| 37 | post_install do |installer| | 37 | post_install do |installer| |
| 38 | installer.pods_project.targets.each do |target| | 38 | installer.pods_project.targets.each do |target| |
| 39 | flutter_additional_ios_build_settings(target) | 39 | flutter_additional_ios_build_settings(target) |
| 40 | + target.build_configurations.each do |config| | ||
| 41 | + config.build_settings['ENABLE_BITCODE'] = 'NO' | ||
| 42 | + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0' | ||
| 43 | + end | ||
| 40 | end | 44 | end |
| 41 | end | 45 | end |
| @@ -13,7 +13,7 @@ | @@ -13,7 +13,7 @@ | ||
| 13 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; | 13 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; |
| 14 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; | 14 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; |
| 15 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; | 15 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; |
| 16 | - C80F46710D9B9F4F17AD4E3D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E133769572782C32D37D8AC /* Pods_Runner.framework */; }; | 16 | + A5A2C2B73A9F26060DE9FB22 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54E006799E73DEAB41FD3623 /* Pods_Runner.framework */; }; |
| 17 | /* End PBXBuildFile section */ | 17 | /* End PBXBuildFile section */ |
| 18 | 18 | ||
| 19 | /* Begin PBXCopyFilesBuildPhase section */ | 19 | /* Begin PBXCopyFilesBuildPhase section */ |
| @@ -32,9 +32,9 @@ | @@ -32,9 +32,9 @@ | ||
| 32 | /* Begin PBXFileReference section */ | 32 | /* Begin PBXFileReference section */ |
| 33 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; }; | 33 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; }; |
| 34 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; }; | 34 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; }; |
| 35 | - 1CD9C88F6BFEF6CB7CA6746B /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; }; | 35 | + 32FD382A786B3A0080FE63FD /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; }; |
| 36 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; }; | 36 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; }; |
| 37 | - 5E133769572782C32D37D8AC /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; | 37 | + 54E006799E73DEAB41FD3623 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; |
| 38 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; }; | 38 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; }; |
| 39 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; | 39 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; |
| 40 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; }; | 40 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; }; |
| @@ -45,8 +45,8 @@ | @@ -45,8 +45,8 @@ | ||
| 45 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; | 45 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; |
| 46 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; | 46 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; |
| 47 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; | 47 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; |
| 48 | - E29A089CD1D61281C49DBB79 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; }; | ||
| 49 | - E33BE6AC5C06F7A45470ADE0 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; }; | 48 | + D5B36FCD262B39F867CFDEEE /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; }; |
| 49 | + F0D5742F0690BE32D07B033A /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; }; | ||
| 50 | /* End PBXFileReference section */ | 50 | /* End PBXFileReference section */ |
| 51 | 51 | ||
| 52 | /* Begin PBXFrameworksBuildPhase section */ | 52 | /* Begin PBXFrameworksBuildPhase section */ |
| @@ -54,27 +54,19 @@ | @@ -54,27 +54,19 @@ | ||
| 54 | isa = PBXFrameworksBuildPhase; | 54 | isa = PBXFrameworksBuildPhase; |
| 55 | buildActionMask = 2147483647; | 55 | buildActionMask = 2147483647; |
| 56 | files = ( | 56 | files = ( |
| 57 | - C80F46710D9B9F4F17AD4E3D /* Pods_Runner.framework in Frameworks */, | 57 | + A5A2C2B73A9F26060DE9FB22 /* Pods_Runner.framework in Frameworks */, |
| 58 | ); | 58 | ); |
| 59 | runOnlyForDeploymentPostprocessing = 0; | 59 | runOnlyForDeploymentPostprocessing = 0; |
| 60 | }; | 60 | }; |
| 61 | /* End PBXFrameworksBuildPhase section */ | 61 | /* End PBXFrameworksBuildPhase section */ |
| 62 | 62 | ||
| 63 | /* Begin PBXGroup section */ | 63 | /* Begin PBXGroup section */ |
| 64 | - 0F766276E0F46921DEBF581B /* Frameworks */ = { | ||
| 65 | - isa = PBXGroup; | ||
| 66 | - children = ( | ||
| 67 | - 5E133769572782C32D37D8AC /* Pods_Runner.framework */, | ||
| 68 | - ); | ||
| 69 | - name = Frameworks; | ||
| 70 | - sourceTree = "<group>"; | ||
| 71 | - }; | ||
| 72 | 203D5C95A734778D93D18369 /* Pods */ = { | 64 | 203D5C95A734778D93D18369 /* Pods */ = { |
| 73 | isa = PBXGroup; | 65 | isa = PBXGroup; |
| 74 | children = ( | 66 | children = ( |
| 75 | - E33BE6AC5C06F7A45470ADE0 /* Pods-Runner.debug.xcconfig */, | ||
| 76 | - 1CD9C88F6BFEF6CB7CA6746B /* Pods-Runner.release.xcconfig */, | ||
| 77 | - E29A089CD1D61281C49DBB79 /* Pods-Runner.profile.xcconfig */, | 67 | + D5B36FCD262B39F867CFDEEE /* Pods-Runner.debug.xcconfig */, |
| 68 | + 32FD382A786B3A0080FE63FD /* Pods-Runner.release.xcconfig */, | ||
| 69 | + F0D5742F0690BE32D07B033A /* Pods-Runner.profile.xcconfig */, | ||
| 78 | ); | 70 | ); |
| 79 | path = Pods; | 71 | path = Pods; |
| 80 | sourceTree = "<group>"; | 72 | sourceTree = "<group>"; |
| @@ -97,7 +89,7 @@ | @@ -97,7 +89,7 @@ | ||
| 97 | 97C146F01CF9000F007C117D /* Runner */, | 89 | 97C146F01CF9000F007C117D /* Runner */, |
| 98 | 97C146EF1CF9000F007C117D /* Products */, | 90 | 97C146EF1CF9000F007C117D /* Products */, |
| 99 | 203D5C95A734778D93D18369 /* Pods */, | 91 | 203D5C95A734778D93D18369 /* Pods */, |
| 100 | - 0F766276E0F46921DEBF581B /* Frameworks */, | 92 | + FF36E403CAC9E06A5A96BB9F /* Frameworks */, |
| 101 | ); | 93 | ); |
| 102 | sourceTree = "<group>"; | 94 | sourceTree = "<group>"; |
| 103 | }; | 95 | }; |
| @@ -124,6 +116,14 @@ | @@ -124,6 +116,14 @@ | ||
| 124 | path = Runner; | 116 | path = Runner; |
| 125 | sourceTree = "<group>"; | 117 | sourceTree = "<group>"; |
| 126 | }; | 118 | }; |
| 119 | + FF36E403CAC9E06A5A96BB9F /* Frameworks */ = { | ||
| 120 | + isa = PBXGroup; | ||
| 121 | + children = ( | ||
| 122 | + 54E006799E73DEAB41FD3623 /* Pods_Runner.framework */, | ||
| 123 | + ); | ||
| 124 | + name = Frameworks; | ||
| 125 | + sourceTree = "<group>"; | ||
| 126 | + }; | ||
| 127 | /* End PBXGroup section */ | 127 | /* End PBXGroup section */ |
| 128 | 128 | ||
| 129 | /* Begin PBXNativeTarget section */ | 129 | /* Begin PBXNativeTarget section */ |
| @@ -131,14 +131,14 @@ | @@ -131,14 +131,14 @@ | ||
| 131 | isa = PBXNativeTarget; | 131 | isa = PBXNativeTarget; |
| 132 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; | 132 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; |
| 133 | buildPhases = ( | 133 | buildPhases = ( |
| 134 | - 1C759CA63421B131D22BB688 /* [CP] Check Pods Manifest.lock */, | 134 | + B086C54F5791A4E759CB6822 /* [CP] Check Pods Manifest.lock */, |
| 135 | 9740EEB61CF901F6004384FC /* Run Script */, | 135 | 9740EEB61CF901F6004384FC /* Run Script */, |
| 136 | 97C146EA1CF9000F007C117D /* Sources */, | 136 | 97C146EA1CF9000F007C117D /* Sources */, |
| 137 | 97C146EB1CF9000F007C117D /* Frameworks */, | 137 | 97C146EB1CF9000F007C117D /* Frameworks */, |
| 138 | 97C146EC1CF9000F007C117D /* Resources */, | 138 | 97C146EC1CF9000F007C117D /* Resources */, |
| 139 | 9705A1C41CF9048500538489 /* Embed Frameworks */, | 139 | 9705A1C41CF9048500538489 /* Embed Frameworks */, |
| 140 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, | 140 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, |
| 141 | - EE97B31B239E017B5516C6AD /* [CP] Embed Pods Frameworks */, | 141 | + F825A499E8C466DB9DC6247D /* [CP] Embed Pods Frameworks */, |
| 142 | ); | 142 | ); |
| 143 | buildRules = ( | 143 | buildRules = ( |
| 144 | ); | 144 | ); |
| @@ -197,57 +197,57 @@ | @@ -197,57 +197,57 @@ | ||
| 197 | /* End PBXResourcesBuildPhase section */ | 197 | /* End PBXResourcesBuildPhase section */ |
| 198 | 198 | ||
| 199 | /* Begin PBXShellScriptBuildPhase section */ | 199 | /* Begin PBXShellScriptBuildPhase section */ |
| 200 | - 1C759CA63421B131D22BB688 /* [CP] Check Pods Manifest.lock */ = { | 200 | + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { |
| 201 | isa = PBXShellScriptBuildPhase; | 201 | isa = PBXShellScriptBuildPhase; |
| 202 | buildActionMask = 2147483647; | 202 | buildActionMask = 2147483647; |
| 203 | files = ( | 203 | files = ( |
| 204 | ); | 204 | ); |
| 205 | - inputFileListPaths = ( | ||
| 206 | - ); | ||
| 207 | inputPaths = ( | 205 | inputPaths = ( |
| 208 | - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", | ||
| 209 | - "${PODS_ROOT}/Manifest.lock", | ||
| 210 | - ); | ||
| 211 | - name = "[CP] Check Pods Manifest.lock"; | ||
| 212 | - outputFileListPaths = ( | ||
| 213 | ); | 206 | ); |
| 207 | + name = "Thin Binary"; | ||
| 214 | outputPaths = ( | 208 | outputPaths = ( |
| 215 | - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", | ||
| 216 | ); | 209 | ); |
| 217 | runOnlyForDeploymentPostprocessing = 0; | 210 | runOnlyForDeploymentPostprocessing = 0; |
| 218 | shellPath = /bin/sh; | 211 | shellPath = /bin/sh; |
| 219 | - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; | ||
| 220 | - showEnvVarsInLog = 0; | 212 | + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; |
| 221 | }; | 213 | }; |
| 222 | - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { | 214 | + 9740EEB61CF901F6004384FC /* Run Script */ = { |
| 223 | isa = PBXShellScriptBuildPhase; | 215 | isa = PBXShellScriptBuildPhase; |
| 224 | buildActionMask = 2147483647; | 216 | buildActionMask = 2147483647; |
| 225 | files = ( | 217 | files = ( |
| 226 | ); | 218 | ); |
| 227 | inputPaths = ( | 219 | inputPaths = ( |
| 228 | ); | 220 | ); |
| 229 | - name = "Thin Binary"; | 221 | + name = "Run Script"; |
| 230 | outputPaths = ( | 222 | outputPaths = ( |
| 231 | ); | 223 | ); |
| 232 | runOnlyForDeploymentPostprocessing = 0; | 224 | runOnlyForDeploymentPostprocessing = 0; |
| 233 | shellPath = /bin/sh; | 225 | shellPath = /bin/sh; |
| 234 | - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; | 226 | + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; |
| 235 | }; | 227 | }; |
| 236 | - 9740EEB61CF901F6004384FC /* Run Script */ = { | 228 | + B086C54F5791A4E759CB6822 /* [CP] Check Pods Manifest.lock */ = { |
| 237 | isa = PBXShellScriptBuildPhase; | 229 | isa = PBXShellScriptBuildPhase; |
| 238 | buildActionMask = 2147483647; | 230 | buildActionMask = 2147483647; |
| 239 | files = ( | 231 | files = ( |
| 240 | ); | 232 | ); |
| 233 | + inputFileListPaths = ( | ||
| 234 | + ); | ||
| 241 | inputPaths = ( | 235 | inputPaths = ( |
| 236 | + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", | ||
| 237 | + "${PODS_ROOT}/Manifest.lock", | ||
| 238 | + ); | ||
| 239 | + name = "[CP] Check Pods Manifest.lock"; | ||
| 240 | + outputFileListPaths = ( | ||
| 242 | ); | 241 | ); |
| 243 | - name = "Run Script"; | ||
| 244 | outputPaths = ( | 242 | outputPaths = ( |
| 243 | + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", | ||
| 245 | ); | 244 | ); |
| 246 | runOnlyForDeploymentPostprocessing = 0; | 245 | runOnlyForDeploymentPostprocessing = 0; |
| 247 | shellPath = /bin/sh; | 246 | shellPath = /bin/sh; |
| 248 | - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; | 247 | + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; |
| 248 | + showEnvVarsInLog = 0; | ||
| 249 | }; | 249 | }; |
| 250 | - EE97B31B239E017B5516C6AD /* [CP] Embed Pods Frameworks */ = { | 250 | + F825A499E8C466DB9DC6247D /* [CP] Embed Pods Frameworks */ = { |
| 251 | isa = PBXShellScriptBuildPhase; | 251 | isa = PBXShellScriptBuildPhase; |
| 252 | buildActionMask = 2147483647; | 252 | buildActionMask = 2147483647; |
| 253 | files = ( | 253 | files = ( |
| @@ -339,7 +339,7 @@ | @@ -339,7 +339,7 @@ | ||
| 339 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; | 339 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; |
| 340 | GCC_WARN_UNUSED_FUNCTION = YES; | 340 | GCC_WARN_UNUSED_FUNCTION = YES; |
| 341 | GCC_WARN_UNUSED_VARIABLE = YES; | 341 | GCC_WARN_UNUSED_VARIABLE = YES; |
| 342 | - IPHONEOS_DEPLOYMENT_TARGET = 9.0; | 342 | + IPHONEOS_DEPLOYMENT_TARGET = 11.0; |
| 343 | MTL_ENABLE_DEBUG_INFO = NO; | 343 | MTL_ENABLE_DEBUG_INFO = NO; |
| 344 | SDKROOT = iphoneos; | 344 | SDKROOT = iphoneos; |
| 345 | SUPPORTED_PLATFORMS = iphoneos; | 345 | SUPPORTED_PLATFORMS = iphoneos; |
| @@ -364,7 +364,7 @@ | @@ -364,7 +364,7 @@ | ||
| 364 | "$(inherited)", | 364 | "$(inherited)", |
| 365 | "@executable_path/Frameworks", | 365 | "@executable_path/Frameworks", |
| 366 | ); | 366 | ); |
| 367 | - PRODUCT_BUNDLE_IDENTIFIER = dev.casvanluijtelaar.mobileScannerExample; | 367 | + PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScanner; |
| 368 | PRODUCT_NAME = "$(TARGET_NAME)"; | 368 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 369 | PROVISIONING_PROFILE_SPECIFIER = ""; | 369 | PROVISIONING_PROFILE_SPECIFIER = ""; |
| 370 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; | 370 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; |
| @@ -420,7 +420,7 @@ | @@ -420,7 +420,7 @@ | ||
| 420 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; | 420 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; |
| 421 | GCC_WARN_UNUSED_FUNCTION = YES; | 421 | GCC_WARN_UNUSED_FUNCTION = YES; |
| 422 | GCC_WARN_UNUSED_VARIABLE = YES; | 422 | GCC_WARN_UNUSED_VARIABLE = YES; |
| 423 | - IPHONEOS_DEPLOYMENT_TARGET = 9.0; | 423 | + IPHONEOS_DEPLOYMENT_TARGET = 11.0; |
| 424 | MTL_ENABLE_DEBUG_INFO = YES; | 424 | MTL_ENABLE_DEBUG_INFO = YES; |
| 425 | ONLY_ACTIVE_ARCH = YES; | 425 | ONLY_ACTIVE_ARCH = YES; |
| 426 | SDKROOT = iphoneos; | 426 | SDKROOT = iphoneos; |
| @@ -469,7 +469,7 @@ | @@ -469,7 +469,7 @@ | ||
| 469 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; | 469 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; |
| 470 | GCC_WARN_UNUSED_FUNCTION = YES; | 470 | GCC_WARN_UNUSED_FUNCTION = YES; |
| 471 | GCC_WARN_UNUSED_VARIABLE = YES; | 471 | GCC_WARN_UNUSED_VARIABLE = YES; |
| 472 | - IPHONEOS_DEPLOYMENT_TARGET = 9.0; | 472 | + IPHONEOS_DEPLOYMENT_TARGET = 11.0; |
| 473 | MTL_ENABLE_DEBUG_INFO = NO; | 473 | MTL_ENABLE_DEBUG_INFO = NO; |
| 474 | SDKROOT = iphoneos; | 474 | SDKROOT = iphoneos; |
| 475 | SUPPORTED_PLATFORMS = iphoneos; | 475 | SUPPORTED_PLATFORMS = iphoneos; |
| @@ -496,7 +496,7 @@ | @@ -496,7 +496,7 @@ | ||
| 496 | "$(inherited)", | 496 | "$(inherited)", |
| 497 | "@executable_path/Frameworks", | 497 | "@executable_path/Frameworks", |
| 498 | ); | 498 | ); |
| 499 | - PRODUCT_BUNDLE_IDENTIFIER = dev.casvanluijtelaar.mobileScannerExample; | 499 | + PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScanner; |
| 500 | PRODUCT_NAME = "$(TARGET_NAME)"; | 500 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 501 | PROVISIONING_PROFILE_SPECIFIER = ""; | 501 | PROVISIONING_PROFILE_SPECIFIER = ""; |
| 502 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; | 502 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; |
| @@ -522,7 +522,7 @@ | @@ -522,7 +522,7 @@ | ||
| 522 | "$(inherited)", | 522 | "$(inherited)", |
| 523 | "@executable_path/Frameworks", | 523 | "@executable_path/Frameworks", |
| 524 | ); | 524 | ); |
| 525 | - PRODUCT_BUNDLE_IDENTIFIER = dev.casvanluijtelaar.mobileScannerExample; | 525 | + PRODUCT_BUNDLE_IDENTIFIER = dev.steenbakker.mobileScanner; |
| 526 | PRODUCT_NAME = "$(TARGET_NAME)"; | 526 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 527 | PROVISIONING_PROFILE_SPECIFIER = ""; | 527 | PROVISIONING_PROFILE_SPECIFIER = ""; |
| 528 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; | 528 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; |
| 1 | +import 'package:flutter/material.dart'; | ||
| 2 | +import 'package:image_picker/image_picker.dart'; | ||
| 3 | +import 'package:mobile_scanner/mobile_scanner.dart'; | ||
| 4 | + | ||
| 5 | +class BarcodeListScannerWithController extends StatefulWidget { | ||
| 6 | + const BarcodeListScannerWithController({Key? key}) : super(key: key); | ||
| 7 | + | ||
| 8 | + @override | ||
| 9 | + _BarcodeListScannerWithControllerState createState() => | ||
| 10 | + _BarcodeListScannerWithControllerState(); | ||
| 11 | +} | ||
| 12 | + | ||
| 13 | +class _BarcodeListScannerWithControllerState | ||
| 14 | + extends State<BarcodeListScannerWithController> | ||
| 15 | + with SingleTickerProviderStateMixin { | ||
| 16 | + BarcodeCapture? barcodeCapture; | ||
| 17 | + | ||
| 18 | + final MobileScannerController controller = MobileScannerController( | ||
| 19 | + torchEnabled: true, | ||
| 20 | + // formats: [BarcodeFormat.qrCode] | ||
| 21 | + // facing: CameraFacing.front, | ||
| 22 | + // detectionSpeed: DetectionSpeed.normal | ||
| 23 | + // detectionTimeoutMs: 1000, | ||
| 24 | + // returnImage: false, | ||
| 25 | + ); | ||
| 26 | + | ||
| 27 | + bool isStarted = true; | ||
| 28 | + | ||
| 29 | + void _startOrStop() { | ||
| 30 | + if (isStarted) { | ||
| 31 | + controller.stop(); | ||
| 32 | + } else { | ||
| 33 | + controller.start().catchError((error) { | ||
| 34 | + final exception = error as MobileScannerException; | ||
| 35 | + | ||
| 36 | + switch (exception.errorCode) { | ||
| 37 | + case MobileScannerErrorCode.controllerUninitialized: | ||
| 38 | + break; // This error code is not used by `start()`. | ||
| 39 | + case MobileScannerErrorCode.genericError: | ||
| 40 | + debugPrint('Scanner failed to start'); | ||
| 41 | + break; | ||
| 42 | + case MobileScannerErrorCode.permissionDenied: | ||
| 43 | + debugPrint('Camera permission denied'); | ||
| 44 | + break; | ||
| 45 | + } | ||
| 46 | + }); | ||
| 47 | + } | ||
| 48 | + | ||
| 49 | + setState(() { | ||
| 50 | + isStarted = !isStarted; | ||
| 51 | + }); | ||
| 52 | + } | ||
| 53 | + | ||
| 54 | + @override | ||
| 55 | + Widget build(BuildContext context) { | ||
| 56 | + return Scaffold( | ||
| 57 | + backgroundColor: Colors.black, | ||
| 58 | + body: Builder( | ||
| 59 | + builder: (context) { | ||
| 60 | + return Stack( | ||
| 61 | + children: [ | ||
| 62 | + MobileScanner( | ||
| 63 | + controller: controller, | ||
| 64 | + fit: BoxFit.contain, | ||
| 65 | + onDetect: (barcodeCapture) { | ||
| 66 | + setState(() { | ||
| 67 | + this.barcodeCapture = barcodeCapture; | ||
| 68 | + }); | ||
| 69 | + }, | ||
| 70 | + onScannerStarted: (arguments) { | ||
| 71 | + // Do something with arguments. | ||
| 72 | + }, | ||
| 73 | + ), | ||
| 74 | + Align( | ||
| 75 | + alignment: Alignment.bottomCenter, | ||
| 76 | + child: Container( | ||
| 77 | + alignment: Alignment.bottomCenter, | ||
| 78 | + height: 100, | ||
| 79 | + color: Colors.black.withOpacity(0.4), | ||
| 80 | + child: Row( | ||
| 81 | + mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||
| 82 | + children: [ | ||
| 83 | + IconButton( | ||
| 84 | + color: Colors.white, | ||
| 85 | + icon: ValueListenableBuilder( | ||
| 86 | + valueListenable: controller.torchState, | ||
| 87 | + builder: (context, state, child) { | ||
| 88 | + if (state == null) { | ||
| 89 | + return const Icon( | ||
| 90 | + Icons.flash_off, | ||
| 91 | + color: Colors.grey, | ||
| 92 | + ); | ||
| 93 | + } | ||
| 94 | + switch (state as TorchState) { | ||
| 95 | + case TorchState.off: | ||
| 96 | + return const Icon( | ||
| 97 | + Icons.flash_off, | ||
| 98 | + color: Colors.grey, | ||
| 99 | + ); | ||
| 100 | + case TorchState.on: | ||
| 101 | + return const Icon( | ||
| 102 | + Icons.flash_on, | ||
| 103 | + color: Colors.yellow, | ||
| 104 | + ); | ||
| 105 | + } | ||
| 106 | + }, | ||
| 107 | + ), | ||
| 108 | + iconSize: 32.0, | ||
| 109 | + onPressed: () => controller.toggleTorch(), | ||
| 110 | + ), | ||
| 111 | + IconButton( | ||
| 112 | + color: Colors.white, | ||
| 113 | + icon: isStarted | ||
| 114 | + ? const Icon(Icons.stop) | ||
| 115 | + : const Icon(Icons.play_arrow), | ||
| 116 | + iconSize: 32.0, | ||
| 117 | + onPressed: _startOrStop, | ||
| 118 | + ), | ||
| 119 | + Center( | ||
| 120 | + child: SizedBox( | ||
| 121 | + width: MediaQuery.of(context).size.width - 200, | ||
| 122 | + height: 50, | ||
| 123 | + child: FittedBox( | ||
| 124 | + child: Text( | ||
| 125 | + '${barcodeCapture?.barcodes.map((e) => e.rawValue) ?? 'Scan something!'}', | ||
| 126 | + overflow: TextOverflow.fade, | ||
| 127 | + style: Theme.of(context) | ||
| 128 | + .textTheme | ||
| 129 | + .headline4! | ||
| 130 | + .copyWith(color: Colors.white), | ||
| 131 | + ), | ||
| 132 | + ), | ||
| 133 | + ), | ||
| 134 | + ), | ||
| 135 | + IconButton( | ||
| 136 | + color: Colors.white, | ||
| 137 | + icon: ValueListenableBuilder( | ||
| 138 | + valueListenable: controller.cameraFacingState, | ||
| 139 | + builder: (context, state, child) { | ||
| 140 | + if (state == null) { | ||
| 141 | + return const Icon(Icons.camera_front); | ||
| 142 | + } | ||
| 143 | + switch (state as CameraFacing) { | ||
| 144 | + case CameraFacing.front: | ||
| 145 | + return const Icon(Icons.camera_front); | ||
| 146 | + case CameraFacing.back: | ||
| 147 | + return const Icon(Icons.camera_rear); | ||
| 148 | + } | ||
| 149 | + }, | ||
| 150 | + ), | ||
| 151 | + iconSize: 32.0, | ||
| 152 | + onPressed: () => controller.switchCamera(), | ||
| 153 | + ), | ||
| 154 | + IconButton( | ||
| 155 | + color: Colors.white, | ||
| 156 | + icon: const Icon(Icons.image), | ||
| 157 | + iconSize: 32.0, | ||
| 158 | + onPressed: () async { | ||
| 159 | + final ImagePicker picker = ImagePicker(); | ||
| 160 | + // Pick an image | ||
| 161 | + final XFile? image = await picker.pickImage( | ||
| 162 | + source: ImageSource.gallery, | ||
| 163 | + ); | ||
| 164 | + if (image != null) { | ||
| 165 | + if (await controller.analyzeImage(image.path)) { | ||
| 166 | + if (!mounted) return; | ||
| 167 | + ScaffoldMessenger.of(context).showSnackBar( | ||
| 168 | + const SnackBar( | ||
| 169 | + content: Text('Barcode found!'), | ||
| 170 | + backgroundColor: Colors.green, | ||
| 171 | + ), | ||
| 172 | + ); | ||
| 173 | + } else { | ||
| 174 | + if (!mounted) return; | ||
| 175 | + ScaffoldMessenger.of(context).showSnackBar( | ||
| 176 | + const SnackBar( | ||
| 177 | + content: Text('No barcode found!'), | ||
| 178 | + backgroundColor: Colors.red, | ||
| 179 | + ), | ||
| 180 | + ); | ||
| 181 | + } | ||
| 182 | + } | ||
| 183 | + }, | ||
| 184 | + ), | ||
| 185 | + ], | ||
| 186 | + ), | ||
| 187 | + ), | ||
| 188 | + ), | ||
| 189 | + ], | ||
| 190 | + ); | ||
| 191 | + }, | ||
| 192 | + ), | ||
| 193 | + ); | ||
| 194 | + } | ||
| 195 | + | ||
| 196 | + @override | ||
| 197 | + void dispose() { | ||
| 198 | + controller.dispose(); | ||
| 199 | + super.dispose(); | ||
| 200 | + } | ||
| 201 | +} |
| @@ -13,16 +13,44 @@ class BarcodeScannerWithController extends StatefulWidget { | @@ -13,16 +13,44 @@ class BarcodeScannerWithController extends StatefulWidget { | ||
| 13 | class _BarcodeScannerWithControllerState | 13 | class _BarcodeScannerWithControllerState |
| 14 | extends State<BarcodeScannerWithController> | 14 | extends State<BarcodeScannerWithController> |
| 15 | with SingleTickerProviderStateMixin { | 15 | with SingleTickerProviderStateMixin { |
| 16 | - String? barcode; | 16 | + BarcodeCapture? barcode; |
| 17 | 17 | ||
| 18 | - MobileScannerController controller = MobileScannerController( | 18 | + final MobileScannerController controller = MobileScannerController( |
| 19 | torchEnabled: true, | 19 | torchEnabled: true, |
| 20 | // formats: [BarcodeFormat.qrCode] | 20 | // formats: [BarcodeFormat.qrCode] |
| 21 | // facing: CameraFacing.front, | 21 | // facing: CameraFacing.front, |
| 22 | + // detectionSpeed: DetectionSpeed.normal | ||
| 23 | + // detectionTimeoutMs: 1000, | ||
| 24 | + // returnImage: false, | ||
| 22 | ); | 25 | ); |
| 23 | 26 | ||
| 24 | bool isStarted = true; | 27 | bool isStarted = true; |
| 25 | 28 | ||
| 29 | + void _startOrStop() { | ||
| 30 | + if (isStarted) { | ||
| 31 | + controller.stop(); | ||
| 32 | + } else { | ||
| 33 | + controller.start().catchError((error) { | ||
| 34 | + final exception = error as MobileScannerException; | ||
| 35 | + | ||
| 36 | + switch (exception.errorCode) { | ||
| 37 | + case MobileScannerErrorCode.controllerUninitialized: | ||
| 38 | + break; // This error code is not used by `start()`. | ||
| 39 | + case MobileScannerErrorCode.genericError: | ||
| 40 | + debugPrint('Scanner failed to start'); | ||
| 41 | + break; | ||
| 42 | + case MobileScannerErrorCode.permissionDenied: | ||
| 43 | + debugPrint('Camera permission denied'); | ||
| 44 | + break; | ||
| 45 | + } | ||
| 46 | + }); | ||
| 47 | + } | ||
| 48 | + | ||
| 49 | + setState(() { | ||
| 50 | + isStarted = !isStarted; | ||
| 51 | + }); | ||
| 52 | + } | ||
| 53 | + | ||
| 26 | @override | 54 | @override |
| 27 | Widget build(BuildContext context) { | 55 | Widget build(BuildContext context) { |
| 28 | return Scaffold( | 56 | return Scaffold( |
| @@ -34,14 +62,9 @@ class _BarcodeScannerWithControllerState | @@ -34,14 +62,9 @@ class _BarcodeScannerWithControllerState | ||
| 34 | MobileScanner( | 62 | MobileScanner( |
| 35 | controller: controller, | 63 | controller: controller, |
| 36 | fit: BoxFit.contain, | 64 | fit: BoxFit.contain, |
| 37 | - // allowDuplicates: true, | ||
| 38 | - // controller: MobileScannerController( | ||
| 39 | - // torchEnabled: true, | ||
| 40 | - // facing: CameraFacing.front, | ||
| 41 | - // ), | ||
| 42 | - onDetect: (barcode, args) { | 65 | + onDetect: (barcode) { |
| 43 | setState(() { | 66 | setState(() { |
| 44 | - this.barcode = barcode.rawValue; | 67 | + this.barcode = barcode; |
| 45 | }); | 68 | }); |
| 46 | }, | 69 | }, |
| 47 | ), | 70 | ), |
| @@ -88,10 +111,7 @@ class _BarcodeScannerWithControllerState | @@ -88,10 +111,7 @@ class _BarcodeScannerWithControllerState | ||
| 88 | ? const Icon(Icons.stop) | 111 | ? const Icon(Icons.stop) |
| 89 | : const Icon(Icons.play_arrow), | 112 | : const Icon(Icons.play_arrow), |
| 90 | iconSize: 32.0, | 113 | iconSize: 32.0, |
| 91 | - onPressed: () => setState(() { | ||
| 92 | - isStarted ? controller.stop() : controller.start(); | ||
| 93 | - isStarted = !isStarted; | ||
| 94 | - }), | 114 | + onPressed: _startOrStop, |
| 95 | ), | 115 | ), |
| 96 | Center( | 116 | Center( |
| 97 | child: SizedBox( | 117 | child: SizedBox( |
| @@ -99,7 +119,8 @@ class _BarcodeScannerWithControllerState | @@ -99,7 +119,8 @@ class _BarcodeScannerWithControllerState | ||
| 99 | height: 50, | 119 | height: 50, |
| 100 | child: FittedBox( | 120 | child: FittedBox( |
| 101 | child: Text( | 121 | child: Text( |
| 102 | - barcode ?? 'Scan something!', | 122 | + barcode?.barcodes.first.rawValue ?? |
| 123 | + 'Scan something!', | ||
| 103 | overflow: TextOverflow.fade, | 124 | overflow: TextOverflow.fade, |
| 104 | style: Theme.of(context) | 125 | style: Theme.of(context) |
| 105 | .textTheme | 126 | .textTheme |
| @@ -169,4 +190,10 @@ class _BarcodeScannerWithControllerState | @@ -169,4 +190,10 @@ class _BarcodeScannerWithControllerState | ||
| 169 | ), | 190 | ), |
| 170 | ); | 191 | ); |
| 171 | } | 192 | } |
| 193 | + | ||
| 194 | + @override | ||
| 195 | + void dispose() { | ||
| 196 | + controller.dispose(); | ||
| 197 | + super.dispose(); | ||
| 198 | + } | ||
| 172 | } | 199 | } |
| 1 | +import 'dart:math'; | ||
| 2 | + | ||
| 3 | +import 'package:flutter/material.dart'; | ||
| 4 | +import 'package:mobile_scanner/mobile_scanner.dart'; | ||
| 5 | + | ||
| 6 | +class BarcodeScannerReturningImage extends StatefulWidget { | ||
| 7 | + const BarcodeScannerReturningImage({Key? key}) : super(key: key); | ||
| 8 | + | ||
| 9 | + @override | ||
| 10 | + _BarcodeScannerReturningImageState createState() => | ||
| 11 | + _BarcodeScannerReturningImageState(); | ||
| 12 | +} | ||
| 13 | + | ||
| 14 | +class _BarcodeScannerReturningImageState | ||
| 15 | + extends State<BarcodeScannerReturningImage> | ||
| 16 | + with SingleTickerProviderStateMixin { | ||
| 17 | + BarcodeCapture? barcode; | ||
| 18 | + MobileScannerArguments? arguments; | ||
| 19 | + | ||
| 20 | + final MobileScannerController controller = MobileScannerController( | ||
| 21 | + torchEnabled: true, | ||
| 22 | + // formats: [BarcodeFormat.qrCode] | ||
| 23 | + // facing: CameraFacing.front, | ||
| 24 | + // detectionSpeed: DetectionSpeed.normal | ||
| 25 | + // detectionTimeoutMs: 1000, | ||
| 26 | + returnImage: true, | ||
| 27 | + ); | ||
| 28 | + | ||
| 29 | + bool isStarted = true; | ||
| 30 | + | ||
| 31 | + void _startOrStop() { | ||
| 32 | + if (isStarted) { | ||
| 33 | + controller.stop(); | ||
| 34 | + } else { | ||
| 35 | + controller.start().catchError((error) { | ||
| 36 | + final exception = error as MobileScannerException; | ||
| 37 | + | ||
| 38 | + switch (exception.errorCode) { | ||
| 39 | + case MobileScannerErrorCode.controllerUninitialized: | ||
| 40 | + break; // This error code is not used by `start()`. | ||
| 41 | + case MobileScannerErrorCode.genericError: | ||
| 42 | + debugPrint('Scanner failed to start'); | ||
| 43 | + break; | ||
| 44 | + case MobileScannerErrorCode.permissionDenied: | ||
| 45 | + debugPrint('Camera permission denied'); | ||
| 46 | + break; | ||
| 47 | + } | ||
| 48 | + }); | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + setState(() { | ||
| 52 | + isStarted = !isStarted; | ||
| 53 | + }); | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + @override | ||
| 57 | + Widget build(BuildContext context) { | ||
| 58 | + return Scaffold( | ||
| 59 | + body: SafeArea( | ||
| 60 | + child: Column( | ||
| 61 | + children: [ | ||
| 62 | + Expanded( | ||
| 63 | + child: barcode?.image != null | ||
| 64 | + ? Transform.rotate( | ||
| 65 | + angle: 90 * pi / 180, | ||
| 66 | + child: Image( | ||
| 67 | + gaplessPlayback: true, | ||
| 68 | + image: MemoryImage(barcode!.image!), | ||
| 69 | + fit: BoxFit.contain, | ||
| 70 | + ), | ||
| 71 | + ) | ||
| 72 | + : const Center( | ||
| 73 | + child: Text( | ||
| 74 | + 'Your scanned barcode will appear here!', | ||
| 75 | + ), | ||
| 76 | + ), | ||
| 77 | + ), | ||
| 78 | + Expanded( | ||
| 79 | + flex: 2, | ||
| 80 | + child: ColoredBox( | ||
| 81 | + color: Colors.grey, | ||
| 82 | + child: Stack( | ||
| 83 | + children: [ | ||
| 84 | + MobileScanner( | ||
| 85 | + controller: controller, | ||
| 86 | + fit: BoxFit.contain, | ||
| 87 | + onDetect: (barcode) { | ||
| 88 | + setState(() { | ||
| 89 | + this.barcode = barcode; | ||
| 90 | + }); | ||
| 91 | + }, | ||
| 92 | + ), | ||
| 93 | + Align( | ||
| 94 | + alignment: Alignment.bottomCenter, | ||
| 95 | + child: Container( | ||
| 96 | + alignment: Alignment.bottomCenter, | ||
| 97 | + height: 100, | ||
| 98 | + color: Colors.black.withOpacity(0.4), | ||
| 99 | + child: Row( | ||
| 100 | + mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||
| 101 | + children: [ | ||
| 102 | + IconButton( | ||
| 103 | + color: Colors.white, | ||
| 104 | + icon: ValueListenableBuilder( | ||
| 105 | + valueListenable: controller.torchState, | ||
| 106 | + builder: (context, state, child) { | ||
| 107 | + if (state == null) { | ||
| 108 | + return const Icon( | ||
| 109 | + Icons.flash_off, | ||
| 110 | + color: Colors.grey, | ||
| 111 | + ); | ||
| 112 | + } | ||
| 113 | + switch (state as TorchState) { | ||
| 114 | + case TorchState.off: | ||
| 115 | + return const Icon( | ||
| 116 | + Icons.flash_off, | ||
| 117 | + color: Colors.grey, | ||
| 118 | + ); | ||
| 119 | + case TorchState.on: | ||
| 120 | + return const Icon( | ||
| 121 | + Icons.flash_on, | ||
| 122 | + color: Colors.yellow, | ||
| 123 | + ); | ||
| 124 | + } | ||
| 125 | + }, | ||
| 126 | + ), | ||
| 127 | + iconSize: 32.0, | ||
| 128 | + onPressed: () => controller.toggleTorch(), | ||
| 129 | + ), | ||
| 130 | + IconButton( | ||
| 131 | + color: Colors.white, | ||
| 132 | + icon: isStarted | ||
| 133 | + ? const Icon(Icons.stop) | ||
| 134 | + : const Icon(Icons.play_arrow), | ||
| 135 | + iconSize: 32.0, | ||
| 136 | + onPressed: _startOrStop, | ||
| 137 | + ), | ||
| 138 | + Center( | ||
| 139 | + child: SizedBox( | ||
| 140 | + width: MediaQuery.of(context).size.width - 200, | ||
| 141 | + height: 50, | ||
| 142 | + child: FittedBox( | ||
| 143 | + child: Text( | ||
| 144 | + barcode?.barcodes.first.rawValue ?? | ||
| 145 | + 'Scan something!', | ||
| 146 | + overflow: TextOverflow.fade, | ||
| 147 | + style: Theme.of(context) | ||
| 148 | + .textTheme | ||
| 149 | + .headline4! | ||
| 150 | + .copyWith(color: Colors.white), | ||
| 151 | + ), | ||
| 152 | + ), | ||
| 153 | + ), | ||
| 154 | + ), | ||
| 155 | + IconButton( | ||
| 156 | + color: Colors.white, | ||
| 157 | + icon: ValueListenableBuilder( | ||
| 158 | + valueListenable: controller.cameraFacingState, | ||
| 159 | + builder: (context, state, child) { | ||
| 160 | + if (state == null) { | ||
| 161 | + return const Icon(Icons.camera_front); | ||
| 162 | + } | ||
| 163 | + switch (state as CameraFacing) { | ||
| 164 | + case CameraFacing.front: | ||
| 165 | + return const Icon(Icons.camera_front); | ||
| 166 | + case CameraFacing.back: | ||
| 167 | + return const Icon(Icons.camera_rear); | ||
| 168 | + } | ||
| 169 | + }, | ||
| 170 | + ), | ||
| 171 | + iconSize: 32.0, | ||
| 172 | + onPressed: () => controller.switchCamera(), | ||
| 173 | + ), | ||
| 174 | + ], | ||
| 175 | + ), | ||
| 176 | + ), | ||
| 177 | + ), | ||
| 178 | + ], | ||
| 179 | + ), | ||
| 180 | + ), | ||
| 181 | + ), | ||
| 182 | + ], | ||
| 183 | + ), | ||
| 184 | + ), | ||
| 185 | + ); | ||
| 186 | + } | ||
| 187 | + | ||
| 188 | + @override | ||
| 189 | + void dispose() { | ||
| 190 | + controller.dispose(); | ||
| 191 | + super.dispose(); | ||
| 192 | + } | ||
| 193 | +} |
| @@ -12,7 +12,7 @@ class BarcodeScannerWithoutController extends StatefulWidget { | @@ -12,7 +12,7 @@ class BarcodeScannerWithoutController extends StatefulWidget { | ||
| 12 | class _BarcodeScannerWithoutControllerState | 12 | class _BarcodeScannerWithoutControllerState |
| 13 | extends State<BarcodeScannerWithoutController> | 13 | extends State<BarcodeScannerWithoutController> |
| 14 | with SingleTickerProviderStateMixin { | 14 | with SingleTickerProviderStateMixin { |
| 15 | - String? barcode; | 15 | + BarcodeCapture? capture; |
| 16 | 16 | ||
| 17 | @override | 17 | @override |
| 18 | Widget build(BuildContext context) { | 18 | Widget build(BuildContext context) { |
| @@ -24,10 +24,9 @@ class _BarcodeScannerWithoutControllerState | @@ -24,10 +24,9 @@ class _BarcodeScannerWithoutControllerState | ||
| 24 | children: [ | 24 | children: [ |
| 25 | MobileScanner( | 25 | MobileScanner( |
| 26 | fit: BoxFit.contain, | 26 | fit: BoxFit.contain, |
| 27 | - // allowDuplicates: false, | ||
| 28 | - onDetect: (barcode, args) { | 27 | + onDetect: (capture) { |
| 29 | setState(() { | 28 | setState(() { |
| 30 | - this.barcode = barcode.rawValue; | 29 | + this.capture = capture; |
| 31 | }); | 30 | }); |
| 32 | }, | 31 | }, |
| 33 | ), | 32 | ), |
| @@ -46,7 +45,8 @@ class _BarcodeScannerWithoutControllerState | @@ -46,7 +45,8 @@ class _BarcodeScannerWithoutControllerState | ||
| 46 | height: 50, | 45 | height: 50, |
| 47 | child: FittedBox( | 46 | child: FittedBox( |
| 48 | child: Text( | 47 | child: Text( |
| 49 | - barcode ?? 'Scan something!', | 48 | + capture?.barcodes.first.rawValue ?? |
| 49 | + 'Scan something!', | ||
| 50 | overflow: TextOverflow.fade, | 50 | overflow: TextOverflow.fade, |
| 51 | style: Theme.of(context) | 51 | style: Theme.of(context) |
| 52 | .textTheme | 52 | .textTheme |
| 1 | import 'package:flutter/material.dart'; | 1 | import 'package:flutter/material.dart'; |
| 2 | +import 'package:mobile_scanner_example/barcode_list_scanner_controller.dart'; | ||
| 2 | import 'package:mobile_scanner_example/barcode_scanner_controller.dart'; | 3 | import 'package:mobile_scanner_example/barcode_scanner_controller.dart'; |
| 4 | +import 'package:mobile_scanner_example/barcode_scanner_returning_image.dart'; | ||
| 3 | import 'package:mobile_scanner_example/barcode_scanner_window.dart'; | 5 | import 'package:mobile_scanner_example/barcode_scanner_window.dart'; |
| 4 | import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart'; | 6 | import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart'; |
| 5 | 7 | ||
| @@ -22,6 +24,17 @@ class MyHome extends StatelessWidget { | @@ -22,6 +24,17 @@ class MyHome extends StatelessWidget { | ||
| 22 | onPressed: () { | 24 | onPressed: () { |
| 23 | Navigator.of(context).push( | 25 | Navigator.of(context).push( |
| 24 | MaterialPageRoute( | 26 | MaterialPageRoute( |
| 27 | + builder: (context) => | ||
| 28 | + const BarcodeListScannerWithController(), | ||
| 29 | + ), | ||
| 30 | + ); | ||
| 31 | + }, | ||
| 32 | + child: const Text('MobileScanner with List Controller'), | ||
| 33 | + ), | ||
| 34 | + ElevatedButton( | ||
| 35 | + onPressed: () { | ||
| 36 | + Navigator.of(context).push( | ||
| 37 | + MaterialPageRoute( | ||
| 25 | builder: (context) => const BarcodeScannerWithController(), | 38 | builder: (context) => const BarcodeScannerWithController(), |
| 26 | ), | 39 | ), |
| 27 | ); | 40 | ); |
| @@ -42,6 +55,17 @@ class MyHome extends StatelessWidget { | @@ -42,6 +55,17 @@ class MyHome extends StatelessWidget { | ||
| 42 | onPressed: () { | 55 | onPressed: () { |
| 43 | Navigator.of(context).push( | 56 | Navigator.of(context).push( |
| 44 | MaterialPageRoute( | 57 | MaterialPageRoute( |
| 58 | + builder: (context) => const BarcodeScannerReturningImage(), | ||
| 59 | + ), | ||
| 60 | + ); | ||
| 61 | + }, | ||
| 62 | + child: | ||
| 63 | + const Text('MobileScanner with Controller (returning image)'), | ||
| 64 | + ), | ||
| 65 | + ElevatedButton( | ||
| 66 | + onPressed: () { | ||
| 67 | + Navigator.of(context).push( | ||
| 68 | + MaterialPageRoute( | ||
| 45 | builder: (context) => | 69 | builder: (context) => |
| 46 | const BarcodeScannerWithoutController(), | 70 | const BarcodeScannerWithoutController(), |
| 47 | ), | 71 | ), |
| @@ -28,8 +28,8 @@ | @@ -28,8 +28,8 @@ | ||
| 28 | 28 | ||
| 29 | <title>example</title> | 29 | <title>example</title> |
| 30 | <link rel="manifest" href="manifest.json"> | 30 | <link rel="manifest" href="manifest.json"> |
| 31 | -<!-- <script src="https://cdn.jsdelivr.net/npm/qr-scanner@1.4.1/qr-scanner.min.js"></script>--> | ||
| 32 | <script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script> | 31 | <script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script> |
| 32 | + <script type="text/javascript" src="https://unpkg.com/@zxing/library@0.19.1"></script> | ||
| 33 | </head> | 33 | </head> |
| 34 | <body> | 34 | <body> |
| 35 | <!-- This script installs service_worker.js to provide PWA functionality to | 35 | <!-- This script installs service_worker.js to provide PWA functionality to |
ios/Classes/BarcodeHandler.swift
0 → 100644
| 1 | +// | ||
| 2 | +// BarcodeHandler.swift | ||
| 3 | +// mobile_scanner | ||
| 4 | +// | ||
| 5 | +// Created by Julian Steenbakker on 24/08/2022. | ||
| 6 | +// | ||
| 7 | + | ||
| 8 | +import Foundation | ||
| 9 | + | ||
| 10 | +public class BarcodeHandler: NSObject, FlutterStreamHandler { | ||
| 11 | + | ||
| 12 | + var event: [String: Any?] = [:] | ||
| 13 | + | ||
| 14 | + private var eventSink: FlutterEventSink? | ||
| 15 | + private let eventChannel: FlutterEventChannel | ||
| 16 | + | ||
| 17 | + init(registrar: FlutterPluginRegistrar) { | ||
| 18 | + eventChannel = FlutterEventChannel(name: | ||
| 19 | + "dev.steenbakker.mobile_scanner/scanner/event", binaryMessenger: registrar.messenger()) | ||
| 20 | + super.init() | ||
| 21 | + eventChannel.setStreamHandler(self) | ||
| 22 | + } | ||
| 23 | + | ||
| 24 | + func publishEvent(_ event: [String: Any?]) { | ||
| 25 | + self.event = event | ||
| 26 | + eventSink?(event) | ||
| 27 | + } | ||
| 28 | + | ||
| 29 | + public func onListen(withArguments arguments: Any?, | ||
| 30 | + eventSink: @escaping FlutterEventSink) -> FlutterError? { | ||
| 31 | + self.eventSink = eventSink | ||
| 32 | + return nil | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + public func onCancel(withArguments arguments: Any?) -> FlutterError? { | ||
| 36 | + eventSink = nil | ||
| 37 | + return nil | ||
| 38 | + } | ||
| 39 | +} |
ios/Classes/DetectionSpeed.swift
0 → 100644
ios/Classes/MobileScanner.swift
0 → 100644
| 1 | +// | ||
| 2 | +// SwiftMobileScanner.swift | ||
| 3 | +// mobile_scanner | ||
| 4 | +// | ||
| 5 | +// Created by Julian Steenbakker on 15/02/2022. | ||
| 6 | +// | ||
| 7 | + | ||
| 8 | +import Foundation | ||
| 9 | + | ||
| 10 | +import AVFoundation | ||
| 11 | +import MLKitVision | ||
| 12 | +import MLKitBarcodeScanning | ||
| 13 | + | ||
| 14 | +typealias MobileScannerCallback = ((Array<Barcode>?, Error?, UIImage) -> ()) | ||
| 15 | + | ||
| 16 | +public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, FlutterTexture { | ||
| 17 | + /// Capture session of the camera | ||
| 18 | + var captureSession: AVCaptureSession! | ||
| 19 | + | ||
| 20 | + /// The selected camera | ||
| 21 | + var device: AVCaptureDevice! | ||
| 22 | + | ||
| 23 | + /// Barcode scanner for results | ||
| 24 | + var scanner = BarcodeScanner.barcodeScanner() | ||
| 25 | + | ||
| 26 | + /// Return image buffer with the Barcode event | ||
| 27 | + var returnImage: Bool = false | ||
| 28 | + | ||
| 29 | + /// Default position of camera | ||
| 30 | + var videoPosition: AVCaptureDevice.Position = AVCaptureDevice.Position.back | ||
| 31 | + | ||
| 32 | + /// When results are found, this callback will be called | ||
| 33 | + let mobileScannerCallback: MobileScannerCallback | ||
| 34 | + | ||
| 35 | + /// If provided, the Flutter registry will be used to send the output of the CaptureOutput to a Flutter texture. | ||
| 36 | + private let registry: FlutterTextureRegistry? | ||
| 37 | + | ||
| 38 | + /// Image to be sent to the texture | ||
| 39 | + var latestBuffer: CVImageBuffer! | ||
| 40 | + | ||
| 41 | + /// Texture id of the camera preview for Flutter | ||
| 42 | + private var textureId: Int64! | ||
| 43 | + | ||
| 44 | + var detectionSpeed: DetectionSpeed = DetectionSpeed.noDuplicates | ||
| 45 | + | ||
| 46 | + init(registry: FlutterTextureRegistry?, mobileScannerCallback: @escaping MobileScannerCallback) { | ||
| 47 | + self.registry = registry | ||
| 48 | + self.mobileScannerCallback = mobileScannerCallback | ||
| 49 | + super.init() | ||
| 50 | + } | ||
| 51 | + | ||
| 52 | + /// Check if we already have camera permission. | ||
| 53 | + func checkPermission() -> Int { | ||
| 54 | + let status = AVCaptureDevice.authorizationStatus(for: .video) | ||
| 55 | + switch status { | ||
| 56 | + case .notDetermined: | ||
| 57 | + return 0 | ||
| 58 | + case .authorized: | ||
| 59 | + return 1 | ||
| 60 | + default: | ||
| 61 | + return 2 | ||
| 62 | + } | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + /// Request permissions for video | ||
| 66 | + func requestPermission(_ result: @escaping FlutterResult) { | ||
| 67 | + AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) }) | ||
| 68 | + } | ||
| 69 | + | ||
| 70 | + /// Gets called when a new image is added to the buffer | ||
| 71 | + public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { | ||
| 72 | + guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { | ||
| 73 | + print("Failed to get image buffer from sample buffer.") | ||
| 74 | + return | ||
| 75 | + } | ||
| 76 | + latestBuffer = imageBuffer | ||
| 77 | + registry?.textureFrameAvailable(textureId) | ||
| 78 | + if ((detectionSpeed == DetectionSpeed.normal || detectionSpeed == DetectionSpeed.noDuplicates) && i > 10 || detectionSpeed == DetectionSpeed.unrestricted) { | ||
| 79 | + i = 0 | ||
| 80 | + let ciImage = latestBuffer.image | ||
| 81 | + | ||
| 82 | + let image = VisionImage(image: ciImage) | ||
| 83 | + image.orientation = imageOrientation( | ||
| 84 | + deviceOrientation: UIDevice.current.orientation, | ||
| 85 | + defaultOrientation: .portrait, | ||
| 86 | + position: videoPosition | ||
| 87 | + ) | ||
| 88 | + | ||
| 89 | + scanner.process(image) { [self] barcodes, error in | ||
| 90 | + if (detectionSpeed == DetectionSpeed.noDuplicates) { | ||
| 91 | + let newScannedBarcodes = barcodes?.map { barcode in | ||
| 92 | + return barcode.rawValue | ||
| 93 | + } | ||
| 94 | + if (error == nil && barcodesString != nil && newScannedBarcodes != nil && barcodesString!.elementsEqual(newScannedBarcodes!)) { | ||
| 95 | + return | ||
| 96 | + } else { | ||
| 97 | + barcodesString = newScannedBarcodes | ||
| 98 | + } | ||
| 99 | + } | ||
| 100 | + | ||
| 101 | + mobileScannerCallback(barcodes, error, ciImage) | ||
| 102 | + } | ||
| 103 | + } else { | ||
| 104 | + i+=1 | ||
| 105 | + } | ||
| 106 | + } | ||
| 107 | + | ||
| 108 | + /// Start scanning for barcodes | ||
| 109 | + func start(barcodeScannerOptions: BarcodeScannerOptions?, returnImage: Bool, cameraPosition: AVCaptureDevice.Position, torch: AVCaptureDevice.TorchMode, detectionSpeed: DetectionSpeed) throws -> MobileScannerStartParameters { | ||
| 110 | + self.detectionSpeed = detectionSpeed | ||
| 111 | + if (device != nil) { | ||
| 112 | + throw MobileScannerError.alreadyStarted | ||
| 113 | + } | ||
| 114 | + | ||
| 115 | + scanner = barcodeScannerOptions != nil ? BarcodeScanner.barcodeScanner(options: barcodeScannerOptions!) : BarcodeScanner.barcodeScanner() | ||
| 116 | + captureSession = AVCaptureSession() | ||
| 117 | + textureId = registry?.register(self) | ||
| 118 | + | ||
| 119 | + // Open the camera device | ||
| 120 | + if #available(iOS 10.0, *) { | ||
| 121 | + device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: cameraPosition).devices.first | ||
| 122 | + } else { | ||
| 123 | + device = AVCaptureDevice.devices(for: .video).filter({$0.position == cameraPosition}).first | ||
| 124 | + } | ||
| 125 | + | ||
| 126 | + if (device == nil) { | ||
| 127 | + throw MobileScannerError.noCamera | ||
| 128 | + } | ||
| 129 | + | ||
| 130 | + // Enable the torch if parameter is set and torch is available | ||
| 131 | + if (device.hasTorch && device.isTorchAvailable) { | ||
| 132 | + do { | ||
| 133 | + try device.lockForConfiguration() | ||
| 134 | + device.torchMode = torch | ||
| 135 | + device.unlockForConfiguration() | ||
| 136 | + } catch { | ||
| 137 | + throw MobileScannerError.torchError(error) | ||
| 138 | + } | ||
| 139 | + } | ||
| 140 | + | ||
| 141 | + device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil) | ||
| 142 | + captureSession.beginConfiguration() | ||
| 143 | + | ||
| 144 | + // Add device input | ||
| 145 | + do { | ||
| 146 | + let input = try AVCaptureDeviceInput(device: device) | ||
| 147 | + captureSession.addInput(input) | ||
| 148 | + } catch { | ||
| 149 | + throw MobileScannerError.cameraError(error) | ||
| 150 | + } | ||
| 151 | + | ||
| 152 | + captureSession.sessionPreset = AVCaptureSession.Preset.photo; | ||
| 153 | + // Add video output. | ||
| 154 | + let videoOutput = AVCaptureVideoDataOutput() | ||
| 155 | + | ||
| 156 | + videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA] | ||
| 157 | + videoOutput.alwaysDiscardsLateVideoFrames = true | ||
| 158 | + | ||
| 159 | + videoPosition = cameraPosition | ||
| 160 | + // calls captureOutput() | ||
| 161 | + videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main) | ||
| 162 | + | ||
| 163 | + captureSession.addOutput(videoOutput) | ||
| 164 | + for connection in videoOutput.connections { | ||
| 165 | + connection.videoOrientation = .portrait | ||
| 166 | + if cameraPosition == .front && connection.isVideoMirroringSupported { | ||
| 167 | + connection.isVideoMirrored = true | ||
| 168 | + } | ||
| 169 | + } | ||
| 170 | + captureSession.commitConfiguration() | ||
| 171 | + captureSession.startRunning() | ||
| 172 | + let dimensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription) | ||
| 173 | + | ||
| 174 | + return MobileScannerStartParameters(width: Double(dimensions.height), height: Double(dimensions.width), hasTorch: device.hasTorch, textureId: textureId) | ||
| 175 | + } | ||
| 176 | + | ||
| 177 | + /// Stop scanning for barcodes | ||
| 178 | + func stop() throws { | ||
| 179 | + if (device == nil) { | ||
| 180 | + throw MobileScannerError.alreadyStopped | ||
| 181 | + } | ||
| 182 | + captureSession.stopRunning() | ||
| 183 | + for input in captureSession.inputs { | ||
| 184 | + captureSession.removeInput(input) | ||
| 185 | + } | ||
| 186 | + for output in captureSession.outputs { | ||
| 187 | + captureSession.removeOutput(output) | ||
| 188 | + } | ||
| 189 | + device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode)) | ||
| 190 | + registry?.unregisterTexture(textureId) | ||
| 191 | + textureId = nil | ||
| 192 | + captureSession = nil | ||
| 193 | + device = nil | ||
| 194 | + } | ||
| 195 | + | ||
| 196 | + /// Toggle the flashlight between on and off | ||
| 197 | + func toggleTorch(_ torch: AVCaptureDevice.TorchMode) throws { | ||
| 198 | + if (device == nil) { | ||
| 199 | + throw MobileScannerError.torchWhenStopped | ||
| 200 | + } | ||
| 201 | + do { | ||
| 202 | + try device.lockForConfiguration() | ||
| 203 | + device.torchMode = torch | ||
| 204 | + device.unlockForConfiguration() | ||
| 205 | + } catch { | ||
| 206 | + throw MobileScannerError.torchError(error) | ||
| 207 | + } | ||
| 208 | + } | ||
| 209 | + | ||
| 210 | + /// Analyze a single image | ||
| 211 | + func analyzeImage(image: UIImage, position: AVCaptureDevice.Position, callback: @escaping BarcodeScanningCallback) { | ||
| 212 | + let image = VisionImage(image: image) | ||
| 213 | + image.orientation = imageOrientation( | ||
| 214 | + deviceOrientation: UIDevice.current.orientation, | ||
| 215 | + defaultOrientation: .portrait, | ||
| 216 | + position: position | ||
| 217 | + ) | ||
| 218 | + | ||
| 219 | + scanner.process(image, completion: callback) | ||
| 220 | + } | ||
| 221 | + | ||
| 222 | + var i = 0 | ||
| 223 | + | ||
| 224 | + var barcodesString: Array<String?>? | ||
| 225 | + | ||
| 226 | + | ||
| 227 | + | ||
| 228 | +// /// Convert image buffer to jpeg | ||
| 229 | +// private func ciImageToJpeg(ciImage: CIImage) -> Data { | ||
| 230 | +// | ||
| 231 | +// // let ciImage = CIImage(cvPixelBuffer: latestBuffer) | ||
| 232 | +// let context:CIContext = CIContext.init(options: nil) | ||
| 233 | +// let cgImage:CGImage = context.createCGImage(ciImage, from: ciImage.extent)! | ||
| 234 | +// let uiImage:UIImage = UIImage(cgImage: cgImage, scale: 1, orientation: UIImage.Orientation.up) | ||
| 235 | +// | ||
| 236 | +// return uiImage.jpegData(compressionQuality: 0.8)!; | ||
| 237 | +// } | ||
| 238 | + | ||
| 239 | + /// Rotates images accordingly | ||
| 240 | + func imageOrientation( | ||
| 241 | + deviceOrientation: UIDeviceOrientation, | ||
| 242 | + defaultOrientation: UIDeviceOrientation, | ||
| 243 | + position: AVCaptureDevice.Position | ||
| 244 | + ) -> UIImage.Orientation { | ||
| 245 | + switch deviceOrientation { | ||
| 246 | + case .portrait: | ||
| 247 | + return position == .front ? .leftMirrored : .right | ||
| 248 | + case .landscapeLeft: | ||
| 249 | + return position == .front ? .downMirrored : .up | ||
| 250 | + case .portraitUpsideDown: | ||
| 251 | + return position == .front ? .rightMirrored : .left | ||
| 252 | + case .landscapeRight: | ||
| 253 | + return position == .front ? .upMirrored : .down | ||
| 254 | + case .faceDown, .faceUp, .unknown: | ||
| 255 | + return .up | ||
| 256 | + @unknown default: | ||
| 257 | + return imageOrientation(deviceOrientation: defaultOrientation, defaultOrientation: .portrait, position: .back) | ||
| 258 | + } | ||
| 259 | + } | ||
| 260 | + | ||
| 261 | + /// Sends output of OutputBuffer to a Flutter texture | ||
| 262 | + public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? { | ||
| 263 | + if latestBuffer == nil { | ||
| 264 | + return nil | ||
| 265 | + } | ||
| 266 | + return Unmanaged<CVPixelBuffer>.passRetained(latestBuffer) | ||
| 267 | + } | ||
| 268 | + | ||
| 269 | + struct MobileScannerStartParameters { | ||
| 270 | + var width: Double = 0.0 | ||
| 271 | + var height: Double = 0.0 | ||
| 272 | + var hasTorch = false | ||
| 273 | + var textureId: Int64 = 0 | ||
| 274 | + } | ||
| 275 | +} | ||
| 276 | + |
ios/Classes/MobileScannerError.swift
0 → 100644
| 1 | +// | ||
| 2 | +// MobileScannerError.swift | ||
| 3 | +// mobile_scanner | ||
| 4 | +// | ||
| 5 | +// Created by Julian Steenbakker on 24/08/2022. | ||
| 6 | +// | ||
| 7 | +import Foundation | ||
| 8 | + | ||
| 9 | +enum MobileScannerError: Error { | ||
| 10 | + case noCamera | ||
| 11 | + case alreadyStarted | ||
| 12 | + case alreadyStopped | ||
| 13 | + case torchError(_ error: Error) | ||
| 14 | + case cameraError(_ error: Error) | ||
| 15 | + case torchWhenStopped | ||
| 16 | + case analyzerError(_ error: Error) | ||
| 17 | +} |
| @@ -21,7 +21,7 @@ extension CVBuffer { | @@ -21,7 +21,7 @@ extension CVBuffer { | ||
| 21 | var image: UIImage { | 21 | var image: UIImage { |
| 22 | let ciImage = CIImage(cvPixelBuffer: self) | 22 | let ciImage = CIImage(cvPixelBuffer: self) |
| 23 | let cgImage = CIContext().createCGImage(ciImage, from: ciImage.extent) | 23 | let cgImage = CIContext().createCGImage(ciImage, from: ciImage.extent) |
| 24 | - return UIImage(cgImage: cgImage!) | 24 | + return UIImage(cgImage: cgImage!, scale: 1.0, orientation: UIImage.Orientation.left) |
| 25 | } | 25 | } |
| 26 | 26 | ||
| 27 | var image1: UIImage { | 27 | var image1: UIImage { |
ios/Classes/SwiftMobileScanner.swift
deleted
100644 → 0
| 1 | -import AVFoundation | ||
| 2 | import Flutter | 1 | import Flutter |
| 3 | import MLKitVision | 2 | import MLKitVision |
| 4 | import MLKitBarcodeScanning | 3 | import MLKitBarcodeScanning |
| 4 | +import AVFoundation | ||
| 5 | import UIKit | 5 | import UIKit |
| 6 | 6 | ||
| 7 | -public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, FlutterTexture, AVCaptureVideoDataOutputSampleBufferDelegate { | ||
| 8 | - | ||
| 9 | - let registry: FlutterTextureRegistry | ||
| 10 | - | ||
| 11 | - // Sink for publishing event changes | ||
| 12 | - var sink: FlutterEventSink! | 7 | +public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { |
| 13 | 8 | ||
| 14 | - // Texture id of the camera preview | ||
| 15 | - var textureId: Int64! | 9 | + /// The mobile scanner object that handles all logic |
| 10 | + private let mobileScanner: MobileScanner | ||
| 16 | 11 | ||
| 17 | - // Capture session of the camera | ||
| 18 | - var captureSession: AVCaptureSession! | ||
| 19 | - | ||
| 20 | - // The selected camera | ||
| 21 | - var device: AVCaptureDevice! | ||
| 22 | - | ||
| 23 | - // Image to be sent to the texture | ||
| 24 | - var latestBuffer: CVImageBuffer! | ||
| 25 | - | ||
| 26 | -// var analyzeMode: Int = 0 | ||
| 27 | - var analyzing: Bool = false | ||
| 28 | - var position = AVCaptureDevice.Position.back | 12 | + /// The handler sends all information via an event channel back to Flutter |
| 13 | + private let barcodeHandler: BarcodeHandler | ||
| 29 | 14 | ||
| 30 | var scanWindow: CGRect? | 15 | var scanWindow: CGRect? |
| 31 | 16 | ||
| 32 | - var scanner = BarcodeScanner.barcodeScanner() | ||
| 33 | - | ||
| 34 | - public static func register(with registrar: FlutterPluginRegistrar) { | ||
| 35 | - let instance = SwiftMobileScannerPlugin(registrar.textures()) | ||
| 36 | - | ||
| 37 | - let method = FlutterMethodChannel(name: | ||
| 38 | - "dev.steenbakker.mobile_scanner/scanner/method", binaryMessenger: registrar.messenger()) | ||
| 39 | - let event = FlutterEventChannel(name: | ||
| 40 | - "dev.steenbakker.mobile_scanner/scanner/event", binaryMessenger: registrar.messenger()) | ||
| 41 | - registrar.addMethodCallDelegate(instance, channel: method) | ||
| 42 | - event.setStreamHandler(instance) | ||
| 43 | - } | ||
| 44 | - | ||
| 45 | - init(_ registry: FlutterTextureRegistry) { | ||
| 46 | - self.registry = registry | 17 | + init(barcodeHandler: BarcodeHandler, registry: FlutterTextureRegistry) { |
| 18 | + self.mobileScanner = MobileScanner(registry: registry, mobileScannerCallback: { barcodes, error, image in | ||
| 19 | + if barcodes != nil { | ||
| 20 | + let barcodesMap = barcodes!.map { barcode in | ||
| 21 | + return barcode.data | ||
| 22 | + } | ||
| 23 | + if (!barcodesMap.isEmpty) { | ||
| 24 | + barcodeHandler.publishEvent(["name": "barcode", "data": barcodesMap, "image": FlutterStandardTypedData(bytes: image.jpegData(compressionQuality: 0.8)!)]) | ||
| 25 | + } | ||
| 26 | + } else if (error != nil){ | ||
| 27 | + barcodeHandler.publishEvent(["name": "error", "data": error!.localizedDescription]) | ||
| 28 | + } | ||
| 29 | + }) | ||
| 30 | + self.barcodeHandler = barcodeHandler | ||
| 47 | super.init() | 31 | super.init() |
| 48 | } | 32 | } |
| 49 | - | 33 | + |
| 34 | + public static func register(with registrar: FlutterPluginRegistrar) { | ||
| 35 | + let instance = SwiftMobileScannerPlugin(barcodeHandler: BarcodeHandler(registrar: registrar), registry: registrar.textures()) | ||
| 36 | + let methodChannel = FlutterMethodChannel(name: | ||
| 37 | + "dev.steenbakker.mobile_scanner/scanner/method", binaryMessenger: registrar.messenger()) | ||
| 38 | + registrar.addMethodCallDelegate(instance, channel: methodChannel) | ||
| 39 | + } | ||
| 50 | 40 | ||
| 51 | public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { | 41 | public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
| 52 | switch call.method { | 42 | switch call.method { |
| 53 | case "state": | 43 | case "state": |
| 54 | - checkPermission(call, result) | 44 | + result(mobileScanner.checkPermission()) |
| 55 | case "request": | 45 | case "request": |
| 56 | - requestPermission(call, result) | 46 | + AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) }) |
| 57 | case "start": | 47 | case "start": |
| 58 | start(call, result) | 48 | start(call, result) |
| 59 | - case "torch": | ||
| 60 | - toggleTorch(call, result) | ||
| 61 | -// case "analyze": | ||
| 62 | -// switchAnalyzeMode(call, result) | ||
| 63 | case "stop": | 49 | case "stop": |
| 64 | stop(result) | 50 | stop(result) |
| 51 | + case "torch": | ||
| 52 | + toggleTorch(call, result) | ||
| 65 | case "analyzeImage": | 53 | case "analyzeImage": |
| 66 | analyzeImage(call, result) | 54 | analyzeImage(call, result) |
| 67 | case "updateScanWindow": | 55 | case "updateScanWindow": |
| @@ -71,365 +59,113 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | @@ -71,365 +59,113 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | ||
| 71 | } | 59 | } |
| 72 | } | 60 | } |
| 73 | 61 | ||
| 74 | - // FlutterStreamHandler | ||
| 75 | - public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { | ||
| 76 | - sink = events | ||
| 77 | - return nil | ||
| 78 | - } | ||
| 79 | - | ||
| 80 | - // FlutterStreamHandler | ||
| 81 | - public func onCancel(withArguments arguments: Any?) -> FlutterError? { | ||
| 82 | - sink = nil | ||
| 83 | - return nil | ||
| 84 | - } | ||
| 85 | - | ||
| 86 | - // FlutterTexture | ||
| 87 | - public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? { | ||
| 88 | - if latestBuffer == nil { | ||
| 89 | - return nil | ||
| 90 | - } | ||
| 91 | - return Unmanaged<CVPixelBuffer>.passRetained(latestBuffer) | ||
| 92 | - } | ||
| 93 | - | ||
| 94 | - // Gets called when a new image is added to the buffer | ||
| 95 | - public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { | 62 | + /// Parses all parameters and starts the mobileScanner |
| 63 | + private func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | ||
| 64 | + let torch: Bool = (call.arguments as! Dictionary<String, Any?>)["torch"] as? Bool ?? false | ||
| 65 | + let facing: Int = (call.arguments as! Dictionary<String, Any?>)["facing"] as? Int ?? 1 | ||
| 66 | + let formats: Array<Int> = (call.arguments as! Dictionary<String, Any?>)["formats"] as? Array ?? [] | ||
| 67 | + let returnImage: Bool = (call.arguments as! Dictionary<String, Any?>)["returnImage"] as? Bool ?? false | ||
| 68 | + let speed: Int = (call.arguments as! Dictionary<String, Any?>)["speed"] as? Int ?? 0 | ||
| 96 | 69 | ||
| 97 | - latestBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) | ||
| 98 | - registry.textureFrameAvailable(textureId) | 70 | + let formatList = formats.map { format in return BarcodeFormat(rawValue: format)} |
| 71 | + var barcodeOptions: BarcodeScannerOptions? = nil | ||
| 99 | 72 | ||
| 100 | -// switch analyzeMode { | ||
| 101 | -// case 1: // barcode | ||
| 102 | - if analyzing { | ||
| 103 | - return | ||
| 104 | - } | ||
| 105 | - analyzing = true | ||
| 106 | - let buffer = CMSampleBufferGetImageBuffer(sampleBuffer) | ||
| 107 | - var image = VisionImage(image: buffer!.image) | ||
| 108 | - | ||
| 109 | - image.orientation = imageOrientation( | ||
| 110 | - deviceOrientation: UIDevice.current.orientation, | ||
| 111 | - defaultOrientation: .portrait | ||
| 112 | - ) | ||
| 113 | - | ||
| 114 | - scanner.process(image) { [self] barcodes, error in | ||
| 115 | - if error == nil && barcodes != nil { | ||
| 116 | - for barcode in barcodes! { | ||
| 117 | - | ||
| 118 | - if scanWindow != nil { | ||
| 119 | - let match = isbarCodeInScanWindow(scanWindow!, barcode, buffer!.image) | ||
| 120 | - if (!match) { | ||
| 121 | - continue | ||
| 122 | - } | ||
| 123 | - } | ||
| 124 | - | ||
| 125 | - let event: [String: Any?] = ["name": "barcode", "data": barcode.data] | ||
| 126 | - sink?(event) | ||
| 127 | - } | ||
| 128 | - } | ||
| 129 | - analyzing = false | ||
| 130 | - } | ||
| 131 | -// default: // none | ||
| 132 | -// break | ||
| 133 | -// } | ||
| 134 | - } | ||
| 135 | - | ||
| 136 | - func imageOrientation( | ||
| 137 | - deviceOrientation: UIDeviceOrientation, | ||
| 138 | - defaultOrientation: UIDeviceOrientation | ||
| 139 | - ) -> UIImage.Orientation { | ||
| 140 | - switch deviceOrientation { | ||
| 141 | - case .portrait: | ||
| 142 | - return position == .front ? .leftMirrored : .right | ||
| 143 | - case .landscapeLeft: | ||
| 144 | - return position == .front ? .downMirrored : .up | ||
| 145 | - case .portraitUpsideDown: | ||
| 146 | - return position == .front ? .rightMirrored : .left | ||
| 147 | - case .landscapeRight: | ||
| 148 | - return position == .front ? .upMirrored : .down | ||
| 149 | - case .faceDown, .faceUp, .unknown: | ||
| 150 | - return .up | ||
| 151 | - @unknown default: | ||
| 152 | - return imageOrientation(deviceOrientation: defaultOrientation, defaultOrientation: .portrait) | ||
| 153 | - } | ||
| 154 | - } | ||
| 155 | - | ||
| 156 | - func checkPermission(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | ||
| 157 | - let status = AVCaptureDevice.authorizationStatus(for: .video) | ||
| 158 | - switch status { | ||
| 159 | - case .notDetermined: | ||
| 160 | - result(0) | ||
| 161 | - case .authorized: | ||
| 162 | - result(1) | ||
| 163 | - default: | ||
| 164 | - result(2) | ||
| 165 | - } | ||
| 166 | - } | ||
| 167 | - | ||
| 168 | - func requestPermission(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | ||
| 169 | - AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) }) | ||
| 170 | - } | ||
| 171 | - | ||
| 172 | - func updateScanWindow(_ call: FlutterMethodCall) { | ||
| 173 | - let argReader = MapArgumentReader(call.arguments as? [String: Any]) | ||
| 174 | - let scanWindowData: Array? = argReader.floatArray(key: "rect") | ||
| 175 | - | ||
| 176 | - if (scanWindowData == nil) { | ||
| 177 | - return | ||
| 178 | - } | ||
| 179 | - | ||
| 180 | - let minX = scanWindowData![0] | ||
| 181 | - let minY = scanWindowData![1] | 73 | + if (formatList.count != 0) { |
| 74 | + var barcodeFormats: BarcodeFormat = [] | ||
| 75 | + for index in formats { | ||
| 76 | + barcodeFormats.insert(BarcodeFormat(rawValue: index)) | ||
| 77 | + } | ||
| 78 | + barcodeOptions = BarcodeScannerOptions(formats: barcodeFormats) | ||
| 79 | + } | ||
| 182 | 80 | ||
| 183 | - let width = scanWindowData![2] - minX | ||
| 184 | - let height = scanWindowData![3] - minY | ||
| 185 | - | ||
| 186 | - scanWindow = CGRect(x: minX, y: minY, width: width, height: height) | ||
| 187 | - } | ||
| 188 | 81 | ||
| 189 | - func isbarCodeInScanWindow(_ scanWindow: CGRect, _ barcode: Barcode, _ inputImage: UIImage) -> Bool { | ||
| 190 | - let barcodeBoundingBox = barcode.frame | 82 | + let position = facing == 0 ? AVCaptureDevice.Position.front : .back |
| 83 | + let detectionSpeed: DetectionSpeed = DetectionSpeed(rawValue: speed)! | ||
| 191 | 84 | ||
| 192 | - let imageWidth = inputImage.size.width; | ||
| 193 | - let imageHeight = inputImage.size.height; | ||
| 194 | - | ||
| 195 | - let minX = scanWindow.minX * imageWidth | ||
| 196 | - let minY = scanWindow.minY * imageHeight | ||
| 197 | - let width = scanWindow.width * imageWidth | ||
| 198 | - let height = scanWindow.height * imageHeight | ||
| 199 | - | ||
| 200 | - let scaledScanWindow = CGRect(x: minX, y: minY, width: width, height: height) | ||
| 201 | - return scaledScanWindow.contains(barcodeBoundingBox) | ||
| 202 | - } | ||
| 203 | - | ||
| 204 | - func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | ||
| 205 | - if (device != nil) { | 85 | + do { |
| 86 | + let parameters = try mobileScanner.start(barcodeScannerOptions: barcodeOptions, returnImage: returnImage, cameraPosition: position, torch: torch ? .on : .off, detectionSpeed: detectionSpeed) | ||
| 87 | + result(["textureId": parameters.textureId, "size": ["width": parameters.width, "height": parameters.height], "torchable": parameters.hasTorch]) | ||
| 88 | + } catch MobileScannerError.alreadyStarted { | ||
| 206 | result(FlutterError(code: "MobileScanner", | 89 | result(FlutterError(code: "MobileScanner", |
| 207 | - message: "Called start() while already started!", | ||
| 208 | - details: nil)) | ||
| 209 | - return | ||
| 210 | - } | ||
| 211 | - | ||
| 212 | - textureId = registry.register(self) | ||
| 213 | - captureSession = AVCaptureSession() | ||
| 214 | - | ||
| 215 | - let argReader = MapArgumentReader(call.arguments as? [String: Any]) | ||
| 216 | - | ||
| 217 | -// let ratio: Int = argReader.int(key: "ratio") | ||
| 218 | - let torch: Bool = argReader.bool(key: "torch") ?? false | ||
| 219 | - let facing: Int = argReader.int(key: "facing") ?? 1 | ||
| 220 | - let formats: Array = argReader.intArray(key: "formats") ?? [] | ||
| 221 | - | ||
| 222 | - let formatList: NSMutableArray = [] | ||
| 223 | - for index in formats { | ||
| 224 | - formatList.add(BarcodeFormat(rawValue: index)) | ||
| 225 | - } | ||
| 226 | - | ||
| 227 | - if (formatList.count != 0) { | ||
| 228 | - let barcodeOptions = BarcodeScannerOptions(formats: formatList.firstObject as! BarcodeFormat) | ||
| 229 | - scanner = BarcodeScanner.barcodeScanner(options: barcodeOptions) | ||
| 230 | - } | ||
| 231 | - | ||
| 232 | - // Set the camera to use | ||
| 233 | - position = facing == 0 ? AVCaptureDevice.Position.front : .back | ||
| 234 | - | ||
| 235 | - // Open the camera device | ||
| 236 | - if #available(iOS 10.0, *) { | ||
| 237 | - device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: position).devices.first | ||
| 238 | - } else { | ||
| 239 | - device = AVCaptureDevice.devices(for: .video).filter({$0.position == position}).first | ||
| 240 | - } | ||
| 241 | - | ||
| 242 | - if (device == nil) { | 90 | + message: "Called start() while already started!", |
| 91 | + details: nil)) | ||
| 92 | + } catch MobileScannerError.noCamera { | ||
| 243 | result(FlutterError(code: "MobileScanner", | 93 | result(FlutterError(code: "MobileScanner", |
| 244 | - message: "No camera found or failed to open camera!", | ||
| 245 | - details: nil)) | ||
| 246 | - return | ||
| 247 | - } | ||
| 248 | - | ||
| 249 | - // Enable the torch if parameter is set and torch is available | ||
| 250 | - if (device.hasTorch && device.isTorchAvailable) { | ||
| 251 | - do { | ||
| 252 | - try device.lockForConfiguration() | ||
| 253 | - device.torchMode = torch ? .on : .off | ||
| 254 | - device.unlockForConfiguration() | ||
| 255 | - } catch { | ||
| 256 | - error.throwNative(result) | ||
| 257 | - } | ||
| 258 | - } | ||
| 259 | - | ||
| 260 | - device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil) | ||
| 261 | - captureSession.beginConfiguration() | ||
| 262 | - | ||
| 263 | - // Add device input | ||
| 264 | - do { | ||
| 265 | - let input = try AVCaptureDeviceInput(device: device) | ||
| 266 | - captureSession.addInput(input) | 94 | + message: "No camera found or failed to open camera!", |
| 95 | + details: nil)) | ||
| 96 | + } catch MobileScannerError.torchError(let error) { | ||
| 97 | + result(FlutterError(code: "MobileScanner", | ||
| 98 | + message: "Error occured when setting torch!", | ||
| 99 | + details: error)) | ||
| 100 | + } catch MobileScannerError.cameraError(let error) { | ||
| 101 | + result(FlutterError(code: "MobileScanner", | ||
| 102 | + message: "Error occured when setting up camera!", | ||
| 103 | + details: error)) | ||
| 267 | } catch { | 104 | } catch { |
| 268 | - error.throwNative(result) | ||
| 269 | - } | ||
| 270 | - captureSession.sessionPreset = AVCaptureSession.Preset.photo; | ||
| 271 | - // Add video output. | ||
| 272 | - let videoOutput = AVCaptureVideoDataOutput() | ||
| 273 | - | ||
| 274 | - videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA] | ||
| 275 | - videoOutput.alwaysDiscardsLateVideoFrames = true | ||
| 276 | - | ||
| 277 | - videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main) | ||
| 278 | - | ||
| 279 | - captureSession.addOutput(videoOutput) | ||
| 280 | - for connection in videoOutput.connections { | ||
| 281 | - connection.videoOrientation = .portrait | ||
| 282 | - if position == .front && connection.isVideoMirroringSupported { | ||
| 283 | - connection.isVideoMirrored = true | ||
| 284 | - } | 105 | + result(FlutterError(code: "MobileScanner", |
| 106 | + message: "Unknown error occured..", | ||
| 107 | + details: nil)) | ||
| 285 | } | 108 | } |
| 286 | - captureSession.commitConfiguration() | ||
| 287 | - captureSession.startRunning() | 109 | + } |
| 288 | 110 | ||
| 289 | - let demensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription) | ||
| 290 | - let width = Double(demensions.height) | ||
| 291 | - let height = Double(demensions.width) | ||
| 292 | - let size = ["width": width, "height": height] | ||
| 293 | - let answer: [String : Any?] = ["textureId": textureId, "size": size, "torchable": device.hasTorch] | ||
| 294 | - result(answer) | 111 | + /// Stops the mobileScanner and closes the texture |
| 112 | + private func stop(_ result: @escaping FlutterResult) { | ||
| 113 | + do { | ||
| 114 | + try mobileScanner.stop() | ||
| 115 | + } catch {} | ||
| 116 | + result(nil) | ||
| 295 | } | 117 | } |
| 296 | - | ||
| 297 | - func toggleTorch(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | ||
| 298 | - if (device == nil) { | ||
| 299 | - result(FlutterError(code: "MobileScanner", | ||
| 300 | - message: "Called toggleTorch() while stopped!", | ||
| 301 | - details: nil)) | ||
| 302 | - return | ||
| 303 | - } | 118 | + |
| 119 | + /// Toggles the torch | ||
| 120 | + private func toggleTorch(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | ||
| 304 | do { | 121 | do { |
| 305 | - try device.lockForConfiguration() | ||
| 306 | - device.torchMode = call.arguments as! Int == 1 ? .on : .off | ||
| 307 | - device.unlockForConfiguration() | ||
| 308 | - result(nil) | 122 | + try mobileScanner.toggleTorch(call.arguments as? Int == 1 ? .on : .off) |
| 309 | } catch { | 123 | } catch { |
| 310 | - error.throwNative(result) | 124 | + result(FlutterError(code: "MobileScanner", |
| 125 | + message: "Called toggleTorch() while stopped!", | ||
| 126 | + details: nil)) | ||
| 311 | } | 127 | } |
| 128 | + result(nil) | ||
| 312 | } | 129 | } |
| 313 | 130 | ||
| 314 | -// func switchAnalyzeMode(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | ||
| 315 | -// analyzeMode = call.arguments as! Int | ||
| 316 | -// result(nil) | ||
| 317 | -// } | ||
| 318 | - | ||
| 319 | - func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | ||
| 320 | - let uiImage = UIImage(contentsOfFile: call.arguments as! String) | 131 | + /// Analyzes a single image |
| 132 | + private func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | ||
| 133 | + let uiImage = UIImage(contentsOfFile: call.arguments as? String ?? "") | ||
| 321 | 134 | ||
| 322 | if (uiImage == nil) { | 135 | if (uiImage == nil) { |
| 323 | result(FlutterError(code: "MobileScanner", | 136 | result(FlutterError(code: "MobileScanner", |
| 324 | - message: "No image found in analyzeImage!", | ||
| 325 | - details: nil)) | 137 | + message: "No image found in analyzeImage!", |
| 138 | + details: nil)) | ||
| 326 | return | 139 | return |
| 327 | } | 140 | } |
| 328 | - | ||
| 329 | - let image = VisionImage(image: uiImage!) | ||
| 330 | - image.orientation = imageOrientation( | ||
| 331 | - deviceOrientation: UIDevice.current.orientation, | ||
| 332 | - defaultOrientation: .portrait | ||
| 333 | - ) | ||
| 334 | - | ||
| 335 | - var barcodeFound = false | ||
| 336 | - | ||
| 337 | - scanner.process(image) { [self] barcodes, error in | 141 | + mobileScanner.analyzeImage(image: uiImage!, position: AVCaptureDevice.Position.back, callback: { [self] barcodes, error in |
| 338 | if error == nil && barcodes != nil { | 142 | if error == nil && barcodes != nil { |
| 339 | for barcode in barcodes! { | 143 | for barcode in barcodes! { |
| 340 | - | ||
| 341 | - if scanWindow != nil { | ||
| 342 | - let match = isbarCodeInScanWindow(scanWindow!, barcode, uiImage!) | ||
| 343 | - if (!match) { | ||
| 344 | - continue | ||
| 345 | - } | ||
| 346 | - } | ||
| 347 | - | ||
| 348 | - barcodeFound = true | 144 | + if scanWindow != nil { |
| 145 | + let match = isbarCodeInScanWindow(scanWindow!, barcode, buffer!.image) | ||
| 146 | + if (!match) { | ||
| 147 | + continue | ||
| 148 | + } | ||
| 149 | + } | ||
| 349 | let event: [String: Any?] = ["name": "barcode", "data": barcode.data] | 150 | let event: [String: Any?] = ["name": "barcode", "data": barcode.data] |
| 350 | - sink?(event) | 151 | + barcodeHandler.publishEvent(event) |
| 351 | } | 152 | } |
| 352 | } else if error != nil { | 153 | } else if error != nil { |
| 353 | - result(FlutterError(code: "MobileScanner", | ||
| 354 | - message: error?.localizedDescription, | ||
| 355 | - details: "analyzeImage()")) | 154 | + barcodeHandler.publishEvent(["name": "error", "message": error?.localizedDescription]) |
| 356 | } | 155 | } |
| 357 | - analyzing = false | ||
| 358 | - result(barcodeFound) | ||
| 359 | - } | ||
| 360 | - | ||
| 361 | - } | ||
| 362 | - | ||
| 363 | - func stop(_ result: FlutterResult) { | ||
| 364 | - if (device == nil) { | ||
| 365 | - result(FlutterError(code: "MobileScanner", | ||
| 366 | - message: "Called stop() while already stopped!", | ||
| 367 | - details: nil)) | ||
| 368 | - return | ||
| 369 | - } | ||
| 370 | - captureSession.stopRunning() | ||
| 371 | - for input in captureSession.inputs { | ||
| 372 | - captureSession.removeInput(input) | ||
| 373 | - } | ||
| 374 | - for output in captureSession.outputs { | ||
| 375 | - captureSession.removeOutput(output) | ||
| 376 | - } | ||
| 377 | - device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode)) | ||
| 378 | - registry.unregisterTexture(textureId) | ||
| 379 | - | ||
| 380 | -// analyzeMode = 0 | ||
| 381 | - latestBuffer = nil | ||
| 382 | - captureSession = nil | ||
| 383 | - device = nil | ||
| 384 | - textureId = nil | ||
| 385 | - | 156 | + }) |
| 386 | result(nil) | 157 | result(nil) |
| 387 | } | 158 | } |
| 388 | 159 | ||
| 389 | - // Observer for torch state | 160 | + /// Observer for torch state |
| 390 | public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { | 161 | public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { |
| 391 | switch keyPath { | 162 | switch keyPath { |
| 392 | case "torchMode": | 163 | case "torchMode": |
| 393 | // off = 0; on = 1; auto = 2; | 164 | // off = 0; on = 1; auto = 2; |
| 394 | let state = change?[.newKey] as? Int | 165 | let state = change?[.newKey] as? Int |
| 395 | - let event: [String: Any?] = ["name": "torchState", "data": state] | ||
| 396 | - sink?(event) | 166 | + barcodeHandler.publishEvent(["name": "torchState", "data": state]) |
| 397 | default: | 167 | default: |
| 398 | break | 168 | break |
| 399 | } | 169 | } |
| 400 | } | 170 | } |
| 401 | } | 171 | } |
| 402 | - | ||
| 403 | -class MapArgumentReader { | ||
| 404 | - | ||
| 405 | - let args: [String: Any]? | ||
| 406 | - | ||
| 407 | - init(_ args: [String: Any]?) { | ||
| 408 | - self.args = args | ||
| 409 | - } | ||
| 410 | - | ||
| 411 | - func string(key: String) -> String? { | ||
| 412 | - return args?[key] as? String | ||
| 413 | - } | ||
| 414 | - | ||
| 415 | - func int(key: String) -> Int? { | ||
| 416 | - return (args?[key] as? NSNumber)?.intValue | ||
| 417 | - } | ||
| 418 | - | ||
| 419 | - func bool(key: String) -> Bool? { | ||
| 420 | - return (args?[key] as? NSNumber)?.boolValue | ||
| 421 | - } | ||
| 422 | - | ||
| 423 | - func stringArray(key: String) -> [String]? { | ||
| 424 | - return args?[key] as? [String] | ||
| 425 | - } | ||
| 426 | - | ||
| 427 | - func intArray(key: String) -> [Int]? { | ||
| 428 | - return args?[key] as? [Int] | ||
| 429 | - } | ||
| 430 | - | ||
| 431 | - func floatArray(key: String) -> [CGFloat]? { | ||
| 432 | - return args?[key] as? [CGFloat] | ||
| 433 | - } | ||
| 434 | - | ||
| 435 | -} |
| @@ -4,7 +4,7 @@ | @@ -4,7 +4,7 @@ | ||
| 4 | # | 4 | # |
| 5 | Pod::Spec.new do |s| | 5 | Pod::Spec.new do |s| |
| 6 | s.name = 'mobile_scanner' | 6 | s.name = 'mobile_scanner' |
| 7 | - s.version = '0.0.1' | 7 | + s.version = '3.0.0' |
| 8 | s.summary = 'An universal scanner for Flutter based on MLKit.' | 8 | s.summary = 'An universal scanner for Flutter based on MLKit.' |
| 9 | s.description = <<-DESC | 9 | s.description = <<-DESC |
| 10 | An universal scanner for Flutter based on MLKit. | 10 | An universal scanner for Flutter based on MLKit. |
| @@ -15,8 +15,8 @@ An universal scanner for Flutter based on MLKit. | @@ -15,8 +15,8 @@ An universal scanner for Flutter based on MLKit. | ||
| 15 | s.source = { :path => '.' } | 15 | s.source = { :path => '.' } |
| 16 | s.source_files = 'Classes/**/*' | 16 | s.source_files = 'Classes/**/*' |
| 17 | s.dependency 'Flutter' | 17 | s.dependency 'Flutter' |
| 18 | - s.dependency 'GoogleMLKit/BarcodeScanning', '~> 2.6.0' | ||
| 19 | - s.platform = :ios, '10.0' | 18 | + s.dependency 'GoogleMLKit/BarcodeScanning', '~> 3.2.0' |
| 19 | + s.platform = :ios, '11.0' | ||
| 20 | s.static_framework = true | 20 | s.static_framework = true |
| 21 | # Flutter.framework does not contain a i386 slice. | 21 | # Flutter.framework does not contain a i386 slice. |
| 22 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } | 22 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } |
| 1 | library mobile_scanner; | 1 | library mobile_scanner; |
| 2 | 2 | ||
| 3 | +export 'src/enums/camera_facing.dart'; | ||
| 4 | +export 'src/enums/detection_speed.dart'; | ||
| 5 | +export 'src/enums/mobile_scanner_error_code.dart'; | ||
| 6 | +export 'src/enums/mobile_scanner_state.dart'; | ||
| 7 | +export 'src/enums/ratio.dart'; | ||
| 8 | +export 'src/enums/torch_state.dart'; | ||
| 3 | export 'src/mobile_scanner.dart'; | 9 | export 'src/mobile_scanner.dart'; |
| 4 | -export 'src/mobile_scanner_arguments.dart'; | ||
| 5 | export 'src/mobile_scanner_controller.dart'; | 10 | export 'src/mobile_scanner_controller.dart'; |
| 11 | +export 'src/mobile_scanner_exception.dart'; | ||
| 6 | export 'src/objects/barcode.dart'; | 12 | export 'src/objects/barcode.dart'; |
| 13 | +export 'src/objects/barcode_capture.dart'; | ||
| 14 | +export 'src/objects/mobile_scanner_arguments.dart'; |
lib/mobile_scanner_web.dart
0 → 100644
| @@ -2,12 +2,10 @@ import 'dart:async'; | @@ -2,12 +2,10 @@ import 'dart:async'; | ||
| 2 | import 'dart:html' as html; | 2 | import 'dart:html' as html; |
| 3 | import 'dart:ui' as ui; | 3 | import 'dart:ui' as ui; |
| 4 | 4 | ||
| 5 | -import 'package:flutter/material.dart'; | ||
| 6 | import 'package:flutter/services.dart'; | 5 | import 'package:flutter/services.dart'; |
| 7 | import 'package:flutter_web_plugins/flutter_web_plugins.dart'; | 6 | import 'package:flutter_web_plugins/flutter_web_plugins.dart'; |
| 8 | -import 'package:mobile_scanner/mobile_scanner.dart'; | ||
| 9 | -import 'package:mobile_scanner/src/web/jsqr.dart'; | ||
| 10 | -import 'package:mobile_scanner/src/web/media.dart'; | 7 | +import 'package:mobile_scanner/mobile_scanner_web.dart'; |
| 8 | +import 'package:mobile_scanner/src/enums/camera_facing.dart'; | ||
| 11 | 9 | ||
| 12 | /// This plugin is the web implementation of mobile_scanner. | 10 | /// This plugin is the web implementation of mobile_scanner. |
| 13 | /// It only supports QR codes. | 11 | /// It only supports QR codes. |
| @@ -24,7 +22,6 @@ class MobileScannerWebPlugin { | @@ -24,7 +22,6 @@ class MobileScannerWebPlugin { | ||
| 24 | registrar, | 22 | registrar, |
| 25 | ); | 23 | ); |
| 26 | final MobileScannerWebPlugin instance = MobileScannerWebPlugin(); | 24 | final MobileScannerWebPlugin instance = MobileScannerWebPlugin(); |
| 27 | - WidgetsFlutterBinding.ensureInitialized(); | ||
| 28 | 25 | ||
| 29 | channel.setMethodCallHandler(instance.handleMethodCall); | 26 | channel.setMethodCallHandler(instance.handleMethodCall); |
| 30 | event.setController(instance.controller); | 27 | event.setController(instance.controller); |
| @@ -33,20 +30,14 @@ class MobileScannerWebPlugin { | @@ -33,20 +30,14 @@ class MobileScannerWebPlugin { | ||
| 33 | // Controller to send events back to the framework | 30 | // Controller to send events back to the framework |
| 34 | StreamController controller = StreamController.broadcast(); | 31 | StreamController controller = StreamController.broadcast(); |
| 35 | 32 | ||
| 36 | - // The video stream. Will be initialized later to see which camera needs to be used. | ||
| 37 | - html.MediaStream? _localStream; | ||
| 38 | - html.VideoElement video = html.VideoElement(); | ||
| 39 | - | ||
| 40 | // ID of the video feed | 33 | // ID of the video feed |
| 41 | String viewID = 'WebScanner-${DateTime.now().millisecondsSinceEpoch}'; | 34 | String viewID = 'WebScanner-${DateTime.now().millisecondsSinceEpoch}'; |
| 42 | 35 | ||
| 43 | - // Determine wether device has flas | ||
| 44 | - bool hasFlash = false; | ||
| 45 | - | ||
| 46 | - // Timer used to capture frames to be analyzed | ||
| 47 | - Timer? _frameInterval; | 36 | + static final html.DivElement vidDiv = html.DivElement(); |
| 48 | 37 | ||
| 49 | - html.DivElement vidDiv = html.DivElement(); | 38 | + static WebBarcodeReaderBase barCodeReader = |
| 39 | + ZXingBarcodeReader(videoContainer: vidDiv); | ||
| 40 | + StreamSubscription? _barCodeStreamSubscription; | ||
| 50 | 41 | ||
| 51 | /// Handle incomming messages | 42 | /// Handle incomming messages |
| 52 | Future<dynamic> handleMethodCall(MethodCall call) async { | 43 | Future<dynamic> handleMethodCall(MethodCall call) async { |
| @@ -68,20 +59,11 @@ class MobileScannerWebPlugin { | @@ -68,20 +59,11 @@ class MobileScannerWebPlugin { | ||
| 68 | 59 | ||
| 69 | /// Can enable or disable the flash if available | 60 | /// Can enable or disable the flash if available |
| 70 | Future<void> _torch(arguments) async { | 61 | Future<void> _torch(arguments) async { |
| 71 | - if (hasFlash) { | ||
| 72 | - final track = _localStream?.getVideoTracks(); | ||
| 73 | - await track!.first.applyConstraints({ | ||
| 74 | - 'advanced': {'torch': arguments == 1} | ||
| 75 | - }); | ||
| 76 | - } else { | ||
| 77 | - controller.addError('Device has no flash'); | ||
| 78 | - } | 62 | + barCodeReader.toggleTorch(enabled: arguments == 1); |
| 79 | } | 63 | } |
| 80 | 64 | ||
| 81 | /// Starts the video stream and the scanner | 65 | /// Starts the video stream and the scanner |
| 82 | Future<Map> _start(Map arguments) async { | 66 | Future<Map> _start(Map arguments) async { |
| 83 | - vidDiv.children = [video]; | ||
| 84 | - | ||
| 85 | var cameraFacing = CameraFacing.front; | 67 | var cameraFacing = CameraFacing.front; |
| 86 | if (arguments.containsKey('facing')) { | 68 | if (arguments.containsKey('facing')) { |
| 87 | cameraFacing = CameraFacing.values[arguments['facing'] as int]; | 69 | cameraFacing = CameraFacing.values[arguments['facing'] as int]; |
| @@ -91,64 +73,45 @@ class MobileScannerWebPlugin { | @@ -91,64 +73,45 @@ class MobileScannerWebPlugin { | ||
| 91 | // ignore: UNDEFINED_PREFIXED_NAME, avoid_dynamic_calls | 73 | // ignore: UNDEFINED_PREFIXED_NAME, avoid_dynamic_calls |
| 92 | ui.platformViewRegistry.registerViewFactory( | 74 | ui.platformViewRegistry.registerViewFactory( |
| 93 | viewID, | 75 | viewID, |
| 94 | - (int id) => vidDiv | ||
| 95 | - ..style.width = '100%' | ||
| 96 | - ..style.height = '100%', | 76 | + (int id) { |
| 77 | + return vidDiv | ||
| 78 | + ..style.width = '100%' | ||
| 79 | + ..style.height = '100%'; | ||
| 80 | + }, | ||
| 97 | ); | 81 | ); |
| 98 | 82 | ||
| 99 | // Check if stream is running | 83 | // Check if stream is running |
| 100 | - if (_localStream != null) { | 84 | + if (barCodeReader.isStarted) { |
| 101 | return { | 85 | return { |
| 102 | 'ViewID': viewID, | 86 | 'ViewID': viewID, |
| 103 | - 'videoWidth': video.videoWidth, | ||
| 104 | - 'videoHeight': video.videoHeight | 87 | + 'videoWidth': barCodeReader.videoWidth, |
| 88 | + 'videoHeight': barCodeReader.videoHeight, | ||
| 89 | + 'torchable': barCodeReader.hasTorch, | ||
| 105 | }; | 90 | }; |
| 106 | } | 91 | } |
| 107 | - | ||
| 108 | try { | 92 | try { |
| 109 | - // Check if browser supports multiple camera's and set if supported | ||
| 110 | - final Map? capabilities = | ||
| 111 | - html.window.navigator.mediaDevices?.getSupportedConstraints(); | ||
| 112 | - if (capabilities != null && capabilities['facingMode'] as bool) { | ||
| 113 | - final constraints = { | ||
| 114 | - 'video': VideoOptions( | ||
| 115 | - facingMode: | ||
| 116 | - cameraFacing == CameraFacing.front ? 'user' : 'environment', | ||
| 117 | - ) | ||
| 118 | - }; | ||
| 119 | - | ||
| 120 | - _localStream = | ||
| 121 | - await html.window.navigator.mediaDevices?.getUserMedia(constraints); | ||
| 122 | - } else { | ||
| 123 | - _localStream = await html.window.navigator.mediaDevices | ||
| 124 | - ?.getUserMedia({'video': true}); | ||
| 125 | - } | ||
| 126 | - | ||
| 127 | - video.srcObject = _localStream; | ||
| 128 | - | ||
| 129 | - // TODO: fix flash light. See https://github.com/dart-lang/sdk/issues/48533 | ||
| 130 | - // final track = _localStream?.getVideoTracks(); | ||
| 131 | - // if (track != null) { | ||
| 132 | - // final imageCapture = html.ImageCapture(track.first); | ||
| 133 | - // final photoCapabilities = await imageCapture.getPhotoCapabilities(); | ||
| 134 | - // } | ||
| 135 | - | ||
| 136 | - // required to tell iOS safari we don't want fullscreen | ||
| 137 | - video.setAttribute('playsinline', 'true'); | ||
| 138 | - | ||
| 139 | - await video.play(); | ||
| 140 | - | ||
| 141 | - // Then capture a frame to be analyzed every 200 miliseconds | ||
| 142 | - _frameInterval = | ||
| 143 | - Timer.periodic(const Duration(milliseconds: 200), (timer) { | ||
| 144 | - _captureFrame(); | 93 | + await barCodeReader.start( |
| 94 | + cameraFacing: cameraFacing, | ||
| 95 | + ); | ||
| 96 | + | ||
| 97 | + _barCodeStreamSubscription = | ||
| 98 | + barCodeReader.detectBarcodeContinuously().listen((code) { | ||
| 99 | + if (code != null) { | ||
| 100 | + controller.add({ | ||
| 101 | + 'name': 'barcodeWeb', | ||
| 102 | + 'data': { | ||
| 103 | + 'rawValue': code.rawValue, | ||
| 104 | + 'rawBytes': code.rawBytes, | ||
| 105 | + }, | ||
| 106 | + }); | ||
| 107 | + } | ||
| 145 | }); | 108 | }); |
| 146 | 109 | ||
| 147 | return { | 110 | return { |
| 148 | 'ViewID': viewID, | 111 | 'ViewID': viewID, |
| 149 | - 'videoWidth': video.videoWidth, | ||
| 150 | - 'videoHeight': video.videoHeight, | ||
| 151 | - 'torchable': hasFlash | 112 | + 'videoWidth': barCodeReader.videoWidth, |
| 113 | + 'videoHeight': barCodeReader.videoHeight, | ||
| 114 | + 'torchable': barCodeReader.hasTorch, | ||
| 152 | }; | 115 | }; |
| 153 | } catch (e) { | 116 | } catch (e) { |
| 154 | throw PlatformException(code: 'MobileScannerWeb', message: '$e'); | 117 | throw PlatformException(code: 'MobileScannerWeb', message: '$e'); |
| @@ -171,36 +134,8 @@ class MobileScannerWebPlugin { | @@ -171,36 +134,8 @@ class MobileScannerWebPlugin { | ||
| 171 | 134 | ||
| 172 | /// Stops the video feed and analyzer | 135 | /// Stops the video feed and analyzer |
| 173 | Future<void> cancel() async { | 136 | Future<void> cancel() async { |
| 174 | - try { | ||
| 175 | - // Stop the camera stream | ||
| 176 | - _localStream?.getTracks().forEach((track) { | ||
| 177 | - if (track.readyState == 'live') { | ||
| 178 | - track.stop(); | ||
| 179 | - } | ||
| 180 | - }); | ||
| 181 | - } catch (e) { | ||
| 182 | - debugPrint('Failed to stop stream: $e'); | ||
| 183 | - } | ||
| 184 | - | ||
| 185 | - video.srcObject = null; | ||
| 186 | - _localStream = null; | ||
| 187 | - _frameInterval?.cancel(); | ||
| 188 | - _frameInterval = null; | ||
| 189 | - } | ||
| 190 | - | ||
| 191 | - /// Captures a frame and analyzes it for QR codes | ||
| 192 | - Future<dynamic> _captureFrame() async { | ||
| 193 | - if (_localStream == null) return null; | ||
| 194 | - final canvas = | ||
| 195 | - html.CanvasElement(width: video.videoWidth, height: video.videoHeight); | ||
| 196 | - final ctx = canvas.context2D; | ||
| 197 | - | ||
| 198 | - ctx.drawImage(video, 0, 0); | ||
| 199 | - final imgData = ctx.getImageData(0, 0, canvas.width!, canvas.height!); | ||
| 200 | - | ||
| 201 | - final code = jsQR(imgData.data, canvas.width, canvas.height); | ||
| 202 | - if (code != null) { | ||
| 203 | - controller.add({'name': 'barcodeWeb', 'data': code.data}); | ||
| 204 | - } | 137 | + barCodeReader.stop(); |
| 138 | + await _barCodeStreamSubscription?.cancel(); | ||
| 139 | + _barCodeStreamSubscription = null; | ||
| 205 | } | 140 | } |
| 206 | } | 141 | } |
lib/src/enums/camera_facing.dart
0 → 100644
lib/src/enums/detection_speed.dart
0 → 100644
| 1 | +/// The detection speed of the scanner. | ||
| 2 | +enum DetectionSpeed { | ||
| 3 | + /// The scanner will only scan a barcode once, and never again until another | ||
| 4 | + /// barcode has been scanned. | ||
| 5 | + /// | ||
| 6 | + /// NOTE: This mode does analyze every frame in order to check if the value | ||
| 7 | + /// has changed. | ||
| 8 | + noDuplicates, | ||
| 9 | + | ||
| 10 | + /// The barcode scanner will scan one barcode, and wait 250 Miliseconds before | ||
| 11 | + /// scanning again. This will prevent memory issues on older devices. | ||
| 12 | + /// | ||
| 13 | + /// You can change the timeout duration with [detectionTimeout] parameter. | ||
| 14 | + normal, | ||
| 15 | + | ||
| 16 | + /// Let the scanner detect barcodes without restriction. | ||
| 17 | + /// | ||
| 18 | + /// NOTE: This can cause memory issues with older devices. | ||
| 19 | + unrestricted, | ||
| 20 | +} |
lib/src/enums/mobile_scanner_error_code.dart
0 → 100644
| 1 | +/// This enum defines the different error codes for the mobile scanner. | ||
| 2 | +enum MobileScannerErrorCode { | ||
| 3 | + /// The controller was used | ||
| 4 | + /// while it was not yet initialized using [MobileScannerController.start]. | ||
| 5 | + controllerUninitialized, | ||
| 6 | + | ||
| 7 | + /// A generic error occurred. | ||
| 8 | + /// | ||
| 9 | + /// This error code is used for all errors that do not have a specific error code. | ||
| 10 | + genericError, | ||
| 11 | + | ||
| 12 | + /// The permission to use the camera was denied. | ||
| 13 | + permissionDenied, | ||
| 14 | +} |
lib/src/enums/mobile_scanner_state.dart
0 → 100644
| 1 | +/// The authorization state of the scanner. | ||
| 2 | +enum MobileScannerState { | ||
| 3 | + /// The scanner has not yet requested the required permissions. | ||
| 4 | + undetermined, | ||
| 5 | + | ||
| 6 | + /// The scanner has the required permissions. | ||
| 7 | + authorized, | ||
| 8 | + | ||
| 9 | + /// The user denied the required permissions. | ||
| 10 | + denied | ||
| 11 | +} |
lib/src/enums/ratio.dart
0 → 100644
lib/src/enums/torch_state.dart
0 → 100644
| 1 | +import 'dart:async'; | ||
| 2 | + | ||
| 1 | import 'package:flutter/foundation.dart'; | 3 | import 'package:flutter/foundation.dart'; |
| 2 | import 'package:flutter/material.dart' hide applyBoxFit; | 4 | import 'package:flutter/material.dart' hide applyBoxFit; |
| 3 | -import 'package:mobile_scanner/mobile_scanner.dart'; | ||
| 4 | -import 'package:mobile_scanner/src/objects/barcode_utility.dart'; | ||
| 5 | - | ||
| 6 | -enum Ratio { ratio_4_3, ratio_16_9 } | 5 | +import 'package:mobile_scanner/src/mobile_scanner_controller.dart'; |
| 6 | +import 'package:mobile_scanner/src/objects/barcode_capture.dart'; | ||
| 7 | +import 'package:mobile_scanner/src/objects/mobile_scanner_arguments.dart'; | ||
| 7 | 8 | ||
| 8 | -/// A widget showing a live camera preview. | 9 | +/// The [MobileScanner] widget displays a live camera preview. |
| 9 | class MobileScanner extends StatefulWidget { | 10 | class MobileScanner extends StatefulWidget { |
| 10 | - /// The controller of the camera. | 11 | + /// The controller that manages the barcode scanner. |
| 12 | + /// | ||
| 13 | + /// If this is null, the scanner will manage its own controller. | ||
| 11 | final MobileScannerController? controller; | 14 | final MobileScannerController? controller; |
| 12 | 15 | ||
| 13 | - /// Function that gets called when a Barcode is detected. | 16 | + /// The [BoxFit] for the camera preview. |
| 14 | /// | 17 | /// |
| 15 | - /// [barcode] The barcode object with all information about the scanned code. | ||
| 16 | - /// [args] Information about the state of the MobileScanner widget | ||
| 17 | - final Function(Barcode barcode, MobileScannerArguments? args) onDetect; | 18 | + /// Defaults to [BoxFit.cover]. |
| 19 | + final BoxFit fit; | ||
| 18 | 20 | ||
| 19 | - /// TODO: Function that gets called when the Widget is initialized. Can be usefull | ||
| 20 | - /// to check wether the device has a torch(flash) or not. | ||
| 21 | - /// | ||
| 22 | - /// [args] Information about the state of the MobileScanner widget | ||
| 23 | - // final Function(MobileScannerArguments args)? onInitialize; | 21 | + /// The function that signals when new codes were detected by the [controller]. |
| 22 | + final void Function(BarcodeCapture barcodes) onDetect; | ||
| 24 | 23 | ||
| 25 | - /// Handles how the widget should fit the screen. | ||
| 26 | - final BoxFit fit; | 24 | + /// The function that signals when the barcode scanner is started. |
| 25 | + @Deprecated('Use onScannerStarted() instead.') | ||
| 26 | + final void Function(MobileScannerArguments? arguments)? onStart; | ||
| 27 | 27 | ||
| 28 | - /// Set to false if you don't want duplicate scans. | ||
| 29 | - final bool allowDuplicates; | 28 | + /// The function that signals when the barcode scanner is started. |
| 29 | + final void Function(MobileScannerArguments? arguments)? onScannerStarted; | ||
| 30 | + | ||
| 31 | + /// The function that builds a placeholder widget when the scanner | ||
| 32 | + /// is not yet displaying its camera preview. | ||
| 33 | + /// | ||
| 34 | + /// If this is null, a black [ColoredBox] is used as placeholder. | ||
| 35 | + final Widget Function(BuildContext, Widget?)? placeholderBuilder; | ||
| 30 | 36 | ||
| 31 | /// if set barcodes will only be scanned if they fall within this [Rect] | 37 | /// if set barcodes will only be scanned if they fall within this [Rect] |
| 32 | /// useful for having a cut-out overlay for example. these [Rect] | 38 | /// useful for having a cut-out overlay for example. these [Rect] |
| @@ -35,14 +41,17 @@ class MobileScanner extends StatefulWidget { | @@ -35,14 +41,17 @@ class MobileScanner extends StatefulWidget { | ||
| 35 | /// [BoxFit] | 41 | /// [BoxFit] |
| 36 | final Rect? scanWindow; | 42 | final Rect? scanWindow; |
| 37 | 43 | ||
| 38 | - /// Create a [MobileScanner] with a [controller], the [controller] must has been initialized. | 44 | + /// Create a new [MobileScanner] using the provided [controller] |
| 45 | + /// and [onBarcodeDetected] callback. | ||
| 39 | const MobileScanner({ | 46 | const MobileScanner({ |
| 40 | - super.key, | ||
| 41 | - required this.onDetect, | ||
| 42 | this.controller, | 47 | this.controller, |
| 43 | this.fit = BoxFit.cover, | 48 | this.fit = BoxFit.cover, |
| 44 | - this.allowDuplicates = false, | 49 | + required this.onDetect, |
| 50 | + @Deprecated('Use onScannerStarted() instead.') this.onStart, | ||
| 51 | + this.onScannerStarted, | ||
| 52 | + this.placeholderBuilder, | ||
| 45 | this.scanWindow, | 53 | this.scanWindow, |
| 54 | + super.key, | ||
| 46 | }); | 55 | }); |
| 47 | 56 | ||
| 48 | @override | 57 | @override |
| @@ -51,32 +60,71 @@ class MobileScanner extends StatefulWidget { | @@ -51,32 +60,71 @@ class MobileScanner extends StatefulWidget { | ||
| 51 | 60 | ||
| 52 | class _MobileScannerState extends State<MobileScanner> | 61 | class _MobileScannerState extends State<MobileScanner> |
| 53 | with WidgetsBindingObserver { | 62 | with WidgetsBindingObserver { |
| 54 | - late MobileScannerController controller; | 63 | + /// The subscription that listens to barcode detection. |
| 64 | + StreamSubscription<BarcodeCapture>? _barcodesSubscription; | ||
| 65 | + | ||
| 66 | + /// The internally managed controller. | ||
| 67 | + late MobileScannerController _controller; | ||
| 68 | + | ||
| 69 | + /// Whether the controller should resume | ||
| 70 | + /// when the application comes back to the foreground. | ||
| 71 | + bool _resumeFromBackground = false; | ||
| 72 | + | ||
| 73 | + /// Start the given [scanner]. | ||
| 74 | + void _startScanner(MobileScannerController scanner) { | ||
| 75 | + if (!_controller.autoStart) { | ||
| 76 | + debugPrint( | ||
| 77 | + 'mobile_scanner: not starting automatically because autoStart is set to false in the controller.', | ||
| 78 | + ); | ||
| 79 | + return; | ||
| 80 | + } | ||
| 81 | + scanner.start().then((arguments) { | ||
| 82 | + // ignore: deprecated_member_use_from_same_package | ||
| 83 | + widget.onStart?.call(arguments); | ||
| 84 | + widget.onScannerStarted?.call(arguments); | ||
| 85 | + }); | ||
| 86 | + } | ||
| 55 | 87 | ||
| 56 | @override | 88 | @override |
| 57 | void initState() { | 89 | void initState() { |
| 58 | super.initState(); | 90 | super.initState(); |
| 59 | WidgetsBinding.instance.addObserver(this); | 91 | WidgetsBinding.instance.addObserver(this); |
| 60 | - controller = widget.controller ?? MobileScannerController(); | ||
| 61 | - if (!controller.isStarting) controller.start(); | 92 | + _controller = widget.controller ?? MobileScannerController(); |
| 93 | + | ||
| 94 | + _barcodesSubscription = _controller.barcodes.listen( | ||
| 95 | + widget.onDetect, | ||
| 96 | + ); | ||
| 97 | + | ||
| 98 | + if (!_controller.isStarting) { | ||
| 99 | + _startScanner(_controller); | ||
| 100 | + } | ||
| 62 | } | 101 | } |
| 63 | 102 | ||
| 64 | @override | 103 | @override |
| 65 | void didChangeAppLifecycleState(AppLifecycleState state) { | 104 | void didChangeAppLifecycleState(AppLifecycleState state) { |
| 105 | + // App state changed before the controller was initialized. | ||
| 106 | + if (_controller.isStarting) { | ||
| 107 | + return; | ||
| 108 | + } | ||
| 109 | + | ||
| 66 | switch (state) { | 110 | switch (state) { |
| 67 | case AppLifecycleState.resumed: | 111 | case AppLifecycleState.resumed: |
| 68 | - if (!controller.isStarting && controller.autoResume) controller.start(); | 112 | + _resumeFromBackground = false; |
| 113 | + _startScanner(_controller); | ||
| 69 | break; | 114 | break; |
| 70 | - case AppLifecycleState.inactive: | ||
| 71 | case AppLifecycleState.paused: | 115 | case AppLifecycleState.paused: |
| 116 | + _resumeFromBackground = true; | ||
| 117 | + break; | ||
| 118 | + case AppLifecycleState.inactive: | ||
| 119 | + if (!_resumeFromBackground) { | ||
| 120 | + _controller.stop(); | ||
| 121 | + } | ||
| 122 | + break; | ||
| 72 | case AppLifecycleState.detached: | 123 | case AppLifecycleState.detached: |
| 73 | - controller.stop(); | ||
| 74 | break; | 124 | break; |
| 75 | } | 125 | } |
| 76 | } | 126 | } |
| 77 | 127 | ||
| 78 | - String? lastScanned; | ||
| 79 | - | ||
| 80 | /// the [scanWindow] rect will be relative and scaled to the [widgetSize] not the texture. so it is possible, | 128 | /// the [scanWindow] rect will be relative and scaled to the [widgetSize] not the texture. so it is possible, |
| 81 | /// depending on the [fit], for the [scanWindow] to partially or not at all overlap the [textureSize] | 129 | /// depending on the [fit], for the [scanWindow] to partially or not at all overlap the [textureSize] |
| 82 | /// | 130 | /// |
| @@ -86,11 +134,11 @@ class _MobileScannerState extends State<MobileScanner> | @@ -86,11 +134,11 @@ class _MobileScannerState extends State<MobileScanner> | ||
| 86 | /// since the textures size and the actuall image (on the texture size) might not be the same, we also need to | 134 | /// since the textures size and the actuall image (on the texture size) might not be the same, we also need to |
| 87 | /// calculate the scanWindow in terms of percentages of the texture, not pixels. | 135 | /// calculate the scanWindow in terms of percentages of the texture, not pixels. |
| 88 | Rect calculateScanWindowRelativeToTextureInPercentage( | 136 | Rect calculateScanWindowRelativeToTextureInPercentage( |
| 89 | - BoxFit fit, | ||
| 90 | - Rect scanWindow, | ||
| 91 | - Size textureSize, | ||
| 92 | - Size widgetSize, | ||
| 93 | - ) { | 137 | + BoxFit fit, |
| 138 | + Rect scanWindow, | ||
| 139 | + Size textureSize, | ||
| 140 | + Size widgetSize, | ||
| 141 | + ) { | ||
| 94 | /// map the texture size to get its new size after fitted to screen | 142 | /// map the texture size to get its new size after fitted to screen |
| 95 | final fittedTextureSize = applyBoxFit(fit, textureSize, widgetSize); | 143 | final fittedTextureSize = applyBoxFit(fit, textureSize, widgetSize); |
| 96 | 144 | ||
| @@ -128,78 +176,53 @@ class _MobileScannerState extends State<MobileScanner> | @@ -128,78 +176,53 @@ class _MobileScannerState extends State<MobileScanner> | ||
| 128 | 176 | ||
| 129 | @override | 177 | @override |
| 130 | Widget build(BuildContext context) { | 178 | Widget build(BuildContext context) { |
| 131 | - return LayoutBuilder( | ||
| 132 | - builder: (context, BoxConstraints constraints) { | ||
| 133 | - return ValueListenableBuilder( | ||
| 134 | - valueListenable: controller.args, | ||
| 135 | - builder: (context, value, child) { | ||
| 136 | - value = value as MobileScannerArguments?; | ||
| 137 | - if (value == null) { | ||
| 138 | - return const ColoredBox(color: Colors.black); | ||
| 139 | - } else { | ||
| 140 | - if (widget.scanWindow != null) { | ||
| 141 | - final window = calculateScanWindowRelativeToTextureInPercentage( | ||
| 142 | - widget.fit, | ||
| 143 | - widget.scanWindow!, | ||
| 144 | - value.size, | ||
| 145 | - Size(constraints.maxWidth, constraints.maxHeight), | ||
| 146 | - ); | ||
| 147 | - controller.updateScanWindow(window); | ||
| 148 | - } | ||
| 149 | - | ||
| 150 | - controller.barcodes.listen((barcode) { | ||
| 151 | - if (!widget.allowDuplicates) { | ||
| 152 | - if (lastScanned == barcode.rawValue) return; | ||
| 153 | - lastScanned = barcode.rawValue; | ||
| 154 | - widget.onDetect(barcode, value! as MobileScannerArguments); | ||
| 155 | - } else { | ||
| 156 | - widget.onDetect(barcode, value! as MobileScannerArguments); | ||
| 157 | - } | ||
| 158 | - }); | ||
| 159 | - return ClipRect( | ||
| 160 | - child: SizedBox( | ||
| 161 | - width: MediaQuery.of(context).size.width, | ||
| 162 | - height: MediaQuery.of(context).size.height, | ||
| 163 | - child: FittedBox( | ||
| 164 | - fit: widget.fit, | ||
| 165 | - child: SizedBox( | ||
| 166 | - width: value.size.width, | ||
| 167 | - height: value.size.height, | ||
| 168 | - child: kIsWeb | ||
| 169 | - ? HtmlElementView(viewType: value.webId!) | ||
| 170 | - : Texture(textureId: value.textureId!), | ||
| 171 | - ), | 179 | + return ValueListenableBuilder<MobileScannerArguments?>( |
| 180 | + valueListenable: _controller.startArguments, | ||
| 181 | + builder: (context, value, child) { | ||
| 182 | + if (value == null) { | ||
| 183 | + return widget.placeholderBuilder?.call(context, child) ?? | ||
| 184 | + const ColoredBox(color: Colors.black); | ||
| 185 | + } | ||
| 186 | + | ||
| 187 | + if (widget.scanWindow != null) { | ||
| 188 | + final window = calculateScanWindowRelativeToTextureInPercentage( | ||
| 189 | + widget.fit, | ||
| 190 | + widget.scanWindow!, | ||
| 191 | + value.size, | ||
| 192 | + Size(constraints.maxWidth, constraints.maxHeight), | ||
| 193 | + ); | ||
| 194 | + controller.updateScanWindow(window); | ||
| 195 | + } | ||
| 196 | + | ||
| 197 | + | ||
| 198 | + return ClipRect( | ||
| 199 | + child: LayoutBuilder( | ||
| 200 | + builder: (_, constraints) { | ||
| 201 | + return SizedBox.fromSize( | ||
| 202 | + size: constraints.biggest, | ||
| 203 | + child: FittedBox( | ||
| 204 | + fit: widget.fit, | ||
| 205 | + child: SizedBox( | ||
| 206 | + width: value.size.width, | ||
| 207 | + height: value.size.height, | ||
| 208 | + child: kIsWeb | ||
| 209 | + ? HtmlElementView(viewType: value.webId!) | ||
| 210 | + : Texture(textureId: value.textureId!), | ||
| 172 | ), | 211 | ), |
| 173 | ), | 212 | ), |
| 174 | ); | 213 | ); |
| 175 | - } | ||
| 176 | - }, | 214 | + }, |
| 215 | + ), | ||
| 177 | ); | 216 | ); |
| 178 | }, | 217 | }, |
| 179 | ); | 218 | ); |
| 180 | } | 219 | } |
| 181 | 220 | ||
| 182 | @override | 221 | @override |
| 183 | - void didUpdateWidget(covariant MobileScanner oldWidget) { | ||
| 184 | - super.didUpdateWidget(oldWidget); | ||
| 185 | - if (oldWidget.controller == null) { | ||
| 186 | - if (widget.controller != null) { | ||
| 187 | - controller.dispose(); | ||
| 188 | - controller = widget.controller!; | ||
| 189 | - } | ||
| 190 | - } else { | ||
| 191 | - if (widget.controller == null) { | ||
| 192 | - controller = MobileScannerController(); | ||
| 193 | - } else if (oldWidget.controller != widget.controller) { | ||
| 194 | - controller = widget.controller!; | ||
| 195 | - } | ||
| 196 | - } | ||
| 197 | - } | ||
| 198 | - | ||
| 199 | - @override | ||
| 200 | void dispose() { | 222 | void dispose() { |
| 201 | - controller.dispose(); | ||
| 202 | WidgetsBinding.instance.removeObserver(this); | 223 | WidgetsBinding.instance.removeObserver(this); |
| 224 | + _barcodesSubscription?.cancel(); | ||
| 225 | + _controller.dispose(); | ||
| 203 | super.dispose(); | 226 | super.dispose(); |
| 204 | } | 227 | } |
| 205 | } | 228 | } |
| @@ -5,160 +5,125 @@ import 'package:flutter/cupertino.dart'; | @@ -5,160 +5,125 @@ import 'package:flutter/cupertino.dart'; | ||
| 5 | import 'package:flutter/foundation.dart'; | 5 | import 'package:flutter/foundation.dart'; |
| 6 | import 'package:flutter/services.dart'; | 6 | import 'package:flutter/services.dart'; |
| 7 | import 'package:mobile_scanner/mobile_scanner.dart'; | 7 | import 'package:mobile_scanner/mobile_scanner.dart'; |
| 8 | -import 'package:mobile_scanner/src/objects/barcode_utility.dart'; | 8 | +import 'package:mobile_scanner/src/barcode_utility.dart'; |
| 9 | 9 | ||
| 10 | -/// The facing of a camera. | ||
| 11 | -enum CameraFacing { | ||
| 12 | - /// Front facing camera. | ||
| 13 | - front, | ||
| 14 | - | ||
| 15 | - /// Back facing camera. | ||
| 16 | - back, | ||
| 17 | -} | ||
| 18 | - | ||
| 19 | -enum MobileScannerState { undetermined, authorized, denied } | ||
| 20 | - | ||
| 21 | -/// The state of torch. | ||
| 22 | -enum TorchState { | ||
| 23 | - /// Torch is off. | ||
| 24 | - off, | 10 | +/// The [MobileScannerController] holds all the logic of this plugin, |
| 11 | +/// where as the [MobileScanner] class is the frontend of this plugin. | ||
| 12 | +class MobileScannerController { | ||
| 13 | + MobileScannerController({ | ||
| 14 | + this.facing = CameraFacing.back, | ||
| 15 | + this.detectionSpeed = DetectionSpeed.normal, | ||
| 16 | + this.detectionTimeoutMs = 250, | ||
| 17 | + this.torchEnabled = false, | ||
| 18 | + this.formats, | ||
| 19 | + this.returnImage = false, | ||
| 20 | + @Deprecated('Instead, use the result of calling `start()` to determine if permissions were granted.') | ||
| 21 | + this.onPermissionSet, | ||
| 22 | + this.autoStart = true, | ||
| 23 | + }) { | ||
| 24 | + // In case a new instance is created before calling dispose() | ||
| 25 | + if (controllerHashcode != null) { | ||
| 26 | + stop(); | ||
| 27 | + } | ||
| 28 | + controllerHashcode = hashCode; | ||
| 29 | + events = _eventChannel | ||
| 30 | + .receiveBroadcastStream() | ||
| 31 | + .listen((data) => _handleEvent(data as Map)); | ||
| 32 | + } | ||
| 25 | 33 | ||
| 26 | - /// Torch is on. | ||
| 27 | - on, | ||
| 28 | -} | 34 | + /// The hashcode of the controller to check if the correct object is mounted. |
| 35 | + /// Must be static to keep the same value on new instances | ||
| 36 | + static int? controllerHashcode; | ||
| 29 | 37 | ||
| 30 | -// enum AnalyzeMode { none, barcode } | 38 | + /// Select which camera should be used. |
| 39 | + /// | ||
| 40 | + /// Default: CameraFacing.back | ||
| 41 | + final CameraFacing facing; | ||
| 31 | 42 | ||
| 32 | -class MobileScannerController { | ||
| 33 | - MethodChannel methodChannel = | ||
| 34 | - const MethodChannel('dev.steenbakker.mobile_scanner/scanner/method'); | ||
| 35 | - EventChannel eventChannel = | ||
| 36 | - const EventChannel('dev.steenbakker.mobile_scanner/scanner/event'); | 43 | + /// Enable or disable the torch (Flash) on start |
| 44 | + /// | ||
| 45 | + /// Default: disabled | ||
| 46 | + final bool torchEnabled; | ||
| 37 | 47 | ||
| 38 | - //Must be static to keep the same value on new instances | ||
| 39 | - static int? _controllerHashcode; | ||
| 40 | - StreamSubscription? events; | 48 | + /// Set to true if you want to return the image buffer with the Barcode event |
| 49 | + /// | ||
| 50 | + /// Only supported on iOS and Android | ||
| 51 | + final bool returnImage; | ||
| 41 | 52 | ||
| 42 | - final ValueNotifier<MobileScannerArguments?> args = ValueNotifier(null); | ||
| 43 | - final ValueNotifier<TorchState> torchState = ValueNotifier(TorchState.off); | ||
| 44 | - late final ValueNotifier<CameraFacing> cameraFacingState; | ||
| 45 | - final Ratio? ratio; | ||
| 46 | - final bool? torchEnabled; | 53 | + /// If provided, the scanner will only detect those specific formats |
| 54 | + final List<BarcodeFormat>? formats; | ||
| 47 | 55 | ||
| 48 | - /// If provided, the scanner will only detect those specific formats. | 56 | + /// Sets the speed of detections. |
| 49 | /// | 57 | /// |
| 50 | - /// WARNING: On iOS, only 1 format is supported. | ||
| 51 | - final List<BarcodeFormat>? formats; | 58 | + /// WARNING: DetectionSpeed.unrestricted can cause memory issues on some devices |
| 59 | + final DetectionSpeed detectionSpeed; | ||
| 52 | 60 | ||
| 53 | - CameraFacing facing; | ||
| 54 | - bool hasTorch = false; | ||
| 55 | - late StreamController<Barcode> barcodesController; | 61 | + /// Sets the timeout of scanner. |
| 62 | + /// The timeout is set in miliseconds. | ||
| 63 | + /// | ||
| 64 | + /// NOTE: The timeout only works if the [detectionSpeed] is set to | ||
| 65 | + /// [DetectionSpeed.normal] (which is the default value). | ||
| 66 | + final int detectionTimeoutMs; | ||
| 56 | 67 | ||
| 57 | - /// Whether to automatically resume the camera when the application is resumed | ||
| 58 | - bool autoResume; | 68 | + /// Automatically start the mobileScanner on initialization. |
| 69 | + final bool autoStart; | ||
| 59 | 70 | ||
| 60 | - Stream<Barcode> get barcodes => barcodesController.stream; | 71 | + /// Sets the barcode stream |
| 72 | + final StreamController<BarcodeCapture> _barcodesController = | ||
| 73 | + StreamController.broadcast(); | ||
| 74 | + Stream<BarcodeCapture> get barcodes => _barcodesController.stream; | ||
| 61 | 75 | ||
| 62 | - MobileScannerController({ | ||
| 63 | - this.facing = CameraFacing.back, | ||
| 64 | - this.ratio, | ||
| 65 | - this.torchEnabled, | ||
| 66 | - this.formats, | ||
| 67 | - this.autoResume = true, | ||
| 68 | - }) { | ||
| 69 | - // In case a new instance is created before calling dispose() | ||
| 70 | - if (_controllerHashcode != null) { | ||
| 71 | - stop(); | ||
| 72 | - } | ||
| 73 | - _controllerHashcode = hashCode; | 76 | + static const MethodChannel _methodChannel = |
| 77 | + MethodChannel('dev.steenbakker.mobile_scanner/scanner/method'); | ||
| 78 | + static const EventChannel _eventChannel = | ||
| 79 | + EventChannel('dev.steenbakker.mobile_scanner/scanner/event'); | ||
| 74 | 80 | ||
| 75 | - cameraFacingState = ValueNotifier(facing); | 81 | + @Deprecated( |
| 82 | + 'Instead, use the result of calling `start()` to determine if permissions were granted.', | ||
| 83 | + ) | ||
| 84 | + Function(bool permissionGranted)? onPermissionSet; | ||
| 76 | 85 | ||
| 77 | - // Sets analyze mode and barcode stream | ||
| 78 | - barcodesController = StreamController.broadcast( | ||
| 79 | - // onListen: () => setAnalyzeMode(AnalyzeMode.barcode.index), | ||
| 80 | - // onCancel: () => setAnalyzeMode(AnalyzeMode.none.index), | ||
| 81 | - ); | 86 | + /// Listen to events from the platform specific code |
| 87 | + late StreamSubscription events; | ||
| 82 | 88 | ||
| 83 | - // Listen to events from the platform specific code | ||
| 84 | - events = eventChannel | ||
| 85 | - .receiveBroadcastStream() | ||
| 86 | - .listen((data) => handleEvent(data as Map)); | ||
| 87 | - } | 89 | + /// A notifier that provides several arguments about the MobileScanner |
| 90 | + final ValueNotifier<MobileScannerArguments?> startArguments = | ||
| 91 | + ValueNotifier(null); | ||
| 88 | 92 | ||
| 89 | - void handleEvent(Map event) { | ||
| 90 | - final name = event['name']; | ||
| 91 | - final data = event['data']; | ||
| 92 | - switch (name) { | ||
| 93 | - case 'torchState': | ||
| 94 | - final state = TorchState.values[data as int? ?? 0]; | ||
| 95 | - torchState.value = state; | ||
| 96 | - break; | ||
| 97 | - case 'barcode': | ||
| 98 | - final barcode = Barcode.fromNative(data as Map? ?? {}); | ||
| 99 | - barcodesController.add(barcode); | ||
| 100 | - break; | ||
| 101 | - case 'barcodeMac': | ||
| 102 | - barcodesController.add( | ||
| 103 | - Barcode( | ||
| 104 | - rawValue: (data as Map)['payload'] as String?, | ||
| 105 | - ), | ||
| 106 | - ); | ||
| 107 | - break; | ||
| 108 | - case 'barcodeWeb': | ||
| 109 | - barcodesController.add(Barcode(rawValue: data as String?)); | ||
| 110 | - break; | ||
| 111 | - default: | ||
| 112 | - throw UnimplementedError(); | ||
| 113 | - } | ||
| 114 | - } | 93 | + /// A notifier that provides the state of the Torch (Flash) |
| 94 | + final ValueNotifier<TorchState> torchState = ValueNotifier(TorchState.off); | ||
| 115 | 95 | ||
| 116 | - // TODO: Add more analyzers like text analyzer | ||
| 117 | - // void setAnalyzeMode(int mode) { | ||
| 118 | - // if (hashCode != _controllerHashcode) { | ||
| 119 | - // return; | ||
| 120 | - // } | ||
| 121 | - // methodChannel.invokeMethod('analyze', mode); | ||
| 122 | - // } | 96 | + /// A notifier that provides the state of which camera is being used |
| 97 | + late final ValueNotifier<CameraFacing> cameraFacingState = | ||
| 98 | + ValueNotifier(facing); | ||
| 123 | 99 | ||
| 124 | - // List<BarcodeFormats>? formats = _defaultBarcodeFormats, | ||
| 125 | bool isStarting = false; | 100 | bool isStarting = false; |
| 126 | 101 | ||
| 127 | - /// Start barcode scanning. This will first check if the required permissions | ||
| 128 | - /// are set. | ||
| 129 | - Future<void> start() async { | ||
| 130 | - ensure('startAsync'); | ||
| 131 | - if (isStarting) { | ||
| 132 | - throw Exception('mobile_scanner: Called start() while already starting.'); | ||
| 133 | - } | ||
| 134 | - isStarting = true; | ||
| 135 | - // setAnalyzeMode(AnalyzeMode.barcode.index); | 102 | + bool? _hasTorch; |
| 136 | 103 | ||
| 137 | - // Check authorization status | ||
| 138 | - if (!kIsWeb) { | ||
| 139 | - MobileScannerState state = MobileScannerState | ||
| 140 | - .values[await methodChannel.invokeMethod('state') as int? ?? 0]; | ||
| 141 | - switch (state) { | ||
| 142 | - case MobileScannerState.undetermined: | ||
| 143 | - final bool result = | ||
| 144 | - await methodChannel.invokeMethod('request') as bool? ?? false; | ||
| 145 | - state = result | ||
| 146 | - ? MobileScannerState.authorized | ||
| 147 | - : MobileScannerState.denied; | ||
| 148 | - break; | ||
| 149 | - case MobileScannerState.denied: | ||
| 150 | - isStarting = false; | ||
| 151 | - throw PlatformException(code: 'NO ACCESS'); | ||
| 152 | - case MobileScannerState.authorized: | ||
| 153 | - break; | ||
| 154 | - } | 104 | + /// Returns whether the device has a torch. |
| 105 | + /// | ||
| 106 | + /// Throws an error if the controller is not initialized. | ||
| 107 | + bool get hasTorch { | ||
| 108 | + if (_hasTorch == null) { | ||
| 109 | + throw const MobileScannerException( | ||
| 110 | + errorCode: MobileScannerErrorCode.controllerUninitialized, | ||
| 111 | + ); | ||
| 155 | } | 112 | } |
| 156 | 113 | ||
| 157 | - cameraFacingState.value = facing; | 114 | + return _hasTorch!; |
| 115 | + } | ||
| 116 | + | ||
| 117 | + /// Set the starting arguments for the camera | ||
| 118 | + Map<String, dynamic> _argumentsToMap({CameraFacing? cameraFacingOverride}) { | ||
| 119 | + final Map<String, dynamic> arguments = {}; | ||
| 120 | + | ||
| 121 | + cameraFacingState.value = cameraFacingOverride ?? facing; | ||
| 122 | + arguments['facing'] = cameraFacingState.value.index; | ||
| 123 | + arguments['torch'] = torchEnabled; | ||
| 124 | + arguments['speed'] = detectionSpeed.index; | ||
| 125 | + arguments['timeout'] = detectionTimeoutMs; | ||
| 158 | 126 | ||
| 159 | - // Set the starting arguments for the camera | ||
| 160 | - final Map arguments = {}; | ||
| 161 | - arguments['facing'] = facing.index; | ||
| 162 | /* if (scanWindow != null) { | 127 | /* if (scanWindow != null) { |
| 163 | arguments['scanWindow'] = [ | 128 | arguments['scanWindow'] = [ |
| 164 | scanWindow!.left, | 129 | scanWindow!.left, |
| @@ -167,8 +132,6 @@ class MobileScannerController { | @@ -167,8 +132,6 @@ class MobileScannerController { | ||
| 167 | scanWindow!.bottom, | 132 | scanWindow!.bottom, |
| 168 | ]; | 133 | ]; |
| 169 | } */ | 134 | } */ |
| 170 | - if (ratio != null) arguments['ratio'] = ratio; | ||
| 171 | - if (torchEnabled != null) arguments['torch'] = torchEnabled; | ||
| 172 | 135 | ||
| 173 | if (formats != null) { | 136 | if (formats != null) { |
| 174 | if (Platform.isAndroid) { | 137 | if (Platform.isAndroid) { |
| @@ -177,92 +140,145 @@ class MobileScannerController { | @@ -177,92 +140,145 @@ class MobileScannerController { | ||
| 177 | arguments['formats'] = formats!.map((e) => e.rawValue).toList(); | 140 | arguments['formats'] = formats!.map((e) => e.rawValue).toList(); |
| 178 | } | 141 | } |
| 179 | } | 142 | } |
| 143 | + arguments['returnImage'] = true; | ||
| 144 | + return arguments; | ||
| 145 | + } | ||
| 146 | + | ||
| 147 | + /// Start scanning for barcodes. | ||
| 148 | + /// Upon calling this method, the necessary camera permission will be requested. | ||
| 149 | + /// | ||
| 150 | + /// Returns an instance of [MobileScannerArguments] | ||
| 151 | + /// when the scanner was successfully started. | ||
| 152 | + /// Returns null if the scanner is currently starting. | ||
| 153 | + /// | ||
| 154 | + /// Throws a [MobileScannerException] if starting the scanner failed. | ||
| 155 | + Future<MobileScannerArguments?> start({ | ||
| 156 | + CameraFacing? cameraFacingOverride, | ||
| 157 | + }) async { | ||
| 158 | + if (isStarting) { | ||
| 159 | + debugPrint("Called start() while starting."); | ||
| 160 | + return null; | ||
| 161 | + } | ||
| 162 | + | ||
| 163 | + isStarting = true; | ||
| 164 | + | ||
| 165 | + // Check authorization status | ||
| 166 | + if (!kIsWeb) { | ||
| 167 | + final MobileScannerState state = MobileScannerState | ||
| 168 | + .values[await _methodChannel.invokeMethod('state') as int? ?? 0]; | ||
| 169 | + switch (state) { | ||
| 170 | + case MobileScannerState.undetermined: | ||
| 171 | + final bool result = | ||
| 172 | + await _methodChannel.invokeMethod('request') as bool? ?? false; | ||
| 173 | + if (!result) { | ||
| 174 | + isStarting = false; | ||
| 175 | + throw const MobileScannerException( | ||
| 176 | + errorCode: MobileScannerErrorCode.permissionDenied, | ||
| 177 | + ); | ||
| 178 | + } | ||
| 179 | + break; | ||
| 180 | + case MobileScannerState.denied: | ||
| 181 | + isStarting = false; | ||
| 182 | + throw const MobileScannerException( | ||
| 183 | + errorCode: MobileScannerErrorCode.permissionDenied, | ||
| 184 | + ); | ||
| 185 | + case MobileScannerState.authorized: | ||
| 186 | + break; | ||
| 187 | + } | ||
| 188 | + } | ||
| 180 | 189 | ||
| 181 | // Start the camera with arguments | 190 | // Start the camera with arguments |
| 182 | Map<String, dynamic>? startResult = {}; | 191 | Map<String, dynamic>? startResult = {}; |
| 183 | try { | 192 | try { |
| 184 | - startResult = await methodChannel.invokeMapMethod<String, dynamic>( | 193 | + startResult = await _methodChannel.invokeMapMethod<String, dynamic>( |
| 185 | 'start', | 194 | 'start', |
| 186 | - arguments, | 195 | + _argumentsToMap(cameraFacingOverride: cameraFacingOverride), |
| 187 | ); | 196 | ); |
| 188 | } on PlatformException catch (error) { | 197 | } on PlatformException catch (error) { |
| 189 | - debugPrint('${error.code}: ${error.message}'); | 198 | + MobileScannerErrorCode errorCode = MobileScannerErrorCode.genericError; |
| 199 | + | ||
| 200 | + if (error.code == "MobileScannerWeb") { | ||
| 201 | + errorCode = MobileScannerErrorCode.permissionDenied; | ||
| 202 | + } | ||
| 190 | isStarting = false; | 203 | isStarting = false; |
| 191 | - // setAnalyzeMode(AnalyzeMode.none.index); | ||
| 192 | - return; | 204 | + |
| 205 | + throw MobileScannerException( | ||
| 206 | + errorCode: errorCode, | ||
| 207 | + errorDetails: MobileScannerErrorDetails( | ||
| 208 | + code: error.code, | ||
| 209 | + details: error.details as Object?, | ||
| 210 | + message: error.message, | ||
| 211 | + ), | ||
| 212 | + ); | ||
| 193 | } | 213 | } |
| 194 | 214 | ||
| 195 | if (startResult == null) { | 215 | if (startResult == null) { |
| 196 | isStarting = false; | 216 | isStarting = false; |
| 197 | - throw PlatformException(code: 'INITIALIZATION ERROR'); | 217 | + throw const MobileScannerException( |
| 218 | + errorCode: MobileScannerErrorCode.genericError, | ||
| 219 | + ); | ||
| 198 | } | 220 | } |
| 199 | 221 | ||
| 200 | - hasTorch = startResult['torchable'] as bool? ?? false; | ||
| 201 | - | ||
| 202 | - if (kIsWeb) { | ||
| 203 | - args.value = MobileScannerArguments( | ||
| 204 | - webId: startResult['ViewID'] as String?, | ||
| 205 | - size: Size( | ||
| 206 | - startResult['videoWidth'] as double? ?? 0, | ||
| 207 | - startResult['videoHeight'] as double? ?? 0, | ||
| 208 | - ), | ||
| 209 | - hasTorch: hasTorch, | ||
| 210 | - ); | ||
| 211 | - } else { | ||
| 212 | - args.value = MobileScannerArguments( | ||
| 213 | - textureId: startResult['textureId'] as int?, | ||
| 214 | - size: toSize(startResult['size'] as Map? ?? {}), | ||
| 215 | - hasTorch: hasTorch, | ||
| 216 | - ); | 222 | + _hasTorch = startResult['torchable'] as bool? ?? false; |
| 223 | + if (_hasTorch! && torchEnabled) { | ||
| 224 | + torchState.value = TorchState.on; | ||
| 217 | } | 225 | } |
| 218 | 226 | ||
| 219 | isStarting = false; | 227 | isStarting = false; |
| 228 | + return startArguments.value = MobileScannerArguments( | ||
| 229 | + size: kIsWeb | ||
| 230 | + ? Size( | ||
| 231 | + startResult['videoWidth'] as double? ?? 0, | ||
| 232 | + startResult['videoHeight'] as double? ?? 0, | ||
| 233 | + ) | ||
| 234 | + : toSize(startResult['size'] as Map? ?? {}), | ||
| 235 | + hasTorch: _hasTorch!, | ||
| 236 | + textureId: kIsWeb ? null : startResult['textureId'] as int?, | ||
| 237 | + webId: kIsWeb ? startResult['ViewID'] as String? : null, | ||
| 238 | + ); | ||
| 220 | } | 239 | } |
| 221 | 240 | ||
| 241 | + /// Stops the camera, but does not dispose this controller. | ||
| 222 | Future<void> stop() async { | 242 | Future<void> stop() async { |
| 223 | try { | 243 | try { |
| 224 | - await methodChannel.invokeMethod('stop'); | ||
| 225 | - } on PlatformException catch (error) { | ||
| 226 | - debugPrint('${error.code}: ${error.message}'); | 244 | + await _methodChannel.invokeMethod('stop'); |
| 245 | + } catch (e) { | ||
| 246 | + debugPrint('$e'); | ||
| 227 | } | 247 | } |
| 228 | } | 248 | } |
| 229 | 249 | ||
| 230 | /// Switches the torch on or off. | 250 | /// Switches the torch on or off. |
| 231 | /// | 251 | /// |
| 232 | - /// Only works if torch is available. | 252 | + /// Does nothing if the device has no torch. |
| 253 | + /// | ||
| 254 | + /// Throws if the controller was not initialized. | ||
| 233 | Future<void> toggleTorch() async { | 255 | Future<void> toggleTorch() async { |
| 234 | - ensure('toggleTorch'); | ||
| 235 | - if (!hasTorch) { | ||
| 236 | - debugPrint('Device has no torch/flash.'); | 256 | + final hasTorch = _hasTorch; |
| 257 | + | ||
| 258 | + if (hasTorch == null) { | ||
| 259 | + throw const MobileScannerException( | ||
| 260 | + errorCode: MobileScannerErrorCode.controllerUninitialized, | ||
| 261 | + ); | ||
| 262 | + } else if (!hasTorch) { | ||
| 237 | return; | 263 | return; |
| 238 | } | 264 | } |
| 239 | 265 | ||
| 240 | - final TorchState state = | 266 | + torchState.value = |
| 241 | torchState.value == TorchState.off ? TorchState.on : TorchState.off; | 267 | torchState.value == TorchState.off ? TorchState.on : TorchState.off; |
| 242 | 268 | ||
| 243 | - try { | ||
| 244 | - await methodChannel.invokeMethod('torch', state.index); | ||
| 245 | - } on PlatformException catch (error) { | ||
| 246 | - debugPrint('${error.code}: ${error.message}'); | ||
| 247 | - } | 269 | + await _methodChannel.invokeMethod('torch', torchState.value.index); |
| 248 | } | 270 | } |
| 249 | 271 | ||
| 250 | /// Switches the torch on or off. | 272 | /// Switches the torch on or off. |
| 251 | /// | 273 | /// |
| 252 | /// Only works if torch is available. | 274 | /// Only works if torch is available. |
| 253 | Future<void> switchCamera() async { | 275 | Future<void> switchCamera() async { |
| 254 | - ensure('switchCamera'); | ||
| 255 | - try { | ||
| 256 | - await methodChannel.invokeMethod('stop'); | ||
| 257 | - } on PlatformException catch (error) { | ||
| 258 | - debugPrint( | ||
| 259 | - '${error.code}: camera is stopped! Please start before switching camera.', | ||
| 260 | - ); | ||
| 261 | - return; | ||
| 262 | - } | ||
| 263 | - facing = | ||
| 264 | - facing == CameraFacing.back ? CameraFacing.front : CameraFacing.back; | ||
| 265 | - await start(); | 276 | + await _methodChannel.invokeMethod('stop'); |
| 277 | + final CameraFacing facingToUse = | ||
| 278 | + cameraFacingState.value == CameraFacing.back | ||
| 279 | + ? CameraFacing.front | ||
| 280 | + : CameraFacing.back; | ||
| 281 | + await start(cameraFacingOverride: facingToUse); | ||
| 266 | } | 282 | } |
| 267 | 283 | ||
| 268 | /// Handles a local image file. | 284 | /// Handles a local image file. |
| @@ -271,28 +287,77 @@ class MobileScannerController { | @@ -271,28 +287,77 @@ class MobileScannerController { | ||
| 271 | /// | 287 | /// |
| 272 | /// [path] The path of the image on the devices | 288 | /// [path] The path of the image on the devices |
| 273 | Future<bool> analyzeImage(String path) async { | 289 | Future<bool> analyzeImage(String path) async { |
| 274 | - return methodChannel | 290 | + return _methodChannel |
| 275 | .invokeMethod<bool>('analyzeImage', path) | 291 | .invokeMethod<bool>('analyzeImage', path) |
| 276 | .then<bool>((bool? value) => value ?? false); | 292 | .then<bool>((bool? value) => value ?? false); |
| 277 | } | 293 | } |
| 278 | 294 | ||
| 279 | /// Disposes the MobileScannerController and closes all listeners. | 295 | /// Disposes the MobileScannerController and closes all listeners. |
| 296 | + /// | ||
| 297 | + /// If you call this, you cannot use this controller object anymore. | ||
| 280 | void dispose() { | 298 | void dispose() { |
| 281 | - if (hashCode == _controllerHashcode) { | ||
| 282 | - stop(); | ||
| 283 | - events?.cancel(); | ||
| 284 | - events = null; | ||
| 285 | - _controllerHashcode = null; | 299 | + stop(); |
| 300 | + events.cancel(); | ||
| 301 | + _barcodesController.close(); | ||
| 302 | + if (hashCode == controllerHashcode) { | ||
| 303 | + controllerHashcode = null; | ||
| 286 | } | 304 | } |
| 287 | - barcodesController.close(); | ||
| 288 | } | 305 | } |
| 289 | 306 | ||
| 290 | - /// Checks if the MobileScannerController is bound to the correct MobileScanner object. | ||
| 291 | - void ensure(String name) { | ||
| 292 | - final message = | ||
| 293 | - 'MobileScannerController.$name called after MobileScannerController.dispose\n' | ||
| 294 | - 'MobileScannerController methods should not be used after calling dispose.'; | ||
| 295 | - assert(hashCode == _controllerHashcode, message); | 307 | + /// Handles a returning event from the platform side |
| 308 | + void _handleEvent(Map event) { | ||
| 309 | + final name = event['name']; | ||
| 310 | + final data = event['data']; | ||
| 311 | + | ||
| 312 | + switch (name) { | ||
| 313 | + case 'torchState': | ||
| 314 | + final state = TorchState.values[data as int? ?? 0]; | ||
| 315 | + torchState.value = state; | ||
| 316 | + break; | ||
| 317 | + case 'barcode': | ||
| 318 | + if (data == null) return; | ||
| 319 | + final parsed = (data as List) | ||
| 320 | + .map((value) => Barcode.fromNative(value as Map)) | ||
| 321 | + .toList(); | ||
| 322 | + _barcodesController.add( | ||
| 323 | + BarcodeCapture( | ||
| 324 | + barcodes: parsed, | ||
| 325 | + image: event['image'] as Uint8List?, | ||
| 326 | + ), | ||
| 327 | + ); | ||
| 328 | + break; | ||
| 329 | + case 'barcodeMac': | ||
| 330 | + _barcodesController.add( | ||
| 331 | + BarcodeCapture( | ||
| 332 | + barcodes: [ | ||
| 333 | + Barcode( | ||
| 334 | + rawValue: (data as Map)['payload'] as String?, | ||
| 335 | + ) | ||
| 336 | + ], | ||
| 337 | + ), | ||
| 338 | + ); | ||
| 339 | + break; | ||
| 340 | + case 'barcodeWeb': | ||
| 341 | + final barcode = data as Map?; | ||
| 342 | + _barcodesController.add( | ||
| 343 | + BarcodeCapture( | ||
| 344 | + barcodes: [ | ||
| 345 | + Barcode( | ||
| 346 | + rawValue: barcode?['rawValue'] as String?, | ||
| 347 | + rawBytes: barcode?['rawBytes'] as Uint8List?, | ||
| 348 | + ) | ||
| 349 | + ], | ||
| 350 | + ), | ||
| 351 | + ); | ||
| 352 | + break; | ||
| 353 | + case 'error': | ||
| 354 | + throw MobileScannerException( | ||
| 355 | + errorCode: MobileScannerErrorCode.genericError, | ||
| 356 | + errorDetails: MobileScannerErrorDetails(message: data as String?), | ||
| 357 | + ); | ||
| 358 | + default: | ||
| 359 | + throw UnimplementedError(name as String?); | ||
| 360 | + } | ||
| 296 | } | 361 | } |
| 297 | 362 | ||
| 298 | /// updates the native scanwindow | 363 | /// updates the native scanwindow |
lib/src/mobile_scanner_exception.dart
0 → 100644
| 1 | +import 'package:mobile_scanner/src/enums/mobile_scanner_error_code.dart'; | ||
| 2 | + | ||
| 3 | +/// This class represents an exception thrown by the mobile scanner. | ||
| 4 | +class MobileScannerException implements Exception { | ||
| 5 | + const MobileScannerException({ | ||
| 6 | + required this.errorCode, | ||
| 7 | + this.errorDetails, | ||
| 8 | + }); | ||
| 9 | + | ||
| 10 | + /// The error code of the exception. | ||
| 11 | + final MobileScannerErrorCode errorCode; | ||
| 12 | + | ||
| 13 | + /// The additional error details that came with the [errorCode]. | ||
| 14 | + final MobileScannerErrorDetails? errorDetails; | ||
| 15 | + | ||
| 16 | + @override | ||
| 17 | + String toString() { | ||
| 18 | + if (errorDetails != null && errorDetails?.message != null) { | ||
| 19 | + return "MobileScannerException: code ${errorCode.name}, message: ${errorDetails?.message}"; | ||
| 20 | + } | ||
| 21 | + return "MobileScannerException: ${errorCode.name}"; | ||
| 22 | + } | ||
| 23 | +} | ||
| 24 | + | ||
| 25 | +/// The raw error details for a [MobileScannerException]. | ||
| 26 | +class MobileScannerErrorDetails { | ||
| 27 | + const MobileScannerErrorDetails({ | ||
| 28 | + this.code, | ||
| 29 | + this.details, | ||
| 30 | + this.message, | ||
| 31 | + }); | ||
| 32 | + | ||
| 33 | + /// The error code from the [PlatformException]. | ||
| 34 | + final String? code; | ||
| 35 | + | ||
| 36 | + /// The details from the [PlatformException]. | ||
| 37 | + final Object? details; | ||
| 38 | + | ||
| 39 | + /// The error message from the [PlatformException]. | ||
| 40 | + final String? message; | ||
| 41 | +} |
| 1 | import 'dart:typed_data'; | 1 | import 'dart:typed_data'; |
| 2 | import 'dart:ui'; | 2 | import 'dart:ui'; |
| 3 | 3 | ||
| 4 | -import 'package:mobile_scanner/src/objects/barcode_utility.dart'; | 4 | +import 'package:mobile_scanner/src/barcode_utility.dart'; |
| 5 | 5 | ||
| 6 | /// Represents a single recognized barcode and its value. | 6 | /// Represents a single recognized barcode and its value. |
| 7 | class Barcode { | 7 | class Barcode { |
lib/src/objects/barcode_capture.dart
0 → 100644
| 1 | +import 'dart:typed_data'; | ||
| 2 | + | ||
| 3 | +import 'package:mobile_scanner/src/objects/barcode.dart'; | ||
| 4 | + | ||
| 5 | +/// The return object after a frame is scanned. | ||
| 6 | +/// | ||
| 7 | +/// [barcodes] A list with barcodes. A scanned frame can contain multiple | ||
| 8 | +/// barcodes. | ||
| 9 | +/// [image] If enabled, an image of the scanned frame. | ||
| 10 | +class BarcodeCapture { | ||
| 11 | + final List<Barcode> barcodes; | ||
| 12 | + | ||
| 13 | + final Uint8List? image; | ||
| 14 | + | ||
| 15 | + BarcodeCapture({ | ||
| 16 | + required this.barcodes, | ||
| 17 | + this.image, | ||
| 18 | + }); | ||
| 19 | +} |
| 1 | import 'package:flutter/material.dart'; | 1 | import 'package:flutter/material.dart'; |
| 2 | 2 | ||
| 3 | -/// Camera args for [CameraView]. | 3 | +/// The start arguments of the scanner. |
| 4 | class MobileScannerArguments { | 4 | class MobileScannerArguments { |
| 5 | - /// The texture id. | ||
| 6 | - final int? textureId; | ||
| 7 | - | ||
| 8 | - /// Size of the texture. | 5 | + /// The output size of the camera. |
| 6 | + /// This value can be used to draw a box in the image. | ||
| 9 | final Size size; | 7 | final Size size; |
| 10 | 8 | ||
| 9 | + /// A bool which is true if the device has a torch. | ||
| 11 | final bool hasTorch; | 10 | final bool hasTorch; |
| 12 | 11 | ||
| 12 | + /// The texture id of the capture used internally. | ||
| 13 | + final int? textureId; | ||
| 14 | + | ||
| 15 | + /// The texture id of the capture used internally if device is web. | ||
| 13 | final String? webId; | 16 | final String? webId; |
| 14 | 17 | ||
| 15 | - /// Create a [MobileScannerArguments]. | ||
| 16 | MobileScannerArguments({ | 18 | MobileScannerArguments({ |
| 17 | - this.textureId, | ||
| 18 | required this.size, | 19 | required this.size, |
| 19 | required this.hasTorch, | 20 | required this.hasTorch, |
| 21 | + this.textureId, | ||
| 20 | this.webId, | 22 | this.webId, |
| 21 | }); | 23 | }); |
| 22 | } | 24 | } |
lib/src/web/base.dart
0 → 100644
| 1 | +import 'dart:html'; | ||
| 2 | + | ||
| 3 | +import 'package:flutter/material.dart'; | ||
| 4 | +import 'package:mobile_scanner/src/enums/camera_facing.dart'; | ||
| 5 | +import 'package:mobile_scanner/src/objects/barcode.dart'; | ||
| 6 | +import 'package:mobile_scanner/src/web/media.dart'; | ||
| 7 | + | ||
| 8 | +abstract class WebBarcodeReaderBase { | ||
| 9 | + /// Timer used to capture frames to be analyzed | ||
| 10 | + final Duration frameInterval; | ||
| 11 | + final DivElement videoContainer; | ||
| 12 | + | ||
| 13 | + const WebBarcodeReaderBase({ | ||
| 14 | + required this.videoContainer, | ||
| 15 | + this.frameInterval = const Duration(milliseconds: 200), | ||
| 16 | + }); | ||
| 17 | + | ||
| 18 | + bool get isStarted; | ||
| 19 | + | ||
| 20 | + int get videoWidth; | ||
| 21 | + int get videoHeight; | ||
| 22 | + | ||
| 23 | + /// Starts streaming video | ||
| 24 | + Future<void> start({ | ||
| 25 | + required CameraFacing cameraFacing, | ||
| 26 | + }); | ||
| 27 | + | ||
| 28 | + /// Starts scanning QR codes or barcodes | ||
| 29 | + Stream<Barcode?> detectBarcodeContinuously(); | ||
| 30 | + | ||
| 31 | + /// Stops streaming video | ||
| 32 | + Future<void> stop(); | ||
| 33 | + | ||
| 34 | + /// Can enable or disable the flash if available | ||
| 35 | + Future<void> toggleTorch({required bool enabled}); | ||
| 36 | + | ||
| 37 | + /// Determine whether device has flash | ||
| 38 | + bool get hasTorch; | ||
| 39 | +} | ||
| 40 | + | ||
| 41 | +mixin InternalStreamCreation on WebBarcodeReaderBase { | ||
| 42 | + /// The video stream. | ||
| 43 | + /// Will be initialized later to see which camera needs to be used. | ||
| 44 | + MediaStream? localMediaStream; | ||
| 45 | + final VideoElement video = VideoElement(); | ||
| 46 | + | ||
| 47 | + @override | ||
| 48 | + int get videoWidth => video.videoWidth; | ||
| 49 | + @override | ||
| 50 | + int get videoHeight => video.videoHeight; | ||
| 51 | + | ||
| 52 | + Future<MediaStream?> initMediaStream(CameraFacing cameraFacing) async { | ||
| 53 | + // Check if browser supports multiple camera's and set if supported | ||
| 54 | + final Map? capabilities = | ||
| 55 | + window.navigator.mediaDevices?.getSupportedConstraints(); | ||
| 56 | + final Map<String, dynamic> constraints; | ||
| 57 | + if (capabilities != null && capabilities['facingMode'] as bool) { | ||
| 58 | + constraints = { | ||
| 59 | + 'video': VideoOptions( | ||
| 60 | + facingMode: | ||
| 61 | + cameraFacing == CameraFacing.front ? 'user' : 'environment', | ||
| 62 | + ) | ||
| 63 | + }; | ||
| 64 | + } else { | ||
| 65 | + constraints = {'video': true}; | ||
| 66 | + } | ||
| 67 | + final stream = | ||
| 68 | + await window.navigator.mediaDevices?.getUserMedia(constraints); | ||
| 69 | + return stream; | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + void prepareVideoElement(VideoElement videoSource); | ||
| 73 | + | ||
| 74 | + Future<void> attachStreamToVideo( | ||
| 75 | + MediaStream stream, | ||
| 76 | + VideoElement videoSource, | ||
| 77 | + ); | ||
| 78 | + | ||
| 79 | + @override | ||
| 80 | + Future<void> stop() async { | ||
| 81 | + try { | ||
| 82 | + // Stop the camera stream | ||
| 83 | + localMediaStream?.getTracks().forEach((track) { | ||
| 84 | + if (track.readyState == 'live') { | ||
| 85 | + track.stop(); | ||
| 86 | + } | ||
| 87 | + }); | ||
| 88 | + } catch (e) { | ||
| 89 | + debugPrint('Failed to stop stream: $e'); | ||
| 90 | + } | ||
| 91 | + video.srcObject = null; | ||
| 92 | + localMediaStream = null; | ||
| 93 | + videoContainer.children = []; | ||
| 94 | + } | ||
| 95 | +} | ||
| 96 | + | ||
| 97 | +/// Mixin for libraries that don't have built-in torch support | ||
| 98 | +mixin InternalTorchDetection on InternalStreamCreation { | ||
| 99 | + @override | ||
| 100 | + bool get hasTorch { | ||
| 101 | + // TODO: fix flash light. See https://github.com/dart-lang/sdk/issues/48533 | ||
| 102 | + // final track = _localStream?.getVideoTracks(); | ||
| 103 | + // if (track != null) { | ||
| 104 | + // final imageCapture = html.ImageCapture(track.first); | ||
| 105 | + // final photoCapabilities = await imageCapture.getPhotoCapabilities(); | ||
| 106 | + // } | ||
| 107 | + return false; | ||
| 108 | + } | ||
| 109 | + | ||
| 110 | + @override | ||
| 111 | + Future<void> toggleTorch({required bool enabled}) async { | ||
| 112 | + if (hasTorch) { | ||
| 113 | + final track = localMediaStream?.getVideoTracks(); | ||
| 114 | + await track?.first.applyConstraints({ | ||
| 115 | + 'advanced': [ | ||
| 116 | + {'torch': enabled} | ||
| 117 | + ] | ||
| 118 | + }); | ||
| 119 | + } | ||
| 120 | + } | ||
| 121 | +} |
| 1 | @JS() | 1 | @JS() |
| 2 | library jsqr; | 2 | library jsqr; |
| 3 | 3 | ||
| 4 | +import 'dart:async'; | ||
| 5 | +import 'dart:html'; | ||
| 6 | +import 'dart:typed_data'; | ||
| 7 | + | ||
| 4 | import 'package:js/js.dart'; | 8 | import 'package:js/js.dart'; |
| 9 | +import 'package:mobile_scanner/src/enums/camera_facing.dart'; | ||
| 10 | +import 'package:mobile_scanner/src/objects/barcode.dart'; | ||
| 11 | +import 'package:mobile_scanner/src/web/base.dart'; | ||
| 5 | 12 | ||
| 6 | @JS('jsQR') | 13 | @JS('jsQR') |
| 7 | external Code? jsQR(dynamic data, int? width, int? height); | 14 | external Code? jsQR(dynamic data, int? width, int? height); |
| @@ -9,4 +16,75 @@ external Code? jsQR(dynamic data, int? width, int? height); | @@ -9,4 +16,75 @@ external Code? jsQR(dynamic data, int? width, int? height); | ||
| 9 | @JS() | 16 | @JS() |
| 10 | class Code { | 17 | class Code { |
| 11 | external String get data; | 18 | external String get data; |
| 19 | + | ||
| 20 | + external Uint8ClampedList get binaryData; | ||
| 21 | +} | ||
| 22 | + | ||
| 23 | +class JsQrCodeReader extends WebBarcodeReaderBase | ||
| 24 | + with InternalStreamCreation, InternalTorchDetection { | ||
| 25 | + JsQrCodeReader({required super.videoContainer}); | ||
| 26 | + | ||
| 27 | + @override | ||
| 28 | + bool get isStarted => localMediaStream != null; | ||
| 29 | + | ||
| 30 | + @override | ||
| 31 | + Future<void> start({ | ||
| 32 | + required CameraFacing cameraFacing, | ||
| 33 | + }) async { | ||
| 34 | + videoContainer.children = [video]; | ||
| 35 | + | ||
| 36 | + final stream = await initMediaStream(cameraFacing); | ||
| 37 | + | ||
| 38 | + prepareVideoElement(video); | ||
| 39 | + if (stream != null) { | ||
| 40 | + await attachStreamToVideo(stream, video); | ||
| 41 | + } | ||
| 42 | + } | ||
| 43 | + | ||
| 44 | + @override | ||
| 45 | + void prepareVideoElement(VideoElement videoSource) { | ||
| 46 | + // required to tell iOS safari we don't want fullscreen | ||
| 47 | + videoSource.setAttribute('playsinline', 'true'); | ||
| 48 | + } | ||
| 49 | + | ||
| 50 | + @override | ||
| 51 | + Future<void> attachStreamToVideo( | ||
| 52 | + MediaStream stream, | ||
| 53 | + VideoElement videoSource, | ||
| 54 | + ) async { | ||
| 55 | + localMediaStream = stream; | ||
| 56 | + videoSource.srcObject = stream; | ||
| 57 | + await videoSource.play(); | ||
| 58 | + } | ||
| 59 | + | ||
| 60 | + @override | ||
| 61 | + Stream<Barcode?> detectBarcodeContinuously() async* { | ||
| 62 | + yield* Stream.periodic(frameInterval, (_) { | ||
| 63 | + return _captureFrame(video); | ||
| 64 | + }).asyncMap((event) async { | ||
| 65 | + final code = await event; | ||
| 66 | + if (code == null) { | ||
| 67 | + return null; | ||
| 68 | + } | ||
| 69 | + return Barcode( | ||
| 70 | + rawValue: code.data, | ||
| 71 | + rawBytes: Uint8List.fromList(code.binaryData), | ||
| 72 | + format: BarcodeFormat.qrCode, | ||
| 73 | + ); | ||
| 74 | + }); | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | + /// Captures a frame and analyzes it for QR codes | ||
| 78 | + Future<Code?> _captureFrame(VideoElement video) async { | ||
| 79 | + if (localMediaStream == null) return null; | ||
| 80 | + final canvas = | ||
| 81 | + CanvasElement(width: video.videoWidth, height: video.videoHeight); | ||
| 82 | + final ctx = canvas.context2D; | ||
| 83 | + | ||
| 84 | + ctx.drawImage(video, 0, 0); | ||
| 85 | + final imgData = ctx.getImageData(0, 0, canvas.width!, canvas.height!); | ||
| 86 | + | ||
| 87 | + final code = jsQR(imgData.data, canvas.width, canvas.height); | ||
| 88 | + return code; | ||
| 89 | + } | ||
| 12 | } | 90 | } |
-
Please register or login to post a comment