├── ios ├── Assets │ └── .gitkeep ├── Classes │ ├── MLKitScanPlugin.h │ ├── MLKitScanPlugin.m │ ├── SwiftMLKitScanPlugin.swift │ └── Scan │ │ ├── TextRule.swift │ │ ├── ScanState.swift │ │ ├── ScanViewFactory.swift │ │ ├── Orientation.swift │ │ ├── ImageParser.swift │ │ ├── ChannelManager.swift │ │ └── ScanView.swift ├── .gitignore └── mlkit_scan_plugin.podspec ├── CODEOWNERS ├── CHANGELOG.md ├── android ├── settings.gradle ├── gradle.properties ├── .gitignore ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ └── com │ │ └── alexv525 │ │ └── mlkit_scan_plugin │ │ ├── PluginStreamHandler.kt │ │ ├── ScanFactory.kt │ │ ├── vision │ │ ├── processor │ │ │ ├── VisionImageProcessor.kt │ │ │ ├── TextRecognitionProcessor.kt │ │ │ ├── BarcodeScannerProcessor.kt │ │ │ └── VisionProcessorBase.kt │ │ ├── FrameMetadata.kt │ │ └── ScopedExecutor.kt │ │ ├── decode │ │ ├── PreviewCallback.kt │ │ ├── ScanResult.kt │ │ └── Decoder.kt │ │ ├── Shared.kt │ │ ├── Constant.kt │ │ ├── camera │ │ ├── AutoFitTextureView.kt │ │ ├── CameraConfigurationUtils.kt │ │ └── CameraConfigurationManager.kt │ │ ├── Extension.kt │ │ └── ScanView.kt └── build.gradle ├── README.md ├── example ├── ios │ ├── Runner │ │ ├── Runner-Bridging-Header.h │ │ ├── Assets.xcassets │ │ │ ├── LaunchImage.imageset │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ ├── README.md │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ └── Contents.json │ │ ├── AppDelegate.swift │ │ ├── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ │ └── Info.plist │ ├── Flutter │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── AppFrameworkInfo.plist │ ├── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── .gitignore │ └── Podfile ├── android │ ├── gradle.properties │ ├── app │ │ ├── src │ │ │ └── main │ │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ └── values │ │ │ │ │ └── styles.xml │ │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── alexv525 │ │ │ │ │ └── mlkit_scan_plugin_example │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── .gitignore │ ├── settings.gradle │ └── build.gradle ├── .metadata ├── pubspec.yaml ├── .gitignore └── lib │ └── main.dart ├── assets ├── torch.png ├── scan_line.png ├── torch_active.png ├── notification_success_icon.png └── notification_warning_icon.png ├── .gitignore ├── .metadata ├── lib ├── src │ ├── plugin │ │ ├── constants.dart │ │ ├── log_util.dart │ │ ├── enums.dart │ │ ├── scan_result.dart │ │ └── scan_plugin.dart │ ├── resources.dart │ ├── mixins │ │ ├── focus_listener_mixin.dart │ │ ├── lifecycle_mixin.dart │ │ ├── flashlight_mixin.dart │ │ └── notification_mixin.dart │ └── widgets │ │ ├── scan_rect_painter.dart │ │ ├── scan_view.dart │ │ └── scan_rect_widget.dart └── mlkit_scan_plugin.dart ├── pubspec.yaml ├── flutter_mlkit_scan_plugin.iml ├── analysis_options.yaml └── LICENSE /ios/Assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @AlexV525 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 2 | 3 | - Initial release. 4 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'mlkit_scan_plugin' 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mlkit_scan_plugin 2 | 3 | A new flutter plugin project. 4 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /assets/torch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_mlkit_scan_plugin/HEAD/assets/torch.png -------------------------------------------------------------------------------- /assets/scan_line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_mlkit_scan_plugin/HEAD/assets/scan_line.png -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /assets/torch_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_mlkit_scan_plugin/HEAD/assets/torch_active.png -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /ios/Classes/MLKitScanPlugin.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface MLKitScanPlugin : NSObject 4 | @end 5 | -------------------------------------------------------------------------------- /assets/notification_success_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_mlkit_scan_plugin/HEAD/assets/notification_success_icon.png -------------------------------------------------------------------------------- /assets/notification_warning_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_mlkit_scan_plugin/HEAD/assets/notification_warning_icon.png -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | 10 | .cxx/ 11 | build/ -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_mlkit_scan_plugin/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_mlkit_scan_plugin/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_mlkit_scan_plugin/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_mlkit_scan_plugin/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_mlkit_scan_plugin/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | .idea/ 4 | .iml 5 | 6 | .packages 7 | .pub/ 8 | 9 | build/ 10 | 11 | .env 12 | pubspec.lock 13 | 14 | .vscode/settings.json 15 | .vscode/launch.json 16 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_mlkit_scan_plugin/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_mlkit_scan_plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_mlkit_scan_plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_mlkit_scan_plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_mlkit_scan_plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_mlkit_scan_plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_mlkit_scan_plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_mlkit_scan_plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_mlkit_scan_plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_mlkit_scan_plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_mlkit_scan_plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_mlkit_scan_plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_mlkit_scan_plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_mlkit_scan_plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_mlkit_scan_plugin/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_mlkit_scan_plugin/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_mlkit_scan_plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fluttercandies/flutter_mlkit_scan_plugin/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/alexv525/mlkit_scan_plugin_example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.alexv525.mlkit_scan_plugin_example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip 6 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip 7 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 1aafb3a8b9b0c36241c5f5b34ee914770f015818 8 | channel: stable 9 | 10 | project_type: plugin 11 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 1aafb3a8b9b0c36241c5f5b34ee914770f015818 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | .cxx/ 10 | build/ 11 | 12 | # Remember to never publicly share your keystore. 13 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 14 | key.properties 15 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /lib/src/plugin/constants.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// [Author] Alex (https://github.com/AlexV525) 3 | /// [Date] 2021/8/20 14:08 4 | /// 5 | const String ScanPluginPackage = 'mlkit_scan_plugin'; 6 | 7 | /// 扫描取景区域的高度 8 | const double SCAN_RECT_HEIGHT_CODE_OR_MOBILE = 100; // 扫描单号或手机号 9 | const double SCAN_RECT_HEIGHT_FULL = 400; // 扫描整个面单 10 | 11 | const Duration SCAN_INTERVAL = Duration(milliseconds: 500); // 每次扫描的间隔 12 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: mlkit_scan_plugin_example 2 | description: Demonstrates how to use the mlkit_scan_plugin plugin. 3 | 4 | publish_to: 'none' 5 | 6 | environment: 7 | sdk: ">=2.17.0 <3.0.0" 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | 13 | image_picker: ^0.8.5+3 14 | mlkit_scan_plugin: 15 | path: ../ 16 | 17 | permission_handler: ^10.0.0 18 | 19 | flutter: 20 | uses-material-design: true 21 | 22 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | .generated/ 16 | 17 | *.pbxuser 18 | *.mode1v3 19 | *.mode2v3 20 | *.perspectivev3 21 | 22 | !default.pbxuser 23 | !default.mode1v3 24 | !default.mode2v3 25 | !default.perspectivev3 26 | 27 | xcuserdata 28 | 29 | *.moved-aside 30 | 31 | *.pyc 32 | *sync/ 33 | Icon? 34 | .tags* 35 | 36 | /Flutter/Generated.xcconfig 37 | /Flutter/flutter_export_environment.sh -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: mlkit_scan_plugin 2 | description: The MLKit scan plugin for Flutter. 3 | version: 1.0.0 4 | homepage: https://github.com/AlexV525/flutter_mlkit_scan_plugin 5 | 6 | environment: 7 | sdk: ">=2.17.0 <3.0.0" 8 | flutter: ">=2.0.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | flutter: 15 | plugin: 16 | platforms: 17 | android: 18 | package: com.alexv525.mlkit_scan_plugin 19 | pluginClass: MLKitScanPlugin 20 | ios: 21 | pluginClass: MLKitScanPlugin 22 | assets: 23 | - assets/ 24 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/alexv525/mlkit_scan_plugin/PluginStreamHandler.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Alex (https://github.com/AlexV525) 3 | * Date: 1/18/21 8:49 PM 4 | */ 5 | 6 | package com.alexv525.mlkit_scan_plugin 7 | 8 | import io.flutter.plugin.common.EventChannel 9 | 10 | class PluginStreamHandler : EventChannel.StreamHandler { 11 | override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { 12 | Shared.eventSink = events 13 | } 14 | 15 | override fun onCancel(arguments: Any?) { 16 | Shared.eventSink = null 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/mlkit_scan_plugin.dart: -------------------------------------------------------------------------------- 1 | library mlkit_scan_plugin; 2 | 3 | export 'src/mixins/flashlight_mixin.dart'; 4 | export 'src/mixins/focus_listener_mixin.dart'; 5 | export 'src/mixins/lifecycle_mixin.dart'; 6 | export 'src/mixins/notification_mixin.dart'; 7 | 8 | export 'src/plugin/constants.dart'; 9 | export 'src/plugin/enums.dart'; 10 | export 'src/plugin/scan_plugin.dart'; 11 | export 'src/plugin/scan_result.dart'; 12 | 13 | export 'src/resources.dart'; 14 | 15 | export 'src/widgets/scan_rect_painter.dart'; 16 | export 'src/widgets/scan_rect_widget.dart'; 17 | export 'src/widgets/scan_view.dart'; 18 | -------------------------------------------------------------------------------- /ios/Classes/MLKitScanPlugin.m: -------------------------------------------------------------------------------- 1 | #import "MLKitScanPlugin.h" 2 | #if __has_include() 3 | #import 4 | #else 5 | // Support project import fallback if the generated compatibility header 6 | // is not copied when this plugin is created as a library. 7 | // https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 8 | #import "mlkit_scan_plugin-Swift.h" 9 | #endif 10 | 11 | @implementation MLKitScanPlugin 12 | + (void)registerWithRegistrar:(NSObject*)registrar { 13 | [SwiftMLKitScanPlugin registerWithRegistrar:registrar]; 14 | } 15 | @end 16 | -------------------------------------------------------------------------------- /lib/src/resources.dart: -------------------------------------------------------------------------------- 1 | /// Generate by [resource_generator](https://github.com/CaiJingLong/flutter_resource_generator) library. 2 | /// PLEASE DO NOT EDIT MANUALLY. 3 | class ScanPluginR { 4 | const ScanPluginR._(); 5 | 6 | static const String ASSETS_NOTIFICATION_SUCCESS_ICON_PNG = 7 | 'assets/notification_success_icon.png'; 8 | 9 | static const String ASSETS_NOTIFICATION_WARNING_ICON_PNG = 10 | 'assets/notification_warning_icon.png'; 11 | 12 | static const String ASSETS_SCAN_LINE_PNG = 'assets/scan_line.png'; 13 | 14 | static const String ASSETS_TORCH_PNG = 'assets/torch.png'; 15 | 16 | static const String ASSETS_TORCH_ACTIVE_PNG = 'assets/torch_active.png'; 17 | } 18 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | Podfile.lock 28 | 29 | # Exceptions to above rules. 30 | !default.mode1v3 31 | !default.mode2v3 32 | !default.pbxuser 33 | !default.perspectivev3 34 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.2.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/alexv525/mlkit_scan_plugin/ScanFactory.kt: -------------------------------------------------------------------------------- 1 | package com.alexv525.mlkit_scan_plugin 2 | 3 | import android.content.Context 4 | import android.util.Size 5 | import io.flutter.plugin.common.StandardMessageCodec 6 | import io.flutter.plugin.platform.PlatformView 7 | import io.flutter.plugin.platform.PlatformViewFactory 8 | 9 | class ScanFactory( 10 | private val plugin: MLKitScanPlugin 11 | ) : PlatformViewFactory(StandardMessageCodec.INSTANCE) { 12 | var view: ScanView? = null 13 | 14 | @Suppress("UNCHECKED_CAST") 15 | override fun create(context: Context?, viewId: Int, args: Any?): PlatformView { 16 | if (view == null) { 17 | val sizeMap = args as HashMap? 18 | val size = if (sizeMap != null) Size(sizeMap["w"]!!, sizeMap["h"]!!) else null 19 | view = ScanView(plugin, size) 20 | } 21 | return view!! 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 9.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/alexv525/mlkit_scan_plugin/vision/processor/VisionImageProcessor.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Alex (https://github.com/AlexV525) 3 | * Date: 2022/1/10 17:32 4 | */ 5 | 6 | package com.alexv525.mlkit_scan_plugin.vision.processor 7 | 8 | import android.graphics.Bitmap 9 | import com.google.mlkit.common.MlKitException 10 | import com.alexv525.mlkit_scan_plugin.vision.FrameMetadata 11 | import java.nio.ByteBuffer 12 | 13 | /** An interface to process the images with different vision detectors and custom image models. */ 14 | interface VisionImageProcessor { 15 | /** Processes a bitmap image. */ 16 | fun processBitmap(bitmap: Bitmap?, rotation: Int = 0) 17 | 18 | /** Processes ByteBuffer image data, e.g. used for Camera1 live preview case. */ 19 | @Throws(MlKitException::class) 20 | fun processByteBuffer(data: ByteBuffer?, frameMetadata: FrameMetadata?) 21 | 22 | /** Stops the underlying machine learning model and release resources. */ 23 | fun stop() 24 | } -------------------------------------------------------------------------------- /flutter_mlkit_scan_plugin.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /ios/mlkit_scan_plugin.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'mlkit_scan_plugin' 3 | s.version = '1.0.0' 4 | s.summary = 'The MLKit scan plugin for Flutter' 5 | s.description = <<-DESC 6 | The MLKit scan plugin for Flutter 7 | DESC 8 | s.homepage = 'https://github.com/AlexV525/flutter_mlkit_scan_plugin' 9 | s.license = { :file => '../LICENSE' } 10 | s.author = { 'AlexV525' => 'github@alexv525.com' } 11 | s.source = { :path => '.' } 12 | s.source_files = 'Classes/**/*' 13 | s.dependency 'Flutter' 14 | s.platform = :ios, '11.0' 15 | 16 | s.dependency 'GoogleMLKit/BarcodeScanning', '~> 3.2.0' 17 | s.dependency 'GoogleMLKit/TextRecognition', '~> 3.2.0' 18 | s.static_framework = true 19 | 20 | # Flutter.framework does not contain a i386 slice. 21 | s.pod_target_xcconfig = { 22 | 'DEFINES_MODULE' => 'YES', 23 | 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', 24 | } 25 | s.swift_version = '5.0' 26 | end 27 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/alexv525/mlkit_scan_plugin/decode/PreviewCallback.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Alex (https://github.com/AlexV525) 3 | * Date: 2022/1/11 13:32 4 | */ 5 | 6 | package com.alexv525.mlkit_scan_plugin.decode 7 | 8 | import android.hardware.Camera 9 | import android.util.Size 10 | import com.alexv525.mlkit_scan_plugin.camera.CameraConfigurationManager 11 | import com.alexv525.mlkit_scan_plugin.vision.FrameMetadata 12 | 13 | internal class PreviewCallback( 14 | private val mConfigManager: CameraConfigurationManager, 15 | private val onPreviewFrame: ((data: ByteArray, frameMetadata: FrameMetadata) -> Unit)? = null 16 | ) : Camera.PreviewCallback { 17 | private var cameraRotation: Int = 90 18 | 19 | @Deprecated("Deprecated in Java") 20 | override fun onPreviewFrame(data: ByteArray, camera: Camera) { 21 | val size: Size = mConfigManager.cameraResolution ?: return 22 | onPreviewFrame?.invoke(data, FrameMetadata(size, cameraRotation)) 23 | } 24 | 25 | fun setRotation(rotation: Int) { 26 | this.cameraRotation = rotation 27 | } 28 | } -------------------------------------------------------------------------------- /lib/src/mixins/focus_listener_mixin.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// [Author] Alex (https://github.com/AlexV525) 3 | /// [Date] 2021/11/9 19:23 4 | /// 5 | import 'package:flutter/material.dart'; 6 | 7 | import '../plugin/scan_plugin.dart'; 8 | 9 | @optionalTypeArgs 10 | mixin ScanFocusListenerMixin on State { 11 | int get focusPointers => _pointers; 12 | int _pointers = 0; 13 | 14 | bool get shouldReFocus => _pointers == 0; 15 | 16 | @override 17 | void dispose() { 18 | _pointers = 0; 19 | super.dispose(); 20 | } 21 | 22 | /// 触摸点移除 23 | void _removePointer(PointerUpEvent event) { 24 | if (_pointers == 0) { 25 | return; 26 | } 27 | _pointers--; 28 | if (shouldReFocus) { 29 | Future(() { 30 | ScanPlugin.reFocus(event.position); 31 | }); 32 | } 33 | } 34 | 35 | Widget focusWrapper({required Widget child}) { 36 | return Listener( 37 | onPointerDown: (_) => _pointers++, 38 | onPointerUp: _removePointer, 39 | behavior: HitTestBehavior.opaque, 40 | child: child, 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/mixins/lifecycle_mixin.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// [Author] Alex (https://github.com/AlexV525) 3 | /// [Date] 2021/11/10 15:12 4 | /// 5 | import 'package:flutter/material.dart'; 6 | 7 | import '../plugin/scan_plugin.dart'; 8 | 9 | mixin ScanLifecycleMixin on WidgetsBindingObserver, RouteAware { 10 | bool isInCurrentPage = true; 11 | 12 | @override 13 | void didPushNext() { 14 | super.didPushNext(); 15 | isInCurrentPage = false; 16 | } 17 | 18 | @override 19 | void didPopNext() { 20 | super.didPopNext(); 21 | isInCurrentPage = true; 22 | } 23 | 24 | @override 25 | void didChangeAppLifecycleState(AppLifecycleState state) { 26 | switch (state) { 27 | case AppLifecycleState.resumed: 28 | if (ScanPlugin.isScanningPaused) { 29 | ScanPlugin.resumeScan(); 30 | } 31 | break; 32 | case AppLifecycleState.inactive: 33 | case AppLifecycleState.paused: 34 | case AppLifecycleState.detached: 35 | if (!ScanPlugin.isScanningPaused) { 36 | ScanPlugin.pauseScan(); 37 | } 38 | break; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/alexv525/mlkit_scan_plugin/vision/FrameMetadata.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Alex (https://github.com/AlexV525) 3 | * Date: 2022/1/10 17:32 4 | */ 5 | 6 | package com.alexv525.mlkit_scan_plugin.vision 7 | 8 | import android.util.Size 9 | 10 | /** Describing a frame info. */ 11 | class FrameMetadata(val width: Int = 0, val height: Int = 0, val rotation: Int = 0) { 12 | constructor(size: Size, rotation: Int = 0) : this(size.width, size.height, rotation) 13 | 14 | /** Builder of [FrameMetadata]. */ 15 | class Builder { 16 | private var width = 0 17 | private var height = 0 18 | private var rotation = 0 19 | 20 | fun setWidth(width: Int): Builder { 21 | this.width = width 22 | return this 23 | } 24 | 25 | fun setHeight(height: Int): Builder { 26 | this.height = height 27 | return this 28 | } 29 | 30 | fun setRotation(rotation: Int): Builder { 31 | this.rotation = rotation 32 | return this 33 | } 34 | 35 | fun build(): FrameMetadata { 36 | return FrameMetadata(width, height) 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/com/alexv525/mlkit_scan_plugin/vision/processor/TextRecognitionProcessor.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Alex (https://github.com/AlexV525) 3 | * Date: 2022/1/10 17:32 4 | */ 5 | 6 | package com.alexv525.mlkit_scan_plugin.vision.processor 7 | 8 | import com.google.android.gms.tasks.Task 9 | import com.google.mlkit.vision.common.InputImage 10 | import com.google.mlkit.vision.text.Text 11 | import com.google.mlkit.vision.text.TextRecognition 12 | import com.google.mlkit.vision.text.latin.TextRecognizerOptions 13 | 14 | /** Processor for the text detector. */ 15 | class TextRecognitionProcessor( 16 | onSuccessUnit: ((results: Text) -> Unit)? = null, 17 | onFailureUnit: ((e: Exception) -> Unit)? = null, 18 | imageMaxWidth: Int = 0, 19 | imageMaxHeight: Int = 0 20 | ) : VisionProcessorBase(onSuccessUnit, onFailureUnit, imageMaxWidth, imageMaxHeight) { 21 | private val textRecognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS) 22 | 23 | override fun stop() { 24 | super.stop() 25 | textRecognizer.close() 26 | } 27 | 28 | override fun detectInImage(image: InputImage): Task { 29 | return textRecognizer.process(image) 30 | } 31 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/com/alexv525/mlkit_scan_plugin/Shared.kt: -------------------------------------------------------------------------------- 1 | package com.alexv525.mlkit_scan_plugin 2 | 3 | import io.flutter.plugin.common.BinaryMessenger 4 | import io.flutter.plugin.common.EventChannel 5 | import io.flutter.plugin.common.MethodChannel 6 | import java.util.concurrent.ArrayBlockingQueue 7 | import java.util.concurrent.ThreadPoolExecutor 8 | import java.util.concurrent.TimeUnit 9 | 10 | object Shared { 11 | var scanChannel: MethodChannel? = null 12 | var eventSink: EventChannel.EventSink? = null 13 | val threadPool: ThreadPoolExecutor = ThreadPoolExecutor( 14 | 8 + 3, 15 | 1000, 16 | 10, 17 | TimeUnit.SECONDS, 18 | ArrayBlockingQueue(8 + 3) 19 | ) 20 | 21 | fun initChannels(messenger: BinaryMessenger, plugin: MLKitScanPlugin): ScanFactory { 22 | scanChannel = 23 | MethodChannel(messenger, Constant.METHOD_CHANNEL_NAME).apply { 24 | setMethodCallHandler(plugin) 25 | } 26 | EventChannel(messenger, Constant.EVENT_CHANNEL_NAME).setStreamHandler(PluginStreamHandler()) 27 | return ScanFactory(plugin) 28 | } 29 | } 30 | 31 | fun runInBackground(runnable: Runnable) { 32 | Shared.threadPool.execute(runnable) 33 | } 34 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/alexv525/mlkit_scan_plugin/vision/processor/BarcodeScannerProcessor.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Alex (https://github.com/AlexV525) 3 | * Date: 2022/1/10 17:32 4 | */ 5 | 6 | package com.alexv525.mlkit_scan_plugin.vision.processor 7 | 8 | import com.alexv525.mlkit_scan_plugin.getBarcodeScanner 9 | import com.google.android.gms.tasks.Task 10 | import com.google.mlkit.vision.barcode.BarcodeScanner 11 | import com.google.mlkit.vision.barcode.common.Barcode 12 | import com.google.mlkit.vision.common.InputImage 13 | 14 | /** Processor for the barcode detector. */ 15 | class BarcodeScannerProcessor( 16 | formats: IntArray? = null, 17 | onSuccessUnit: ((results: List) -> Unit)? = null, 18 | onFailureUnit: ((e: Exception) -> Unit)? = null, 19 | imageMaxWidth: Int = 0, 20 | imageMaxHeight: Int = 0 21 | ) : VisionProcessorBase>(onSuccessUnit, onFailureUnit, imageMaxWidth, imageMaxHeight) { 22 | private val barcodeScanner: BarcodeScanner = formats.getBarcodeScanner() 23 | 24 | override fun stop() { 25 | super.stop() 26 | barcodeScanner.close() 27 | } 28 | 29 | override fun detectInImage(image: InputImage): Task> { 30 | return barcodeScanner.process(image) 31 | } 32 | } -------------------------------------------------------------------------------- /lib/src/plugin/log_util.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// [Author] Alex (https://github.com/AlexV525) 3 | /// [Date] 11/26/20 8:23 PM 4 | /// 5 | import 'dart:developer'; 6 | 7 | DateTime get _currentTime => DateTime.now(); 8 | 9 | int get _currentTimeStamp => _currentTime.millisecondsSinceEpoch; 10 | 11 | class LogUtil { 12 | const LogUtil._(); 13 | 14 | static const String _TAG = 'SCAN PLUGIN - LOG'; 15 | 16 | static void i(dynamic message, {String tag = _TAG, StackTrace? stackTrace}) { 17 | _printLog(message, tag, stackTrace); 18 | } 19 | 20 | static void d(dynamic message, {String tag = _TAG, StackTrace? stackTrace}) { 21 | _printLog(message, tag, stackTrace); 22 | } 23 | 24 | static void w(dynamic message, {String tag = _TAG, StackTrace? stackTrace}) { 25 | _printLog(message, tag, stackTrace); 26 | } 27 | 28 | static void e(dynamic message, {String tag = _TAG, StackTrace? stackTrace}) { 29 | _printLog(message, tag, stackTrace); 30 | } 31 | 32 | static void json( 33 | dynamic message, { 34 | String tag = _TAG, 35 | StackTrace? stackTrace, 36 | }) { 37 | _printLog(message, tag, stackTrace); 38 | } 39 | 40 | static void _printLog(dynamic message, String tag, StackTrace? stackTrace) { 41 | log( 42 | '$_currentTimeStamp - $message', 43 | name: tag, 44 | stackTrace: stackTrace, 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/alexv525/mlkit_scan_plugin/decode/ScanResult.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Alex (https://github.com/AlexV525) 3 | * Date: 2022/1/10 17:34 4 | */ 5 | 6 | package com.alexv525.mlkit_scan_plugin.decode 7 | 8 | import com.alexv525.mlkit_scan_plugin.Constant 9 | 10 | class ScanResult(var code: String? = null, var phone: LinkedHashSet = linkedSetOf()) { 11 | private val isValid get() = !code.isNullOrBlank() || phone.isNotEmpty() 12 | val isCodeOnly get() = !code.isNullOrBlank() && phone.isEmpty() 13 | val isFullFilled get() = !code.isNullOrBlank() && phone.isNotEmpty() 14 | 15 | private fun obtainStateInt(scanType: Int): Int { 16 | return when { 17 | isCodeOnly && scanType == Constant.SCAN_TYPE_BARCODE_AND_MOBILE -> Constant.SCAN_RESULT_CODE_ONLY 18 | isValid -> Constant.SCAN_RESULT_SUCCEED 19 | else -> Constant.SCAN_RESULT_FAILED 20 | } 21 | } 22 | 23 | fun reset() { 24 | code = null 25 | phone.clear() 26 | } 27 | 28 | fun toMap(scanType: Int): Map { 29 | val map = mutableMapOf("state" to obtainStateInt(scanType)) 30 | if (!code.isNullOrEmpty()) { 31 | map["code"] = code 32 | } 33 | if (phone.isNotEmpty()) { 34 | map["phone"] = ArrayList(phone) 35 | } 36 | return map 37 | } 38 | } -------------------------------------------------------------------------------- /ios/Classes/SwiftMLKitScanPlugin.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | public class SwiftMLKitScanPlugin: NSObject, FlutterPlugin { 5 | var factory: ScanViewFactory? 6 | 7 | public static func register(with registrar: FlutterPluginRegistrar) { 8 | // 初始化 channel 9 | ChannelManager.initFlutterMethodChannel(registrar.messenger()) 10 | 11 | let instance = SwiftMLKitScanPlugin() 12 | if let scanViewChannel = ChannelManager.shared.scanViewChannel { 13 | registrar.addMethodCallDelegate(instance, channel: scanViewChannel) 14 | } 15 | // 注册 ScanView 到 Factory 16 | let factory = ScanViewFactory(messenger: registrar.messenger()) 17 | instance.factory = factory 18 | registrar.register(factory, withId: ChannelMethod.factoryId) 19 | } 20 | 21 | public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { 22 | guard let viewFactory = factory else { 23 | debugPrint("ScanViewFactory is null.") 24 | result( 25 | FlutterError( 26 | code: "ScanViewFactory", 27 | message: "0", 28 | details: "ScanViewFactory is null" 29 | ) 30 | ) 31 | return 32 | } 33 | ChannelManager.handleFlutterChannel(call, factory: viewFactory, result: result) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/mixins/flashlight_mixin.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// [Author] Alex (https://github.com/AlexV525) 3 | /// [Date] 2021/11/9 20:03 4 | /// 5 | import 'package:flutter/material.dart'; 6 | 7 | import '../plugin/constants.dart'; 8 | import '../plugin/scan_plugin.dart'; 9 | import '../resources.dart'; 10 | 11 | @optionalTypeArgs 12 | mixin ScanFlashlightMixin on State { 13 | /// 是否开启了闪光灯 14 | final ValueNotifier _flashlightActive = ValueNotifier(false); 15 | 16 | Future toggleFlashlight() async { 17 | if (_flashlightActive.value) { 18 | await ScanPlugin.closeFlashlight(); 19 | _flashlightActive.value = false; 20 | } else { 21 | await ScanPlugin.openFlashlight(); 22 | _flashlightActive.value = true; 23 | } 24 | } 25 | 26 | @override 27 | void dispose() { 28 | _flashlightActive.dispose(); 29 | super.dispose(); 30 | } 31 | 32 | Widget flashlightButton(BuildContext context, {double size = 45}) { 33 | return GestureDetector( 34 | onTap: toggleFlashlight, 35 | child: ValueListenableBuilder( 36 | valueListenable: _flashlightActive, 37 | builder: (_, bool value, __) => Image.asset( 38 | value 39 | ? ScanPluginR.ASSETS_TORCH_ACTIVE_PNG 40 | : ScanPluginR.ASSETS_TORCH_PNG, 41 | package: ScanPluginPackage, 42 | width: size, 43 | ), 44 | ), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/alexv525/mlkit_scan_plugin/Constant.kt: -------------------------------------------------------------------------------- 1 | package com.alexv525.mlkit_scan_plugin 2 | 3 | object Constant { 4 | private const val PACKAGE = "MLKitScanPlugin" 5 | 6 | /// 三端约定的方法 7 | const val METHOD_CHANNEL_NAME = "$PACKAGE/scanChannel" 8 | const val METHOD_LOAD_SCAN_VIEW = "loadScanView" 9 | const val METHOD_SWITCH_SCAN_TYPE = "switchScanType" 10 | const val METHOD_STOP_SCAN = "stopScan" 11 | const val METHOD_REFOCUS = "reFocus" 12 | const val METHOD_RESUME_SCAN = "resumeScan" 13 | const val METHOD_PAUSE_SCAN = "pauseScan" 14 | const val METHOD_SCAN_FROM_FILE = "scanFromFile" 15 | const val METHOD_OPEN_FLASHLIGHT = "openFlashlight" 16 | const val METHOD_CLOSE_FLASHLIGHT = "closeFlashlight" 17 | const val METHOD_REQUEST_WAKE_LOCK = "requestWakeLock" 18 | 19 | const val EVENT_CHANNEL_NAME = "$PACKAGE/resultChannel" 20 | const val VIEW_TYPE_ID = "$PACKAGE/ScanViewFactory" 21 | 22 | /// 扫描的四种类别 23 | const val SCAN_TYPE_BARCODE_AND_MOBILE = 0 24 | const val SCAN_TYPE_MOBILE = 1 25 | const val SCAN_TYPE_WAIT = -1 26 | const val SCAN_TYPE_BARCODE = 2 27 | const val SCAN_TYPE_QR_CODE = 3 28 | const val SCAN_TYPE_GOODS_CODE = 4 29 | 30 | /// 扫描结果 31 | const val SCAN_RESULT_CODE_ONLY = -1 32 | const val SCAN_RESULT_FAILED = 0 33 | const val SCAN_RESULT_SUCCEED = 1 34 | 35 | /// 循环间隔时长设定 36 | const val DECODE_INTERVAL = 300 37 | const val FOCUS_INTERVAL = 1200 38 | } -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'com.alexv525.mlkit_scan_plugin' 2 | version '1.0-SNAPSHOT' 3 | 4 | buildscript { 5 | ext.kotlin_version = '1.7.10' 6 | repositories { 7 | google() 8 | mavenCentral() 9 | } 10 | 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:7.2.2' 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | } 15 | } 16 | 17 | rootProject.allprojects { 18 | repositories { 19 | google() 20 | mavenCentral() 21 | } 22 | } 23 | 24 | apply plugin: 'com.android.library' 25 | apply plugin: 'kotlin-android' 26 | 27 | android { 28 | compileSdkVersion 32 29 | 30 | sourceSets { 31 | main.java.srcDirs += 'src/main/kotlin' 32 | } 33 | 34 | defaultConfig { 35 | minSdkVersion 21 36 | ndk { 37 | abiFilters "arm64-v8a", "armeabi-v7a" 38 | } 39 | } 40 | 41 | lintOptions { 42 | disable 'InvalidPackage' 43 | } 44 | 45 | aaptOptions { 46 | noCompress "tflite" 47 | } 48 | } 49 | 50 | dependencies { 51 | implementation "androidx.annotation:annotation:1.5.0" 52 | implementation 'com.google.mlkit:barcode-scanning:17.1.0' 53 | implementation 'com.google.mlkit:text-recognition:16.0.0-beta6' 54 | } 55 | 56 | configurations { 57 | // Resolves dependency conflict caused by some dependencies use 58 | // com.google.guava:guava and com.google.guava:listenablefuture together. 59 | all*.exclude group: 'com.google.guava', module: 'listenablefuture' 60 | } 61 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Classes/Scan/TextRule.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MLKitBarcodeScanning 3 | import MLKitTextRecognition 4 | 5 | // MARK: - Validate code text. 6 | public func checkCodeText(_ barcodes: [Barcode], _ scanningType: ScanningType) -> String? { 7 | guard !barcodes.isEmpty else { 8 | return nil 9 | } 10 | var resultString: String? 11 | for barcode in barcodes { 12 | if (scanningType == .qrCode || scanningType == .goodsCode) { 13 | resultString = barcode.rawValue 14 | break 15 | } 16 | let reg = "^[0-9a-zA-Z\\-]{10,}" 17 | let predicate = NSPredicate(format: "SELF MATCHES %@", reg) 18 | if predicate.evaluate(with: barcode.rawValue) { 19 | resultString = barcode.rawValue 20 | break 21 | } 22 | } 23 | return resultString 24 | } 25 | 26 | // MARK: - Validate phone text. 27 | public func checkPhoneText(_ texts: Array) -> Array { 28 | var results = [] as [String] 29 | for text in texts { 30 | let content = text.filter(\.isNumber) 31 | if (content.isEmpty || content.count < 11) { 32 | continue 33 | } 34 | results.append(content) 35 | } 36 | return Array(Set(results)) 37 | } 38 | 39 | public func enhanceNumberText(_ text: String) -> String { 40 | return text.replacingOccurrences(of: "O", with: "0") 41 | .replacingOccurrences(of: "I", with: "1") 42 | .replacingOccurrences(of: "i", with: "1") 43 | .replacingOccurrences(of: "l", with: "1") 44 | .replacingOccurrences(of: "|", with: "1") 45 | .replacingOccurrences(of: "!", with: "1") 46 | .replacingOccurrences(of: "Z", with: "2") 47 | .replacingOccurrences(of: "z", with: "2") 48 | } 49 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | mlkit_scan_plugin_example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | NSCameraUsageDescription 26 | Try with camera scanning 27 | NSPhotoLibraryUsageDescription 28 | Try with image scanning 29 | UILaunchStoryboardName 30 | LaunchScreen 31 | UIMainStoryboardFile 32 | Main 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | CADisableMinimumFrameDurationOnPhone 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 33 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | 36 | defaultConfig { 37 | applicationId "com.alexv525.mlkit_scan_plugin_example" 38 | minSdkVersion 21 39 | targetSdkVersion flutter.targetSdkVersion 40 | versionCode flutterVersionCode.toInteger() 41 | versionName flutterVersionName 42 | } 43 | 44 | buildTypes { 45 | release { 46 | // TODO: Add your own signing config for the release build. 47 | // Signing with the debug keys for now, so `flutter run --release` works. 48 | signingConfig signingConfigs.debug 49 | } 50 | } 51 | 52 | lint { 53 | disable 'InvalidPackage' 54 | } 55 | } 56 | 57 | flutter { 58 | source '../..' 59 | } 60 | 61 | dependencies { 62 | } 63 | -------------------------------------------------------------------------------- /ios/Classes/Scan/ScanState.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// 扫描的一些配置项 4 | public struct ScanConfig { 5 | static let scanTimeOutDuration: TimeInterval = 1 // 扫描到条码后未扫描到手机号的超时时间,默认1s 6 | } 7 | 8 | /// 扫描模式的类型 9 | public enum ScanningTaskMode { 10 | case wait // 暂停 11 | case barcodeAndMobile // 条码和手机号 12 | case mobile // 手机号 13 | case barcode // 条码 14 | case qrCode // 二维码 15 | case goodsCode // 商品条码 16 | 17 | static func toMode(_ value: Int) -> ScanningTaskMode { 18 | switch value { 19 | case -1: 20 | return .wait 21 | case 0: 22 | return .barcodeAndMobile 23 | case 1: 24 | return .mobile 25 | case 2: 26 | return .barcode 27 | case 3: 28 | return .qrCode 29 | case 4: 30 | return .goodsCode 31 | default: 32 | return .wait 33 | } 34 | } 35 | } 36 | 37 | /// 扫描任务的类型 38 | public enum ScanningType { 39 | case wait // 暂停 40 | case barcodeAndMobile // 条码和手机号 41 | case mobile // 手机号 42 | case barcode // 条码 43 | case qrCode // 二维码 44 | case goodsCode // 商品条码 45 | } 46 | 47 | /// 扫描结果的状态 48 | public enum ScanningResultState: Int { 49 | case success = 1 //扫描成功 50 | case failed = 0 //扫描失败 51 | case progress = -1 //任务尚未完成 52 | } 53 | 54 | /// 扫描结果数据映射 55 | public struct ScanResult { 56 | var state: ScanningResultState = .failed // 状态码 57 | var code: String? // 条形码解析数值 58 | var phone: Array = [] // 提取到的手机号 59 | 60 | public func toJSON() -> [String: Any] { 61 | var params: [String: Any] = ["state": state.rawValue] 62 | if let codeString = code { 63 | params["code"] = codeString 64 | } 65 | if (!phone.isEmpty) { 66 | params["phone"] = Array(Set(phone)) 67 | } 68 | return params 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /ios/Classes/Scan/ScanViewFactory.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class ScanViewFactory: NSObject, FlutterPlatformViewFactory { 4 | var platformView: ScanPlatformView? 5 | private weak var messenger: FlutterBinaryMessenger? 6 | 7 | init(messenger: FlutterBinaryMessenger) { 8 | super.init() 9 | self.messenger = messenger 10 | debugPrint("ScanViewFactory init.") 11 | } 12 | 13 | func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?) -> FlutterPlatformView { 14 | debugPrint("Creating new platformView.") 15 | let viewRect: CGRect? 16 | if let argsMap = args as? Dictionary { 17 | viewRect = CGRect(x: 0, y: 0, width: argsMap["w"]!, height: argsMap["h"]!); 18 | } else { 19 | viewRect = nil 20 | } 21 | let scanPlatformView = ScanPlatformView( 22 | frame: frame, 23 | viewIdentifier: viewId, 24 | arguments: args, 25 | binaryMessenger: messenger, 26 | viewRect: viewRect 27 | ) 28 | platformView = scanPlatformView 29 | return scanPlatformView 30 | } 31 | 32 | func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol { 33 | return FlutterStandardMessageCodec.sharedInstance() 34 | } 35 | } 36 | 37 | class ScanPlatformView: NSObject, FlutterPlatformView { 38 | var scanView: UIView? 39 | 40 | init( 41 | frame: CGRect, 42 | viewIdentifier viewId: Int64, 43 | arguments args: Any?, 44 | binaryMessenger messenger: FlutterBinaryMessenger?, 45 | viewRect: CGRect? 46 | ) { 47 | super.init() 48 | scanView = ScanView.init(frame: frame, viewRect: viewRect) 49 | debugPrint("ScanPlatformView init.") 50 | } 51 | 52 | func view() -> UIView { 53 | return scanView ?? UIView() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/alexv525/mlkit_scan_plugin/vision/ScopedExecutor.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.alexv525.mlkit_scan_plugin.vision 18 | 19 | import java.util.concurrent.Executor 20 | import java.util.concurrent.atomic.AtomicBoolean 21 | 22 | /** 23 | * Wraps an existing executor to provide a [.shutdown] method that allows subsequent 24 | * cancellation of submitted runnables. 25 | */ 26 | class ScopedExecutor(private val executor: Executor) : Executor { 27 | private val shutdown = AtomicBoolean() 28 | 29 | override fun execute(command: Runnable) { 30 | // Return early if this object has been shut down. 31 | if (shutdown.get()) { 32 | return 33 | } 34 | executor.execute inner@ { 35 | // Check again in case it has been shut down in the meantime. 36 | if (shutdown.get()) { 37 | return@inner 38 | } 39 | command.run() 40 | } 41 | } 42 | 43 | /** 44 | * After this method is called, no runnables that have been submitted or are subsequently 45 | * submitted will start to execute, turning this executor into a no-op. 46 | * 47 | * 48 | * Runnables that have already started to execute will continue. 49 | */ 50 | fun shutdown() { 51 | shutdown.set(true) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ios/Classes/Scan/Orientation.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AVFoundation 3 | 4 | /// 根据设备的方向,调整图片的方向属性 5 | // MARK: - Get the orientation from the image. 6 | public func imageOrientation(fromDevicePosition devicePosition: AVCaptureDevice.Position = .back) -> UIImage.Orientation { 7 | var deviceOrientation = UIDevice.current.orientation 8 | if deviceOrientation == .faceDown || 9 | deviceOrientation == .faceUp || 10 | deviceOrientation == .unknown { 11 | deviceOrientation = currentUIOrientation() 12 | } 13 | switch deviceOrientation { 14 | case .portrait: 15 | return devicePosition == .front ? .leftMirrored : .right 16 | case .landscapeLeft: 17 | return devicePosition == .front ? .downMirrored : .up 18 | case .portraitUpsideDown: 19 | return devicePosition == .front ? .rightMirrored : .left 20 | case .landscapeRight: 21 | return devicePosition == .front ? .upMirrored : .down 22 | case .faceDown, .faceUp, .unknown: 23 | return .up 24 | @unknown default: 25 | fatalError() 26 | } 27 | } 28 | 29 | // MARK: - Get the orientation from the current screen. 30 | public func currentUIOrientation() -> UIDeviceOrientation { 31 | let deviceOrientation = { () -> UIDeviceOrientation in 32 | switch UIApplication.shared.statusBarOrientation { 33 | case .landscapeLeft: 34 | return .landscapeRight 35 | case .landscapeRight: 36 | return .landscapeLeft 37 | case .portraitUpsideDown: 38 | return .portraitUpsideDown 39 | case .portrait, .unknown: 40 | return .portrait 41 | @unknown default: 42 | fatalError() 43 | } 44 | } 45 | guard Thread.isMainThread else { 46 | var currentOrientation: UIDeviceOrientation = .portrait 47 | DispatchQueue.main.sync { 48 | currentOrientation = deviceOrientation() 49 | } 50 | return currentOrientation 51 | } 52 | return deviceOrientation() 53 | } 54 | -------------------------------------------------------------------------------- /lib/src/plugin/enums.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// [Author] Alex (https://github.com/AlexV525) 3 | /// [Date] 2021/11/4 10:57 4 | /// 5 | /// 扫描的类别 6 | /// 7 | /// -1 暂停扫描 8 | /// 0 邮政条码和手机号扫描 9 | /// 1 手机号扫描 10 | /// 2 邮政条码扫描 11 | /// 3 二维码扫描 12 | /// 4 商品条码扫描 13 | class ScanType extends _EnumClass { 14 | const ScanType._(int value) : super(value); 15 | 16 | static const ScanType wait = ScanType._(-1); 17 | static const ScanType barcodeAndMobile = ScanType._(0); 18 | static const ScanType mobile = ScanType._(1); 19 | static const ScanType barcode = ScanType._(2); 20 | static const ScanType qrCode = ScanType._(3); 21 | static const ScanType goodsCode = ScanType._(4); 22 | 23 | static ScanType from(int value) { 24 | return values.firstWhere((ScanType e) => e.value == value); 25 | } 26 | 27 | static const List values = [ 28 | wait, 29 | barcodeAndMobile, 30 | mobile, 31 | barcode, 32 | qrCode, 33 | goodsCode, 34 | ]; 35 | } 36 | 37 | /// 扫描结果的状态 38 | /// 39 | /// -1 仅有条形码 40 | /// 0 扫描失败 41 | /// 1 扫描成功 42 | class ScanResultStatus extends _EnumClass { 43 | const ScanResultStatus._(int value) : super(value); 44 | 45 | static const ScanResultStatus codeOnly = ScanResultStatus._(-1); 46 | static const ScanResultStatus failed = ScanResultStatus._(0); 47 | 48 | /// 注意:Android 下仍有可能返回 0 个电话号码,仍然需要作为 [codeOnly] 处理。 49 | static const ScanResultStatus succeed = ScanResultStatus._(1); 50 | 51 | static ScanResultStatus from(int value) { 52 | return values.firstWhere((ScanResultStatus e) => e.value == value); 53 | } 54 | 55 | static const List values = [ 56 | codeOnly, 57 | failed, 58 | succeed, 59 | ]; 60 | } 61 | 62 | class _EnumClass { 63 | const _EnumClass(this.value); 64 | 65 | final int value; 66 | 67 | bool operator <(_EnumClass other) => value < other.value; 68 | 69 | bool operator <=(_EnumClass other) => value <= other.value; 70 | 71 | bool operator >(_EnumClass other) => value > other.value; 72 | 73 | bool operator >=(_EnumClass other) => value >= other.value; 74 | 75 | @override 76 | String toString() => '$runtimeType._($value)'; 77 | } 78 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/alexv525/mlkit_scan_plugin/camera/AutoFitTextureView.kt: -------------------------------------------------------------------------------- 1 | package com.alexv525.mlkit_scan_plugin.camera 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.util.Size 6 | import android.view.TextureView 7 | 8 | /** 9 | * A [TextureView] that can be adjusted to a specified fitted size. 10 | */ 11 | open class AutoFitTextureView @JvmOverloads constructor( 12 | context: Context, 13 | attrs: AttributeSet? = null, 14 | defStyle: Int = 0, 15 | private val viewSize: Size?, 16 | private var resolution: Size? 17 | ) : TextureView(context, attrs, defStyle) { 18 | constructor(context: Context) : this(context, null, 0, null, null) 19 | 20 | companion object { 21 | private val zeroSize = Size(0, 0) 22 | } 23 | 24 | override fun getKeepScreenOn(): Boolean { 25 | return true 26 | } 27 | 28 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 29 | super.onMeasure(widthMeasureSpec, heightMeasureSpec) 30 | val validSize = viewSize 31 | ?: getSize( 32 | MeasureSpec.getSize(widthMeasureSpec), 33 | MeasureSpec.getSize(heightMeasureSpec) 34 | ) 35 | if (resolution == null || resolution == zeroSize) { 36 | setMeasuredDimension(validSize.width, validSize.height) 37 | } else { 38 | val fittedSize = coverFit(validSize, resolution!!) 39 | setMeasuredDimension(fittedSize.width, fittedSize.height) 40 | } 41 | } 42 | 43 | private fun getSize(width: Int, height: Int): Size { 44 | return Size(width, height) 45 | } 46 | 47 | // fun setOriginalResolution(size: Size) { 48 | // resolution = size 49 | // requestLayout() 50 | // } 51 | 52 | /** 53 | * Fit the size to cover the whole view according to the given size. 54 | * 55 | * @param inputSize The original size of the view. 56 | * @param outputSize The target size of the view. 57 | */ 58 | private fun coverFit(inputSize: Size, outputSize: Size): Size { 59 | return if (outputSize.width / outputSize.height > inputSize.width / inputSize.height) { 60 | Size(inputSize.width, inputSize.width * outputSize.height / outputSize.width) 61 | } else { 62 | Size(inputSize.height * outputSize.width / outputSize.height, inputSize.height) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/alexv525/mlkit_scan_plugin/Extension.kt: -------------------------------------------------------------------------------- 1 | package com.alexv525.mlkit_scan_plugin 2 | 3 | import android.content.Context 4 | import android.util.DisplayMetrics 5 | import android.util.Size 6 | import android.util.TypedValue 7 | import android.view.WindowManager 8 | import com.google.mlkit.vision.barcode.BarcodeScanner 9 | import com.google.mlkit.vision.barcode.BarcodeScannerOptions 10 | import com.google.mlkit.vision.barcode.BarcodeScanning 11 | 12 | internal object Extension { 13 | fun getScreenSize(context: Context): Size { 14 | return Size(getScreenWidth(context), getScreenHeight(context)) 15 | } 16 | 17 | private fun getDisplayMetrics(context: Context): DisplayMetrics { 18 | val displayMetrics = DisplayMetrics() 19 | (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager) 20 | .defaultDisplay 21 | .getRealMetrics(displayMetrics) 22 | return displayMetrics 23 | } 24 | 25 | fun getScreenWidth(context: Context): Int { 26 | return getDisplayMetrics(context).widthPixels 27 | } 28 | 29 | fun getScreenHeight(context: Context): Int { 30 | return getDisplayMetrics(context).heightPixels 31 | } 32 | 33 | fun Int.dp2px(context: Context): Int { 34 | return TypedValue.applyDimension( 35 | TypedValue.COMPLEX_UNIT_DIP, 36 | this.toFloat(), 37 | context.resources.displayMetrics 38 | ).toInt() 39 | } 40 | 41 | fun Double.dp2px(context: Context): Int { 42 | return TypedValue.applyDimension( 43 | TypedValue.COMPLEX_UNIT_DIP, 44 | this.toFloat(), 45 | context.resources.displayMetrics 46 | ).toInt() 47 | } 48 | 49 | fun Int.px2dp(context: Context): Int { 50 | return (this / (getDisplayMetrics(context).density + 0.5f)).toInt() 51 | } 52 | } 53 | 54 | internal fun IntArray?.getBarcodeScanner(): BarcodeScanner { 55 | val formats = this 56 | return when { 57 | formats == null || formats.isEmpty() -> BarcodeScanning.getClient() 58 | formats.size == 1 -> BarcodeScanning.getClient( 59 | BarcodeScannerOptions.Builder() 60 | .setBarcodeFormats(formats.first()) 61 | .build() 62 | ) 63 | else -> BarcodeScanning.getClient( 64 | BarcodeScannerOptions.Builder() 65 | .setBarcodeFormats(formats.first(), *formats) 66 | .build() 67 | ) 68 | } 69 | } -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 21 | 25 | 29 | 34 | 38 | 39 | 40 | 41 | 42 | 43 | 45 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/alexv525/mlkit_scan_plugin/ScanView.kt: -------------------------------------------------------------------------------- 1 | package com.alexv525.mlkit_scan_plugin 2 | 3 | import android.content.pm.ApplicationInfo 4 | import android.graphics.Color 5 | import android.graphics.Rect 6 | import android.graphics.SurfaceTexture 7 | import android.util.Size 8 | import android.view.Gravity 9 | import android.view.TextureView 10 | import android.view.View 11 | import android.widget.FrameLayout 12 | import android.widget.RelativeLayout 13 | import com.alexv525.mlkit_scan_plugin.camera.AutoFitTextureView 14 | import io.flutter.plugin.common.MethodChannel 15 | import io.flutter.plugin.platform.PlatformView 16 | import java.io.IOException 17 | 18 | class ScanView( 19 | private val mPlugin: MLKitScanPlugin, 20 | private val size: Size? 21 | ) : PlatformView, TextureView.SurfaceTextureListener { 22 | private val context get() = mPlugin.context!! 23 | private val mLayout: FrameLayout = FrameLayout(context).apply { 24 | layoutParams = FrameLayout.LayoutParams( 25 | FrameLayout.LayoutParams.MATCH_PARENT, 26 | FrameLayout.LayoutParams.MATCH_PARENT 27 | ) 28 | setBackgroundColor(Color.TRANSPARENT) 29 | keepScreenOn = true 30 | } 31 | private var mTextureView: TextureView? = null 32 | private var mRectView: View? = null 33 | private var result: MethodChannel.Result? = null 34 | 35 | override fun getView() = mLayout 36 | 37 | override fun dispose() { 38 | mTextureView?.surfaceTextureListener = null 39 | mLayout.removeAllViews() 40 | mTextureView = null 41 | mRectView = null 42 | result = null 43 | mPlugin.viewFactory?.view = null // Deallocate the view itself. 44 | } 45 | 46 | /// Called everytime when we initialize the view or resumed from the background. 47 | override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) { 48 | try { 49 | result?.success(null) 50 | mPlugin.mCamera?.setPreviewTexture(surface) 51 | mPlugin.restartPreviewAndDecode() 52 | } catch (_: IOException) { 53 | // Something bad happened 54 | } finally { 55 | result = null 56 | } 57 | } 58 | 59 | override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) { 60 | } 61 | 62 | override fun onSurfaceTextureUpdated(surface: SurfaceTexture) { 63 | } 64 | 65 | override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean { 66 | mTextureView = null 67 | return true 68 | } 69 | 70 | fun createTexture(result: MethodChannel.Result, resolution: Size) { 71 | this.result = result 72 | mTextureView = AutoFitTextureView( 73 | context, 74 | viewSize = size, 75 | resolution = Size(resolution.height, resolution.width) 76 | ).apply { 77 | layoutParams = FrameLayout.LayoutParams( 78 | FrameLayout.LayoutParams.WRAP_CONTENT, 79 | FrameLayout.LayoutParams.WRAP_CONTENT, 80 | Gravity.CENTER 81 | ) 82 | surfaceTextureListener = this@ScanView 83 | mLayout.addView(this) 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | 2 | # Uncomment this line to define a global platform for your project 3 | platform :ios, '11.0' 4 | 5 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 6 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 7 | 8 | project 'Runner', { 9 | 'Debug' => :debug, 10 | 'Profile' => :release, 11 | 'Release' => :release, 12 | } 13 | 14 | def flutter_root 15 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 16 | unless File.exist?(generated_xcode_build_settings_path) 17 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 18 | end 19 | 20 | File.foreach(generated_xcode_build_settings_path) do |line| 21 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 22 | return matches[1].strip if matches 23 | end 24 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 25 | end 26 | 27 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 28 | 29 | flutter_ios_podfile_setup 30 | 31 | def install_plugin_pods(application_path = nil, relative_symlink_dir, platform) 32 | # defined_in_file is set by CocoaPods and is a Pathname to the Podfile. 33 | application_path ||= File.dirname(defined_in_file.realpath) if self.respond_to?(:defined_in_file) 34 | raise 'Could not find application path' unless application_path 35 | 36 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock 37 | # referring to absolute paths on developers' machines. 38 | 39 | symlink_dir = File.expand_path(relative_symlink_dir, application_path) 40 | system('rm', '-rf', symlink_dir) # Avoid the complication of dependencies like FileUtils. 41 | 42 | symlink_plugins_dir = File.expand_path('plugins', symlink_dir) 43 | system('mkdir', '-p', symlink_plugins_dir) 44 | 45 | plugins_file = File.join(application_path, '..', '.flutter-plugins-dependencies') 46 | plugin_pods = flutter_parse_plugins_file(plugins_file, platform) 47 | plugin_pods.each do |plugin_hash| 48 | plugin_name = plugin_hash['name'] 49 | plugin_path = plugin_hash['path'] 50 | if (plugin_name && plugin_path) 51 | specPath = "#{plugin_path}/#{platform}/#{plugin_name}.podspec" 52 | pod plugin_name, :path => specPath 53 | end 54 | end 55 | end 56 | 57 | target 'Runner' do 58 | use_frameworks! 59 | use_modular_headers! 60 | 61 | flutter_install_ios_engine_pod(File.dirname(File.realpath(__FILE__))) 62 | install_plugin_pods(File.dirname(File.realpath(__FILE__)), '.symlinks', 'ios') 63 | end 64 | 65 | post_install do |installer| 66 | installer.pods_project.targets.each do |target| 67 | flutter_additional_ios_build_settings(target) 68 | 69 | # Workaround https://github.com/flutter/flutter/issues/111475. 70 | target_is_resource_bundle = target.respond_to?(:product_type) && target.product_type == 'com.apple.product-type.bundle' 71 | target.build_configurations.each do |config| 72 | if target_is_resource_bundle 73 | config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO' 74 | config.build_settings['CODE_SIGNING_REQUIRED'] = 'NO' 75 | config.build_settings['CODE_SIGNING_IDENTITY'] = '-' 76 | config.build_settings['EXPANDED_CODE_SIGN_IDENTITY'] = '-' 77 | end 78 | if Gem::Version.new('11.0') > Gem::Version.new(config.build_settings['IPHONEOS_DEPLOYMENT_TARGET']) 79 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0' 80 | end 81 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ 82 | '$(inherited)', 83 | 'PERMISSION_CAMERA=1' 84 | ] 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/alexv525/mlkit_scan_plugin/camera/CameraConfigurationUtils.kt: -------------------------------------------------------------------------------- 1 | package com.alexv525.mlkit_scan_plugin.camera 2 | 3 | import android.hardware.Camera 4 | import android.util.Log 5 | import android.util.Size 6 | import java.lang.IllegalStateException 7 | import java.lang.StringBuilder 8 | import kotlin.math.abs 9 | 10 | /** 11 | * Utility methods for configuring the Android camera. 12 | * 13 | * @author Sean Owen 14 | */ 15 | object CameraConfigurationUtils { 16 | private const val TAG = "CameraConfiguration" 17 | private const val MIN_PREVIEW_PIXELS = 480 * 320 // normal screen 18 | 19 | fun findBestPreviewSizeValue(parameters: Camera.Parameters, screenResolution: Size): Size { 20 | val rawSupportedSizes = parameters.supportedPreviewSizes 21 | if (rawSupportedSizes == null) { 22 | Log.w(TAG, "Device returned no supported preview sizes; using default") 23 | val defaultSize = parameters.previewSize 24 | ?: throw IllegalStateException("Parameters contained no preview size!") 25 | return Size(defaultSize.width, defaultSize.height) 26 | } 27 | if (Log.isLoggable(TAG, Log.INFO)) { 28 | val previewSizesString = StringBuilder() 29 | for (size in rawSupportedSizes) { 30 | previewSizesString.append(size.width).append('x').append(size.height).append(' ') 31 | } 32 | Log.i( 33 | TAG, 34 | "Supported preview sizes: $previewSizesString" 35 | ) 36 | } 37 | 38 | // double screenAspectRatio = screenResolution.x / (double) screenResolution.y; 39 | 40 | // Find a suitable size, with max resolution 41 | // int maxResolution = 0; 42 | var maxResPreviewSize: Camera.Size? = null 43 | var diff = Int.MAX_VALUE 44 | for (size in rawSupportedSizes) { 45 | val realWidth = size.width 46 | val realHeight = size.height 47 | val resolution = realWidth * realHeight 48 | if (resolution < MIN_PREVIEW_PIXELS) { 49 | continue 50 | } 51 | val isCandidatePortrait = realWidth < realHeight 52 | val maybeFlippedWidth = if (isCandidatePortrait) realHeight else realWidth 53 | val maybeFlippedHeight = if (isCandidatePortrait) realWidth else realHeight 54 | if (maybeFlippedWidth == screenResolution.width && maybeFlippedHeight == screenResolution.height) { 55 | val exactPoint = Size(realWidth, realHeight) 56 | Log.i( 57 | TAG, 58 | "Found preview size exactly matching screen size: $exactPoint" 59 | ) 60 | return exactPoint 61 | } 62 | val newDiff = 63 | abs(maybeFlippedWidth - screenResolution.width) + abs(maybeFlippedHeight - screenResolution.height) 64 | if (newDiff < diff) { 65 | maxResPreviewSize = size 66 | diff = newDiff 67 | } 68 | } 69 | 70 | // If no exact match, use largest preview size. 71 | // This was not a great idea on older devices because of the additional computation needed. 72 | // We're likely to get here on newer Android 4+ devices, where the CPU is much more powerful. 73 | if (maxResPreviewSize != null) { 74 | return Size(maxResPreviewSize.width, maxResPreviewSize.height) 75 | } 76 | 77 | // If there is nothing at all suitable, return current preview size 78 | val defaultPreview = parameters.previewSize 79 | ?: throw IllegalStateException("Parameters contained no preview size!") 80 | val defaultSize = Size(defaultPreview.width, defaultPreview.height) 81 | Log.i(TAG, "No suitable preview sizes, using default: $defaultSize") 82 | return defaultSize 83 | } 84 | } -------------------------------------------------------------------------------- /lib/src/widgets/scan_rect_painter.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// [Author] Alex (https://github.com/AlexV525) 3 | /// [Date] 1/6/21 10:04 PM 4 | /// 5 | import 'dart:ui' as ui; 6 | 7 | import 'package:flutter/material.dart'; 8 | 9 | double get _screenWidth => MediaQueryData.fromWindow(ui.window).size.width; 10 | 11 | /// 使用非零填充环绕实现的矩形镂空 12 | /// 13 | /// [height] 镂空的高度 14 | /// [paddingOffset] 镂空区域距离起始点的偏移量,且作为边距 15 | class ScanRectPainter extends CustomPainter { 16 | const ScanRectPainter({ 17 | required this.height, 18 | required this.padding, 19 | this.backgroundColor = Colors.black45, 20 | this.color = const Color(0xff3271f6), 21 | }); 22 | 23 | final double height; 24 | final EdgeInsets padding; 25 | final Color backgroundColor; 26 | final Color color; 27 | 28 | void drawInnerRect(Canvas canvas, Size size) { 29 | final Path path = Path()..fillType = PathFillType.evenOdd; 30 | // 先画外部矩形 31 | path 32 | ..moveTo(0, 0) 33 | ..lineTo(size.width, 0) 34 | ..lineTo(size.width, size.height) 35 | ..lineTo(0, size.height) 36 | ..close(); 37 | // 再画内部矩形 38 | path 39 | ..moveTo(padding.left, padding.top) 40 | ..lineTo(size.width - padding.right, padding.top) 41 | ..lineTo(size.width - padding.right, padding.top + height) 42 | ..lineTo(padding.left, padding.top + height) 43 | ..close(); 44 | // Canvas 绘制路径 45 | canvas.drawPath( 46 | path, 47 | Paint() 48 | ..color = backgroundColor 49 | ..strokeWidth = 10 50 | ..strokeCap = StrokeCap.round 51 | ..style = PaintingStyle.fill, 52 | ); 53 | } 54 | 55 | void drawBorder(Canvas canvas, Size size) { 56 | // 绘制外框 57 | canvas.drawRect( 58 | Rect.fromLTWH( 59 | padding.left, 60 | padding.top, 61 | size.width - padding.left - padding.right, 62 | height, 63 | ), 64 | Paint() 65 | ..color = Colors.white 66 | ..strokeWidth = 2 67 | ..strokeCap = StrokeCap.round 68 | ..style = PaintingStyle.stroke, 69 | ); 70 | } 71 | 72 | void drawCorners(Canvas canvas, Size size) { 73 | const double strokeLength = 18; 74 | const double strokeThickness = 5; 75 | final Offset offset = Offset(padding.left, padding.top) - 76 | const Offset(strokeThickness / 2, strokeThickness / 2); 77 | final Paint paint = Paint() 78 | ..color = color 79 | ..strokeWidth = strokeThickness 80 | ..style = PaintingStyle.fill; 81 | final List> cornersRectStartPoints = >[ 82 | [offset, offset], 83 | [ 84 | Offset(_screenWidth - offset.dx - strokeLength, offset.dy), 85 | Offset(_screenWidth - offset.dx - strokeThickness, offset.dy), 86 | ], 87 | [ 88 | Offset( 89 | _screenWidth - offset.dx - strokeLength, 90 | offset.dy + height, 91 | ), 92 | Offset( 93 | _screenWidth - offset.dx - strokeThickness, 94 | offset.dy + height - strokeLength + strokeThickness, 95 | ), 96 | ], 97 | [ 98 | offset + Offset(0, height), 99 | offset + Offset(0, height - strokeLength + strokeThickness), 100 | ], 101 | ]; 102 | for (final List points in cornersRectStartPoints) { 103 | canvas.drawRect( 104 | Rect.fromLTWH( 105 | points[0].dx, 106 | points[0].dy, 107 | strokeLength, 108 | strokeThickness, 109 | ), 110 | paint, 111 | ); 112 | canvas.drawRect( 113 | Rect.fromLTWH( 114 | points[1].dx, 115 | points[1].dy, 116 | strokeThickness, 117 | strokeLength, 118 | ), 119 | paint, 120 | ); 121 | } 122 | } 123 | 124 | @override 125 | void paint(Canvas canvas, Size size) { 126 | drawInnerRect(canvas, size); 127 | drawBorder(canvas, size); 128 | drawCorners(canvas, size); 129 | } 130 | 131 | @override 132 | bool shouldRepaint(ScanRectPainter oldDelegate) => 133 | height != oldDelegate.height; 134 | } 135 | -------------------------------------------------------------------------------- /lib/src/plugin/scan_result.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// [Author] Alex (https://github.com/AlexV525) 3 | /// [Date] 12/11/20 4:31 PM 4 | /// 5 | import 'dart:convert'; 6 | 7 | import 'package:flutter/foundation.dart'; 8 | import 'package:flutter/rendering.dart'; 9 | 10 | import 'enums.dart'; 11 | 12 | @immutable 13 | class ScanResult { 14 | const ScanResult({ 15 | required this.state, 16 | this.code, 17 | this.phone = const [], 18 | }); 19 | 20 | factory ScanResult.fromJson(Map json) { 21 | return ScanResult( 22 | state: json['state'] as int, 23 | code: json['code']?.toString(), 24 | phone: filterPhoneList(json), 25 | ); 26 | } 27 | 28 | final int state; 29 | final String? code; 30 | final List phone; 31 | 32 | static List filterPhoneList(Map json) { 33 | final String? code = json['code']?.toString(); 34 | final Object? list = json['phone']; 35 | if (list == null || list is! List || list.isEmpty) { 36 | return []; 37 | } 38 | final Iterable notEmptyList = 39 | list.cast().where((String e) => e.isNotEmpty); 40 | if (notEmptyList.isEmpty) { 41 | return []; 42 | } 43 | // Obtain all phone numbers from the list. 44 | final Set numbers = notEmptyList.fold( 45 | {}, 46 | (Set p, String e) => p 47 | ..addAll( 48 | RegExp(r'1[3-9]\d{9}') 49 | .allMatches(e) 50 | .map((Match m) => m.group(0)) 51 | .whereType(), 52 | ), 53 | ); 54 | if (code != null) { 55 | numbers.removeWhere(code.contains); 56 | } 57 | return numbers.toList(); 58 | } 59 | 60 | ScanResultStatus get status => ScanResultStatus.from(state); 61 | 62 | Map toJson() { 63 | return { 64 | 'state': state, 65 | if (code != null) 'code': code, 66 | 'phone': phone, 67 | }; 68 | } 69 | 70 | @override 71 | bool operator ==(Object other) => 72 | identical(this, other) || 73 | other is ScanResult && 74 | runtimeType == other.runtimeType && 75 | state == other.state && 76 | code == other.code && 77 | phone == other.phone; 78 | 79 | @override 80 | int get hashCode => state.hashCode ^ code.hashCode ^ phone.hashCode; 81 | 82 | @override 83 | String toString() { 84 | return const JsonEncoder.withIndent(' ').convert(toJson()); 85 | } 86 | } 87 | 88 | @immutable 89 | class Barcode { 90 | const Barcode({ 91 | required this.value, 92 | this.boundingBox, 93 | }); 94 | 95 | factory Barcode.fromJson(Map map) { 96 | final Map? box = 97 | (map['boundingBox'] as Map?)?.cast(); 98 | return Barcode( 99 | value: map['value'].toString(), 100 | boundingBox: box == null 101 | ? null 102 | : Rect.fromLTWH( 103 | box['left']!.toDouble(), 104 | box['top']!.toDouble(), 105 | box['width']!.toDouble(), 106 | box['height']!.toDouble(), 107 | ), 108 | ); 109 | } 110 | 111 | final String value; 112 | final Rect? boundingBox; 113 | 114 | @override 115 | bool operator ==(Object other) => 116 | identical(this, other) || 117 | other is Barcode && 118 | runtimeType == other.runtimeType && 119 | value == other.value && 120 | boundingBox == other.boundingBox; 121 | 122 | @override 123 | int get hashCode => value.hashCode ^ boundingBox.hashCode; 124 | 125 | @override 126 | String toString() { 127 | return 'Barcode(value: $value, boundingBox: $boundingBox)'; 128 | } 129 | } 130 | 131 | enum BarcodeFormat { 132 | ALL_FORMATS(0), 133 | CODE_128(1), 134 | CODE_39(2), 135 | CODE_93(4), 136 | CODABAR(8), 137 | DATA_MATRIX(16), 138 | EAN_13(32), 139 | EAN_8(64), 140 | ITF(128), 141 | QR_CODE(256), 142 | UPC_A(512), 143 | UPC_E(1024), 144 | PDF417(2048), 145 | AZTEC(4096), 146 | ; 147 | 148 | const BarcodeFormat(this.code); 149 | 150 | final int code; 151 | } 152 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/alexv525/mlkit_scan_plugin/camera/CameraConfigurationManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2008 ZXing authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.alexv525.mlkit_scan_plugin.camera 17 | 18 | import android.content.Context 19 | import android.graphics.Rect 20 | import android.hardware.Camera 21 | import android.util.Log 22 | import android.util.Size 23 | import java.util.ArrayList 24 | 25 | /** 26 | * 设置相机的参数信息,获取最佳的预览界面 27 | */ 28 | class CameraConfigurationManager { 29 | companion object { 30 | private const val TAG = "CameraConfiguration" 31 | } 32 | 33 | private var mContext: Context? = null 34 | private var mCameraResolution: Size? = null 35 | 36 | // 相机分辨率 37 | val cameraResolution: Size? 38 | get() = mCameraResolution 39 | 40 | fun setContext(context: Context?) { 41 | mContext = context 42 | } 43 | 44 | fun release() { 45 | mCameraResolution = null 46 | mContext = null 47 | } 48 | 49 | fun initFromCameraParameters(camera: Camera, screenResolution: Size) { 50 | // 需要判断摄像头是否支持缩放 51 | val parameters = camera.parameters 52 | if (parameters.maxNumFocusAreas > 0) { 53 | val focusAreas: MutableList = ArrayList() 54 | val focusRect = Rect(-900, -900, 900, 0) 55 | focusAreas.add(Camera.Area(focusRect, 1000)) 56 | parameters.focusAreas = focusAreas 57 | } 58 | // 因为换成了竖屏显示,所以不替换屏幕宽高得出的预览图是变形的 59 | val screenResolutionForCamera = if (screenResolution.width < screenResolution.height) { 60 | Size(screenResolution.height, screenResolution.width) 61 | } else { 62 | Size(screenResolution.width, screenResolution.height) 63 | } 64 | mCameraResolution = CameraConfigurationUtils.findBestPreviewSizeValue( 65 | parameters, 66 | screenResolutionForCamera 67 | ) 68 | } 69 | 70 | fun setDesiredCameraParameters(camera: Camera) { 71 | val parameters = camera.parameters 72 | if (parameters == null) { 73 | Log.w( 74 | TAG, 75 | "Device error: no camera parameters are available. " + 76 | "Proceeding without configuration." 77 | ) 78 | return 79 | } 80 | Log.i(TAG, "Initial camera parameters: " + parameters.flatten()) 81 | parameters.setPreviewSize(mCameraResolution!!.width, mCameraResolution!!.height) 82 | camera.parameters = parameters 83 | val afterParameters = camera.parameters 84 | val afterSize = afterParameters.previewSize 85 | if (afterSize != null && (mCameraResolution?.width != afterSize.width || mCameraResolution?.height != afterSize.height)) { 86 | Log.w( 87 | TAG, 88 | "Camera said it supported preview size ${mCameraResolution?.width}x${mCameraResolution?.height}, " + 89 | "but after setting it, preview size is ${afterSize.width}x${afterSize.height}" 90 | ) 91 | mCameraResolution = Size(afterSize.width, afterSize.height) 92 | } 93 | 94 | /// 设置相机预览为竖屏 95 | camera.setDisplayOrientation(90) 96 | } 97 | 98 | fun toggleFlashlight(camera: Camera, enable: Boolean) { 99 | camera.parameters = camera.parameters?.apply { 100 | flashMode = if (enable) { 101 | Camera.Parameters.FLASH_MODE_TORCH 102 | } else { 103 | Camera.Parameters.FLASH_MODE_OFF 104 | } 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /lib/src/widgets/scan_view.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// [Author] Alex (https://github.com/AlexV525) 3 | /// [Date] 2021/11/29 16:30 4 | /// 5 | import 'dart:io' show Platform; 6 | import 'dart:ui' show window; 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter/services.dart'; 10 | 11 | import '../plugin/enums.dart'; 12 | import '../plugin/log_util.dart'; 13 | import '../plugin/scan_plugin.dart'; 14 | import '../plugin/scan_result.dart'; 15 | 16 | class ScanView extends StatefulWidget { 17 | const ScanView({ 18 | Key? key, 19 | required this.scanType, 20 | required this.scanRect, 21 | this.onViewCreated, 22 | this.resultListener, 23 | }) : super(key: key); 24 | 25 | final ScanType scanType; 26 | final Rect scanRect; 27 | final PlatformViewCreatedCallback? onViewCreated; 28 | final ScanResultCallback? resultListener; 29 | 30 | @override 31 | _ScanViewState createState() => _ScanViewState(); 32 | } 33 | 34 | class _ScanViewState extends State with WidgetsBindingObserver { 35 | late Widget? _scanView = createView(); 36 | late ScanResultCallback? _innerResultListener = widget.resultListener; 37 | late ScanResultCallback? _resultListener; 38 | 39 | @override 40 | void initState() { 41 | super.initState(); 42 | WidgetsBinding.instance.addObserver(this); 43 | if (_innerResultListener != null) { 44 | _resultListener = _resultListenerWrapper(_innerResultListener!); 45 | } 46 | } 47 | 48 | @override 49 | void didUpdateWidget(ScanView oldWidget) { 50 | super.didUpdateWidget(oldWidget); 51 | if (widget.scanRect != oldWidget.scanRect) { 52 | ScanPlugin.switchScanType(ScanPlugin.scanningType, rect: widget.scanRect); 53 | } 54 | if (widget.resultListener != oldWidget.resultListener) { 55 | if (oldWidget.resultListener != null) { 56 | ScanPlugin.removeListener(_innerResultListener!); 57 | _innerResultListener = null; 58 | _resultListener = null; 59 | } 60 | if (widget.resultListener != null) { 61 | _innerResultListener = widget.resultListener; 62 | _resultListener = _resultListenerWrapper(_innerResultListener!); 63 | ScanPlugin.addListener(_resultListener!); 64 | } 65 | } 66 | } 67 | 68 | @override 69 | void didChangeAppLifecycleState(AppLifecycleState state) { 70 | if (state == AppLifecycleState.resumed) { 71 | WidgetsBinding.instance.addPostFrameCallback((_) async { 72 | if (ScanPlugin.isScanningPaused) { 73 | ScanPlugin.resumeScan(); 74 | } 75 | }); 76 | } else if (!ScanPlugin.isScanningPaused) { 77 | ScanPlugin.pauseScan(); 78 | } 79 | } 80 | 81 | @override 82 | void dispose() { 83 | // Remove the scan view widget. 84 | _scanView = null; 85 | WidgetsBinding.instance.removeObserver(this); 86 | if (_resultListener != null) { 87 | ScanPlugin.removeListener(_resultListener!); 88 | } 89 | ScanPlugin.destroy(); 90 | ScanPlugin.stopScan().whenComplete(() => LogUtil.d('Scanning stopped.')); 91 | super.dispose(); 92 | } 93 | 94 | Future _onViewCreated(int id) async { 95 | widget.onViewCreated?.call(id); 96 | ScanPlugin.init(); 97 | if (_innerResultListener != null) { 98 | ScanPlugin.addListener(_resultListenerWrapper(widget.resultListener!)); 99 | } 100 | await ScanPlugin.initializeScanning( 101 | widget.scanRect, 102 | scanType: widget.scanType, 103 | ); 104 | } 105 | 106 | ScanResultCallback _resultListenerWrapper(ScanResultCallback cb) { 107 | return (ScanResult result) { 108 | if (!mounted) { 109 | return; 110 | } 111 | cb(result); 112 | }; 113 | } 114 | 115 | /// 根据平台获取原生 View 116 | Widget createView() { 117 | if (!Platform.isAndroid && !Platform.isIOS) { 118 | throw UnimplementedError( 119 | 'Scan view is not implemented for ' 120 | '${Platform.operatingSystem}.', 121 | ); 122 | } 123 | 124 | Widget _buildView(BoxConstraints cs) { 125 | final double ratio = MediaQueryData.fromWindow(window).devicePixelRatio; 126 | final int w = (cs.maxWidth * ratio).toInt(); 127 | final int h = (cs.maxHeight * ratio).toInt(); 128 | 129 | if (Platform.isIOS) { 130 | return UiKitView( 131 | viewType: ScanPlugin.platformViewType, 132 | onPlatformViewCreated: (int id) { 133 | LogUtil.d('UiKitView $id created.'); 134 | _onViewCreated(id); 135 | }, 136 | creationParams: {'w': w, 'h': h}, 137 | creationParamsCodec: const StandardMessageCodec(), 138 | ); 139 | } 140 | return AndroidView( 141 | viewType: ScanPlugin.platformViewType, 142 | onPlatformViewCreated: (int id) { 143 | LogUtil.d('AndroidView $id created.'); 144 | _onViewCreated(id); 145 | }, 146 | creationParams: {'w': w, 'h': h}, 147 | creationParamsCodec: const StandardMessageCodec(), 148 | ); 149 | } 150 | 151 | return LayoutBuilder(builder: (_, BoxConstraints cs) => _buildView(cs)); 152 | } 153 | 154 | @override 155 | Widget build(BuildContext context) => _scanView ?? const SizedBox.shrink(); 156 | } 157 | -------------------------------------------------------------------------------- /lib/src/mixins/notification_mixin.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// [Author] Alex (https://github.com/AlexV525) 3 | /// [Date] 2021/11/9 19:40 4 | /// 5 | import 'dart:async'; 6 | import 'dart:convert'; 7 | import 'dart:ui' as ui; 8 | 9 | import 'package:flutter/material.dart'; 10 | 11 | import '../plugin/constants.dart'; 12 | import '../resources.dart'; 13 | 14 | @optionalTypeArgs 15 | mixin ScanNotificationMixin on State { 16 | final ValueNotifier _isShowing = ValueNotifier(false); 17 | final ValueNotifier _content = 18 | ValueNotifier(null); 19 | 20 | /// 用于控制通知显示的计时器 21 | Timer? _notificationTimer; 22 | 23 | @override 24 | void dispose() { 25 | _isShowing.dispose(); 26 | _content.dispose(); 27 | _notificationTimer?.cancel(); 28 | super.dispose(); 29 | } 30 | 31 | void showNotification(ScanNotificationContent content) { 32 | if (mounted) { 33 | _content.value = content; 34 | _isShowing.value = true; 35 | _notificationTimer?.cancel(); 36 | _notificationTimer = Timer(const Duration(seconds: 3), () { 37 | _isShowing.value = false; 38 | }); 39 | } 40 | } 41 | 42 | Color _typeColor(ScanNotificationContent? content) { 43 | switch (content?.type) { 44 | case ScanNotificationType.error: 45 | return const Color(0xffe2423f); 46 | case ScanNotificationType.success: 47 | return const Color(0xff2ddf6e); 48 | case ScanNotificationType.warning: 49 | return const Color(0xfff6b005); 50 | default: 51 | return const Color(0xff3271f6); 52 | } 53 | } 54 | 55 | Widget _typeIcon(ScanNotificationContent? content) { 56 | switch (content?.type) { 57 | case ScanNotificationType.info: 58 | case ScanNotificationType.warning: 59 | return Padding( 60 | padding: const EdgeInsets.only(right: 4), 61 | child: Image.asset( 62 | ScanPluginR.ASSETS_NOTIFICATION_WARNING_ICON_PNG, 63 | width: 20, 64 | height: 20, 65 | package: ScanPluginPackage, 66 | ), 67 | ); 68 | case ScanNotificationType.success: 69 | return Padding( 70 | padding: const EdgeInsets.only(right: 4), 71 | child: Image.asset( 72 | ScanPluginR.ASSETS_NOTIFICATION_SUCCESS_ICON_PNG, 73 | width: 20, 74 | height: 20, 75 | package: ScanPluginPackage, 76 | ), 77 | ); 78 | default: 79 | return const SizedBox.shrink(); 80 | } 81 | } 82 | 83 | Widget _contentWidget( 84 | BuildContext context, 85 | ScanNotificationContent? content, 86 | ) { 87 | return Flexible( 88 | child: Text( 89 | content?.content ?? '', 90 | style: const TextStyle( 91 | color: Colors.white, 92 | fontSize: 20, 93 | fontWeight: FontWeight.w500, 94 | ), 95 | textAlign: TextAlign.center, 96 | ), 97 | ); 98 | } 99 | 100 | Positioned notificationOverlay(BuildContext context) { 101 | return Positioned.fill( 102 | top: 0, 103 | bottom: null, 104 | child: ValueListenableBuilder( 105 | valueListenable: _isShowing, 106 | builder: (_, bool isShowing, Widget? child) => AnimatedAlign( 107 | curve: Curves.ease, 108 | duration: kThemeAnimationDuration * 2, 109 | alignment: Alignment.bottomCenter, 110 | heightFactor: isShowing ? 1 : 0, 111 | child: child!, 112 | ), 113 | child: ValueListenableBuilder( 114 | valueListenable: _content, 115 | builder: (_, ScanNotificationContent? content, __) { 116 | return AnimatedContainer( 117 | duration: kThemeAnimationDuration, 118 | decoration: BoxDecoration( 119 | borderRadius: const BorderRadius.only( 120 | bottomLeft: Radius.circular(15), 121 | bottomRight: Radius.circular(15), 122 | ), 123 | color: _typeColor(content), 124 | ), 125 | child: Container( 126 | margin: EdgeInsets.only( 127 | top: MediaQueryData.fromWindow(ui.window).padding.top, 128 | ), 129 | padding: const EdgeInsets.all(10), 130 | constraints: const BoxConstraints(minHeight: kToolbarHeight), 131 | child: DefaultTextStyle.merge( 132 | style: const TextStyle( 133 | color: Colors.white, 134 | fontSize: 20, 135 | inherit: false, 136 | ), 137 | child: Row( 138 | mainAxisAlignment: MainAxisAlignment.center, 139 | children: [ 140 | _typeIcon(content), 141 | _contentWidget(context, content), 142 | ], 143 | ), 144 | ), 145 | ), 146 | ); 147 | }, 148 | ), 149 | ), 150 | ); 151 | } 152 | } 153 | 154 | enum ScanNotificationType { success, info, warning, error } 155 | 156 | class ScanNotificationContent { 157 | const ScanNotificationContent({ 158 | this.content = '', 159 | this.type = ScanNotificationType.info, 160 | }); 161 | 162 | factory ScanNotificationContent.success([String content = '']) { 163 | return ScanNotificationContent( 164 | content: content, 165 | type: ScanNotificationType.success, 166 | ); 167 | } 168 | 169 | factory ScanNotificationContent.warning([String content = '']) { 170 | return ScanNotificationContent( 171 | content: content, 172 | type: ScanNotificationType.warning, 173 | ); 174 | } 175 | 176 | factory ScanNotificationContent.error([String content = '']) { 177 | return ScanNotificationContent( 178 | content: content, 179 | type: ScanNotificationType.error, 180 | ); 181 | } 182 | 183 | final String content; 184 | final ScanNotificationType type; 185 | 186 | Map toJson() => { 187 | 'content': content, 188 | 'type': type.toString(), 189 | }; 190 | 191 | @override 192 | String toString() => const JsonEncoder.withIndent(' ').convert(toJson()); 193 | } 194 | -------------------------------------------------------------------------------- /ios/Classes/Scan/ImageParser.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class ImageParser { 4 | var isProcessing: Bool = false 5 | 6 | func reScale(image: UIImage, maxSize: CGSize) -> UIImage { 7 | let scaleFactor = max( 8 | image.size.width / maxSize.width, 9 | image.size.height / maxSize.height 10 | ) 11 | let newSize = CGSize( 12 | width: image.size.width / scaleFactor, 13 | height: image.size.height / scaleFactor 14 | ) 15 | UIGraphicsBeginImageContextWithOptions(newSize, false, 0.0) 16 | image.draw(in: CGRectMake(0, 0, newSize.width, newSize.height)) 17 | let newImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()! 18 | UIGraphicsEndImageContext() 19 | return newImage 20 | } 21 | 22 | /// 裁剪图片 23 | /// rect需要裁剪的图片相对于原图的范围 24 | func crop(image: UIImage, rect: CGRect) -> UIImage? { 25 | isProcessing = true 26 | guard let orientatedImage = fixOrientation(image) else { 27 | isProcessing = false 28 | return nil 29 | } 30 | let imageViewScale = max( 31 | orientatedImage.size.width / UIScreen.main.bounds.width, 32 | orientatedImage.size.height / UIScreen.main.bounds.height 33 | ) 34 | let newRect = CGRect( 35 | x: rect.origin.x * imageViewScale, 36 | y: rect.origin.y * imageViewScale, 37 | width: rect.size.width * imageViewScale, 38 | height: rect.size.height * imageViewScale 39 | ) 40 | guard let cgImage = orientatedImage.cgImage?.cropping(to: newRect) else { 41 | isProcessing = false 42 | return nil 43 | } 44 | let resultImage = UIImage(cgImage: cgImage) 45 | isProcessing = false 46 | return resultImage 47 | } 48 | 49 | func sampleBufferToImage(_ sampleBuffer: CMSampleBuffer) -> UIImage? { 50 | isProcessing = true 51 | // 转为CVImageBuffer 52 | guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { 53 | isProcessing = false 54 | return nil 55 | } 56 | // 地址空间上锁 57 | CVPixelBufferLockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0)) 58 | // 获取图片的内存信息 59 | // 内存指针 60 | let baseAddress = CVPixelBufferGetBaseAddress(imageBuffer) 61 | let bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer) 62 | let width = CVPixelBufferGetWidth(imageBuffer) 63 | let height = CVPixelBufferGetHeight(imageBuffer) 64 | let colorSpace = CGColorSpaceCreateDeviceRGB() 65 | // 位图的配置信息,如Alpha等 66 | let bitmapInfo = CGBitmapInfo( 67 | rawValue: CGImageAlphaInfo.noneSkipFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue 68 | ) 69 | // 获取上下文环境 70 | guard let context = CGContext( 71 | data: baseAddress, 72 | width: width, 73 | height: height, 74 | bitsPerComponent: 8, 75 | bytesPerRow: bytesPerRow, 76 | space: colorSpace, 77 | bitmapInfo: bitmapInfo.rawValue 78 | ) 79 | else { 80 | isProcessing = false 81 | return nil 82 | } 83 | // 上下文转存CGImage 84 | guard let cgImage = context.makeImage() else { 85 | isProcessing = false 86 | return nil 87 | } 88 | // 解锁 89 | CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0)); 90 | isProcessing = false 91 | // 返回Image数据 92 | return UIImage( 93 | cgImage: cgImage, 94 | scale: UIScreen.main.scale, 95 | orientation: imageOrientation(fromDevicePosition: .back) 96 | ) 97 | } 98 | 99 | private func fixOrientation(_ image: UIImage) -> UIImage? { 100 | if image.imageOrientation == .up { 101 | return image 102 | } 103 | guard let cgImage = image.cgImage, let colorSpace = cgImage.colorSpace else { 104 | return nil 105 | } 106 | let size = image.size 107 | var transform = CGAffineTransform.identity 108 | switch image.imageOrientation { 109 | case .down, .downMirrored: 110 | transform = transform.translatedBy(x: size.width, y: size.height) 111 | transform = transform.rotated(by: .pi) 112 | break 113 | case .left, .leftMirrored: 114 | transform = transform.translatedBy(x: size.width, y: 0) 115 | transform = transform.rotated(by: .pi / 2) 116 | break 117 | case .right, .rightMirrored: 118 | transform = transform.translatedBy(x: 0, y: size.height) 119 | transform = transform.rotated(by: -.pi / 2) 120 | break 121 | default: 122 | break 123 | } 124 | switch image.imageOrientation { 125 | case .upMirrored, .downMirrored: 126 | transform = transform.translatedBy(x: size.width, y: 0) 127 | transform = transform.scaledBy(x: -1, y: 1) 128 | break 129 | case .leftMirrored, .rightMirrored: 130 | transform = transform.translatedBy(x: size.height, y: 0); 131 | transform = transform.scaledBy(x: -1, y: 1) 132 | break 133 | default: 134 | break 135 | } 136 | let context = CGContext( 137 | data: nil, 138 | width: Int(size.width), 139 | height: Int(size.height), 140 | bitsPerComponent: cgImage.bitsPerComponent, 141 | bytesPerRow: 0, 142 | space: colorSpace, 143 | bitmapInfo: cgImage.bitmapInfo.rawValue 144 | ) 145 | context?.concatenate(transform) 146 | switch image.imageOrientation { 147 | case .left, .leftMirrored, .right, .rightMirrored: 148 | context?.draw( 149 | image.cgImage!, 150 | in: CGRect( 151 | x: CGFloat(0), 152 | y: CGFloat(0), 153 | width: CGFloat(size.height), 154 | height: CGFloat(size.width) 155 | ) 156 | ) 157 | break 158 | default: 159 | context?.draw( 160 | image.cgImage!, 161 | in: CGRect( 162 | x: CGFloat(0), 163 | y: CGFloat(0), 164 | width: CGFloat(size.width), 165 | height: CGFloat(size.height) 166 | ) 167 | ) 168 | break 169 | } 170 | guard let fixedCGImage = context?.makeImage() else { return nil } 171 | let resultImage = UIImage(cgImage: fixedCGImage) 172 | return resultImage 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /lib/src/widgets/scan_rect_widget.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// [Author] Alex (https://github.com/AlexV525) 3 | /// [Date] 2021/6/1 16:24 4 | /// 5 | import 'dart:async'; 6 | 7 | import 'package:flutter/material.dart'; 8 | 9 | import '../plugin/constants.dart'; 10 | import '../resources.dart'; 11 | import 'scan_rect_painter.dart'; 12 | 13 | class ScanRectWidget extends ImplicitlyAnimatedWidget { 14 | const ScanRectWidget({ 15 | Key? key, 16 | required this.height, 17 | required this.padding, 18 | this.tipBuilder, 19 | this.result, 20 | this.description, 21 | Curve curve = Curves.easeOutQuart, 22 | required Duration duration, 23 | VoidCallback? onEnd, 24 | }) : super(key: key, curve: curve, duration: duration, onEnd: onEnd); 25 | 26 | final double height; 27 | final EdgeInsets padding; 28 | 29 | /// 中间部分提示展示 30 | final WidgetBuilder? tipBuilder; 31 | 32 | /// 结果展示 33 | final String? result; 34 | 35 | /// 底部描述展示 36 | final String? description; 37 | 38 | @override 39 | _ScanRectWidgetState createState() => _ScanRectWidgetState(); 40 | } 41 | 42 | class _ScanRectWidgetState extends AnimatedWidgetBaseState { 43 | late final ValueNotifier _isResultDisplaying = 44 | ValueNotifier(widget.result != null); 45 | 46 | Tween? _height; 47 | Timer? _resultDisplayTimer; 48 | 49 | @override 50 | void forEachTween(TweenVisitor visitor) { 51 | _height = visitor( 52 | _height, 53 | widget.height, 54 | (dynamic value) => Tween(begin: value as double), 55 | ) as Tween?; 56 | } 57 | 58 | @override 59 | void didUpdateWidget(ScanRectWidget oldWidget) { 60 | super.didUpdateWidget(oldWidget); 61 | if (widget.result != null) { 62 | _showResult(); 63 | } else { 64 | _isResultDisplaying.value = false; 65 | } 66 | } 67 | 68 | void _showResult() { 69 | _isResultDisplaying.value = true; 70 | _resultDisplayTimer?.cancel(); 71 | _resultDisplayTimer = Timer(const Duration(seconds: 3), () { 72 | _isResultDisplaying.value = false; 73 | }); 74 | } 75 | 76 | Widget _tipWidget(BuildContext context) { 77 | return DefaultTextStyle.merge( 78 | style: TextStyle( 79 | color: Colors.grey[600], 80 | fontSize: 34, 81 | fontWeight: FontWeight.bold, 82 | ), 83 | child: widget.tipBuilder!(context), 84 | ); 85 | } 86 | 87 | Widget _resultWidget(BuildContext context) { 88 | return Center( 89 | child: Container( 90 | margin: const EdgeInsets.all(10), 91 | padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), 92 | decoration: BoxDecoration( 93 | borderRadius: BorderRadius.circular(10), 94 | color: Theme.of(context).dividerColor.withOpacity(.25), 95 | ), 96 | child: Text( 97 | widget.result!, 98 | style: const TextStyle( 99 | color: Colors.white, 100 | fontSize: 16, 101 | fontWeight: FontWeight.w500, 102 | ), 103 | textAlign: TextAlign.center, 104 | ), 105 | ), 106 | ); 107 | } 108 | 109 | Widget _descriptionWidget(BuildContext context, Size size) { 110 | return Container( 111 | width: size.width, 112 | margin: const EdgeInsets.all(10), 113 | padding: const EdgeInsets.symmetric(vertical: 6), 114 | decoration: BoxDecoration( 115 | borderRadius: BorderRadius.circular(999999), 116 | color: Theme.of(context).dividerColor.withOpacity(.25), 117 | ), 118 | child: Text( 119 | widget.description!, 120 | style: const TextStyle(color: Colors.white, fontSize: 18), 121 | textAlign: TextAlign.center, 122 | ), 123 | ); 124 | } 125 | 126 | @override 127 | Widget build(BuildContext context) { 128 | final double screenWidth = MediaQuery.of(context).size.width; 129 | final double evaluatingHeight = _height!.evaluate(animation); 130 | final Size size = Size( 131 | screenWidth - widget.padding.horizontal, 132 | evaluatingHeight, 133 | ); 134 | return Material( 135 | type: MaterialType.transparency, 136 | child: SizedBox.fromSize( 137 | size: Size(screenWidth, evaluatingHeight), 138 | child: CustomPaint( 139 | size: size, 140 | painter: ScanRectPainter( 141 | height: evaluatingHeight, 142 | padding: widget.padding, 143 | ), 144 | child: Align( 145 | alignment: Alignment.topCenter, 146 | child: Column( 147 | mainAxisSize: MainAxisSize.min, 148 | children: [ 149 | Container( 150 | width: double.maxFinite, 151 | height: evaluatingHeight, 152 | margin: widget.padding, 153 | child: Stack( 154 | fit: StackFit.expand, 155 | children: [ 156 | if (widget.tipBuilder != null) _tipWidget(context), 157 | _ScanLineWidget(size), 158 | ValueListenableBuilder( 159 | valueListenable: _isResultDisplaying, 160 | builder: (_, bool value, __) => Positioned.fill( 161 | top: null, 162 | child: AnimatedSwitcher( 163 | duration: kThemeChangeDuration, 164 | child: value 165 | ? _resultWidget(context) 166 | : const SizedBox.shrink(), 167 | ), 168 | ), 169 | ), 170 | ], 171 | ), 172 | ), 173 | if (widget.description != null) 174 | _descriptionWidget(context, size), 175 | ], 176 | ), 177 | ), 178 | ), 179 | ), 180 | ); 181 | } 182 | } 183 | 184 | class _ScanLineWidget extends StatefulWidget { 185 | const _ScanLineWidget(this.size, {Key? key}) : super(key: key); 186 | 187 | final Size size; 188 | 189 | @override 190 | _ScanLineWidgetState createState() => _ScanLineWidgetState(); 191 | } 192 | 193 | class _ScanLineWidgetState extends State<_ScanLineWidget> 194 | with SingleTickerProviderStateMixin { 195 | late final AnimationController _controller = AnimationController( 196 | upperBound: .9, 197 | duration: const Duration(seconds: 5), 198 | vsync: this, 199 | )..repeat(); 200 | late final CurvedAnimation _animation = CurvedAnimation( 201 | curve: Curves.easeInOutQuad, 202 | parent: _controller, 203 | ); 204 | 205 | @override 206 | void didUpdateWidget(_ScanLineWidget oldWidget) { 207 | super.didUpdateWidget(oldWidget); 208 | if (widget.size != oldWidget.size) { 209 | _controller 210 | ..stop() 211 | ..reset() 212 | ..repeat(); 213 | } 214 | } 215 | 216 | @override 217 | void dispose() { 218 | _controller.dispose(); 219 | super.dispose(); 220 | } 221 | 222 | @override 223 | Widget build(BuildContext context) { 224 | return Align( 225 | alignment: Alignment.topCenter, 226 | child: AnimatedBuilder( 227 | animation: _animation, 228 | builder: (_, Widget? child) => Transform.translate( 229 | offset: Offset(0, _animation.value * widget.size.height), 230 | child: child, 231 | ), 232 | child: Image.asset( 233 | ScanPluginR.ASSETS_SCAN_LINE_PNG, 234 | package: ScanPluginPackage, 235 | ), 236 | ), 237 | ); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/alexv525/mlkit_scan_plugin/decode/Decoder.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Alex (https://github.com/AlexV525) 3 | * Date: 2022/1/10 17:32 4 | */ 5 | 6 | package com.alexv525.mlkit_scan_plugin.decode 7 | 8 | import android.graphics.Bitmap 9 | import android.graphics.Matrix 10 | import android.os.Handler 11 | import android.os.Looper 12 | import android.renderscript.* 13 | import com.alexv525.mlkit_scan_plugin.* 14 | import com.alexv525.mlkit_scan_plugin.Extension 15 | import com.alexv525.mlkit_scan_plugin.vision.FrameMetadata 16 | import com.alexv525.mlkit_scan_plugin.vision.processor.BarcodeScannerProcessor 17 | import com.alexv525.mlkit_scan_plugin.vision.processor.TextRecognitionProcessor 18 | import com.google.mlkit.vision.barcode.common.Barcode 19 | import com.google.mlkit.vision.text.Text 20 | import java.lang.ref.WeakReference 21 | 22 | class Decoder(private val mScanPlugin: MLKitScanPlugin) { 23 | private val weakSelf = WeakReference(this) 24 | private val weakDecoder: Decoder? get() = weakSelf.get() 25 | 26 | private enum class State { PREVIEW, SUCCESS, DONE } 27 | 28 | private var mLastDecodeTime: Long = System.currentTimeMillis() 29 | private var mState: State = State.PREVIEW 30 | private var mScanResult = ScanResult() 31 | 32 | private val scanType get() = mScanPlugin.scanType 33 | private val isScanningMobile 34 | get() = scanType == Constant.SCAN_TYPE_BARCODE_AND_MOBILE 35 | || scanType == Constant.SCAN_TYPE_MOBILE 36 | 37 | private val barcodeFormats: IntArray? 38 | get() = when (scanType) { 39 | Constant.SCAN_TYPE_BARCODE, Constant.SCAN_TYPE_BARCODE_AND_MOBILE -> intArrayOf( 40 | Barcode.FORMAT_CODE_39, 41 | Barcode.FORMAT_CODE_93, 42 | Barcode.FORMAT_CODE_128 43 | ) 44 | Constant.SCAN_TYPE_GOODS_CODE -> intArrayOf( 45 | Barcode.FORMAT_EAN_8, 46 | Barcode.FORMAT_EAN_13, 47 | Barcode.FORMAT_UPC_A, 48 | Barcode.FORMAT_UPC_E 49 | ) 50 | Constant.SCAN_TYPE_QR_CODE -> intArrayOf(Barcode.FORMAT_QR_CODE) 51 | else -> null 52 | } 53 | 54 | private fun runInMainThread(runnable: Runnable) { 55 | Handler(Looper.getMainLooper()).post(runnable) 56 | } 57 | 58 | fun decode(data: ByteArray, frameMetadata: FrameMetadata) { 59 | if (weakDecoder == null || mState == State.DONE || !mScanPlugin.isDecoding) { 60 | return 61 | } 62 | // Skip decoding too rapidly. 63 | val currentTime = System.currentTimeMillis() 64 | if (currentTime - mLastDecodeTime < Constant.DECODE_INTERVAL) { 65 | validateResult() 66 | return 67 | } 68 | val imageMaxWidth = mScanPlugin.screenSize.width 69 | val imageMaxHeight = mScanPlugin.screenSize.height 70 | mLastDecodeTime = currentTime 71 | runInBackground { 72 | if (scanType == Constant.SCAN_TYPE_WAIT) { 73 | return@runInBackground 74 | } 75 | val firstBitmap = makeMatrixBitmap(data, frameMetadata) 76 | val rect = mScanPlugin.cropRect 77 | val croppedBitmap = Bitmap.createBitmap( 78 | firstBitmap, 79 | rect.left, 80 | rect.top, 81 | rect.width().coerceAtMost(firstBitmap.width), 82 | rect.height().coerceAtMost(firstBitmap.height), 83 | null, 84 | false 85 | ) 86 | firstBitmap.apply { if (!isRecycled) recycle() } 87 | // Decode barcodes when formats are valid and the code in the result is empty. 88 | barcodeFormats?.apply { 89 | if (mScanResult.code.isNullOrBlank()) { 90 | BarcodeScannerProcessor( 91 | formats = this, 92 | onSuccessUnit = { handleBarcodes(it) }, 93 | imageMaxWidth = imageMaxWidth, 94 | imageMaxHeight = imageMaxHeight 95 | ).processBitmap(croppedBitmap) 96 | } 97 | } 98 | // Recognize texts when the scan type is valid and the phone in the result is empty. 99 | if (isScanningMobile && mScanResult.phone.isEmpty() 100 | ) { 101 | TextRecognitionProcessor( 102 | onSuccessUnit = { handleText(it) }, 103 | imageMaxWidth = imageMaxWidth, 104 | imageMaxHeight = imageMaxHeight 105 | ).processBitmap(croppedBitmap) 106 | } 107 | } 108 | } 109 | 110 | private fun validateResult() { 111 | // Skip validating when the reference has been cleaned. 112 | if (weakDecoder == null) { 113 | return 114 | } 115 | val predicate: Boolean = when (scanType) { 116 | Constant.SCAN_TYPE_BARCODE_AND_MOBILE -> mScanResult.isFullFilled || mScanResult.isCodeOnly 117 | Constant.SCAN_TYPE_MOBILE -> mScanResult.phone.isNotEmpty() 118 | Constant.SCAN_TYPE_BARCODE, 119 | Constant.SCAN_TYPE_QR_CODE, 120 | Constant.SCAN_TYPE_GOODS_CODE -> mScanResult.isCodeOnly 121 | else -> false 122 | } 123 | // Post delayed validation when scanning with the hybrid mode. 124 | if (scanType == Constant.SCAN_TYPE_BARCODE_AND_MOBILE && mScanResult.isCodeOnly) { 125 | Handler(Looper.getMainLooper()).postDelayed({ 126 | weakDecoder?.validateResult() 127 | }, Constant.DECODE_INTERVAL * 5L) 128 | return 129 | } 130 | if (predicate) { 131 | val resultMap = mScanResult.toMap(scanType) 132 | mState = State.SUCCESS 133 | runInMainThread { Shared.eventSink?.success(resultMap) } 134 | mScanResult.reset() 135 | return 136 | } 137 | mScanPlugin.restartPreviewAndDecode() 138 | } 139 | 140 | private fun handleBarcodes(list: List) { 141 | val codes = list.fold(LinkedHashSet()) { set, e -> 142 | set.apply set@{ e.displayValue?.apply { this@set.add(this) } } 143 | } 144 | if (codes.isNotEmpty()) { 145 | mScanResult.code = codes.first() 146 | } 147 | validateResult() 148 | } 149 | 150 | private fun handleText(text: Text) { 151 | val texts = text.textBlocks.fold(mutableListOf()) { set, e -> 152 | set.apply { 153 | addAll(e.lines.fold(mutableListOf()) { set, e -> 154 | // Save only digits. 155 | set.apply { add(e.text.filter { it.isDigit() }) } 156 | }) 157 | } 158 | }.filter { it.isNotEmpty() && it.length > 10 } 159 | mScanResult.phone.addAll(texts) 160 | Handler(Looper.getMainLooper()).postDelayed({ 161 | weakDecoder?.validateResult() 162 | }, Constant.DECODE_INTERVAL * 2L) 163 | } 164 | 165 | fun destroy() { 166 | mState = State.DONE 167 | mScanResult.reset() 168 | } 169 | 170 | private fun makeMatrixBitmap(data: ByteArray, frameMetadata: FrameMetadata): Bitmap { 171 | val mWidth = frameMetadata.width 172 | val mHeight = frameMetadata.height 173 | val mRotation = frameMetadata.rotation.toFloat() 174 | val bitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888) 175 | renderScriptNV21ToRGBA8888(mWidth, mHeight, data).apply { 176 | copyTo(bitmap) 177 | destroy() 178 | } 179 | val matrix = Matrix().apply { postRotate(mRotation) } 180 | val rotatedBitmap = Bitmap.createBitmap( 181 | bitmap, 0, 0, mWidth, mHeight, 182 | matrix, true 183 | ) 184 | bitmap.apply { if (!isRecycled) recycle() } 185 | return rotatedBitmap 186 | } 187 | 188 | private fun renderScriptNV21ToRGBA8888(width: Int, height: Int, nv21: ByteArray): Allocation { 189 | val rs: RenderScript = RenderScript.create(mScanPlugin.context!!) 190 | val inAllocation = Allocation.createTyped( 191 | rs, 192 | Type.Builder(rs, Element.U8(rs)).setX(nv21.size).create(), 193 | Allocation.USAGE_SCRIPT 194 | ) 195 | val outAllocation = Allocation.createTyped( 196 | rs, 197 | Type.Builder(rs, Element.RGBA_8888(rs)).setX(width).setY(height).create(), 198 | Allocation.USAGE_SCRIPT 199 | ) 200 | inAllocation.copyFrom(nv21) 201 | ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs)).apply { 202 | setInput(inAllocation) 203 | forEach(outAllocation) 204 | destroy() 205 | } 206 | inAllocation.destroy() 207 | return outAllocation 208 | } 209 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/com/alexv525/mlkit_scan_plugin/vision/processor/VisionProcessorBase.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Alex (https://github.com/AlexV525) 3 | * Date: 2022/1/10 17:32 4 | */ 5 | 6 | package com.alexv525.mlkit_scan_plugin.vision.processor 7 | 8 | import android.graphics.Bitmap 9 | import android.os.SystemClock 10 | import android.util.Log 11 | import android.util.Pair 12 | import androidx.annotation.CallSuper 13 | import androidx.annotation.GuardedBy 14 | import com.google.android.gms.tasks.Task 15 | import com.google.android.gms.tasks.Tasks 16 | import com.google.android.odml.image.MlImage 17 | import com.google.mlkit.common.MlKitException 18 | import com.google.mlkit.vision.common.InputImage 19 | import com.alexv525.mlkit_scan_plugin.Shared 20 | import com.alexv525.mlkit_scan_plugin.vision.FrameMetadata 21 | import com.alexv525.mlkit_scan_plugin.vision.ScopedExecutor 22 | import java.nio.ByteBuffer 23 | import java.util.Timer 24 | import java.util.TimerTask 25 | 26 | /* 27 | * Abstract base class for ML Kit frame processors. Subclasses need to implement {@link 28 | * #onSuccess(T, FrameMetadata, GraphicOverlay)} to define what they want to with the detection 29 | * results and {@link #detectInImage(VisionImage)} to specify the detector object. 30 | * 31 | * @param The type of the detected feature. 32 | */ 33 | abstract class VisionProcessorBase( 34 | private val onSuccessUnit: ((results: T) -> Unit)? = null, 35 | private val onFailureUnit: ((e: Exception) -> Unit)? = null, 36 | private var imageMaxWidth: Int = 0, 37 | private var imageMaxHeight: Int = 0 38 | ) : VisionImageProcessor { 39 | companion object { 40 | private const val TAG = "VisionProcessorBase" 41 | } 42 | 43 | private val fpsTimer = Timer() 44 | private val executor = ScopedExecutor { command -> Shared.threadPool.execute(command) } 45 | 46 | // Whether this processor is already shut down 47 | private var isShutdown = false 48 | 49 | // Used to calculate latency, running in the same thread, no sync needed. 50 | private var numRuns = 0 51 | private var totalFrameMs = 0L 52 | private var maxFrameMs = 0L 53 | private var minFrameMs = Long.MAX_VALUE 54 | private var totalDetectorMs = 0L 55 | private var maxDetectorMs = 0L 56 | private var minDetectorMs = Long.MAX_VALUE 57 | 58 | // Frame count that have been processed so far in an one second interval to calculate FPS. 59 | private var frameProcessedInOneSecondInterval = 0 60 | private var framesPerSecond = 0 61 | 62 | // To keep the latest images and its metadata. 63 | @GuardedBy("this") 64 | private var latestImage: ByteBuffer? = null 65 | 66 | @GuardedBy("this") 67 | private var latestImageMetaData: FrameMetadata? = null 68 | 69 | // To keep the images and metadata in process. 70 | @GuardedBy("this") 71 | private var processingImage: ByteBuffer? = null 72 | 73 | @GuardedBy("this") 74 | private var processingMetaData: FrameMetadata? = null 75 | 76 | init { 77 | fpsTimer.scheduleAtFixedRate( 78 | object : TimerTask() { 79 | override fun run() { 80 | framesPerSecond = frameProcessedInOneSecondInterval 81 | frameProcessedInOneSecondInterval = 0 82 | } 83 | }, 84 | 0, 85 | 1000 86 | ) 87 | } 88 | 89 | // Code for processing single still image 90 | override fun processBitmap(bitmap: Bitmap?, rotation: Int) { 91 | bitmap!! 92 | val frameStartMs = SystemClock.elapsedRealtime() 93 | 94 | val resizedBitmap = if (imageMaxWidth != 0 && imageMaxHeight != 0) { 95 | if (bitmap.width <= imageMaxWidth && bitmap.height <= imageMaxHeight) bitmap else { 96 | // Get the dimensions of the image view. 97 | val targetedSize: Pair = Pair(imageMaxWidth, imageMaxHeight) 98 | // Determine how much to scale down the image. 99 | val scaleFactor = (bitmap.width.toFloat() / targetedSize.first.toFloat()).coerceAtLeast( 100 | bitmap.height.toFloat() / targetedSize.second.toFloat() 101 | ) 102 | Bitmap.createScaledBitmap( 103 | bitmap, 104 | (bitmap.width / scaleFactor).toInt(), 105 | (bitmap.height / scaleFactor).toInt(), 106 | true 107 | ) 108 | } 109 | } else bitmap 110 | requestDetectInImage( 111 | InputImage.fromBitmap(resizedBitmap!!, rotation), 112 | frameStartMs 113 | ) 114 | } 115 | 116 | // Code for processing live preview frame from Camera1 API 117 | @Synchronized 118 | override fun processByteBuffer(data: ByteBuffer?, frameMetadata: FrameMetadata?) { 119 | latestImage = data 120 | latestImageMetaData = frameMetadata 121 | if (processingImage == null && processingMetaData == null) { 122 | processLatestImage() 123 | } 124 | } 125 | 126 | @Synchronized 127 | private fun processLatestImage() { 128 | processingImage = latestImage 129 | processingMetaData = latestImageMetaData 130 | latestImage = null 131 | latestImageMetaData = null 132 | if (processingImage != null && processingMetaData != null && !isShutdown) { 133 | processImage(processingImage!!, processingMetaData!!) 134 | } 135 | } 136 | 137 | private fun processImage(data: ByteBuffer, frameMetadata: FrameMetadata) { 138 | val frameStartMs = SystemClock.elapsedRealtime() 139 | 140 | requestDetectInImage( 141 | InputImage.fromByteBuffer( 142 | data, 143 | frameMetadata.width, 144 | frameMetadata.height, 145 | frameMetadata.rotation, 146 | InputImage.IMAGE_FORMAT_NV21 147 | ), 148 | frameStartMs 149 | ).addOnSuccessListener(executor) { processLatestImage() } 150 | } 151 | 152 | // Common processing logic 153 | private fun requestDetectInImage(image: InputImage, frameStartMs: Long): Task { 154 | return setUpListener(detectInImage(image), frameStartMs) 155 | } 156 | 157 | private fun requestDetectInImage(image: MlImage, frameStartMs: Long): Task { 158 | return setUpListener( 159 | detectInImage(image), 160 | frameStartMs 161 | ) 162 | } 163 | 164 | private fun setUpListener(task: Task, frameStartMs: Long): Task { 165 | val detectorStartMs = SystemClock.elapsedRealtime() 166 | return task.addOnSuccessListener(executor) { results: T -> 167 | val endMs = SystemClock.elapsedRealtime() 168 | val currentFrameLatencyMs = endMs - frameStartMs 169 | val currentDetectorLatencyMs = endMs - detectorStartMs 170 | if (numRuns >= 500) { 171 | resetLatencyStats() 172 | } 173 | numRuns++ 174 | frameProcessedInOneSecondInterval++ 175 | totalFrameMs += currentFrameLatencyMs 176 | maxFrameMs = currentFrameLatencyMs.coerceAtLeast(maxFrameMs) 177 | minFrameMs = currentFrameLatencyMs.coerceAtMost(minFrameMs) 178 | totalDetectorMs += currentDetectorLatencyMs 179 | maxDetectorMs = currentDetectorLatencyMs.coerceAtLeast(maxDetectorMs) 180 | minDetectorMs = currentDetectorLatencyMs.coerceAtMost(minDetectorMs) 181 | 182 | // Only log inference info once per second. When frameProcessedInOneSecondInterval is 183 | // equal to 1, it means this is the first frame processed during the current second. 184 | // if (frameProcessedInOneSecondInterval == 1) { 185 | // Log.d(TAG, "Num of Runs: $numRuns") 186 | // Log.d( 187 | // TAG, 188 | // "Frame latency: max=" + 189 | // maxFrameMs + 190 | // ", min=" + 191 | // minFrameMs + 192 | // ", avg=" + 193 | // totalFrameMs / numRuns 194 | // ) 195 | // Log.d( 196 | // TAG, 197 | // "Detector latency: max=" + 198 | // maxDetectorMs + 199 | // ", min=" + 200 | // minDetectorMs + 201 | // ", avg=" + 202 | // totalDetectorMs / numRuns 203 | // ) 204 | // } 205 | this@VisionProcessorBase.onSuccess(results) 206 | }.addOnFailureListener(executor) { e: Exception -> 207 | val error = "Failed to process. Error: " + e.localizedMessage 208 | Log.e(TAG, error) 209 | e.printStackTrace() 210 | this@VisionProcessorBase.onFailure(e) 211 | } 212 | } 213 | 214 | override fun stop() { 215 | executor.shutdown() 216 | isShutdown = true 217 | resetLatencyStats() 218 | fpsTimer.cancel() 219 | } 220 | 221 | private fun resetLatencyStats() { 222 | numRuns = 0 223 | totalFrameMs = 0 224 | maxFrameMs = 0 225 | minFrameMs = Long.MAX_VALUE 226 | totalDetectorMs = 0 227 | maxDetectorMs = 0 228 | minDetectorMs = Long.MAX_VALUE 229 | } 230 | 231 | protected abstract fun detectInImage(image: InputImage): Task 232 | 233 | protected open fun detectInImage(image: MlImage): Task { 234 | return Tasks.forException( 235 | MlKitException( 236 | "MlImage is currently not demonstrated for this feature", 237 | MlKitException.INVALID_ARGUMENT 238 | ) 239 | ) 240 | } 241 | 242 | @CallSuper 243 | protected open fun onSuccess(results: T) { 244 | onSuccessUnit?.invoke(results) 245 | } 246 | 247 | @CallSuper 248 | protected open fun onFailure(e: Exception) { 249 | onFailureUnit?.invoke(e) 250 | } 251 | } -------------------------------------------------------------------------------- /lib/src/plugin/scan_plugin.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// [Author] Alex (https://github.com/AlexV525) 3 | /// [Date] 12/11/20 4:40 PM 4 | /// 5 | import 'dart:async'; 6 | 7 | import 'package:flutter/foundation.dart'; 8 | import 'package:flutter/services.dart'; 9 | import 'package:flutter/widgets.dart'; 10 | 11 | import 'enums.dart'; 12 | import 'log_util.dart'; 13 | import 'scan_result.dart'; 14 | 15 | /// 固定的 channel 前缀 16 | const String _channelPrefix = 'MLKitScanPlugin'; 17 | 18 | /// 定义扫描结果处理函数回调为类型 19 | typedef ScanResultCallback = void Function(ScanResult result); 20 | 21 | class ScanPlugin { 22 | const ScanPlugin._(); 23 | 24 | /// 是否打印 LOG 25 | static bool isLogging = true; 26 | 27 | static const MethodChannel _scanChannel = MethodChannel( 28 | '$_channelPrefix/scanChannel', 29 | ); 30 | 31 | static const EventChannel _resultChannel = EventChannel( 32 | '$_channelPrefix/resultChannel', 33 | ); 34 | 35 | static const String platformViewType = '$_channelPrefix/ScanViewFactory'; 36 | 37 | //////////////////////////////////////////////////////////////////////////// 38 | //////////////////////////////////////////////////////////////////////////// 39 | //////////////////////////////////////////////////////////////////////////// 40 | 41 | static const String _METHOD_LOAD_SCAN_VIEW = 'loadScanView'; 42 | static const String _METHOD_PAUSE_SCAN = 'pauseScan'; 43 | static const String _METHOD_RESUME_SCAN = 'resumeScan'; 44 | static const String _METHOD_STOP_SCAN = 'stopScan'; 45 | static const String _METHOD_SWITCH_SCAN_TYPE = 'switchScanType'; 46 | static const String _METHOD_RE_FOCUS = 'reFocus'; 47 | static const String _METHOD_OPEN_FLASHLIGHT = 'openFlashlight'; 48 | static const String _METHOD_CLOSE_FLASHLIGHT = 'closeFlashlight'; 49 | static const String _METHOD_REQUEST_WAKE_LOCK = 'requestWakeLock'; 50 | static const String _METHOD_SCAN_FROM_FILE = 'scanFromFile'; 51 | 52 | //////////////////////////////////////////////////////////////////////////// 53 | //////////////////////////////////////////////////////////////////////////// 54 | //////////////////////////////////////////////////////////////////////////// 55 | 56 | static final Stream> _resultStream = 57 | _resultChannel.receiveBroadcastStream().cast>(); 58 | 59 | static StreamSubscription>? _resultSubscription; 60 | 61 | /// 结果回调的监听器列表 62 | /// 63 | /// 在监听列表内的所有监听器,可以在调用时收到内容,从而进行调用。 64 | static ObserverList _resultListeners = 65 | ObserverList(); 66 | 67 | //////////////////////////////////////////////////////////////////////////// 68 | //////////////////////////////////////////////////////////////////////////// 69 | //////////////////////////////////////////////////////////////////////////// 70 | 71 | /// 当前的扫描模式 72 | static ScanType get scanningType => _scanningType ?? ScanType.wait; 73 | static ScanType? _scanningType; 74 | 75 | /// 是否正在调用耗时方法 76 | static final ValueNotifier isDispatching = ValueNotifier(false); 77 | 78 | /// 当前的区域 79 | static Rect? _scanningRect; 80 | 81 | /// 是否已暂停解析 82 | static bool get isDecodingPaused => scanningType == ScanType.wait; 83 | 84 | /// 是否已暂停扫描 85 | static bool get isScanningPaused => _isScanningPaused; 86 | static bool _isScanningPaused = false; 87 | 88 | /// 注册扫描结果的监听基建 89 | /// 90 | /// 必须在初始化扫描前调用,且应只调用一次 91 | static void init() { 92 | assert(_resultSubscription == null); 93 | LogUtil.d('Initializing scan result subscription...'); 94 | _resultSubscription = _resultStream.listen( 95 | (Map event) { 96 | for (final ScanResultCallback listener in _resultListeners) { 97 | listener(ScanResult.fromJson(event)); 98 | } 99 | }, 100 | onError: (Object error, StackTrace stackTrace) { 101 | LogUtil.e('Error in scan result subscription: $error, $stackTrace'); 102 | }, 103 | cancelOnError: false, 104 | ); 105 | } 106 | 107 | /// 将当前的订阅取消 108 | static void destroy() { 109 | LogUtil.d('Destroying scan subscriptions...'); 110 | _resultListeners = ObserverList(); 111 | _resultSubscription?.cancel(); 112 | _resultSubscription = null; 113 | _isScanningPaused = false; 114 | _scanningType = null; 115 | _scanningRect = null; 116 | isDispatching.value = false; 117 | } 118 | 119 | /// 添加监听器实例 120 | static void addListener(ScanResultCallback listener) { 121 | _resultListeners.add(listener); 122 | } 123 | 124 | /// 移除监听器实例 125 | static void removeListener(ScanResultCallback listener) { 126 | _resultListeners.remove(listener); 127 | } 128 | 129 | /// 初始化扫描 130 | /// 131 | /// 扫描初始化在 iOS 有两步: 132 | /// * 1. 加载扫描 View。 133 | /// * 2. 切换扫描模式至 **自定义状态** 或 [ScanType.all],并传入对应的扫描区域。 134 | static Future initializeScanning( 135 | Rect rect, { 136 | ScanType scanType = ScanType.barcodeAndMobile, 137 | }) async { 138 | LogUtil.d('Scanning initialize.'); 139 | try { 140 | await _scanChannel.invokeMethod(_METHOD_LOAD_SCAN_VIEW); 141 | await switchScanType(scanType, rect: rect); 142 | } catch (e) { 143 | LogUtil.e(e); 144 | } 145 | } 146 | 147 | /// 暂停解析 148 | static Future pauseDecode() { 149 | return switchScanType(ScanType.wait, rect: null, updateVariable: false); 150 | } 151 | 152 | /// 恢复解析 153 | static Future resumeDecode() async { 154 | if (_scanningType == null && _resultSubscription == null) { 155 | if (!kDebugMode) { 156 | return; 157 | } 158 | throw StateError('Scanner has already been destroyed.'); 159 | } 160 | return switchScanType( 161 | _scanningType!, 162 | rect: _scanningRect, 163 | updateVariable: false, 164 | ); 165 | } 166 | 167 | /// 暂停扫描 168 | static Future pauseScan() async { 169 | if (_isScanningPaused) { 170 | return; 171 | } 172 | _isScanningPaused = true; 173 | LogUtil.d('Scanning pause.'); 174 | return _invokeMethod(_METHOD_PAUSE_SCAN); 175 | } 176 | 177 | /// 恢复扫描 178 | static Future resumeScan() async { 179 | if (!_isScanningPaused) { 180 | return; 181 | } 182 | _isScanningPaused = false; 183 | LogUtil.d('Scanning resume.'); 184 | return _invokeMethod(_METHOD_RESUME_SCAN); 185 | } 186 | 187 | /// 停止扫描 188 | static Future stopScan() { 189 | assert(_resultSubscription == null, 'Call destroy() first.'); 190 | LogUtil.d('Scanning stop.'); 191 | if (_resultSubscription != null) { 192 | destroy(); 193 | } 194 | return _invokeMethod(_METHOD_STOP_SCAN); 195 | } 196 | 197 | /// 以操作点进行重新聚焦 198 | static Future reFocus(Offset point) async { 199 | if (_resultSubscription == null) { 200 | return; 201 | } 202 | assert(point.dx >= 0 && point.dy >= 0); 203 | LogUtil.d('Re-focus with point: $point'); 204 | return _invokeMethod(_METHOD_RE_FOCUS, [point.dx, point.dy]); 205 | } 206 | 207 | /// 切换扫描模式 208 | /// 209 | /// [type] 扫描模式 210 | /// [rect] 传入扫描的区域。宽和高必须大于 65。 211 | /// [updateVariable] 是否更新变量 212 | static Future switchScanType( 213 | ScanType type, { 214 | required Rect? rect, 215 | bool updateVariable = true, 216 | }) async { 217 | if (_resultSubscription == null) { 218 | return; 219 | } 220 | assert( 221 | type == ScanType.wait || 222 | rect != null && !rect.isEmpty && rect.width > 65 && rect.height > 65, 223 | ); 224 | if (updateVariable) { 225 | _scanningType = type; 226 | _scanningRect = rect!; 227 | } 228 | // 正常模式扫描,谁不是 4 个元素谁砍头。 229 | final List? rectFromLTWH = rect != null 230 | ? [rect.left, rect.top, rect.width, rect.height] 231 | : null; 232 | String _log = 'Switch scan type to $type'; 233 | if (rect != null) { 234 | _log += ' with Rect: $rect'; 235 | } 236 | _log += '.'; 237 | LogUtil.d(_log); 238 | return _invokeMethod( 239 | _METHOD_SWITCH_SCAN_TYPE, 240 | { 241 | 'type': type.value, 242 | if (rectFromLTWH != null) 'rect': rectFromLTWH, 243 | }, 244 | ); 245 | } 246 | 247 | /// 手动调用扫描成功 248 | /// 249 | /// 该方法用于在扫描手机号时,手动输入手机号,调用运单扫描成功的回调 250 | static void manuallyAddResult(ScanResult result) { 251 | LogUtil.d('Manually adding result: $result'); 252 | for (final ScanResultCallback listener in _resultListeners) { 253 | listener(result); 254 | } 255 | } 256 | 257 | static Future openFlashlight() async { 258 | if (_resultSubscription == null) { 259 | return; 260 | } 261 | return _invokeMethod(_METHOD_OPEN_FLASHLIGHT); 262 | } 263 | 264 | static Future closeFlashlight() async { 265 | if (_resultSubscription == null) { 266 | return; 267 | } 268 | return _invokeMethod(_METHOD_CLOSE_FLASHLIGHT); 269 | } 270 | 271 | static Future requestWakeLock(bool value) async { 272 | if (_resultSubscription == null) { 273 | return; 274 | } 275 | return _invokeMethod(_METHOD_REQUEST_WAKE_LOCK, value); 276 | } 277 | 278 | static Future> scanFromFile( 279 | String path, [ 280 | List? formats, 281 | ]) async { 282 | final List? result = await _invokeMethod( 283 | _METHOD_SCAN_FROM_FILE, 284 | { 285 | 'path': path, 286 | 'formats': formats?.map((BarcodeFormat e) => e.code).toList(), 287 | }, 288 | ); 289 | if (result == null || result.isEmpty) { 290 | return []; 291 | } 292 | return result 293 | .cast>() 294 | .map((e) => Barcode.fromJson(e.cast())) 295 | .toList(); 296 | } 297 | 298 | @optionalTypeArgs 299 | static Future _invokeMethod(String method, [Object? arguments]) async { 300 | isDispatching.value = true; 301 | try { 302 | final T? t = await _scanChannel.invokeMethod(method, arguments); 303 | return t; 304 | } catch (e) { 305 | LogUtil.e('Error when invoking method ($method): $e'); 306 | return null; 307 | } finally { 308 | isDispatching.value = false; 309 | } 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /ios/Classes/Scan/ChannelManager.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import MLKitBarcodeScanning 3 | import MLKitVision 4 | import UIKit 5 | 6 | struct ChannelMethod { 7 | static let host = "MLKitScanPlugin" 8 | static let scanChannel = "\(host)/scanChannel" 9 | static let resultChannel = "\(host)/resultChannel" 10 | static let factoryId = "\(host)/ScanViewFactory" 11 | 12 | static let loadScanView = "loadScanView" 13 | static let switchScanType = "switchScanType" 14 | static let pauseScan = "pauseScan" 15 | static let resumeScan = "resumeScan" 16 | static let scanFromFile = "scanFromFile" 17 | static let reFocus = "reFocus" 18 | static let stopScan = "stopScan" 19 | static let openFlashlight = "openFlashlight" 20 | static let closeFlashlight = "closeFlashlight" 21 | static let requestWakeLock = "requestWakeLock" 22 | } 23 | 24 | class ChannelManager: NSObject { 25 | var scanViewChannel: FlutterMethodChannel? 26 | var eventSink: FlutterEventSink? 27 | 28 | static let shared = ChannelManager() 29 | 30 | // MARK: - Bind all channel. 31 | public static func initFlutterMethodChannel(_ messenger: FlutterBinaryMessenger) { 32 | ChannelManager.shared.scanViewChannel = FlutterMethodChannel( 33 | name: ChannelMethod.scanChannel, 34 | binaryMessenger: messenger 35 | ) 36 | FlutterEventChannel( 37 | name: ChannelMethod.resultChannel, 38 | binaryMessenger: messenger 39 | ).setStreamHandler(EventStreamHandler() as? FlutterStreamHandler & NSObjectProtocol) 40 | } 41 | 42 | // MARK: - Handle flutter channel calls. 43 | public static func handleFlutterChannel( 44 | _ call: FlutterMethodCall, 45 | factory: ScanViewFactory, 46 | result: @escaping FlutterResult 47 | ) { 48 | guard let scanView = factory.platformView?.scanView as? ScanView else { 49 | debugPrint("ScanView is null.") 50 | result( 51 | FlutterError( 52 | code: "ScanView", 53 | message: "0", 54 | details: "ScanView is null." 55 | ) 56 | ) 57 | return 58 | } 59 | switch call.method { 60 | case ChannelMethod.loadScanView: 61 | scanView.createDeviceCapture() 62 | result(true) 63 | case ChannelMethod.switchScanType: 64 | switchScanType(call, scanView: scanView, result: result) 65 | case ChannelMethod.pauseScan: 66 | requestWakeLock(false) 67 | scanView.sessionPause() 68 | result(true) 69 | case ChannelMethod.resumeScan: 70 | requestWakeLock(true) 71 | scanView.sessionResume() 72 | result(true) 73 | case ChannelMethod.reFocus: 74 | reFocus(call, scanView: scanView, result: result) 75 | case ChannelMethod.stopScan: 76 | requestWakeLock(false) 77 | scanView.closeScanView() 78 | factory.platformView?.scanView?.removeFromSuperview() 79 | factory.platformView?.scanView = nil 80 | result(true) 81 | case ChannelMethod.scanFromFile: 82 | DispatchQueue.global(qos: .background).async { 83 | scanFromFile(call, result, scanView.imageParser) 84 | } 85 | case ChannelMethod.openFlashlight: 86 | let _err = scanView.toggleFlashlight(enable: true) 87 | if (_err == nil) { 88 | result(nil) 89 | } else { 90 | result(FlutterError(code: "ScanView", message: _err, details: nil)) 91 | } 92 | case ChannelMethod.closeFlashlight: 93 | let _err = scanView.toggleFlashlight(enable: false) 94 | if (_err == nil) { 95 | result(nil) 96 | } else { 97 | result(FlutterError(code: "ScanView", message: _err, details: nil)) 98 | } 99 | case ChannelMethod.requestWakeLock: 100 | requestWakeLock(call) 101 | result(nil) 102 | default: 103 | result(FlutterMethodNotImplemented) 104 | } 105 | } 106 | 107 | // MARK: - Switch the new scan type with the given rect. 108 | private static func switchScanType( 109 | _ call: FlutterMethodCall, 110 | scanView: ScanView, 111 | result: @escaping FlutterResult 112 | ) { 113 | if let argument = call.arguments as? [String: Any], let type = argument["type"] as? Int { 114 | var rect = CGRect.zero 115 | let taskMode = ScanningTaskMode.toMode(type) 116 | if taskMode != .wait { 117 | if let rectList = argument["rect"] as? Array, rectList.count == 4 { 118 | rect = CGRect( 119 | x: rectList[0], 120 | y: rectList[1] - UIApplication.shared.statusBarFrame.height, 121 | width: rectList[2], 122 | height: rectList[3] 123 | ) 124 | } 125 | } 126 | scanView.changeScanState(with: taskMode, rect) 127 | debugPrint("Switching the scan type to: \(type)") 128 | } 129 | result(true) 130 | } 131 | 132 | // MARK: - Adjust focus with the given point. 133 | private static func reFocus( 134 | _ call: FlutterMethodCall, 135 | scanView: ScanView, 136 | result: @escaping FlutterResult 137 | ) { 138 | if let argument = call.arguments as? [Double], argument.count == 2 { 139 | let screenCenterPoint = CGPoint( 140 | x: UIScreen.main.bounds.width / 2, 141 | y: UIScreen.main.bounds.height / 2 142 | ) 143 | let flutterX = argument.first ?? Double(screenCenterPoint.x) 144 | let flutterY = argument.last ?? Double(screenCenterPoint.y) 145 | scanView.adjustFocus(CGPoint(x: flutterX, y: flutterY)) 146 | } 147 | result(true) 148 | } 149 | 150 | private static func scanFromFile( 151 | _ call: FlutterMethodCall, 152 | _ result: @escaping FlutterResult, 153 | _ imageParser: ImageParser 154 | ) { 155 | if let arguments = call.arguments as? Dictionary, 156 | let path = arguments["path"] as? String { 157 | var barcodeFormat = BarcodeFormat() 158 | if let formats = arguments["formats"] as? [Int] { 159 | for format in formats { 160 | barcodeFormat.insert(BarcodeFormat(rawValue: format)) 161 | } 162 | } else { 163 | barcodeFormat = BarcodeFormat(arrayLiteral: .all) 164 | } 165 | var image: UIImage? = UIImage.init(contentsOfFile: path) 166 | if (image == nil) { 167 | result( 168 | FlutterError( 169 | code: "SCAN_FROM_FILE", 170 | message: "No UIImage from the path.", 171 | details: path 172 | ) 173 | ) 174 | return 175 | } 176 | let fileExtension = URL(fileURLWithPath: path).pathExtension.lowercased() 177 | // Convert all PNG to JPEG to avoid unsupported formats from MLKit. 178 | if (fileExtension == "png") { 179 | let imageData = image!.jpegData(compressionQuality: 1) 180 | image = UIImage(data: imageData!) 181 | if (image == nil) { 182 | result( 183 | FlutterError( 184 | code: call.method, 185 | message: "Cannot produce valid image data.", 186 | details: path 187 | ) 188 | ) 189 | return 190 | } 191 | } 192 | guard var image = image else { 193 | debugPrint("UIImage is null.") 194 | result( 195 | FlutterError( 196 | code: call.method, 197 | message: "Cannot produce a valid UIImage from the path.", 198 | details: "UIImage is null." 199 | ) 200 | ) 201 | return 202 | } 203 | let maxBounds = UIScreen.main.bounds 204 | if (image.size.width > maxBounds.width || 205 | image.size.height > maxBounds.height) { 206 | image = imageParser.reScale(image: image, maxSize: maxBounds.size) 207 | } 208 | let visionImage = VisionImage(image: image) 209 | visionImage.orientation = image.imageOrientation 210 | 211 | let barcodeScanner = BarcodeScanner.barcodeScanner( 212 | options: BarcodeScannerOptions(formats: barcodeFormat) 213 | ) 214 | var barcodes: [Barcode] 215 | do { 216 | barcodes = try barcodeScanner.results(in: visionImage) 217 | } catch let error { 218 | result( 219 | FlutterError( 220 | code: "SCAN_FROM_FILE", 221 | message: "Failed to scan barcodes", 222 | details: error.localizedDescription 223 | ) 224 | ) 225 | return 226 | } 227 | if (barcodes.isEmpty) { 228 | result(nil) 229 | return 230 | } 231 | var barcodeResult = [[String: Any]]() 232 | for barcode in barcodes { 233 | if let value = barcode.displayValue { 234 | barcodeResult.append( 235 | [ 236 | "value": value, 237 | "box": [ 238 | "left": barcode.frame.minX, 239 | "top": barcode.frame.minY, 240 | "width": barcode.frame.width, 241 | "height": barcode.frame.height 242 | ] 243 | ] 244 | ) 245 | } 246 | } 247 | result(barcodeResult) 248 | } else { 249 | result( 250 | FlutterError( 251 | code: "SCAN_FROM_FILE", 252 | message: "Invalid arguments.", 253 | details: nil 254 | ) 255 | ) 256 | } 257 | } 258 | 259 | private static func requestWakeLock(_ call: FlutterMethodCall) { 260 | if let enable = call.arguments as? Bool { 261 | requestWakeLock(enable) 262 | } 263 | } 264 | 265 | private static func requestWakeLock(_ value: Bool) { 266 | debugPrint("Requesting wake lock: \(value)") 267 | UIApplication.shared.isIdleTimerDisabled = value 268 | } 269 | } 270 | 271 | class EventStreamHandler: FlutterStreamHandler { 272 | func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { 273 | ChannelManager.shared.eventSink = events 274 | return nil 275 | } 276 | 277 | func onCancel(withArguments arguments: Any?) -> FlutterError? { 278 | ChannelManager.shared.eventSink = nil 279 | return nil 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Specify analysis options. 2 | # 3 | # Until there are meta linter rules, each desired lint must be explicitly enabled. 4 | # See: https://github.com/dart-lang/linter/issues/288 5 | # 6 | # For a list of lints, see: http://dart-lang.github.io/linter/lints/ 7 | # See the configuration guide for more 8 | # https://github.com/dart-lang/sdk/tree/master/pkg/analyzer#configuring-the-analyzer 9 | # 10 | # There are other similar analysis options files in the flutter repos, 11 | # which should be kept in sync with this file: 12 | # 13 | # - analysis_options.yaml (this file) 14 | # - packages/flutter/lib/analysis_options_user.yaml 15 | # - https://github.com/flutter/plugins/blob/master/analysis_options.yaml 16 | # - https://github.com/flutter/engine/blob/master/analysis_options.yaml 17 | # 18 | # This file contains the analysis options used by Flutter tools, such as IntelliJ, 19 | # Android Studio, and the `flutter analyze` command. 20 | 21 | analyzer: 22 | strong-mode: 23 | implicit-casts: false 24 | implicit-dynamic: false 25 | errors: 26 | # treat missing required parameters as a warning (not a hint) 27 | missing_required_param: warning 28 | # treat missing returns as a warning (not a hint) 29 | missing_return: warning 30 | # allow having TODOs in the code 31 | todo: ignore 32 | # allow self-reference to deprecated members (we do this because otherwise we have 33 | # to annotate every member in every test, assert, etc, when we deprecate something) 34 | deprecated_member_use_from_same_package: ignore 35 | # Ignore analyzer hints for updating pubspecs when using Future or 36 | # Stream and not importing dart:async 37 | # Please see https://github.com/flutter/flutter/pull/24528 for details. 38 | sdk_version_async_exported_from_core: ignore 39 | # Turned off until null-safe rollout is complete. 40 | unnecessary_null_comparison: ignore 41 | exclude: 42 | - "bin/cache/**" 43 | # the following two are relative to the stocks example and the flutter package respectively 44 | # see https://github.com/dart-lang/sdk/issues/28463 45 | - "lib/i18n/messages_*.dart" 46 | - "lib/src/http/**" 47 | - "lib/models/*.g.dart" 48 | - "lib/src/models/*.g.dart" 49 | 50 | linter: 51 | rules: 52 | # these rules are documented on and in the same order as 53 | # the Dart Lint rules page to make maintenance easier 54 | # https://github.com/dart-lang/linter/blob/master/example/all.yaml 55 | - always_declare_return_types 56 | - always_put_control_body_on_new_line 57 | # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219 58 | - always_require_non_null_named_parameters 59 | # - always_specify_types 60 | # - always_use_package_imports # we do this commonly 61 | - annotate_overrides 62 | # - avoid_annotating_with_dynamic # conflicts with always_specify_types 63 | - avoid_bool_literals_in_conditional_expressions 64 | # - avoid_catches_without_on_clauses # we do this commonly 65 | # - avoid_catching_errors # we do this commonly 66 | - avoid_classes_with_only_static_members 67 | # - avoid_double_and_int_checks # only useful when targeting JS runtime 68 | # - avoid_dynamic_calls # not yet tested 69 | - avoid_empty_else 70 | - avoid_equals_and_hash_code_on_mutable_classes 71 | # - avoid_escaping_inner_quotes # not yet tested 72 | - avoid_field_initializers_in_const_classes 73 | - avoid_function_literals_in_foreach_calls 74 | # - avoid_implementing_value_types # not yet tested 75 | - avoid_init_to_null 76 | # - avoid_js_rounded_ints # only useful when targeting JS runtime 77 | - avoid_null_checks_in_equality_operators 78 | # - avoid_positional_boolean_parameters # not yet tested 79 | # - avoid_print # not yet tested 80 | # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356) 81 | # - avoid_redundant_argument_values # not yet tested 82 | - avoid_relative_lib_imports 83 | - avoid_renaming_method_parameters 84 | - avoid_return_types_on_setters 85 | # - avoid_returning_null # there are plenty of valid reasons to return null 86 | # - avoid_returning_null_for_future # not yet tested 87 | - avoid_returning_null_for_void 88 | # - avoid_returning_this # there are plenty of valid reasons to return this 89 | # - avoid_setters_without_getters # not yet tested 90 | - avoid_shadowing_type_parameters 91 | - avoid_single_cascade_in_expression_statements 92 | - avoid_slow_async_io 93 | - avoid_type_to_string 94 | - avoid_types_as_parameter_names 95 | # - avoid_types_on_closure_parameters # conflicts with always_specify_types 96 | - avoid_unnecessary_containers 97 | - avoid_unused_constructor_parameters 98 | - avoid_void_async 99 | # - avoid_web_libraries_in_flutter # not yet tested 100 | - await_only_futures 101 | - camel_case_extensions 102 | - camel_case_types 103 | - cancel_subscriptions 104 | # - cascade_invocations # not yet tested 105 | # - close_sinks # not reliable enough 106 | # - comment_references # blocked on https://github.com/dart-lang/linter/issues/1142 107 | # - constant_identifier_names # needs an opt-out https://github.com/dart-lang/linter/issues/204 108 | - control_flow_in_finally 109 | # - curly_braces_in_flow_control_structures # not required by flutter style 110 | - deprecated_consistency 111 | # - diagnostic_describe_all_properties # not yet tested 112 | - directives_ordering 113 | # - do_not_use_environment # we do this commonly 114 | - empty_catches 115 | - empty_constructor_bodies 116 | - empty_statements 117 | - exhaustive_cases 118 | - file_names 119 | - flutter_style_todos 120 | - hash_and_equals 121 | - implementation_imports 122 | # - invariant_booleans # too many false positives: https://github.com/dart-lang/linter/issues/811 123 | - iterable_contains_unrelated_type 124 | # - join_return_with_assignment # not required by flutter style 125 | - leading_newlines_in_multiline_strings 126 | - library_names 127 | - library_prefixes 128 | # - lines_longer_than_80_chars # not required by flutter style 129 | - list_remove_unrelated_type 130 | # - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/sdk/issues/34181 131 | - missing_whitespace_between_adjacent_strings 132 | - no_adjacent_strings_in_list 133 | # - no_default_cases # too many false positives 134 | - no_duplicate_case_values 135 | - no_logic_in_create_state 136 | # - no_runtimeType_toString # ok in tests; we enable this only in packages/ 137 | - non_constant_identifier_names 138 | - null_check_on_nullable_type_parameter 139 | - null_closures 140 | # - omit_local_variable_types # opposite of always_specify_types 141 | # - one_member_abstracts # too many false positives 142 | # - only_throw_errors # https://github.com/flutter/flutter/issues/5792 143 | - overridden_fields 144 | - package_api_docs 145 | - package_names 146 | - package_prefixed_library_names 147 | # - parameter_assignments # we do this commonly 148 | - prefer_adjacent_string_concatenation 149 | - prefer_asserts_in_initializer_lists 150 | # - prefer_asserts_with_message # not required by flutter style 151 | - prefer_collection_literals 152 | - prefer_conditional_assignment 153 | - prefer_const_constructors 154 | - prefer_const_constructors_in_immutables 155 | - prefer_const_declarations 156 | - prefer_const_literals_to_create_immutables 157 | # - prefer_constructors_over_static_methods # far too many false positives 158 | - prefer_contains 159 | # - prefer_double_quotes # opposite of prefer_single_quotes 160 | - prefer_equal_for_default_values 161 | # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods 162 | - prefer_final_fields 163 | - prefer_final_in_for_each 164 | - prefer_final_locals 165 | - prefer_for_elements_to_map_fromIterable 166 | - prefer_foreach 167 | - prefer_function_declarations_over_variables 168 | - prefer_generic_function_type_aliases 169 | - prefer_if_elements_to_conditional_expressions 170 | - prefer_if_null_operators 171 | - prefer_initializing_formals 172 | - prefer_inlined_adds 173 | # - prefer_int_literals # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#use-double-literals-for-double-constants 174 | # - prefer_interpolation_to_compose_strings # doesn't work with raw strings, see https://github.com/dart-lang/linter/issues/2490 175 | - prefer_is_empty 176 | - prefer_is_not_empty 177 | - prefer_is_not_operator 178 | - prefer_iterable_whereType 179 | # - prefer_mixin # https://github.com/dart-lang/language/issues/32 180 | - prefer_null_aware_operators 181 | # - prefer_relative_imports # incompatible with sub-package imports 182 | - prefer_single_quotes 183 | - prefer_spread_collections 184 | - prefer_typing_uninitialized_variables 185 | - prefer_void_to_null 186 | - provide_deprecation_message 187 | # - public_member_api_docs # enabled on a case-by-case basis; see e.g. packages/analysis_options.yaml 188 | - recursive_getters 189 | - sized_box_for_whitespace 190 | - slash_for_doc_comments 191 | # - sort_child_properties_last # not yet tested 192 | - sort_constructors_first 193 | # - sort_pub_dependencies # prevents separating pinned transitive dependencies 194 | - sort_unnamed_constructors_first 195 | - test_types_in_equals 196 | - throw_in_finally 197 | - tighten_type_of_initializing_formals 198 | # - type_annotate_public_apis # subset of always_specify_types 199 | - type_init_formals 200 | # - unawaited_futures # too many false positives 201 | - unnecessary_await_in_return 202 | - unnecessary_brace_in_string_interps 203 | - unnecessary_const 204 | # - unnecessary_final # conflicts with prefer_final_locals 205 | - unnecessary_getters_setters 206 | # - unnecessary_lambdas # has false positives: https://github.com/dart-lang/linter/issues/498 207 | - unnecessary_new 208 | - unnecessary_null_aware_assignments 209 | # - unnecessary_null_checks # not yet tested 210 | - unnecessary_null_in_if_null_operators 211 | - unnecessary_nullable_for_final_variable_declarations 212 | - unnecessary_overrides 213 | - unnecessary_parenthesis 214 | # - unnecessary_raw_strings # not yet tested 215 | - unnecessary_statements 216 | - unnecessary_string_escapes 217 | - unnecessary_string_interpolations 218 | - unnecessary_this 219 | - unrelated_type_equality_checks 220 | # - unsafe_html # not yet tested 221 | - use_full_hex_values_for_flutter_colors 222 | - use_function_type_syntax_for_parameters 223 | # - use_if_null_to_convert_nulls_to_bools # not yet tested 224 | - use_is_even_rather_than_modulo 225 | - use_key_in_widget_constructors 226 | - use_late_for_private_fields_and_variables 227 | # - use_named_constants # not yet tested 228 | - use_raw_strings 229 | - use_rethrow_when_possible 230 | # - use_setters_to_change_properties # not yet tested 231 | # - use_string_buffers # has false positives: https://github.com/dart-lang/sdk/issues/34182 232 | # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review 233 | - valid_regexps 234 | - void_checks 235 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// [Author] Alex (https://github.com/AlexV525) 3 | /// [Date] 11/25/20 1:32 PM 4 | /// 5 | import 'dart:developer' as _developer; 6 | import 'dart:ui' as ui; 7 | import 'dart:ui'; 8 | 9 | import 'package:flutter/material.dart'; 10 | import 'package:image_picker/image_picker.dart'; 11 | import 'package:mlkit_scan_plugin/mlkit_scan_plugin.dart'; 12 | import 'package:permission_handler/permission_handler.dart'; 13 | 14 | void main() { 15 | runApp(const MyApp()); 16 | } 17 | 18 | class MyApp extends StatelessWidget { 19 | const MyApp({Key? key}) : super(key: key); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return const MaterialApp(home: ExamplePage()); 24 | } 25 | } 26 | 27 | class ExamplePage extends StatefulWidget { 28 | const ExamplePage({Key? key}) : super(key: key); 29 | 30 | @override 31 | State createState() => _ExamplePageState(); 32 | } 33 | 34 | class _ExamplePageState extends State { 35 | final ValueNotifier rectNotifier = ValueNotifier(null); 36 | final ValueNotifier scanViewNotifier = ValueNotifier(null); 37 | final ValueNotifier listenerNotifier = 38 | ValueNotifier(null); 39 | 40 | double get screenWidth => MediaQueryData.fromWindow(ui.window).size.width; 41 | 42 | double get screenHeight => MediaQueryData.fromWindow(ui.window).size.height; 43 | 44 | @override 45 | void initState() { 46 | super.initState(); 47 | initialize(); 48 | } 49 | 50 | Future initialize() async { 51 | final bool isAllGranted = await checkAllPermissions(); 52 | if (isAllGranted) { 53 | scanViewNotifier.value = ScanView( 54 | scanType: ScanType.wait, 55 | scanRect: Rect.fromLTWH(30, 100, screenWidth - 60, 300), 56 | ); 57 | } else { 58 | showLackOfPermissionsDialog(); 59 | } 60 | } 61 | 62 | void showLackOfPermissionsDialog() { 63 | _developer.log('请允许权限开启'); 64 | } 65 | 66 | @override 67 | void dispose() { 68 | ScanPlugin.destroy(); 69 | super.dispose(); 70 | } 71 | 72 | Future checkAllPermissions() { 73 | return checkPermissions([ 74 | Permission.camera, 75 | Permission.storage, 76 | ]); 77 | } 78 | 79 | Future checkPermissions(List permissions) async { 80 | try { 81 | final Map status = 82 | await permissions.request(); 83 | status.forEach((Permission key, PermissionStatus value) { 84 | _developer.log('$key: $value'); 85 | }); 86 | return !status.values.any( 87 | (PermissionStatus p) => p != PermissionStatus.granted, 88 | ); 89 | } catch (e) { 90 | _developer.log('Error when requesting permission: $e'); 91 | return false; 92 | } 93 | } 94 | 95 | Future _scanFromFile() async { 96 | final XFile? file = await ImagePicker().pickImage( 97 | source: ImageSource.gallery, 98 | ); 99 | if (file != null) { 100 | final List result = await ScanPlugin.scanFromFile(file.path); 101 | if (result.isEmpty) { 102 | ScaffoldMessenger.of(context).showSnackBar( 103 | const SnackBar(content: Text('Nothing')), 104 | ); 105 | return; 106 | } 107 | showModalBottomSheet( 108 | context: context, 109 | builder: (BuildContext context) => Container( 110 | height: MediaQueryData.fromWindow(window).size.height / 2, 111 | padding: const EdgeInsets.all(24.0), 112 | child: ListView.builder( 113 | itemCount: result.length, 114 | itemBuilder: (BuildContext context, int index) { 115 | final Barcode code = result[index]; 116 | final ui.Rect? box = code.boundingBox; 117 | return Container( 118 | decoration: BoxDecoration( 119 | border: Border.all( 120 | color: Theme.of(context).dividerColor, 121 | ), 122 | ), 123 | padding: const EdgeInsets.all(16.0), 124 | margin: const EdgeInsets.only(bottom: 12.0), 125 | child: Column( 126 | crossAxisAlignment: CrossAxisAlignment.start, 127 | children: [ 128 | Text( 129 | code.value, 130 | style: const TextStyle(fontWeight: FontWeight.bold), 131 | ), 132 | const Divider(), 133 | if (box != null) 134 | Text( 135 | 'boundingBox: left(${box.left}), ' 136 | 'top(${box.top}), ' 137 | 'width(${box.width}), ' 138 | 'height(${box.height})', 139 | style: const TextStyle(fontSize: 12.0), 140 | ), 141 | ], 142 | ), 143 | ); 144 | }, 145 | ), 146 | ), 147 | ); 148 | } 149 | } 150 | 151 | @override 152 | Widget build(BuildContext context) { 153 | return Scaffold( 154 | body: DefaultTextStyle.merge( 155 | style: const TextStyle(color: Colors.white), 156 | child: Stack( 157 | children: [ 158 | ValueListenableBuilder( 159 | valueListenable: scanViewNotifier, 160 | builder: (_, Widget? scanView, __) => 161 | scanView ?? 162 | const Center(child: Text('Platform view not loaded')), 163 | ), 164 | ValueListenableBuilder( 165 | valueListenable: rectNotifier, 166 | builder: (_, Rect? rect, __) { 167 | if (rect != null) 168 | return Positioned.fromRect( 169 | rect: rect, 170 | child: ColoredBox(color: Colors.green.withOpacity(0.5)), 171 | ); 172 | return const SizedBox.shrink(); 173 | }, 174 | ), 175 | Positioned.fill( 176 | child: ListView( 177 | children: [ 178 | TextButton( 179 | onPressed: () { 180 | ScanPlugin.initializeScanning( 181 | Rect.fromLTWH(30, 100, screenWidth - 60, 300), 182 | ); 183 | }, 184 | child: const Text('loadScanView'), 185 | ), 186 | TextButton( 187 | onPressed: () { 188 | ScanPlugin.switchScanType( 189 | ScanType.wait, 190 | rect: null, 191 | ); 192 | }, 193 | child: const Text('Make scanning idle.'), 194 | ), 195 | TextButton( 196 | onPressed: () { 197 | final Rect rect = Rect.fromLTWH( 198 | 30, 199 | 100, 200 | screenWidth - 60, 201 | 300, 202 | ); 203 | ScanPlugin.switchScanType( 204 | ScanType.barcodeAndMobile, 205 | rect: rect, 206 | ); 207 | rectNotifier.value = rect; 208 | }, 209 | child: const Text( 210 | 'Switch scan type to ScanType.barcodeAndMobile', 211 | ), 212 | ), 213 | TextButton( 214 | onPressed: () { 215 | ScanPlugin.reFocus( 216 | Offset(screenWidth / 2, screenHeight / 2), 217 | ); 218 | }, 219 | child: const Text('reFocus'), 220 | ), 221 | TextButton( 222 | onPressed: () { 223 | final Rect rect = Rect.fromLTWH( 224 | 30, 225 | 100, 226 | screenWidth - 60, 227 | 100, 228 | ); 229 | ScanPlugin.switchScanType( 230 | ScanType.mobile, 231 | rect: rect, 232 | ); 233 | rectNotifier.value = rect; 234 | }, 235 | child: const Text( 236 | 'Switch scan type to ScanType.mobile', 237 | ), 238 | ), 239 | TextButton( 240 | onPressed: () { 241 | final Rect rect = Rect.fromLTWH( 242 | 30, 243 | 100, 244 | screenWidth - 60, 245 | 100, 246 | ); 247 | ScanPlugin.switchScanType( 248 | ScanType.barcode, 249 | rect: rect, 250 | ); 251 | rectNotifier.value = rect; 252 | }, 253 | child: const Text( 254 | 'Switch scan type to ScanType.barcode', 255 | ), 256 | ), 257 | TextButton( 258 | onPressed: () { 259 | final Rect rect = Rect.fromLTWH( 260 | 30, 261 | 100, 262 | screenWidth - 60, 263 | screenWidth - 60, 264 | ); 265 | ScanPlugin.switchScanType( 266 | ScanType.qrCode, 267 | rect: rect, 268 | ); 269 | rectNotifier.value = rect; 270 | }, 271 | child: const Text( 272 | 'Switch scan type to ScanType.qrCode', 273 | ), 274 | ), 275 | TextButton( 276 | onPressed: () { 277 | ScanPlugin.destroy(); 278 | ScanPlugin.stopScan(); 279 | }, 280 | child: const Text('Stop scan.'), 281 | ), 282 | TextButton( 283 | onPressed: _scanFromFile, 284 | child: const Text('Scan codes from file.'), 285 | ), 286 | ], 287 | ), 288 | ), 289 | ], 290 | ), 291 | ), 292 | floatingActionButton: FloatingActionButton( 293 | onPressed: () { 294 | if (listenerNotifier.value != null) { 295 | ScanPlugin.removeListener(listenerNotifier.value!); 296 | listenerNotifier.value = null; 297 | } else { 298 | void listener(ScanResult result) { 299 | print(result); 300 | } 301 | 302 | listenerNotifier.value = listener; 303 | ScanPlugin.addListener(listener); 304 | } 305 | }, 306 | child: ValueListenableBuilder( 307 | valueListenable: listenerNotifier, 308 | builder: (_, ScanResultCallback? listener, __) => Icon( 309 | listener == null ? Icons.remove : Icons.add, 310 | ), 311 | ), 312 | ), 313 | ); 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2022] [AlexV525] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /ios/Classes/Scan/ScanView.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AVFoundation 3 | import CoreImage 4 | import MLKitBarcodeScanning 5 | import MLKitTextRecognition 6 | import MLKitVision 7 | import UIKit 8 | 9 | class ScanView: UIView { 10 | private var viewRect: CGRect? 11 | private var scanningFrame: CGRect? 12 | private var captureDevice: AVCaptureDevice? 13 | private var session: AVCaptureSession? 14 | private var previewLayer: AVCaptureVideoPreviewLayer? 15 | private var scanningType: ScanningType = .wait 16 | private var timeOut: Bool = false 17 | private var createTimer: Bool = false 18 | private var codeSuccess: Bool = false 19 | private var phoneSuccess: Bool = false 20 | private var resultModel: ScanResult? 21 | private final var barcodeFormats = BarcodeFormat.init(arrayLiteral: [.code39, .code93, .code128]) 22 | private final var qrCodeFormats = BarcodeFormat.qrCode 23 | private final var goodsCodeFormats = BarcodeFormat.init(arrayLiteral: [.EAN8, .EAN13, .UPCA, .UPCE]) 24 | final var imageParser = ImageParser() 25 | 26 | override init(frame: CGRect) { 27 | super.init(frame: frame) 28 | } 29 | 30 | init(frame: CGRect, viewRect: CGRect?) { 31 | if let rect = viewRect as CGRect? { 32 | let dpi = UIScreen.main.scale 33 | self.viewRect = CGRect(x: 0, y: 0, width: rect.width / dpi, height: rect.height / dpi) 34 | } 35 | super.init(frame: frame) 36 | } 37 | 38 | required init?(coder: NSCoder) { 39 | fatalError("init(coder:) has not been implemented") 40 | } 41 | 42 | // MARK: - Change scan state and update the scanning frame. 43 | public func changeScanState(with mode: ScanningTaskMode, _ frame: CGRect) { 44 | scanningFrame = frame 45 | restartScan() 46 | switch mode { 47 | case .wait: 48 | scanningType = .wait 49 | case .barcodeAndMobile: 50 | scanningType = .barcodeAndMobile 51 | case .mobile: 52 | scanningType = .mobile 53 | case .barcode: 54 | scanningType = .barcode 55 | case .qrCode: 56 | scanningType = .qrCode 57 | case .goodsCode: 58 | scanningType = .goodsCode 59 | } 60 | } 61 | 62 | // MARK: - Pause scanning. 63 | public func sessionPause() { 64 | session?.stopRunning() 65 | UIApplication.shared.isIdleTimerDisabled = false 66 | } 67 | 68 | // MARK: - Resume scanning. 69 | public func sessionResume() { 70 | session?.startRunning() 71 | UIApplication.shared.isIdleTimerDisabled = true 72 | } 73 | 74 | // MARK: - Adjust the focus. 75 | public func adjustFocus(_ point: CGPoint) { 76 | debugPrint("Adjusting focus at: \(point)") 77 | guard let device = captureDevice else { 78 | debugPrint("Find Error: Device adjust focus failed") 79 | return 80 | } 81 | // 转换 point 数据为焦点相对位置 82 | let focusPoint = CGPoint( 83 | x: point.y / UIScreen.main.bounds.size.height, 84 | y: 1 - point.x / UIScreen.main.bounds.size.width 85 | ) 86 | if device.isFocusPointOfInterestSupported && device.isFocusModeSupported(.autoFocus) { 87 | do { 88 | try device.lockForConfiguration() 89 | device.focusPointOfInterest = focusPoint 90 | device.focusMode = .autoFocus 91 | device.unlockForConfiguration() 92 | } catch let error { 93 | debugPrint("Find Error: Set device focus failed\n\(error)") 94 | } 95 | } 96 | } 97 | 98 | // MARK: - Toggle Flashlight during the capture session. 99 | public func toggleFlashlight(enable: Bool) -> String? { 100 | guard 101 | let device = captureDevice, 102 | device.hasTorch 103 | else { return "Device has no torch available." } 104 | 105 | do { 106 | try device.lockForConfiguration() 107 | device.torchMode = enable ? .on : .off 108 | device.unlockForConfiguration() 109 | return nil 110 | } catch { 111 | return "Torch could not be used." 112 | } 113 | } 114 | 115 | // MARK: - Run DeviceCapture 116 | public func createDeviceCapture() { 117 | // 创建 Device 对象 118 | let device = AVCaptureDevice.default(for: .video) 119 | session = AVCaptureSession() 120 | guard let currentDevice = device, let input = try? AVCaptureDeviceInput(device: currentDevice) else { 121 | debugPrint("Device input init failed.") 122 | return 123 | } 124 | // 设置参数 125 | session = AVCaptureSession() 126 | if session?.canAddInput(input) ?? false { 127 | session?.addInput(input) 128 | } 129 | if let currentSession = session { 130 | previewLayer = AVCaptureVideoPreviewLayer(session: currentSession) 131 | } 132 | // 设置预览图层 133 | previewLayer?.videoGravity = .resizeAspectFill 134 | previewLayer?.frame = viewRect ?? UIScreen.main.bounds 135 | if let currentPreviewLayer = previewLayer { 136 | layer.insertSublayer(currentPreviewLayer, at: 0) 137 | } 138 | // 设置视频清晰度,非刘海屏一律使用最低清晰度 139 | if safeAreaInsets.top > 0 && session?.canSetSessionPreset(.high) ?? false { 140 | session?.canSetSessionPreset(.high) 141 | } else if (session?.canSetSessionPreset(.low) ?? false) { 142 | session?.canSetSessionPreset(.low) 143 | } 144 | // 设置输出格式 145 | let videoOutput = AVCaptureVideoDataOutput() 146 | videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: NSNumber(value: kCVPixelFormatType_32BGRA)] 147 | if session?.canAddOutput(videoOutput) ?? false { 148 | session?.addOutput(videoOutput) 149 | videoOutput.accessibilityFrame = UIScreen.main.bounds 150 | } 151 | // 设置分辨率 152 | let fps = 15 153 | if let device = captureDevice, let fpsRange = device.activeFormat.videoSupportedFrameRateRanges.first { 154 | if fps < Int(fpsRange.maxFrameRate) && fps > Int(fpsRange.minFrameRate) { 155 | do { 156 | try device.lockForConfiguration() 157 | device.activeVideoMinFrameDuration = CMTime(value: 1, timescale: CMTimeScale(fps)) 158 | device.activeVideoMaxFrameDuration = CMTime(value: 1, timescale: CMTimeScale(fps)) 159 | device.unlockForConfiguration() 160 | } catch let error { 161 | debugPrint("Find Error: Set device fps failed\n\(error)") 162 | } 163 | } 164 | } 165 | videoOutput.setSampleBufferDelegate(self, queue: .main) 166 | captureDevice = device 167 | sessionResume() 168 | } 169 | 170 | // MARK: - Start scan, prepare for video output. 171 | private func restartScan() { 172 | resultModel = ScanResult() 173 | codeSuccess = false 174 | phoneSuccess = false 175 | timeOut = false 176 | } 177 | 178 | // MARK: - Check the output result. 179 | private func checkOutputState() { 180 | // 处理当前扫描结果 181 | var toFlutterResult = ScanResult() 182 | toFlutterResult.code = resultModel?.code 183 | toFlutterResult.phone = resultModel?.phone ?? [] 184 | // 设置当前扫描结果的状态码 185 | switch scanningType { 186 | case .barcodeAndMobile: 187 | toFlutterResult.state = .progress 188 | if codeSuccess && phoneSuccess { 189 | toFlutterResult.state = .success 190 | } 191 | case .mobile: 192 | if phoneSuccess { 193 | toFlutterResult.state = .success 194 | } 195 | case .barcode, .qrCode, .goodsCode: 196 | if codeSuccess { 197 | toFlutterResult.state = .success 198 | } 199 | default: 200 | break 201 | } 202 | let result = toFlutterResult.toJSON() 203 | debugPrint(result) 204 | // 根据状态码回传扫描结果 205 | switch toFlutterResult.state { 206 | case .success: 207 | scanEnd() 208 | ChannelManager.shared.eventSink?(result) 209 | case .progress: 210 | if timeOut { 211 | resultModel?.phone.removeAll() 212 | scanningType = .wait 213 | createTimer = false 214 | timeOut = false 215 | ChannelManager.shared.eventSink?(result) 216 | } 217 | default: 218 | break 219 | } 220 | } 221 | 222 | // MARK: - Start the OCR decode task. 223 | private func startScanTask(_ buffer: CMSampleBuffer) { 224 | if (imageParser.isProcessing) { 225 | return 226 | } 227 | guard let scanImage = imageParser.sampleBufferToImage(buffer) else { 228 | debugPrint("SampleBuffer does not include UIImage.") 229 | return 230 | } 231 | guard let resultImage = imageParser.crop( 232 | image: scanImage, 233 | rect: scanningFrame! 234 | ) else { 235 | debugPrint("Scan image cannot be cropped.") 236 | return 237 | } 238 | // 根据扫描模式和任务状态标识开启不同的任务 239 | switch scanningType { 240 | case .barcodeAndMobile: // 常规任务情况下根据状态进行识别任务节流 241 | if codeSuccess { 242 | startScanPhoneTask(resultImage) 243 | } else { 244 | startScanCodeTask(resultImage) 245 | if !phoneSuccess { 246 | startScanPhoneTask(resultImage) 247 | } 248 | } 249 | case .barcode, .qrCode, .goodsCode: 250 | startScanCodeTask(resultImage) 251 | case .mobile: 252 | startScanPhoneTask(resultImage) 253 | default: 254 | break 255 | } 256 | } 257 | 258 | // MARK: - Start the barcode scanning task. 259 | private func startScanCodeTask(_ image: UIImage) { 260 | let visionImage = VisionImage(image: image) 261 | // Define the options for a barcode detector. 262 | let format: BarcodeFormat 263 | if (scanningType == .qrCode) { 264 | format = qrCodeFormats 265 | } else if (scanningType == .goodsCode) { 266 | format = goodsCodeFormats 267 | } else { 268 | format = barcodeFormats 269 | } 270 | // Create a barcode scanner. 271 | let barcodeScanner = BarcodeScanner.barcodeScanner(options: BarcodeScannerOptions(formats: format)) 272 | var barcodes: [Barcode] 273 | weak var weakSelf = self 274 | do { 275 | barcodes = try barcodeScanner.results(in: visionImage) 276 | } catch let error { 277 | debugPrint("Failed to scan barcodes with error: \(error.localizedDescription).") 278 | return 279 | } 280 | guard let strongSelf = weakSelf else { 281 | debugPrint("Self is nil!") 282 | return 283 | } 284 | if let strScanned = checkCodeText(barcodes, scanningType) { 285 | debugPrint("Scanned code string = \(strScanned)") 286 | strongSelf.resultModel?.code = strScanned 287 | strongSelf.codeSuccess = true 288 | DispatchQueue.main.async { 289 | if strongSelf.scanningType == .barcodeAndMobile && !strongSelf.createTimer { 290 | strongSelf.createTimer = true 291 | strongSelf.setScanTimer() 292 | } else { 293 | strongSelf.checkOutputState() 294 | } 295 | } 296 | } 297 | } 298 | 299 | // MARK: - Start the phone number scanning task. 300 | private func startScanPhoneTask(_ image: UIImage) { 301 | let visionImage = VisionImage(image: image) 302 | let recognizer = TextRecognizer.textRecognizer(options: TextRecognizerOptions.init()) 303 | var text: Text? 304 | weak var weakSelf = self 305 | do { 306 | text = try recognizer.results(in: visionImage) 307 | } catch let error { 308 | debugPrint("Failed to scan texts with error: \(error.localizedDescription).") 309 | return 310 | } 311 | guard let strongSelf = weakSelf else { 312 | debugPrint("Self is nil!") 313 | return 314 | } 315 | if let text = text { 316 | var texts = Array() 317 | for block in text.blocks { 318 | for line in block.lines { 319 | let text = enhanceNumberText(line.text).filter(\.isNumber) 320 | if (!text.isEmpty) { 321 | texts.append(text) 322 | } 323 | } 324 | } 325 | let phones = checkPhoneText(texts) 326 | if (!phones.isEmpty) { 327 | strongSelf.resultModel?.phone.append(contentsOf: phones) 328 | strongSelf.phoneSuccess = strongSelf.resultModel?.phone.isEmpty == false 329 | // Delayed check after 600ms. 330 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { 331 | strongSelf.checkOutputState() 332 | } 333 | } 334 | } 335 | } 336 | 337 | // MARK: - Init the timeout timer and check the output result. 338 | private func setScanTimer() { 339 | DispatchQueue.main.asyncAfter(deadline: .now() + ScanConfig.scanTimeOutDuration) { 340 | self.timeOut = true 341 | self.checkOutputState() 342 | } 343 | } 344 | 345 | // MARK: - A scanning queue has finished. 346 | private func scanEnd() { 347 | scanningType = .wait 348 | resultModel = nil 349 | codeSuccess = false 350 | phoneSuccess = false 351 | timeOut = false 352 | createTimer = false 353 | } 354 | 355 | // MARK: - deinit 356 | // view 释放时关闭 session 357 | deinit { 358 | debugPrint("deinit") 359 | sessionPause() 360 | session = nil 361 | } 362 | 363 | // MARK: - Close scan device capture. 364 | public func closeScanView() { 365 | debugPrint("close") 366 | let _ = toggleFlashlight(enable: false) 367 | sessionPause() 368 | session = nil 369 | } 370 | } 371 | 372 | extension ScanView: AVCaptureVideoDataOutputSampleBufferDelegate { 373 | static var scanCountMargin = 0 374 | 375 | func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { 376 | if scanningType == .wait { 377 | return 378 | } 379 | ScanView.scanCountMargin += 1 380 | if ScanView.scanCountMargin != 5 { 381 | return 382 | } 383 | ScanView.scanCountMargin = 0 384 | DispatchQueue.global().async { 385 | self.startScanTask(sampleBuffer) 386 | } 387 | } 388 | } 389 | --------------------------------------------------------------------------------