Committed by
GitHub
Merge branch 'master' into dependabot/github_actions/subosito/flutter-action-2.8.0
Showing
33 changed files
with
1226 additions
and
754 deletions
| @@ -11,7 +11,7 @@ jobs: | @@ -11,7 +11,7 @@ jobs: | ||
| 11 | analysis: | 11 | analysis: |
| 12 | runs-on: ubuntu-latest | 12 | runs-on: ubuntu-latest |
| 13 | steps: | 13 | steps: |
| 14 | - - uses: actions/checkout@v3.0.2 | 14 | + - uses: actions/checkout@v3.1.0 |
| 15 | - uses: actions/setup-java@v3.5.1 | 15 | - uses: actions/setup-java@v3.5.1 |
| 16 | with: | 16 | with: |
| 17 | java-version: 11 | 17 | java-version: 11 |
| @@ -28,7 +28,7 @@ jobs: | @@ -28,7 +28,7 @@ jobs: | ||
| 28 | formatting: | 28 | formatting: |
| 29 | runs-on: ubuntu-latest | 29 | runs-on: ubuntu-latest |
| 30 | steps: | 30 | steps: |
| 31 | - - uses: actions/checkout@v3.0.2 | 31 | + - uses: actions/checkout@v3.1.0 |
| 32 | - uses: actions/setup-java@v3.5.1 | 32 | - uses: actions/setup-java@v3.5.1 |
| 33 | with: | 33 | with: |
| 34 | java-version: 11 | 34 | java-version: 11 |
| @@ -2,14 +2,14 @@ group 'dev.steenbakker.mobile_scanner' | @@ -2,14 +2,14 @@ group 'dev.steenbakker.mobile_scanner' | ||
| 2 | version '1.0-SNAPSHOT' | 2 | version '1.0-SNAPSHOT' |
| 3 | 3 | ||
| 4 | buildscript { | 4 | buildscript { |
| 5 | - ext.kotlin_version = '1.7.10' | 5 | + ext.kotlin_version = '1.7.20' |
| 6 | repositories { | 6 | repositories { |
| 7 | google() | 7 | google() |
| 8 | mavenCentral() | 8 | mavenCentral() |
| 9 | } | 9 | } |
| 10 | 10 | ||
| 11 | dependencies { | 11 | dependencies { |
| 12 | - classpath 'com.android.tools.build:gradle:7.3.0' | 12 | + classpath 'com.android.tools.build:gradle:7.3.1' |
| 13 | } | 13 | } |
| 14 | } | 14 | } |
| 15 | 15 | ||
| @@ -50,7 +50,7 @@ dependencies { | @@ -50,7 +50,7 @@ dependencies { | ||
| 50 | // Use this dependency to bundle the model with your app | 50 | // Use this dependency to bundle the model with your app |
| 51 | implementation 'com.google.mlkit:barcode-scanning:17.0.2' | 51 | implementation 'com.google.mlkit:barcode-scanning:17.0.2' |
| 52 | // Use this dependency to use the dynamically downloaded model in Google Play Services | 52 | // Use this dependency to use the dynamically downloaded model in Google Play Services |
| 53 | -// implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.0.0' | 53 | +// implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.1.0' |
| 54 | 54 | ||
| 55 | implementation 'androidx.camera:camera-camera2:1.1.0' | 55 | implementation 'androidx.camera:camera-camera2:1.1.0' |
| 56 | implementation 'androidx.camera:camera-lifecycle:1.1.0' | 56 | implementation 'androidx.camera:camera-lifecycle:1.1.0' |
| @@ -3,7 +3,11 @@ package dev.steenbakker.mobile_scanner | @@ -3,7 +3,11 @@ package dev.steenbakker.mobile_scanner | ||
| 3 | import android.Manifest | 3 | import android.Manifest |
| 4 | import android.app.Activity | 4 | import android.app.Activity |
| 5 | import android.content.pm.PackageManager | 5 | import android.content.pm.PackageManager |
| 6 | +import android.graphics.ImageFormat | ||
| 6 | import android.graphics.Point | 7 | import android.graphics.Point |
| 8 | +import android.graphics.Rect | ||
| 9 | +import android.graphics.YuvImage | ||
| 10 | +import android.media.Image | ||
| 7 | import android.net.Uri | 11 | import android.net.Uri |
| 8 | import android.util.Log | 12 | import android.util.Log |
| 9 | import android.util.Size | 13 | import android.util.Size |
| @@ -23,11 +27,13 @@ import io.flutter.plugin.common.MethodCall | @@ -23,11 +27,13 @@ import io.flutter.plugin.common.MethodCall | ||
| 23 | import io.flutter.plugin.common.MethodChannel | 27 | import io.flutter.plugin.common.MethodChannel |
| 24 | import io.flutter.plugin.common.PluginRegistry | 28 | import io.flutter.plugin.common.PluginRegistry |
| 25 | import io.flutter.view.TextureRegistry | 29 | import io.flutter.view.TextureRegistry |
| 30 | +import java.io.ByteArrayOutputStream | ||
| 26 | import java.io.File | 31 | import java.io.File |
| 27 | 32 | ||
| 28 | 33 | ||
| 29 | -class MobileScanner(private val activity: Activity, private val textureRegistry: TextureRegistry) | ||
| 30 | - : MethodChannel.MethodCallHandler, EventChannel.StreamHandler, PluginRegistry.RequestPermissionsResultListener { | 34 | +class MobileScanner(private val activity: Activity, private val textureRegistry: TextureRegistry) : |
| 35 | + MethodChannel.MethodCallHandler, EventChannel.StreamHandler, | ||
| 36 | + PluginRegistry.RequestPermissionsResultListener { | ||
| 31 | companion object { | 37 | companion object { |
| 32 | /** | 38 | /** |
| 33 | * When the application's activity is [androidx.fragment.app.FragmentActivity], requestCode can only use the lower 16 bits. | 39 | * When the application's activity is [androidx.fragment.app.FragmentActivity], requestCode can only use the lower 16 bits. |
| @@ -70,15 +76,23 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -70,15 +76,23 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 70 | sink = null | 76 | sink = null |
| 71 | } | 77 | } |
| 72 | 78 | ||
| 73 | - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray): Boolean { | 79 | + override fun onRequestPermissionsResult( |
| 80 | + requestCode: Int, | ||
| 81 | + permissions: Array<out String>, | ||
| 82 | + grantResults: IntArray | ||
| 83 | + ): Boolean { | ||
| 74 | return listener?.onRequestPermissionsResult(requestCode, permissions, grantResults) ?: false | 84 | return listener?.onRequestPermissionsResult(requestCode, permissions, grantResults) ?: false |
| 75 | } | 85 | } |
| 76 | 86 | ||
| 77 | private fun checkPermission(result: MethodChannel.Result) { | 87 | private fun checkPermission(result: MethodChannel.Result) { |
| 78 | // Can't get exact denied or not_determined state without request. Just return not_determined when state isn't authorized | 88 | // Can't get exact denied or not_determined state without request. Just return not_determined when state isn't authorized |
| 79 | val state = | 89 | val state = |
| 80 | - if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) 1 | ||
| 81 | - else 0 | 90 | + if (ContextCompat.checkSelfPermission( |
| 91 | + activity, | ||
| 92 | + Manifest.permission.CAMERA | ||
| 93 | + ) == PackageManager.PERMISSION_GRANTED | ||
| 94 | + ) 1 | ||
| 95 | + else 0 | ||
| 82 | result.success(state) | 96 | result.success(state) |
| 83 | } | 97 | } |
| 84 | 98 | ||
| @@ -96,32 +110,83 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -96,32 +110,83 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 96 | val permissions = arrayOf(Manifest.permission.CAMERA) | 110 | val permissions = arrayOf(Manifest.permission.CAMERA) |
| 97 | ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE) | 111 | ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE) |
| 98 | } | 112 | } |
| 99 | - | 113 | +// var lastScanned: List<Barcode>? = null |
| 114 | +// var isAnalyzing: Boolean = false | ||
| 100 | 115 | ||
| 101 | @ExperimentalGetImage | 116 | @ExperimentalGetImage |
| 102 | val analyzer = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format | 117 | val analyzer = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format |
| 103 | -// when (analyzeMode) { | ||
| 104 | -// AnalyzeMode.BARCODE -> { | ||
| 105 | - val mediaImage = imageProxy.image ?: return@Analyzer | ||
| 106 | - val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) | ||
| 107 | - | ||
| 108 | - scanner.process(inputImage) | ||
| 109 | - .addOnSuccessListener { barcodes -> | ||
| 110 | - for (barcode in barcodes) { | ||
| 111 | - val event = mapOf("name" to "barcode", "data" to barcode.data) | ||
| 112 | - sink?.success(event) | ||
| 113 | - } | ||
| 114 | - } | ||
| 115 | - .addOnFailureListener { e -> Log.e(TAG, e.message, e) } | ||
| 116 | - .addOnCompleteListener { imageProxy.close() } | ||
| 117 | -// } | ||
| 118 | -// else -> imageProxy.close() | ||
| 119 | -// } | 118 | + |
| 119 | + val mediaImage = imageProxy.image ?: return@Analyzer | ||
| 120 | + val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) | ||
| 121 | + | ||
| 122 | + scanner.process(inputImage) | ||
| 123 | + .addOnSuccessListener { barcodes -> | ||
| 124 | +// if (isAnalyzing) { | ||
| 125 | +// Log.d("scanner", "SKIPPING" ) | ||
| 126 | +// return@addOnSuccessListener | ||
| 127 | +// } | ||
| 128 | +// isAnalyzing = true | ||
| 129 | + val barcodeMap = barcodes.map { barcode -> barcode.data } | ||
| 130 | + if (barcodeMap.isNotEmpty()) { | ||
| 131 | + sink?.success(mapOf( | ||
| 132 | + "name" to "barcode", | ||
| 133 | + "data" to barcodeMap, | ||
| 134 | + "image" to mediaImage.toByteArray() | ||
| 135 | + )) | ||
| 136 | + } | ||
| 137 | +// for (barcode in barcodes) { | ||
| 138 | +//// if (lastScanned?.contains(barcodes.first) == true) continue; | ||
| 139 | +// if (lastScanned == null) { | ||
| 140 | +// lastScanned = barcodes | ||
| 141 | +// } else if (lastScanned!!.contains(barcode)) { | ||
| 142 | +// // Duplicate, don't send image | ||
| 143 | +// sink?.success(mapOf( | ||
| 144 | +// "name" to "barcode", | ||
| 145 | +// "data" to barcode.data, | ||
| 146 | +// )) | ||
| 147 | +// } else { | ||
| 148 | +// if (byteArray.isEmpty()) { | ||
| 149 | +// Log.d("scanner", "EMPTY" ) | ||
| 150 | +// return@addOnSuccessListener | ||
| 151 | +// } | ||
| 152 | +// | ||
| 153 | +// Log.d("scanner", "SCANNED IMAGE: $byteArray") | ||
| 154 | +// lastScanned = barcodes; | ||
| 155 | +// | ||
| 156 | +// | ||
| 157 | +// } | ||
| 158 | +// | ||
| 159 | +// } | ||
| 160 | +// isAnalyzing = false | ||
| 161 | + } | ||
| 162 | + .addOnFailureListener { e -> sink?.success(mapOf( | ||
| 163 | + "name" to "error", | ||
| 164 | + "data" to e.localizedMessage | ||
| 165 | + )) } | ||
| 166 | + .addOnCompleteListener { imageProxy.close() } | ||
| 120 | } | 167 | } |
| 121 | 168 | ||
| 169 | + private fun Image.toByteArray(): ByteArray { | ||
| 170 | + val yBuffer = planes[0].buffer // Y | ||
| 171 | + val vuBuffer = planes[2].buffer // VU | ||
| 172 | + | ||
| 173 | + val ySize = yBuffer.remaining() | ||
| 174 | + val vuSize = vuBuffer.remaining() | ||
| 175 | + | ||
| 176 | + val nv21 = ByteArray(ySize + vuSize) | ||
| 177 | + | ||
| 178 | + yBuffer.get(nv21, 0, ySize) | ||
| 179 | + vuBuffer.get(nv21, ySize, vuSize) | ||
| 180 | + | ||
| 181 | + val yuvImage = YuvImage(nv21, ImageFormat.NV21, this.width, this.height, null) | ||
| 182 | + val out = ByteArrayOutputStream() | ||
| 183 | + yuvImage.compressToJpeg(Rect(0, 0, yuvImage.width, yuvImage.height), 50, out) | ||
| 184 | + return out.toByteArray() | ||
| 185 | + } | ||
| 122 | 186 | ||
| 123 | private var scanner = BarcodeScanning.getClient() | 187 | private var scanner = BarcodeScanning.getClient() |
| 124 | 188 | ||
| 189 | + | ||
| 125 | @ExperimentalGetImage | 190 | @ExperimentalGetImage |
| 126 | private fun start(call: MethodCall, result: MethodChannel.Result) { | 191 | private fun start(call: MethodCall, result: MethodChannel.Result) { |
| 127 | if (camera?.cameraInfo != null && preview != null && textureEntry != null) { | 192 | if (camera?.cameraInfo != null && preview != null && textureEntry != null) { |
| @@ -129,14 +194,23 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -129,14 +194,23 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 129 | val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0 | 194 | val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0 |
| 130 | val width = resolution.width.toDouble() | 195 | val width = resolution.width.toDouble() |
| 131 | val height = resolution.height.toDouble() | 196 | val height = resolution.height.toDouble() |
| 132 | - val size = if (portrait) mapOf("width" to width, "height" to height) else mapOf("width" to height, "height" to width) | ||
| 133 | - val answer = mapOf("textureId" to textureEntry!!.id(), "size" to size, "torchable" to camera!!.cameraInfo.hasFlashUnit()) | 197 | + val size = if (portrait) mapOf( |
| 198 | + "width" to width, | ||
| 199 | + "height" to height | ||
| 200 | + ) else mapOf("width" to height, "height" to width) | ||
| 201 | + val answer = mapOf( | ||
| 202 | + "textureId" to textureEntry!!.id(), | ||
| 203 | + "size" to size, | ||
| 204 | + "torchable" to camera!!.cameraInfo.hasFlashUnit() | ||
| 205 | + ) | ||
| 134 | result.success(answer) | 206 | result.success(answer) |
| 135 | } else { | 207 | } else { |
| 136 | val facing: Int = call.argument<Int>("facing") ?: 0 | 208 | val facing: Int = call.argument<Int>("facing") ?: 0 |
| 137 | val ratio: Int? = call.argument<Int>("ratio") | 209 | val ratio: Int? = call.argument<Int>("ratio") |
| 138 | val torch: Boolean = call.argument<Boolean>("torch") ?: false | 210 | val torch: Boolean = call.argument<Boolean>("torch") ?: false |
| 139 | val formats: List<Int>? = call.argument<List<Int>>("formats") | 211 | val formats: List<Int>? = call.argument<List<Int>>("formats") |
| 212 | +// val analyzerWidth = call.argument<Int>("ratio") | ||
| 213 | +// val analyzeRHEIG = call.argument<Int>("ratio") | ||
| 140 | 214 | ||
| 141 | if (formats != null) { | 215 | if (formats != null) { |
| 142 | val formatsList: MutableList<Int> = mutableListOf() | 216 | val formatsList: MutableList<Int> = mutableListOf() |
| @@ -144,9 +218,17 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -144,9 +218,17 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 144 | formatsList.add(BarcodeFormats.values()[index].intValue) | 218 | formatsList.add(BarcodeFormats.values()[index].intValue) |
| 145 | } | 219 | } |
| 146 | scanner = if (formatsList.size == 1) { | 220 | scanner = if (formatsList.size == 1) { |
| 147 | - BarcodeScanning.getClient(BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first()).build()) | 221 | + BarcodeScanning.getClient( |
| 222 | + BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first()) | ||
| 223 | + .build() | ||
| 224 | + ) | ||
| 148 | } else { | 225 | } else { |
| 149 | - BarcodeScanning.getClient(BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first(), *formatsList.subList(1, formatsList.size).toIntArray()).build()) | 226 | + BarcodeScanning.getClient( |
| 227 | + BarcodeScannerOptions.Builder().setBarcodeFormats( | ||
| 228 | + formatsList.first(), | ||
| 229 | + *formatsList.subList(1, formatsList.size).toIntArray() | ||
| 230 | + ).build() | ||
| 231 | + ) | ||
| 150 | } | 232 | } |
| 151 | } | 233 | } |
| 152 | 234 | ||
| @@ -168,7 +250,10 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -168,7 +250,10 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 168 | // Preview | 250 | // Preview |
| 169 | val surfaceProvider = Preview.SurfaceProvider { request -> | 251 | val surfaceProvider = Preview.SurfaceProvider { request -> |
| 170 | val texture = textureEntry!!.surfaceTexture() | 252 | val texture = textureEntry!!.surfaceTexture() |
| 171 | - texture.setDefaultBufferSize(request.resolution.width, request.resolution.height) | 253 | + texture.setDefaultBufferSize( |
| 254 | + request.resolution.width, | ||
| 255 | + request.resolution.height | ||
| 256 | + ) | ||
| 172 | val surface = Surface(texture) | 257 | val surface = Surface(texture) |
| 173 | request.provideSurface(surface, executor) { } | 258 | request.provideSurface(surface, executor) { } |
| 174 | } | 259 | } |
| @@ -182,16 +267,23 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -182,16 +267,23 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 182 | 267 | ||
| 183 | // Build the analyzer to be passed on to MLKit | 268 | // Build the analyzer to be passed on to MLKit |
| 184 | val analysisBuilder = ImageAnalysis.Builder() | 269 | val analysisBuilder = ImageAnalysis.Builder() |
| 185 | - .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) | 270 | + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) |
| 186 | if (ratio != null) { | 271 | if (ratio != null) { |
| 187 | analysisBuilder.setTargetAspectRatio(ratio) | 272 | analysisBuilder.setTargetAspectRatio(ratio) |
| 188 | } | 273 | } |
| 274 | +// analysisBuilder.setTargetResolution(Size(1440, 1920)) | ||
| 189 | val analysis = analysisBuilder.build().apply { setAnalyzer(executor, analyzer) } | 275 | val analysis = analysisBuilder.build().apply { setAnalyzer(executor, analyzer) } |
| 190 | 276 | ||
| 191 | // Select the correct camera | 277 | // Select the correct camera |
| 192 | - val selector = if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA | 278 | + val selector = |
| 279 | + if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA | ||
| 193 | 280 | ||
| 194 | - camera = cameraProvider!!.bindToLifecycle(activity as LifecycleOwner, selector, preview, analysis) | 281 | + camera = cameraProvider!!.bindToLifecycle( |
| 282 | + activity as LifecycleOwner, | ||
| 283 | + selector, | ||
| 284 | + preview, | ||
| 285 | + analysis | ||
| 286 | + ) | ||
| 195 | 287 | ||
| 196 | val analysisSize = analysis.resolutionInfo?.resolution ?: Size(0, 0) | 288 | val analysisSize = analysis.resolutionInfo?.resolution ?: Size(0, 0) |
| 197 | val previewSize = preview!!.resolutionInfo?.resolution ?: Size(0, 0) | 289 | val previewSize = preview!!.resolutionInfo?.resolution ?: Size(0, 0) |
| @@ -216,8 +308,15 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -216,8 +308,15 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 216 | val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0 | 308 | val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0 |
| 217 | val width = resolution.width.toDouble() | 309 | val width = resolution.width.toDouble() |
| 218 | val height = resolution.height.toDouble() | 310 | val height = resolution.height.toDouble() |
| 219 | - val size = if (portrait) mapOf("width" to width, "height" to height) else mapOf("width" to height, "height" to width) | ||
| 220 | - val answer = mapOf("textureId" to textureEntry!!.id(), "size" to size, "torchable" to camera!!.cameraInfo.hasFlashUnit()) | 311 | + val size = if (portrait) mapOf( |
| 312 | + "width" to width, | ||
| 313 | + "height" to height | ||
| 314 | + ) else mapOf("width" to height, "height" to width) | ||
| 315 | + val answer = mapOf( | ||
| 316 | + "textureId" to textureEntry!!.id(), | ||
| 317 | + "size" to size, | ||
| 318 | + "torchable" to camera!!.cameraInfo.hasFlashUnit() | ||
| 319 | + ) | ||
| 221 | result.success(answer) | 320 | result.success(answer) |
| 222 | }, executor) | 321 | }, executor) |
| 223 | } | 322 | } |
| @@ -225,7 +324,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -225,7 +324,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 225 | 324 | ||
| 226 | private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) { | 325 | private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) { |
| 227 | if (camera == null) { | 326 | if (camera == null) { |
| 228 | - result.error(TAG,"Called toggleTorch() while stopped!", null) | 327 | + result.error(TAG, "Called toggleTorch() while stopped!", null) |
| 229 | return | 328 | return |
| 230 | } | 329 | } |
| 231 | camera!!.cameraControl.enableTorch(call.arguments == 1) | 330 | camera!!.cameraControl.enableTorch(call.arguments == 1) |
| @@ -238,7 +337,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -238,7 +337,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 238 | // } | 337 | // } |
| 239 | 338 | ||
| 240 | private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) { | 339 | private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) { |
| 241 | - val uri = Uri.fromFile( File(call.arguments.toString())) | 340 | + val uri = Uri.fromFile(File(call.arguments.toString())) |
| 242 | val inputImage = InputImage.fromFilePath(activity, uri) | 341 | val inputImage = InputImage.fromFilePath(activity, uri) |
| 243 | 342 | ||
| 244 | var barcodeFound = false | 343 | var barcodeFound = false |
| @@ -249,15 +348,17 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -249,15 +348,17 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 249 | sink?.success(mapOf("name" to "barcode", "data" to barcode.data)) | 348 | sink?.success(mapOf("name" to "barcode", "data" to barcode.data)) |
| 250 | } | 349 | } |
| 251 | } | 350 | } |
| 252 | - .addOnFailureListener { e -> Log.e(TAG, e.message, e) | ||
| 253 | - result.error(TAG, e.message, e)} | 351 | + .addOnFailureListener { e -> |
| 352 | + Log.e(TAG, e.message, e) | ||
| 353 | + result.error(TAG, e.message, e) | ||
| 354 | + } | ||
| 254 | .addOnCompleteListener { result.success(barcodeFound) } | 355 | .addOnCompleteListener { result.success(barcodeFound) } |
| 255 | 356 | ||
| 256 | } | 357 | } |
| 257 | 358 | ||
| 258 | private fun stop(result: MethodChannel.Result) { | 359 | private fun stop(result: MethodChannel.Result) { |
| 259 | if (camera == null && preview == null) { | 360 | if (camera == null && preview == null) { |
| 260 | - result.error(TAG,"Called stop() while already stopped!", null) | 361 | + result.error(TAG, "Called stop() while already stopped!", null) |
| 261 | return | 362 | return |
| 262 | } | 363 | } |
| 263 | 364 | ||
| @@ -277,41 +378,54 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -277,41 +378,54 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 277 | 378 | ||
| 278 | 379 | ||
| 279 | private val Barcode.data: Map<String, Any?> | 380 | private val Barcode.data: Map<String, Any?> |
| 280 | - get() = mapOf("corners" to cornerPoints?.map { corner -> corner.data }, "format" to format, | ||
| 281 | - "rawBytes" to rawBytes, "rawValue" to rawValue, "type" to valueType, | ||
| 282 | - "calendarEvent" to calendarEvent?.data, "contactInfo" to contactInfo?.data, | ||
| 283 | - "driverLicense" to driverLicense?.data, "email" to email?.data, | ||
| 284 | - "geoPoint" to geoPoint?.data, "phone" to phone?.data, "sms" to sms?.data, | ||
| 285 | - "url" to url?.data, "wifi" to wifi?.data, "displayValue" to displayValue) | 381 | + get() = mapOf( |
| 382 | + "corners" to cornerPoints?.map { corner -> corner.data }, "format" to format, | ||
| 383 | + "rawBytes" to rawBytes, "rawValue" to rawValue, "type" to valueType, | ||
| 384 | + "calendarEvent" to calendarEvent?.data, "contactInfo" to contactInfo?.data, | ||
| 385 | + "driverLicense" to driverLicense?.data, "email" to email?.data, | ||
| 386 | + "geoPoint" to geoPoint?.data, "phone" to phone?.data, "sms" to sms?.data, | ||
| 387 | + "url" to url?.data, "wifi" to wifi?.data, "displayValue" to displayValue | ||
| 388 | + ) | ||
| 286 | 389 | ||
| 287 | private val Point.data: Map<String, Double> | 390 | private val Point.data: Map<String, Double> |
| 288 | get() = mapOf("x" to x.toDouble(), "y" to y.toDouble()) | 391 | get() = mapOf("x" to x.toDouble(), "y" to y.toDouble()) |
| 289 | 392 | ||
| 290 | private val Barcode.CalendarEvent.data: Map<String, Any?> | 393 | private val Barcode.CalendarEvent.data: Map<String, Any?> |
| 291 | - get() = mapOf("description" to description, "end" to end?.rawValue, "location" to location, | ||
| 292 | - "organizer" to organizer, "start" to start?.rawValue, "status" to status, | ||
| 293 | - "summary" to summary) | 394 | + get() = mapOf( |
| 395 | + "description" to description, "end" to end?.rawValue, "location" to location, | ||
| 396 | + "organizer" to organizer, "start" to start?.rawValue, "status" to status, | ||
| 397 | + "summary" to summary | ||
| 398 | + ) | ||
| 294 | 399 | ||
| 295 | private val Barcode.ContactInfo.data: Map<String, Any?> | 400 | private val Barcode.ContactInfo.data: Map<String, Any?> |
| 296 | - get() = mapOf("addresses" to addresses.map { address -> address.data }, | ||
| 297 | - "emails" to emails.map { email -> email.data }, "name" to name?.data, | ||
| 298 | - "organization" to organization, "phones" to phones.map { phone -> phone.data }, | ||
| 299 | - "title" to title, "urls" to urls) | 401 | + get() = mapOf( |
| 402 | + "addresses" to addresses.map { address -> address.data }, | ||
| 403 | + "emails" to emails.map { email -> email.data }, "name" to name?.data, | ||
| 404 | + "organization" to organization, "phones" to phones.map { phone -> phone.data }, | ||
| 405 | + "title" to title, "urls" to urls | ||
| 406 | + ) | ||
| 300 | 407 | ||
| 301 | private val Barcode.Address.data: Map<String, Any?> | 408 | private val Barcode.Address.data: Map<String, Any?> |
| 302 | - get() = mapOf("addressLines" to addressLines.map { addressLine -> addressLine.toString() }, "type" to type) | 409 | + get() = mapOf( |
| 410 | + "addressLines" to addressLines.map { addressLine -> addressLine.toString() }, | ||
| 411 | + "type" to type | ||
| 412 | + ) | ||
| 303 | 413 | ||
| 304 | private val Barcode.PersonName.data: Map<String, Any?> | 414 | private val Barcode.PersonName.data: Map<String, Any?> |
| 305 | - get() = mapOf("first" to first, "formattedName" to formattedName, "last" to last, | ||
| 306 | - "middle" to middle, "prefix" to prefix, "pronunciation" to pronunciation, | ||
| 307 | - "suffix" to suffix) | 415 | + get() = mapOf( |
| 416 | + "first" to first, "formattedName" to formattedName, "last" to last, | ||
| 417 | + "middle" to middle, "prefix" to prefix, "pronunciation" to pronunciation, | ||
| 418 | + "suffix" to suffix | ||
| 419 | + ) | ||
| 308 | 420 | ||
| 309 | private val Barcode.DriverLicense.data: Map<String, Any?> | 421 | private val Barcode.DriverLicense.data: Map<String, Any?> |
| 310 | - get() = mapOf("addressCity" to addressCity, "addressState" to addressState, | ||
| 311 | - "addressStreet" to addressStreet, "addressZip" to addressZip, "birthDate" to birthDate, | ||
| 312 | - "documentType" to documentType, "expiryDate" to expiryDate, "firstName" to firstName, | ||
| 313 | - "gender" to gender, "issueDate" to issueDate, "issuingCountry" to issuingCountry, | ||
| 314 | - "lastName" to lastName, "licenseNumber" to licenseNumber, "middleName" to middleName) | 422 | + get() = mapOf( |
| 423 | + "addressCity" to addressCity, "addressState" to addressState, | ||
| 424 | + "addressStreet" to addressStreet, "addressZip" to addressZip, "birthDate" to birthDate, | ||
| 425 | + "documentType" to documentType, "expiryDate" to expiryDate, "firstName" to firstName, | ||
| 426 | + "gender" to gender, "issueDate" to issueDate, "issuingCountry" to issuingCountry, | ||
| 427 | + "lastName" to lastName, "licenseNumber" to licenseNumber, "middleName" to middleName | ||
| 428 | + ) | ||
| 315 | 429 | ||
| 316 | private val Barcode.Email.data: Map<String, Any?> | 430 | private val Barcode.Email.data: Map<String, Any?> |
| 317 | get() = mapOf("address" to address, "body" to body, "subject" to subject, "type" to type) | 431 | get() = mapOf("address" to address, "body" to body, "subject" to subject, "type" to type) |
| @@ -330,4 +444,4 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -330,4 +444,4 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 330 | 444 | ||
| 331 | private val Barcode.WiFi.data: Map<String, Any?> | 445 | private val Barcode.WiFi.data: Map<String, Any?> |
| 332 | get() = mapOf("encryptionType" to encryptionType, "password" to password, "ssid" to ssid) | 446 | get() = mapOf("encryptionType" to encryptionType, "password" to password, "ssid" to ssid) |
| 333 | -} | 447 | +} |
| @@ -21,6 +21,6 @@ | @@ -21,6 +21,6 @@ | ||
| 21 | <key>CFBundleVersion</key> | 21 | <key>CFBundleVersion</key> |
| 22 | <string>1.0</string> | 22 | <string>1.0</string> |
| 23 | <key>MinimumOSVersion</key> | 23 | <key>MinimumOSVersion</key> |
| 24 | - <string>9.0</string> | 24 | + <string>11.0</string> |
| 25 | </dict> | 25 | </dict> |
| 26 | </plist> | 26 | </plist> |
| 1 | # Uncomment this line to define a global platform for your project | 1 | # Uncomment this line to define a global platform for your project |
| 2 | -platform :ios, '10.0' | 2 | +platform :ios, '11.0' |
| 3 | 3 | ||
| 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. |
| 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' | 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' |
| @@ -37,5 +37,9 @@ end | @@ -37,5 +37,9 @@ end | ||
| 37 | post_install do |installer| | 37 | post_install do |installer| |
| 38 | installer.pods_project.targets.each do |target| | 38 | installer.pods_project.targets.each do |target| |
| 39 | flutter_additional_ios_build_settings(target) | 39 | flutter_additional_ios_build_settings(target) |
| 40 | + target.build_configurations.each do |config| | ||
| 41 | + config.build_settings['ENABLE_BITCODE'] = 'NO' | ||
| 42 | + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0' | ||
| 43 | + end | ||
| 40 | end | 44 | end |
| 41 | end | 45 | end |
| @@ -339,7 +339,7 @@ | @@ -339,7 +339,7 @@ | ||
| 339 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; | 339 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; |
| 340 | GCC_WARN_UNUSED_FUNCTION = YES; | 340 | GCC_WARN_UNUSED_FUNCTION = YES; |
| 341 | GCC_WARN_UNUSED_VARIABLE = YES; | 341 | GCC_WARN_UNUSED_VARIABLE = YES; |
| 342 | - IPHONEOS_DEPLOYMENT_TARGET = 9.0; | 342 | + IPHONEOS_DEPLOYMENT_TARGET = 11.0; |
| 343 | MTL_ENABLE_DEBUG_INFO = NO; | 343 | MTL_ENABLE_DEBUG_INFO = NO; |
| 344 | SDKROOT = iphoneos; | 344 | SDKROOT = iphoneos; |
| 345 | SUPPORTED_PLATFORMS = iphoneos; | 345 | SUPPORTED_PLATFORMS = iphoneos; |
| @@ -355,7 +355,7 @@ | @@ -355,7 +355,7 @@ | ||
| 355 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | 355 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; |
| 356 | CLANG_ENABLE_MODULES = YES; | 356 | CLANG_ENABLE_MODULES = YES; |
| 357 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; | 357 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; |
| 358 | - DEVELOPMENT_TEAM = RCH2VG82SH; | 358 | + DEVELOPMENT_TEAM = 75Y2P2WSQQ; |
| 359 | ENABLE_BITCODE = NO; | 359 | ENABLE_BITCODE = NO; |
| 360 | INFOPLIST_FILE = Runner/Info.plist; | 360 | INFOPLIST_FILE = Runner/Info.plist; |
| 361 | LD_RUNPATH_SEARCH_PATHS = ( | 361 | LD_RUNPATH_SEARCH_PATHS = ( |
| @@ -417,7 +417,7 @@ | @@ -417,7 +417,7 @@ | ||
| 417 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; | 417 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; |
| 418 | GCC_WARN_UNUSED_FUNCTION = YES; | 418 | GCC_WARN_UNUSED_FUNCTION = YES; |
| 419 | GCC_WARN_UNUSED_VARIABLE = YES; | 419 | GCC_WARN_UNUSED_VARIABLE = YES; |
| 420 | - IPHONEOS_DEPLOYMENT_TARGET = 9.0; | 420 | + IPHONEOS_DEPLOYMENT_TARGET = 11.0; |
| 421 | MTL_ENABLE_DEBUG_INFO = YES; | 421 | MTL_ENABLE_DEBUG_INFO = YES; |
| 422 | ONLY_ACTIVE_ARCH = YES; | 422 | ONLY_ACTIVE_ARCH = YES; |
| 423 | SDKROOT = iphoneos; | 423 | SDKROOT = iphoneos; |
| @@ -466,7 +466,7 @@ | @@ -466,7 +466,7 @@ | ||
| 466 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; | 466 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; |
| 467 | GCC_WARN_UNUSED_FUNCTION = YES; | 467 | GCC_WARN_UNUSED_FUNCTION = YES; |
| 468 | GCC_WARN_UNUSED_VARIABLE = YES; | 468 | GCC_WARN_UNUSED_VARIABLE = YES; |
| 469 | - IPHONEOS_DEPLOYMENT_TARGET = 9.0; | 469 | + IPHONEOS_DEPLOYMENT_TARGET = 11.0; |
| 470 | MTL_ENABLE_DEBUG_INFO = NO; | 470 | MTL_ENABLE_DEBUG_INFO = NO; |
| 471 | SDKROOT = iphoneos; | 471 | SDKROOT = iphoneos; |
| 472 | SUPPORTED_PLATFORMS = iphoneos; | 472 | SUPPORTED_PLATFORMS = iphoneos; |
| @@ -484,7 +484,7 @@ | @@ -484,7 +484,7 @@ | ||
| 484 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | 484 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; |
| 485 | CLANG_ENABLE_MODULES = YES; | 485 | CLANG_ENABLE_MODULES = YES; |
| 486 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; | 486 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; |
| 487 | - DEVELOPMENT_TEAM = RCH2VG82SH; | 487 | + DEVELOPMENT_TEAM = 75Y2P2WSQQ; |
| 488 | ENABLE_BITCODE = NO; | 488 | ENABLE_BITCODE = NO; |
| 489 | INFOPLIST_FILE = Runner/Info.plist; | 489 | INFOPLIST_FILE = Runner/Info.plist; |
| 490 | LD_RUNPATH_SEARCH_PATHS = ( | 490 | LD_RUNPATH_SEARCH_PATHS = ( |
| @@ -507,7 +507,7 @@ | @@ -507,7 +507,7 @@ | ||
| 507 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | 507 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; |
| 508 | CLANG_ENABLE_MODULES = YES; | 508 | CLANG_ENABLE_MODULES = YES; |
| 509 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; | 509 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; |
| 510 | - DEVELOPMENT_TEAM = RCH2VG82SH; | 510 | + DEVELOPMENT_TEAM = 75Y2P2WSQQ; |
| 511 | ENABLE_BITCODE = NO; | 511 | ENABLE_BITCODE = NO; |
| 512 | INFOPLIST_FILE = Runner/Info.plist; | 512 | INFOPLIST_FILE = Runner/Info.plist; |
| 513 | LD_RUNPATH_SEARCH_PATHS = ( | 513 | LD_RUNPATH_SEARCH_PATHS = ( |
| 1 | +import 'package:flutter/material.dart'; | ||
| 2 | +import 'package:image_picker/image_picker.dart'; | ||
| 3 | +import 'package:mobile_scanner/mobile_scanner.dart'; | ||
| 4 | + | ||
| 5 | +class BarcodeListScannerWithController extends StatefulWidget { | ||
| 6 | + const BarcodeListScannerWithController({Key? key}) : super(key: key); | ||
| 7 | + | ||
| 8 | + @override | ||
| 9 | + _BarcodeListScannerWithControllerState createState() => | ||
| 10 | + _BarcodeListScannerWithControllerState(); | ||
| 11 | +} | ||
| 12 | + | ||
| 13 | +class _BarcodeListScannerWithControllerState | ||
| 14 | + extends State<BarcodeListScannerWithController> | ||
| 15 | + with SingleTickerProviderStateMixin { | ||
| 16 | + BarcodeCapture? barcodeCapture; | ||
| 17 | + | ||
| 18 | + MobileScannerController controller = MobileScannerController( | ||
| 19 | + torchEnabled: true, | ||
| 20 | + // formats: [BarcodeFormat.qrCode] | ||
| 21 | + // facing: CameraFacing.front, | ||
| 22 | + ); | ||
| 23 | + | ||
| 24 | + bool isStarted = true; | ||
| 25 | + | ||
| 26 | + @override | ||
| 27 | + Widget build(BuildContext context) { | ||
| 28 | + return Scaffold( | ||
| 29 | + backgroundColor: Colors.black, | ||
| 30 | + body: Builder( | ||
| 31 | + builder: (context) { | ||
| 32 | + return Stack( | ||
| 33 | + children: [ | ||
| 34 | + MobileScanner( | ||
| 35 | + controller: controller, | ||
| 36 | + fit: BoxFit.contain, | ||
| 37 | + // allowDuplicates: true, | ||
| 38 | + // controller: MobileScannerController( | ||
| 39 | + // torchEnabled: true, | ||
| 40 | + // facing: CameraFacing.front, | ||
| 41 | + // ), | ||
| 42 | + onDetect: (barcodeCapture, arguments) { | ||
| 43 | + setState(() { | ||
| 44 | + this.barcodeCapture = barcodeCapture; | ||
| 45 | + }); | ||
| 46 | + }, | ||
| 47 | + ), | ||
| 48 | + Align( | ||
| 49 | + alignment: Alignment.bottomCenter, | ||
| 50 | + child: Container( | ||
| 51 | + alignment: Alignment.bottomCenter, | ||
| 52 | + height: 100, | ||
| 53 | + color: Colors.black.withOpacity(0.4), | ||
| 54 | + child: Row( | ||
| 55 | + mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||
| 56 | + children: [ | ||
| 57 | + IconButton( | ||
| 58 | + color: Colors.white, | ||
| 59 | + icon: ValueListenableBuilder( | ||
| 60 | + valueListenable: controller.torchState, | ||
| 61 | + builder: (context, state, child) { | ||
| 62 | + if (state == null) { | ||
| 63 | + return const Icon( | ||
| 64 | + Icons.flash_off, | ||
| 65 | + color: Colors.grey, | ||
| 66 | + ); | ||
| 67 | + } | ||
| 68 | + switch (state as TorchState) { | ||
| 69 | + case TorchState.off: | ||
| 70 | + return const Icon( | ||
| 71 | + Icons.flash_off, | ||
| 72 | + color: Colors.grey, | ||
| 73 | + ); | ||
| 74 | + case TorchState.on: | ||
| 75 | + return const Icon( | ||
| 76 | + Icons.flash_on, | ||
| 77 | + color: Colors.yellow, | ||
| 78 | + ); | ||
| 79 | + } | ||
| 80 | + }, | ||
| 81 | + ), | ||
| 82 | + iconSize: 32.0, | ||
| 83 | + onPressed: () => controller.toggleTorch(), | ||
| 84 | + ), | ||
| 85 | + IconButton( | ||
| 86 | + color: Colors.white, | ||
| 87 | + icon: isStarted | ||
| 88 | + ? const Icon(Icons.stop) | ||
| 89 | + : const Icon(Icons.play_arrow), | ||
| 90 | + iconSize: 32.0, | ||
| 91 | + onPressed: () => setState(() { | ||
| 92 | + isStarted ? controller.stop() : controller.start(); | ||
| 93 | + isStarted = !isStarted; | ||
| 94 | + }), | ||
| 95 | + ), | ||
| 96 | + Center( | ||
| 97 | + child: SizedBox( | ||
| 98 | + width: MediaQuery.of(context).size.width - 200, | ||
| 99 | + height: 50, | ||
| 100 | + child: FittedBox( | ||
| 101 | + child: Text( | ||
| 102 | + '${barcodeCapture?.barcodes.map((e) => e.rawValue)}', | ||
| 103 | + overflow: TextOverflow.fade, | ||
| 104 | + style: Theme.of(context) | ||
| 105 | + .textTheme | ||
| 106 | + .headline4! | ||
| 107 | + .copyWith(color: Colors.white), | ||
| 108 | + ), | ||
| 109 | + ), | ||
| 110 | + ), | ||
| 111 | + ), | ||
| 112 | + IconButton( | ||
| 113 | + color: Colors.white, | ||
| 114 | + icon: ValueListenableBuilder( | ||
| 115 | + valueListenable: controller.cameraFacingState, | ||
| 116 | + builder: (context, state, child) { | ||
| 117 | + if (state == null) { | ||
| 118 | + return const Icon(Icons.camera_front); | ||
| 119 | + } | ||
| 120 | + switch (state as CameraFacing) { | ||
| 121 | + case CameraFacing.front: | ||
| 122 | + return const Icon(Icons.camera_front); | ||
| 123 | + case CameraFacing.back: | ||
| 124 | + return const Icon(Icons.camera_rear); | ||
| 125 | + } | ||
| 126 | + }, | ||
| 127 | + ), | ||
| 128 | + iconSize: 32.0, | ||
| 129 | + onPressed: () => controller.switchCamera(), | ||
| 130 | + ), | ||
| 131 | + IconButton( | ||
| 132 | + color: Colors.white, | ||
| 133 | + icon: const Icon(Icons.image), | ||
| 134 | + iconSize: 32.0, | ||
| 135 | + onPressed: () async { | ||
| 136 | + final ImagePicker picker = ImagePicker(); | ||
| 137 | + // Pick an image | ||
| 138 | + final XFile? image = await picker.pickImage( | ||
| 139 | + source: ImageSource.gallery, | ||
| 140 | + ); | ||
| 141 | + if (image != null) { | ||
| 142 | + if (await controller.analyzeImage(image.path)) { | ||
| 143 | + if (!mounted) return; | ||
| 144 | + ScaffoldMessenger.of(context).showSnackBar( | ||
| 145 | + const SnackBar( | ||
| 146 | + content: Text('Barcode found!'), | ||
| 147 | + backgroundColor: Colors.green, | ||
| 148 | + ), | ||
| 149 | + ); | ||
| 150 | + } else { | ||
| 151 | + if (!mounted) return; | ||
| 152 | + ScaffoldMessenger.of(context).showSnackBar( | ||
| 153 | + const SnackBar( | ||
| 154 | + content: Text('No barcode found!'), | ||
| 155 | + backgroundColor: Colors.red, | ||
| 156 | + ), | ||
| 157 | + ); | ||
| 158 | + } | ||
| 159 | + } | ||
| 160 | + }, | ||
| 161 | + ), | ||
| 162 | + ], | ||
| 163 | + ), | ||
| 164 | + ), | ||
| 165 | + ), | ||
| 166 | + ], | ||
| 167 | + ); | ||
| 168 | + }, | ||
| 169 | + ), | ||
| 170 | + ); | ||
| 171 | + } | ||
| 172 | +} |
| @@ -13,10 +13,10 @@ class BarcodeScannerWithController extends StatefulWidget { | @@ -13,10 +13,10 @@ class BarcodeScannerWithController extends StatefulWidget { | ||
| 13 | class _BarcodeScannerWithControllerState | 13 | class _BarcodeScannerWithControllerState |
| 14 | extends State<BarcodeScannerWithController> | 14 | extends State<BarcodeScannerWithController> |
| 15 | with SingleTickerProviderStateMixin { | 15 | with SingleTickerProviderStateMixin { |
| 16 | - String? barcode; | 16 | + BarcodeCapture? barcode; |
| 17 | 17 | ||
| 18 | MobileScannerController controller = MobileScannerController( | 18 | MobileScannerController controller = MobileScannerController( |
| 19 | - torchEnabled: true, | 19 | + torchEnabled: true, detectionSpeed: DetectionSpeed.unrestricted, |
| 20 | // formats: [BarcodeFormat.qrCode] | 20 | // formats: [BarcodeFormat.qrCode] |
| 21 | // facing: CameraFacing.front, | 21 | // facing: CameraFacing.front, |
| 22 | ); | 22 | ); |
| @@ -41,7 +41,7 @@ class _BarcodeScannerWithControllerState | @@ -41,7 +41,7 @@ class _BarcodeScannerWithControllerState | ||
| 41 | // ), | 41 | // ), |
| 42 | onDetect: (barcode, args) { | 42 | onDetect: (barcode, args) { |
| 43 | setState(() { | 43 | setState(() { |
| 44 | - this.barcode = barcode.rawValue; | 44 | + this.barcode = barcode; |
| 45 | }); | 45 | }); |
| 46 | }, | 46 | }, |
| 47 | ), | 47 | ), |
| @@ -99,7 +99,8 @@ class _BarcodeScannerWithControllerState | @@ -99,7 +99,8 @@ class _BarcodeScannerWithControllerState | ||
| 99 | height: 50, | 99 | height: 50, |
| 100 | child: FittedBox( | 100 | child: FittedBox( |
| 101 | child: Text( | 101 | child: Text( |
| 102 | - barcode ?? 'Scan something!', | 102 | + barcode?.barcodes.first.rawValue ?? |
| 103 | + 'Scan something!', | ||
| 103 | overflow: TextOverflow.fade, | 104 | overflow: TextOverflow.fade, |
| 104 | style: Theme.of(context) | 105 | style: Theme.of(context) |
| 105 | .textTheme | 106 | .textTheme |
| 1 | -import 'dart:typed_data'; | 1 | +import 'dart:math'; |
| 2 | 2 | ||
| 3 | import 'package:flutter/material.dart'; | 3 | import 'package:flutter/material.dart'; |
| 4 | import 'package:mobile_scanner/mobile_scanner.dart'; | 4 | import 'package:mobile_scanner/mobile_scanner.dart'; |
| @@ -14,11 +14,11 @@ class BarcodeScannerReturningImage extends StatefulWidget { | @@ -14,11 +14,11 @@ class BarcodeScannerReturningImage extends StatefulWidget { | ||
| 14 | class _BarcodeScannerReturningImageState | 14 | class _BarcodeScannerReturningImageState |
| 15 | extends State<BarcodeScannerReturningImage> | 15 | extends State<BarcodeScannerReturningImage> |
| 16 | with SingleTickerProviderStateMixin { | 16 | with SingleTickerProviderStateMixin { |
| 17 | - String? barcode; | ||
| 18 | - Uint8List? image; | 17 | + BarcodeCapture? barcode; |
| 18 | + MobileScannerArguments? arguments; | ||
| 19 | 19 | ||
| 20 | MobileScannerController controller = MobileScannerController( | 20 | MobileScannerController controller = MobileScannerController( |
| 21 | - torchEnabled: true, | 21 | + // torchEnabled: true, |
| 22 | returnImage: true, | 22 | returnImage: true, |
| 23 | // formats: [BarcodeFormat.qrCode] | 23 | // formats: [BarcodeFormat.qrCode] |
| 24 | // facing: CameraFacing.front, | 24 | // facing: CameraFacing.front, |
| @@ -32,125 +32,146 @@ class _BarcodeScannerReturningImageState | @@ -32,125 +32,146 @@ class _BarcodeScannerReturningImageState | ||
| 32 | backgroundColor: Colors.black, | 32 | backgroundColor: Colors.black, |
| 33 | body: Builder( | 33 | body: Builder( |
| 34 | builder: (context) { | 34 | builder: (context) { |
| 35 | - return Stack( | 35 | + return Column( |
| 36 | children: [ | 36 | children: [ |
| 37 | - MobileScanner( | ||
| 38 | - controller: controller, | ||
| 39 | - fit: BoxFit.contain, | ||
| 40 | - // allowDuplicates: true, | ||
| 41 | - // controller: MobileScannerController( | ||
| 42 | - // torchEnabled: true, | ||
| 43 | - // facing: CameraFacing.front, | ||
| 44 | - // ), | ||
| 45 | - onDetect: (barcode, args) { | ||
| 46 | - setState(() { | ||
| 47 | - this.barcode = barcode.rawValue; | ||
| 48 | - showDialog( | ||
| 49 | - context: context, | ||
| 50 | - builder: (context) => Image( | ||
| 51 | - image: MemoryImage(image!), | ||
| 52 | - fit: BoxFit.contain, | ||
| 53 | - ), | ||
| 54 | - ); | ||
| 55 | - image = barcode.image; | ||
| 56 | - }); | ||
| 57 | - }, | ||
| 58 | - ), | ||
| 59 | - Align( | ||
| 60 | - alignment: Alignment.bottomCenter, | ||
| 61 | - child: Container( | ||
| 62 | - alignment: Alignment.bottomCenter, | ||
| 63 | - height: 100, | ||
| 64 | - color: Colors.black.withOpacity(0.4), | ||
| 65 | - child: Row( | ||
| 66 | - mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||
| 67 | - children: [ | ||
| 68 | - IconButton( | ||
| 69 | - color: Colors.white, | ||
| 70 | - icon: ValueListenableBuilder( | ||
| 71 | - valueListenable: controller.torchState, | ||
| 72 | - builder: (context, state, child) { | ||
| 73 | - if (state == null) { | ||
| 74 | - return const Icon( | ||
| 75 | - Icons.flash_off, | ||
| 76 | - color: Colors.grey, | ||
| 77 | - ); | ||
| 78 | - } | ||
| 79 | - switch (state as TorchState) { | ||
| 80 | - case TorchState.off: | ||
| 81 | - return const Icon( | ||
| 82 | - Icons.flash_off, | ||
| 83 | - color: Colors.grey, | ||
| 84 | - ); | ||
| 85 | - case TorchState.on: | ||
| 86 | - return const Icon( | ||
| 87 | - Icons.flash_on, | ||
| 88 | - color: Colors.yellow, | ||
| 89 | - ); | ||
| 90 | - } | ||
| 91 | - }, | 37 | + Container( |
| 38 | + color: Colors.blueGrey, | ||
| 39 | + width: double.infinity, | ||
| 40 | + height: 0.33 * MediaQuery.of(context).size.height, | ||
| 41 | + child: barcode?.image != null | ||
| 42 | + ? Transform.rotate( | ||
| 43 | + angle: 90 * pi / 180, | ||
| 44 | + child: Image( | ||
| 45 | + gaplessPlayback: true, | ||
| 46 | + image: MemoryImage(barcode!.image!), | ||
| 47 | + fit: BoxFit.contain, | ||
| 92 | ), | 48 | ), |
| 93 | - iconSize: 32.0, | ||
| 94 | - onPressed: () => controller.toggleTorch(), | ||
| 95 | - ), | ||
| 96 | - IconButton( | 49 | + ) |
| 50 | + : const ColoredBox( | ||
| 97 | color: Colors.white, | 51 | color: Colors.white, |
| 98 | - icon: isStarted | ||
| 99 | - ? const Icon(Icons.stop) | ||
| 100 | - : const Icon(Icons.play_arrow), | ||
| 101 | - iconSize: 32.0, | ||
| 102 | - onPressed: () => setState(() { | ||
| 103 | - isStarted ? controller.stop() : controller.start(); | ||
| 104 | - isStarted = !isStarted; | ||
| 105 | - }), | ||
| 106 | - ), | ||
| 107 | - Center( | ||
| 108 | - child: SizedBox( | ||
| 109 | - width: MediaQuery.of(context).size.width - 200, | ||
| 110 | - height: 50, | ||
| 111 | - child: FittedBox( | ||
| 112 | - child: Text( | ||
| 113 | - barcode ?? 'Scan something!', | ||
| 114 | - overflow: TextOverflow.fade, | ||
| 115 | - style: Theme.of(context) | ||
| 116 | - .textTheme | ||
| 117 | - .headline4! | ||
| 118 | - .copyWith(color: Colors.white), | ||
| 119 | - ), | 52 | + child: Center( |
| 53 | + child: Text( | ||
| 54 | + 'Your scanned barcode will appear here!', | ||
| 120 | ), | 55 | ), |
| 121 | ), | 56 | ), |
| 122 | ), | 57 | ), |
| 123 | - IconButton( | ||
| 124 | - color: Colors.white, | ||
| 125 | - icon: ValueListenableBuilder( | ||
| 126 | - valueListenable: controller.cameraFacingState, | ||
| 127 | - builder: (context, state, child) { | ||
| 128 | - if (state == null) { | ||
| 129 | - return const Icon(Icons.camera_front); | ||
| 130 | - } | ||
| 131 | - switch (state as CameraFacing) { | ||
| 132 | - case CameraFacing.front: | ||
| 133 | - return const Icon(Icons.camera_front); | ||
| 134 | - case CameraFacing.back: | ||
| 135 | - return const Icon(Icons.camera_rear); | ||
| 136 | - } | ||
| 137 | - }, | 58 | + ), |
| 59 | + Container( | ||
| 60 | + height: 0.66 * MediaQuery.of(context).size.height, | ||
| 61 | + color: Colors.grey, | ||
| 62 | + child: Stack( | ||
| 63 | + children: [ | ||
| 64 | + MobileScanner( | ||
| 65 | + controller: controller, | ||
| 66 | + fit: BoxFit.contain, | ||
| 67 | + // allowDuplicates: true, | ||
| 68 | + // controller: MobileScannerController( | ||
| 69 | + // torchEnabled: true, | ||
| 70 | + // facing: CameraFacing.front, | ||
| 71 | + // ), | ||
| 72 | + onDetect: (barcode, arguments) { | ||
| 73 | + setState(() { | ||
| 74 | + this.arguments = arguments; | ||
| 75 | + this.barcode = barcode; | ||
| 76 | + }); | ||
| 77 | + }, | ||
| 78 | + ), | ||
| 79 | + Align( | ||
| 80 | + alignment: Alignment.bottomCenter, | ||
| 81 | + child: Container( | ||
| 82 | + alignment: Alignment.bottomCenter, | ||
| 83 | + height: 100, | ||
| 84 | + color: Colors.black.withOpacity(0.4), | ||
| 85 | + child: Row( | ||
| 86 | + mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||
| 87 | + children: [ | ||
| 88 | + ColoredBox( | ||
| 89 | + color: arguments != null && !arguments!.hasTorch | ||
| 90 | + ? Colors.red | ||
| 91 | + : Colors.white, | ||
| 92 | + child: IconButton( | ||
| 93 | + // color: , | ||
| 94 | + icon: ValueListenableBuilder( | ||
| 95 | + valueListenable: controller.torchState, | ||
| 96 | + builder: (context, state, child) { | ||
| 97 | + if (state == null) { | ||
| 98 | + return const Icon( | ||
| 99 | + Icons.flash_off, | ||
| 100 | + color: Colors.grey, | ||
| 101 | + ); | ||
| 102 | + } | ||
| 103 | + switch (state as TorchState) { | ||
| 104 | + case TorchState.off: | ||
| 105 | + return const Icon( | ||
| 106 | + Icons.flash_off, | ||
| 107 | + color: Colors.grey, | ||
| 108 | + ); | ||
| 109 | + case TorchState.on: | ||
| 110 | + return const Icon( | ||
| 111 | + Icons.flash_on, | ||
| 112 | + color: Colors.yellow, | ||
| 113 | + ); | ||
| 114 | + } | ||
| 115 | + }, | ||
| 116 | + ), | ||
| 117 | + iconSize: 32.0, | ||
| 118 | + onPressed: () => controller.toggleTorch(), | ||
| 119 | + ), | ||
| 120 | + ), | ||
| 121 | + IconButton( | ||
| 122 | + color: Colors.white, | ||
| 123 | + icon: isStarted | ||
| 124 | + ? const Icon(Icons.stop) | ||
| 125 | + : const Icon(Icons.play_arrow), | ||
| 126 | + iconSize: 32.0, | ||
| 127 | + onPressed: () => setState(() { | ||
| 128 | + isStarted | ||
| 129 | + ? controller.stop() | ||
| 130 | + : controller.start(); | ||
| 131 | + isStarted = !isStarted; | ||
| 132 | + }), | ||
| 133 | + ), | ||
| 134 | + Center( | ||
| 135 | + child: SizedBox( | ||
| 136 | + width: MediaQuery.of(context).size.width - 200, | ||
| 137 | + height: 50, | ||
| 138 | + child: FittedBox( | ||
| 139 | + child: Text( | ||
| 140 | + barcode?.barcodes.first.rawValue ?? | ||
| 141 | + 'Scan something!', | ||
| 142 | + overflow: TextOverflow.fade, | ||
| 143 | + style: Theme.of(context) | ||
| 144 | + .textTheme | ||
| 145 | + .headline4! | ||
| 146 | + .copyWith(color: Colors.white), | ||
| 147 | + ), | ||
| 148 | + ), | ||
| 149 | + ), | ||
| 150 | + ), | ||
| 151 | + IconButton( | ||
| 152 | + color: Colors.white, | ||
| 153 | + icon: ValueListenableBuilder( | ||
| 154 | + valueListenable: controller.cameraFacingState, | ||
| 155 | + builder: (context, state, child) { | ||
| 156 | + if (state == null) { | ||
| 157 | + return const Icon(Icons.camera_front); | ||
| 158 | + } | ||
| 159 | + switch (state as CameraFacing) { | ||
| 160 | + case CameraFacing.front: | ||
| 161 | + return const Icon(Icons.camera_front); | ||
| 162 | + case CameraFacing.back: | ||
| 163 | + return const Icon(Icons.camera_rear); | ||
| 164 | + } | ||
| 165 | + }, | ||
| 166 | + ), | ||
| 167 | + iconSize: 32.0, | ||
| 168 | + onPressed: () => controller.switchCamera(), | ||
| 169 | + ), | ||
| 170 | + ], | ||
| 138 | ), | 171 | ), |
| 139 | - iconSize: 32.0, | ||
| 140 | - onPressed: () => controller.switchCamera(), | ||
| 141 | - ), | ||
| 142 | - SizedBox( | ||
| 143 | - width: 50, | ||
| 144 | - height: 50, | ||
| 145 | - child: image != null | ||
| 146 | - ? Image( | ||
| 147 | - image: MemoryImage(image!), | ||
| 148 | - fit: BoxFit.contain, | ||
| 149 | - ) | ||
| 150 | - : Container(), | ||
| 151 | ), | 172 | ), |
| 152 | - ], | ||
| 153 | - ), | 173 | + ), |
| 174 | + ], | ||
| 154 | ), | 175 | ), |
| 155 | ), | 176 | ), |
| 156 | ], | 177 | ], |
| @@ -12,7 +12,7 @@ class BarcodeScannerWithoutController extends StatefulWidget { | @@ -12,7 +12,7 @@ class BarcodeScannerWithoutController extends StatefulWidget { | ||
| 12 | class _BarcodeScannerWithoutControllerState | 12 | class _BarcodeScannerWithoutControllerState |
| 13 | extends State<BarcodeScannerWithoutController> | 13 | extends State<BarcodeScannerWithoutController> |
| 14 | with SingleTickerProviderStateMixin { | 14 | with SingleTickerProviderStateMixin { |
| 15 | - String? barcode; | 15 | + BarcodeCapture? capture; |
| 16 | 16 | ||
| 17 | @override | 17 | @override |
| 18 | Widget build(BuildContext context) { | 18 | Widget build(BuildContext context) { |
| @@ -25,9 +25,9 @@ class _BarcodeScannerWithoutControllerState | @@ -25,9 +25,9 @@ class _BarcodeScannerWithoutControllerState | ||
| 25 | MobileScanner( | 25 | MobileScanner( |
| 26 | fit: BoxFit.contain, | 26 | fit: BoxFit.contain, |
| 27 | // allowDuplicates: false, | 27 | // allowDuplicates: false, |
| 28 | - onDetect: (barcode, args) { | 28 | + onDetect: (capture, arguments) { |
| 29 | setState(() { | 29 | setState(() { |
| 30 | - this.barcode = barcode.rawValue; | 30 | + this.capture = capture; |
| 31 | }); | 31 | }); |
| 32 | }, | 32 | }, |
| 33 | ), | 33 | ), |
| @@ -46,7 +46,8 @@ class _BarcodeScannerWithoutControllerState | @@ -46,7 +46,8 @@ class _BarcodeScannerWithoutControllerState | ||
| 46 | height: 50, | 46 | height: 50, |
| 47 | child: FittedBox( | 47 | child: FittedBox( |
| 48 | child: Text( | 48 | child: Text( |
| 49 | - barcode ?? 'Scan something!', | 49 | + capture?.barcodes.first.rawValue ?? |
| 50 | + 'Scan something!', | ||
| 50 | overflow: TextOverflow.fade, | 51 | overflow: TextOverflow.fade, |
| 51 | style: Theme.of(context) | 52 | style: Theme.of(context) |
| 52 | .textTheme | 53 | .textTheme |
| 1 | import 'package:flutter/material.dart'; | 1 | import 'package:flutter/material.dart'; |
| 2 | +import 'package:mobile_scanner_example/barcode_list_scanner_controller.dart'; | ||
| 2 | import 'package:mobile_scanner_example/barcode_scanner_controller.dart'; | 3 | import 'package:mobile_scanner_example/barcode_scanner_controller.dart'; |
| 3 | import 'package:mobile_scanner_example/barcode_scanner_returning_image.dart'; | 4 | import 'package:mobile_scanner_example/barcode_scanner_returning_image.dart'; |
| 4 | import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart'; | 5 | import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart'; |
| @@ -22,6 +23,17 @@ class MyHome extends StatelessWidget { | @@ -22,6 +23,17 @@ class MyHome extends StatelessWidget { | ||
| 22 | onPressed: () { | 23 | onPressed: () { |
| 23 | Navigator.of(context).push( | 24 | Navigator.of(context).push( |
| 24 | MaterialPageRoute( | 25 | MaterialPageRoute( |
| 26 | + builder: (context) => | ||
| 27 | + const BarcodeListScannerWithController(), | ||
| 28 | + ), | ||
| 29 | + ); | ||
| 30 | + }, | ||
| 31 | + child: const Text('MobileScanner with List Controller'), | ||
| 32 | + ), | ||
| 33 | + ElevatedButton( | ||
| 34 | + onPressed: () { | ||
| 35 | + Navigator.of(context).push( | ||
| 36 | + MaterialPageRoute( | ||
| 25 | builder: (context) => const BarcodeScannerWithController(), | 37 | builder: (context) => const BarcodeScannerWithController(), |
| 26 | ), | 38 | ), |
| 27 | ); | 39 | ); |
ios/Classes/BarcodeHandler.swift
0 → 100644
| 1 | +// | ||
| 2 | +// BarcodeHandler.swift | ||
| 3 | +// mobile_scanner | ||
| 4 | +// | ||
| 5 | +// Created by Julian Steenbakker on 24/08/2022. | ||
| 6 | +// | ||
| 7 | + | ||
| 8 | +import Foundation | ||
| 9 | + | ||
| 10 | +public class BarcodeHandler: NSObject, FlutterStreamHandler { | ||
| 11 | + | ||
| 12 | + var event: [String: Any?] = [:] | ||
| 13 | + | ||
| 14 | + private var eventSink: FlutterEventSink? | ||
| 15 | + private let eventChannel: FlutterEventChannel | ||
| 16 | + | ||
| 17 | + init(registrar: FlutterPluginRegistrar) { | ||
| 18 | + eventChannel = FlutterEventChannel(name: | ||
| 19 | + "dev.steenbakker.mobile_scanner/scanner/event", binaryMessenger: registrar.messenger()) | ||
| 20 | + super.init() | ||
| 21 | + eventChannel.setStreamHandler(self) | ||
| 22 | + } | ||
| 23 | + | ||
| 24 | + func publishEvent(_ event: [String: Any?]) { | ||
| 25 | + self.event = event | ||
| 26 | + eventSink?(event) | ||
| 27 | + } | ||
| 28 | + | ||
| 29 | + public func onListen(withArguments arguments: Any?, | ||
| 30 | + eventSink: @escaping FlutterEventSink) -> FlutterError? { | ||
| 31 | + self.eventSink = eventSink | ||
| 32 | + return nil | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + public func onCancel(withArguments arguments: Any?) -> FlutterError? { | ||
| 36 | + eventSink = nil | ||
| 37 | + return nil | ||
| 38 | + } | ||
| 39 | +} |
ios/Classes/MobileScanner.swift
0 → 100644
| 1 | +// | ||
| 2 | +// SwiftMobileScanner.swift | ||
| 3 | +// mobile_scanner | ||
| 4 | +// | ||
| 5 | +// Created by Julian Steenbakker on 15/02/2022. | ||
| 6 | +// | ||
| 7 | + | ||
| 8 | +import Foundation | ||
| 9 | + | ||
| 10 | +import AVFoundation | ||
| 11 | +import MLKitVision | ||
| 12 | +import MLKitBarcodeScanning | ||
| 13 | + | ||
| 14 | +typealias MobileScannerCallback = ((Array<Barcode>?, Error?, UIImage) -> ()) | ||
| 15 | + | ||
| 16 | +public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, FlutterTexture { | ||
| 17 | + /// Capture session of the camera | ||
| 18 | + var captureSession: AVCaptureSession! | ||
| 19 | + | ||
| 20 | + /// The selected camera | ||
| 21 | + var device: AVCaptureDevice! | ||
| 22 | + | ||
| 23 | + /// Barcode scanner for results | ||
| 24 | + var scanner = BarcodeScanner.barcodeScanner() | ||
| 25 | + | ||
| 26 | + /// Return image buffer with the Barcode event | ||
| 27 | + var returnImage: Bool = false | ||
| 28 | + | ||
| 29 | + /// Default position of camera | ||
| 30 | + var videoPosition: AVCaptureDevice.Position = AVCaptureDevice.Position.back | ||
| 31 | + | ||
| 32 | + /// When results are found, this callback will be called | ||
| 33 | + let mobileScannerCallback: MobileScannerCallback | ||
| 34 | + | ||
| 35 | + /// If provided, the Flutter registry will be used to send the output of the CaptureOutput to a Flutter texture. | ||
| 36 | + private let registry: FlutterTextureRegistry? | ||
| 37 | + | ||
| 38 | + /// Image to be sent to the texture | ||
| 39 | + var latestBuffer: CVImageBuffer! | ||
| 40 | + | ||
| 41 | + /// Texture id of the camera preview for Flutter | ||
| 42 | + private var textureId: Int64! | ||
| 43 | + | ||
| 44 | + var detectionSpeed: DetectionSpeed = DetectionSpeed.noDuplicates | ||
| 45 | + | ||
| 46 | + init(registry: FlutterTextureRegistry?, mobileScannerCallback: @escaping MobileScannerCallback) { | ||
| 47 | + self.registry = registry | ||
| 48 | + self.mobileScannerCallback = mobileScannerCallback | ||
| 49 | + super.init() | ||
| 50 | + } | ||
| 51 | + | ||
| 52 | + /// Check permissions for video | ||
| 53 | + func checkPermission() -> Int { | ||
| 54 | + let status = AVCaptureDevice.authorizationStatus(for: .video) | ||
| 55 | + switch status { | ||
| 56 | + case .notDetermined: | ||
| 57 | + return 0 | ||
| 58 | + case .authorized: | ||
| 59 | + return 1 | ||
| 60 | + default: | ||
| 61 | + return 2 | ||
| 62 | + } | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + /// Request permissions for video | ||
| 66 | + func requestPermission(_ result: @escaping FlutterResult) { | ||
| 67 | + AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) }) | ||
| 68 | + } | ||
| 69 | + | ||
| 70 | + /// Start scanning for barcodes | ||
| 71 | + func start(barcodeScannerOptions: BarcodeScannerOptions?, returnImage: Bool, cameraPosition: AVCaptureDevice.Position, torch: AVCaptureDevice.TorchMode, detectionSpeed: DetectionSpeed) throws -> MobileScannerStartParameters { | ||
| 72 | + self.detectionSpeed = detectionSpeed | ||
| 73 | + if (device != nil) { | ||
| 74 | + throw MobileScannerError.alreadyStarted | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | + scanner = barcodeScannerOptions != nil ? BarcodeScanner.barcodeScanner(options: barcodeScannerOptions!) : BarcodeScanner.barcodeScanner() | ||
| 78 | + captureSession = AVCaptureSession() | ||
| 79 | + textureId = registry?.register(self) | ||
| 80 | + | ||
| 81 | + // Open the camera device | ||
| 82 | + if #available(iOS 10.0, *) { | ||
| 83 | + device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: cameraPosition).devices.first | ||
| 84 | + } else { | ||
| 85 | + device = AVCaptureDevice.devices(for: .video).filter({$0.position == cameraPosition}).first | ||
| 86 | + } | ||
| 87 | + | ||
| 88 | + if (device == nil) { | ||
| 89 | + throw MobileScannerError.noCamera | ||
| 90 | + } | ||
| 91 | + | ||
| 92 | + // Enable the torch if parameter is set and torch is available | ||
| 93 | + if (device.hasTorch && device.isTorchAvailable) { | ||
| 94 | + do { | ||
| 95 | + try device.lockForConfiguration() | ||
| 96 | + device.torchMode = torch | ||
| 97 | + device.unlockForConfiguration() | ||
| 98 | + } catch { | ||
| 99 | + throw MobileScannerError.torchError(error) | ||
| 100 | + } | ||
| 101 | + } | ||
| 102 | + | ||
| 103 | + device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil) | ||
| 104 | + captureSession.beginConfiguration() | ||
| 105 | + | ||
| 106 | + // Add device input | ||
| 107 | + do { | ||
| 108 | + let input = try AVCaptureDeviceInput(device: device) | ||
| 109 | + captureSession.addInput(input) | ||
| 110 | + } catch { | ||
| 111 | + throw MobileScannerError.cameraError(error) | ||
| 112 | + } | ||
| 113 | + | ||
| 114 | + captureSession.sessionPreset = AVCaptureSession.Preset.photo; | ||
| 115 | + // Add video output. | ||
| 116 | + let videoOutput = AVCaptureVideoDataOutput() | ||
| 117 | + | ||
| 118 | + videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA] | ||
| 119 | + videoOutput.alwaysDiscardsLateVideoFrames = true | ||
| 120 | + | ||
| 121 | + videoPosition = cameraPosition | ||
| 122 | + // calls captureOutput() | ||
| 123 | + videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main) | ||
| 124 | + | ||
| 125 | + captureSession.addOutput(videoOutput) | ||
| 126 | + for connection in videoOutput.connections { | ||
| 127 | + connection.videoOrientation = .portrait | ||
| 128 | + if cameraPosition == .front && connection.isVideoMirroringSupported { | ||
| 129 | + connection.isVideoMirrored = true | ||
| 130 | + } | ||
| 131 | + } | ||
| 132 | + captureSession.commitConfiguration() | ||
| 133 | + captureSession.startRunning() | ||
| 134 | + let dimensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription) | ||
| 135 | + | ||
| 136 | + return MobileScannerStartParameters(width: Double(dimensions.height), height: Double(dimensions.width), hasTorch: device.hasTorch, textureId: textureId) | ||
| 137 | + } | ||
| 138 | + | ||
| 139 | + struct MobileScannerStartParameters { | ||
| 140 | + var width: Double = 0.0 | ||
| 141 | + var height: Double = 0.0 | ||
| 142 | + var hasTorch = false | ||
| 143 | + var textureId: Int64 = 0 | ||
| 144 | + } | ||
| 145 | + | ||
| 146 | + /// Stop scanning for barcodes | ||
| 147 | + func stop() throws { | ||
| 148 | + if (device == nil) { | ||
| 149 | + throw MobileScannerError.alreadyStopped | ||
| 150 | + } | ||
| 151 | + captureSession.stopRunning() | ||
| 152 | + for input in captureSession.inputs { | ||
| 153 | + captureSession.removeInput(input) | ||
| 154 | + } | ||
| 155 | + for output in captureSession.outputs { | ||
| 156 | + captureSession.removeOutput(output) | ||
| 157 | + } | ||
| 158 | + device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode)) | ||
| 159 | + registry?.unregisterTexture(textureId) | ||
| 160 | + textureId = nil | ||
| 161 | + captureSession = nil | ||
| 162 | + device = nil | ||
| 163 | + } | ||
| 164 | + | ||
| 165 | + /// Toggle the flashlight between on and off | ||
| 166 | + func toggleTorch(_ torch: AVCaptureDevice.TorchMode) throws { | ||
| 167 | + if (device == nil) { | ||
| 168 | + throw MobileScannerError.torchWhenStopped | ||
| 169 | + } | ||
| 170 | + do { | ||
| 171 | + try device.lockForConfiguration() | ||
| 172 | + device.torchMode = torch | ||
| 173 | + device.unlockForConfiguration() | ||
| 174 | + } catch { | ||
| 175 | + throw MobileScannerError.torchError(error) | ||
| 176 | + } | ||
| 177 | + } | ||
| 178 | + | ||
| 179 | + /// Analyze a single image | ||
| 180 | + func analyzeImage(image: UIImage, position: AVCaptureDevice.Position, callback: @escaping BarcodeScanningCallback) { | ||
| 181 | + let image = VisionImage(image: image) | ||
| 182 | + image.orientation = imageOrientation( | ||
| 183 | + deviceOrientation: UIDevice.current.orientation, | ||
| 184 | + defaultOrientation: .portrait, | ||
| 185 | + position: position | ||
| 186 | + ) | ||
| 187 | + | ||
| 188 | + scanner.process(image, completion: callback) | ||
| 189 | + } | ||
| 190 | + | ||
| 191 | + var i = 0 | ||
| 192 | + | ||
| 193 | + var barcodesString: Array<String?>? | ||
| 194 | + | ||
| 195 | + /// Gets called when a new image is added to the buffer | ||
| 196 | + public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { | ||
| 197 | + guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { | ||
| 198 | + print("Failed to get image buffer from sample buffer.") | ||
| 199 | + return | ||
| 200 | + } | ||
| 201 | + latestBuffer = imageBuffer | ||
| 202 | + registry?.textureFrameAvailable(textureId) | ||
| 203 | + if ((detectionSpeed == DetectionSpeed.normal || detectionSpeed == DetectionSpeed.noDuplicates) && i > 10 || detectionSpeed == DetectionSpeed.unrestricted) { | ||
| 204 | + i = 0 | ||
| 205 | + let ciImage = latestBuffer.image | ||
| 206 | + | ||
| 207 | + let image = VisionImage(image: ciImage) | ||
| 208 | + image.orientation = imageOrientation( | ||
| 209 | + deviceOrientation: UIDevice.current.orientation, | ||
| 210 | + defaultOrientation: .portrait, | ||
| 211 | + position: videoPosition | ||
| 212 | + ) | ||
| 213 | + | ||
| 214 | + scanner.process(image) { [self] barcodes, error in | ||
| 215 | + if (detectionSpeed == DetectionSpeed.noDuplicates) { | ||
| 216 | + let newScannedBarcodes = barcodes?.map { barcode in | ||
| 217 | + return barcode.rawValue | ||
| 218 | + } | ||
| 219 | + if (error == nil && barcodesString != nil && newScannedBarcodes != nil && barcodesString!.elementsEqual(newScannedBarcodes!)) { | ||
| 220 | + return | ||
| 221 | + } else { | ||
| 222 | + barcodesString = newScannedBarcodes | ||
| 223 | + } | ||
| 224 | + } | ||
| 225 | + | ||
| 226 | + mobileScannerCallback(barcodes, error, ciImage) | ||
| 227 | + } | ||
| 228 | + } else { | ||
| 229 | + i+=1 | ||
| 230 | + } | ||
| 231 | + } | ||
| 232 | + | ||
| 233 | + /// Convert image buffer to jpeg | ||
| 234 | + private func ciImageToJpeg(ciImage: CIImage) -> Data { | ||
| 235 | + | ||
| 236 | + // let ciImage = CIImage(cvPixelBuffer: latestBuffer) | ||
| 237 | + let context:CIContext = CIContext.init(options: nil) | ||
| 238 | + let cgImage:CGImage = context.createCGImage(ciImage, from: ciImage.extent)! | ||
| 239 | + let uiImage:UIImage = UIImage(cgImage: cgImage, scale: 1, orientation: UIImage.Orientation.up) | ||
| 240 | + | ||
| 241 | + return uiImage.jpegData(compressionQuality: 0.8)!; | ||
| 242 | + } | ||
| 243 | + | ||
| 244 | + /// Rotates images accordingly | ||
| 245 | + func imageOrientation( | ||
| 246 | + deviceOrientation: UIDeviceOrientation, | ||
| 247 | + defaultOrientation: UIDeviceOrientation, | ||
| 248 | + position: AVCaptureDevice.Position | ||
| 249 | + ) -> UIImage.Orientation { | ||
| 250 | + switch deviceOrientation { | ||
| 251 | + case .portrait: | ||
| 252 | + return position == .front ? .leftMirrored : .right | ||
| 253 | + case .landscapeLeft: | ||
| 254 | + return position == .front ? .downMirrored : .up | ||
| 255 | + case .portraitUpsideDown: | ||
| 256 | + return position == .front ? .rightMirrored : .left | ||
| 257 | + case .landscapeRight: | ||
| 258 | + return position == .front ? .upMirrored : .down | ||
| 259 | + case .faceDown, .faceUp, .unknown: | ||
| 260 | + return .up | ||
| 261 | + @unknown default: | ||
| 262 | + return imageOrientation(deviceOrientation: defaultOrientation, defaultOrientation: .portrait, position: .back) | ||
| 263 | + } | ||
| 264 | + } | ||
| 265 | + | ||
| 266 | + /// Sends output of OutputBuffer to a Flutter texture | ||
| 267 | + public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? { | ||
| 268 | + if latestBuffer == nil { | ||
| 269 | + return nil | ||
| 270 | + } | ||
| 271 | + return Unmanaged<CVPixelBuffer>.passRetained(latestBuffer) | ||
| 272 | + } | ||
| 273 | + | ||
| 274 | +} | ||
| 275 | + |
ios/Classes/MobileScannerError.swift
0 → 100644
| 1 | +// | ||
| 2 | +// MobileScannerError.swift | ||
| 3 | +// mobile_scanner | ||
| 4 | +// | ||
| 5 | +// Created by Julian Steenbakker on 24/08/2022. | ||
| 6 | +// | ||
| 7 | +import Foundation | ||
| 8 | + | ||
| 9 | +enum MobileScannerError: Error { | ||
| 10 | + case noCamera | ||
| 11 | + case alreadyStarted | ||
| 12 | + case alreadyStopped | ||
| 13 | + case torchError(_ error: Error) | ||
| 14 | + case cameraError(_ error: Error) | ||
| 15 | + case torchWhenStopped | ||
| 16 | + case analyzerError(_ error: Error) | ||
| 17 | +} |
ios/Classes/SwiftMobileScanner.swift
deleted
100644 → 0
| 1 | -import AVFoundation | ||
| 2 | import Flutter | 1 | import Flutter |
| 3 | import MLKitVision | 2 | import MLKitVision |
| 4 | import MLKitBarcodeScanning | 3 | import MLKitBarcodeScanning |
| 4 | +import AVFoundation | ||
| 5 | 5 | ||
| 6 | -public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, FlutterTexture, AVCaptureVideoDataOutputSampleBufferDelegate { | ||
| 7 | - | ||
| 8 | - let registry: FlutterTextureRegistry | ||
| 9 | - | ||
| 10 | - // Sink for publishing event changes | ||
| 11 | - var sink: FlutterEventSink! | 6 | +public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin { |
| 12 | 7 | ||
| 13 | - // Texture id of the camera preview | ||
| 14 | - var textureId: Int64! | 8 | + /// The mobile scanner object that handles all logic |
| 9 | + private let mobileScanner: MobileScanner | ||
| 15 | 10 | ||
| 16 | - // Capture session of the camera | ||
| 17 | - var captureSession: AVCaptureSession! | 11 | + /// The handler sends all information via an event channel back to Flutter |
| 12 | + private let barcodeHandler: BarcodeHandler | ||
| 18 | 13 | ||
| 19 | - // The selected camera | ||
| 20 | - var device: AVCaptureDevice! | ||
| 21 | - | ||
| 22 | - // Image to be sent to the texture | ||
| 23 | - var latestBuffer: CVImageBuffer! | 14 | + init(barcodeHandler: BarcodeHandler, registry: FlutterTextureRegistry) { |
| 15 | + self.mobileScanner = MobileScanner(registry: registry, mobileScannerCallback: { barcodes, error, image in | ||
| 16 | + if barcodes != nil { | ||
| 17 | + let barcodesMap = barcodes!.map { barcode in | ||
| 18 | + return barcode.data | ||
| 19 | + } | ||
| 20 | + if (!barcodesMap.isEmpty) { | ||
| 21 | + barcodeHandler.publishEvent(["name": "barcode", "data": barcodesMap, "image": FlutterStandardTypedData(bytes: image.jpegData(compressionQuality: 0.8)!)]) | ||
| 22 | + } | ||
| 23 | + } else if (error != nil){ | ||
| 24 | + barcodeHandler.publishEvent(["name": "error", "data": error!.localizedDescription]) | ||
| 25 | + } | ||
| 26 | + }) | ||
| 27 | + self.barcodeHandler = barcodeHandler | ||
| 28 | + super.init() | ||
| 29 | + } | ||
| 24 | 30 | ||
| 25 | - // Return image buffer with the Barcode event | ||
| 26 | - var returnImage: Bool = false | ||
| 27 | - | ||
| 28 | -// var analyzeMode: Int = 0 | ||
| 29 | - var analyzing: Bool = false | ||
| 30 | - var position = AVCaptureDevice.Position.back | ||
| 31 | - | ||
| 32 | - var scanner = BarcodeScanner.barcodeScanner() | ||
| 33 | - | ||
| 34 | public static func register(with registrar: FlutterPluginRegistrar) { | 31 | public static func register(with registrar: FlutterPluginRegistrar) { |
| 35 | - let instance = SwiftMobileScannerPlugin(registrar.textures()) | ||
| 36 | - | ||
| 37 | - let method = FlutterMethodChannel(name: | ||
| 38 | - "dev.steenbakker.mobile_scanner/scanner/method", binaryMessenger: registrar.messenger()) | ||
| 39 | - let event = FlutterEventChannel(name: | ||
| 40 | - "dev.steenbakker.mobile_scanner/scanner/event", binaryMessenger: registrar.messenger()) | ||
| 41 | - registrar.addMethodCallDelegate(instance, channel: method) | ||
| 42 | - event.setStreamHandler(instance) | 32 | + let instance = SwiftMobileScannerPlugin(barcodeHandler: BarcodeHandler(registrar: registrar), registry: registrar.textures()) |
| 33 | + let methodChannel = FlutterMethodChannel(name: | ||
| 34 | + "dev.steenbakker.mobile_scanner/scanner/method", binaryMessenger: registrar.messenger()) | ||
| 35 | + registrar.addMethodCallDelegate(instance, channel: methodChannel) | ||
| 43 | } | 36 | } |
| 44 | 37 | ||
| 45 | - init(_ registry: FlutterTextureRegistry) { | ||
| 46 | - self.registry = registry | ||
| 47 | - super.init() | ||
| 48 | - } | ||
| 49 | - | ||
| 50 | - | ||
| 51 | public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { | 38 | public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
| 52 | switch call.method { | 39 | switch call.method { |
| 53 | case "state": | 40 | case "state": |
| 54 | - checkPermission(call, result) | 41 | + result(mobileScanner.checkPermission()) |
| 55 | case "request": | 42 | case "request": |
| 56 | - requestPermission(call, result) | 43 | + AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) }) |
| 57 | case "start": | 44 | case "start": |
| 58 | start(call, result) | 45 | start(call, result) |
| 59 | - case "torch": | ||
| 60 | - toggleTorch(call, result) | ||
| 61 | -// case "analyze": | ||
| 62 | -// switchAnalyzeMode(call, result) | ||
| 63 | case "stop": | 46 | case "stop": |
| 64 | stop(result) | 47 | stop(result) |
| 48 | + case "torch": | ||
| 49 | + toggleTorch(call, result) | ||
| 65 | case "analyzeImage": | 50 | case "analyzeImage": |
| 66 | analyzeImage(call, result) | 51 | analyzeImage(call, result) |
| 67 | - | ||
| 68 | default: | 52 | default: |
| 69 | result(FlutterMethodNotImplemented) | 53 | result(FlutterMethodNotImplemented) |
| 70 | } | 54 | } |
| 71 | } | 55 | } |
| 72 | 56 | ||
| 73 | - // FlutterStreamHandler | ||
| 74 | - public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { | ||
| 75 | - sink = events | ||
| 76 | - return nil | ||
| 77 | - } | ||
| 78 | - | ||
| 79 | - // FlutterStreamHandler | ||
| 80 | - public func onCancel(withArguments arguments: Any?) -> FlutterError? { | ||
| 81 | - sink = nil | ||
| 82 | - return nil | ||
| 83 | - } | ||
| 84 | - | ||
| 85 | - // FlutterTexture | ||
| 86 | - public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? { | ||
| 87 | - if latestBuffer == nil { | ||
| 88 | - return nil | ||
| 89 | - } | ||
| 90 | - return Unmanaged<CVPixelBuffer>.passRetained(latestBuffer) | ||
| 91 | - } | ||
| 92 | - | ||
| 93 | - private func ciImageToJpeg(ciImage: CIImage) -> Data { | ||
| 94 | - | ||
| 95 | - // let ciImage = CIImage(cvPixelBuffer: latestBuffer) | ||
| 96 | - let context:CIContext = CIContext.init(options: nil) | ||
| 97 | - let cgImage:CGImage = context.createCGImage(ciImage, from: ciImage.extent)! | ||
| 98 | - let uiImage:UIImage = UIImage(cgImage: cgImage, scale: 1, orientation: UIImage.Orientation.up) | ||
| 99 | - | ||
| 100 | - return uiImage.jpegData(compressionQuality: 0.8)!; | ||
| 101 | - } | ||
| 102 | - | ||
| 103 | - // Gets called when a new image is added to the buffer | ||
| 104 | - public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { | 57 | + /// Parses all parameters and starts the mobileScanner |
| 58 | + private func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | ||
| 59 | + // let ratio: Int = (call.arguments as! Dictionary<String, Any?>)["ratio"] as! Int | ||
| 60 | + let torch: Bool = (call.arguments as! Dictionary<String, Any?>)["torch"] as? Bool ?? false | ||
| 61 | + let facing: Int = (call.arguments as! Dictionary<String, Any?>)["facing"] as? Int ?? 1 | ||
| 62 | + let formats: Array<Int> = (call.arguments as! Dictionary<String, Any?>)["formats"] as? Array ?? [] | ||
| 63 | + let returnImage: Bool = (call.arguments as! Dictionary<String, Any?>)["returnImage"] as? Bool ?? false | ||
| 105 | 64 | ||
| 106 | - latestBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) | ||
| 107 | - registry.textureFrameAvailable(textureId) | 65 | + let formatList = formats.map { format in return BarcodeFormat(rawValue: format)} |
| 66 | + var barcodeOptions: BarcodeScannerOptions? = nil | ||
| 67 | + | ||
| 68 | + if (formatList.count != 0) { | ||
| 69 | + var barcodeFormats: BarcodeFormat = [] | ||
| 70 | + for index in formats { | ||
| 71 | + barcodeFormats.insert(BarcodeFormat(rawValue: index)) | ||
| 72 | + } | ||
| 73 | + barcodeOptions = BarcodeScannerOptions(formats: barcodeFormats) | ||
| 74 | + } | ||
| 108 | 75 | ||
| 109 | -// switch analyzeMode { | ||
| 110 | -// case 1: // barcode | ||
| 111 | - if analyzing { | ||
| 112 | - return | ||
| 113 | - } | ||
| 114 | - analyzing = true | ||
| 115 | - let buffer = CMSampleBufferGetImageBuffer(sampleBuffer) | ||
| 116 | - let image = VisionImage(image: buffer!.image) | ||
| 117 | - image.orientation = imageOrientation( | ||
| 118 | - deviceOrientation: UIDevice.current.orientation, | ||
| 119 | - defaultOrientation: .portrait | ||
| 120 | - ) | ||
| 121 | 76 | ||
| 122 | - scanner.process(image) { [self] barcodes, error in | ||
| 123 | - if error == nil && barcodes != nil { | ||
| 124 | - for barcode in barcodes! { | ||
| 125 | - | ||
| 126 | - var event: [String: Any?] = ["name": "barcode", "data": barcode.data] | ||
| 127 | - if (returnImage && latestBuffer != nil) { | ||
| 128 | - let image: CIImage = CIImage(cvPixelBuffer: latestBuffer) | 77 | + let position = facing == 0 ? AVCaptureDevice.Position.front : .back |
| 78 | + let speed: DetectionSpeed = DetectionSpeed(rawValue: (call.arguments as! Dictionary<String, Any?>)["speed"] as? Int ?? 0)! | ||
| 129 | 79 | ||
| 130 | - event["image"] = FlutterStandardTypedData(bytes: ciImageToJpeg(ciImage: image)) | ||
| 131 | - } | ||
| 132 | - sink?(event) | ||
| 133 | - } | ||
| 134 | - } | ||
| 135 | - analyzing = false | ||
| 136 | - } | ||
| 137 | -// default: // none | ||
| 138 | -// break | ||
| 139 | -// } | ||
| 140 | - } | ||
| 141 | - | ||
| 142 | - func imageOrientation( | ||
| 143 | - deviceOrientation: UIDeviceOrientation, | ||
| 144 | - defaultOrientation: UIDeviceOrientation | ||
| 145 | - ) -> UIImage.Orientation { | ||
| 146 | - switch deviceOrientation { | ||
| 147 | - case .portrait: | ||
| 148 | - return position == .front ? .leftMirrored : .right | ||
| 149 | - case .landscapeLeft: | ||
| 150 | - return position == .front ? .downMirrored : .up | ||
| 151 | - case .portraitUpsideDown: | ||
| 152 | - return position == .front ? .rightMirrored : .left | ||
| 153 | - case .landscapeRight: | ||
| 154 | - return position == .front ? .upMirrored : .down | ||
| 155 | - case .faceDown, .faceUp, .unknown: | ||
| 156 | - return .up | ||
| 157 | - @unknown default: | ||
| 158 | - return imageOrientation(deviceOrientation: defaultOrientation, defaultOrientation: .portrait) | ||
| 159 | - } | ||
| 160 | - } | ||
| 161 | - | ||
| 162 | - func checkPermission(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | ||
| 163 | - let status = AVCaptureDevice.authorizationStatus(for: .video) | ||
| 164 | - switch status { | ||
| 165 | - case .notDetermined: | ||
| 166 | - result(0) | ||
| 167 | - case .authorized: | ||
| 168 | - result(1) | ||
| 169 | - default: | ||
| 170 | - result(2) | ||
| 171 | - } | ||
| 172 | - } | ||
| 173 | - | ||
| 174 | - func requestPermission(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | ||
| 175 | - AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) }) | ||
| 176 | - } | ||
| 177 | - | ||
| 178 | - func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | ||
| 179 | - if (device != nil) { | 80 | + do { |
| 81 | + let parameters = try mobileScanner.start(barcodeScannerOptions: barcodeOptions, returnImage: returnImage, cameraPosition: position, torch: torch ? .on : .off, detectionSpeed: speed) | ||
| 82 | + result(["textureId": parameters.textureId, "size": ["width": parameters.width, "height": parameters.height], "torchable": parameters.hasTorch]) | ||
| 83 | + } catch MobileScannerError.alreadyStarted { | ||
| 180 | result(FlutterError(code: "MobileScanner", | 84 | result(FlutterError(code: "MobileScanner", |
| 181 | - message: "Called start() while already started!", | ||
| 182 | - details: nil)) | ||
| 183 | - return | ||
| 184 | - } | ||
| 185 | - | ||
| 186 | - textureId = registry.register(self) | ||
| 187 | - captureSession = AVCaptureSession() | ||
| 188 | - | ||
| 189 | - let argReader = MapArgumentReader(call.arguments as? [String: Any]) | ||
| 190 | - | ||
| 191 | - returnImage = argReader.bool(key: "returnImage") ?? false | ||
| 192 | - | ||
| 193 | -// let ratio: Int = argReader.int(key: "ratio") | ||
| 194 | - let torch: Bool = argReader.bool(key: "torch") ?? false | ||
| 195 | - let facing: Int = argReader.int(key: "facing") ?? 1 | ||
| 196 | - let formats: Array = argReader.intArray(key: "formats") ?? [] | ||
| 197 | - | ||
| 198 | - if (formats.count != 0) { | ||
| 199 | - var barcodeFormats: BarcodeFormat = [] | ||
| 200 | - for index in formats { | ||
| 201 | - barcodeFormats.insert(BarcodeFormat(rawValue: index)) | ||
| 202 | - } | ||
| 203 | - | ||
| 204 | - let barcodeOptions = BarcodeScannerOptions(formats: barcodeFormats) | ||
| 205 | - scanner = BarcodeScanner.barcodeScanner(options: barcodeOptions) | ||
| 206 | - } | ||
| 207 | - | ||
| 208 | - // Set the camera to use | ||
| 209 | - position = facing == 0 ? AVCaptureDevice.Position.front : .back | ||
| 210 | - | ||
| 211 | - // Open the camera device | ||
| 212 | - if #available(iOS 10.0, *) { | ||
| 213 | - device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: position).devices.first | ||
| 214 | - } else { | ||
| 215 | - device = AVCaptureDevice.devices(for: .video).filter({$0.position == position}).first | ||
| 216 | - } | ||
| 217 | - | ||
| 218 | - if (device == nil) { | 85 | + message: "Called start() while already started!", |
| 86 | + details: nil)) | ||
| 87 | + } catch MobileScannerError.noCamera { | ||
| 219 | result(FlutterError(code: "MobileScanner", | 88 | result(FlutterError(code: "MobileScanner", |
| 220 | - message: "No camera found or failed to open camera!", | ||
| 221 | - details: nil)) | ||
| 222 | - return | ||
| 223 | - } | ||
| 224 | - | ||
| 225 | - // Enable the torch if parameter is set and torch is available | ||
| 226 | - if (device.hasTorch && device.isTorchAvailable) { | ||
| 227 | - do { | ||
| 228 | - try device.lockForConfiguration() | ||
| 229 | - device.torchMode = torch ? .on : .off | ||
| 230 | - device.unlockForConfiguration() | 89 | + message: "No camera found or failed to open camera!", |
| 90 | + details: nil)) | ||
| 91 | + } catch MobileScannerError.torchError(let error) { | ||
| 92 | + result(FlutterError(code: "MobileScanner", | ||
| 93 | + message: "Error occured when setting toch!", | ||
| 94 | + details: error)) | ||
| 95 | + } catch MobileScannerError.cameraError(let error) { | ||
| 96 | + result(FlutterError(code: "MobileScanner", | ||
| 97 | + message: "Error occured when setting up camera!", | ||
| 98 | + details: error)) | ||
| 231 | } catch { | 99 | } catch { |
| 232 | - error.throwNative(result) | ||
| 233 | - } | 100 | + result(FlutterError(code: "MobileScanner", |
| 101 | + message: "Unknown error occured..", | ||
| 102 | + details: nil)) | ||
| 234 | } | 103 | } |
| 235 | - | ||
| 236 | - device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil) | ||
| 237 | - captureSession.beginConfiguration() | ||
| 238 | - | ||
| 239 | - // Add device input | 104 | + } |
| 105 | + | ||
| 106 | + /// Stops the mobileScanner and closes the texture | ||
| 107 | + private func stop(_ result: @escaping FlutterResult) { | ||
| 240 | do { | 108 | do { |
| 241 | - let input = try AVCaptureDeviceInput(device: device) | ||
| 242 | - captureSession.addInput(input) | 109 | + try mobileScanner.stop() |
| 243 | } catch { | 110 | } catch { |
| 244 | - error.throwNative(result) | ||
| 245 | - } | ||
| 246 | - captureSession.sessionPreset = AVCaptureSession.Preset.photo; | ||
| 247 | - // Add video output. | ||
| 248 | - let videoOutput = AVCaptureVideoDataOutput() | ||
| 249 | - | ||
| 250 | - videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA] | ||
| 251 | - videoOutput.alwaysDiscardsLateVideoFrames = true | ||
| 252 | - | ||
| 253 | - videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main) | ||
| 254 | - captureSession.addOutput(videoOutput) | ||
| 255 | - for connection in videoOutput.connections { | ||
| 256 | - connection.videoOrientation = .portrait | ||
| 257 | - if position == .front && connection.isVideoMirroringSupported { | ||
| 258 | - connection.isVideoMirrored = true | ||
| 259 | - } | ||
| 260 | - } | ||
| 261 | - captureSession.commitConfiguration() | ||
| 262 | - captureSession.startRunning() | ||
| 263 | - let demensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription) | ||
| 264 | - let width = Double(demensions.height) | ||
| 265 | - let height = Double(demensions.width) | ||
| 266 | - let size = ["width": width, "height": height] | ||
| 267 | - let answer: [String : Any?] = ["textureId": textureId, "size": size, "torchable": device.hasTorch] | ||
| 268 | - result(answer) | ||
| 269 | - } | ||
| 270 | - | ||
| 271 | - func toggleTorch(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | ||
| 272 | - if (device == nil) { | ||
| 273 | result(FlutterError(code: "MobileScanner", | 111 | result(FlutterError(code: "MobileScanner", |
| 274 | - message: "Called toggleTorch() while stopped!", | ||
| 275 | - details: nil)) | ||
| 276 | - return | 112 | + message: "Called stop() while already stopped!", |
| 113 | + details: nil)) | ||
| 277 | } | 114 | } |
| 115 | + result(nil) | ||
| 116 | + } | ||
| 117 | + | ||
| 118 | + /// Toggles the torch | ||
| 119 | + private func toggleTorch(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | ||
| 278 | do { | 120 | do { |
| 279 | - try device.lockForConfiguration() | ||
| 280 | - device.torchMode = call.arguments as! Int == 1 ? .on : .off | ||
| 281 | - device.unlockForConfiguration() | ||
| 282 | - result(nil) | 121 | + try mobileScanner.toggleTorch(call.arguments as? Int == 1 ? .on : .off) |
| 283 | } catch { | 122 | } catch { |
| 284 | - error.throwNative(result) | 123 | + result(FlutterError(code: "MobileScanner", |
| 124 | + message: "Called toggleTorch() while stopped!", | ||
| 125 | + details: nil)) | ||
| 285 | } | 126 | } |
| 127 | + result(nil) | ||
| 286 | } | 128 | } |
| 287 | 129 | ||
| 288 | -// func switchAnalyzeMode(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | ||
| 289 | -// analyzeMode = call.arguments as! Int | ||
| 290 | -// result(nil) | ||
| 291 | -// } | ||
| 292 | - | ||
| 293 | - func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | ||
| 294 | - let uiImage = UIImage(contentsOfFile: call.arguments as! String) | 130 | + /// Analyzes a single image |
| 131 | + private func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | ||
| 132 | + let uiImage = UIImage(contentsOfFile: call.arguments as? String ?? "") | ||
| 295 | 133 | ||
| 296 | if (uiImage == nil) { | 134 | if (uiImage == nil) { |
| 297 | result(FlutterError(code: "MobileScanner", | 135 | result(FlutterError(code: "MobileScanner", |
| 298 | - message: "No image found in analyzeImage!", | ||
| 299 | - details: nil)) | 136 | + message: "No image found in analyzeImage!", |
| 137 | + details: nil)) | ||
| 300 | return | 138 | return |
| 301 | } | 139 | } |
| 302 | - | ||
| 303 | - let image = VisionImage(image: uiImage!) | ||
| 304 | - image.orientation = imageOrientation( | ||
| 305 | - deviceOrientation: UIDevice.current.orientation, | ||
| 306 | - defaultOrientation: .portrait | ||
| 307 | - ) | ||
| 308 | - | ||
| 309 | - var barcodeFound = false | ||
| 310 | - | ||
| 311 | - scanner.process(image) { [self] barcodes, error in | 140 | + mobileScanner.analyzeImage(image: uiImage!, position: AVCaptureDevice.Position.back, callback: { [self] barcodes, error in |
| 312 | if error == nil && barcodes != nil { | 141 | if error == nil && barcodes != nil { |
| 313 | for barcode in barcodes! { | 142 | for barcode in barcodes! { |
| 314 | - barcodeFound = true | ||
| 315 | let event: [String: Any?] = ["name": "barcode", "data": barcode.data] | 143 | let event: [String: Any?] = ["name": "barcode", "data": barcode.data] |
| 316 | - sink?(event) | 144 | + barcodeHandler.publishEvent(event) |
| 317 | } | 145 | } |
| 318 | } else if error != nil { | 146 | } else if error != nil { |
| 319 | - result(FlutterError(code: "MobileScanner", | ||
| 320 | - message: error?.localizedDescription, | ||
| 321 | - details: "analyzeImage()")) | 147 | + barcodeHandler.publishEvent(["name": "error", "message": error?.localizedDescription]) |
| 322 | } | 148 | } |
| 323 | - analyzing = false | ||
| 324 | - result(barcodeFound) | ||
| 325 | - } | ||
| 326 | - | ||
| 327 | - } | ||
| 328 | - | ||
| 329 | - func stop(_ result: FlutterResult) { | ||
| 330 | - if (device == nil) { | ||
| 331 | - result(FlutterError(code: "MobileScanner", | ||
| 332 | - message: "Called stop() while already stopped!", | ||
| 333 | - details: nil)) | ||
| 334 | - return | ||
| 335 | - } | ||
| 336 | - captureSession.stopRunning() | ||
| 337 | - for input in captureSession.inputs { | ||
| 338 | - captureSession.removeInput(input) | ||
| 339 | - } | ||
| 340 | - for output in captureSession.outputs { | ||
| 341 | - captureSession.removeOutput(output) | ||
| 342 | - } | ||
| 343 | - device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode)) | ||
| 344 | - registry.unregisterTexture(textureId) | ||
| 345 | - | ||
| 346 | -// analyzeMode = 0 | ||
| 347 | - latestBuffer = nil | ||
| 348 | - captureSession = nil | ||
| 349 | - device = nil | ||
| 350 | - textureId = nil | ||
| 351 | - | 149 | + }) |
| 352 | result(nil) | 150 | result(nil) |
| 353 | } | 151 | } |
| 354 | 152 | ||
| 355 | - // Observer for torch state | 153 | + /// Observer for torch state |
| 356 | public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { | 154 | public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { |
| 357 | switch keyPath { | 155 | switch keyPath { |
| 358 | case "torchMode": | 156 | case "torchMode": |
| 359 | // off = 0; on = 1; auto = 2; | 157 | // off = 0; on = 1; auto = 2; |
| 360 | let state = change?[.newKey] as? Int | 158 | let state = change?[.newKey] as? Int |
| 361 | - let event: [String: Any?] = ["name": "torchState", "data": state] | ||
| 362 | - sink?(event) | 159 | + barcodeHandler.publishEvent(["name": "torchState", "data": state]) |
| 363 | default: | 160 | default: |
| 364 | break | 161 | break |
| 365 | } | 162 | } |
| 366 | } | 163 | } |
| 367 | } | 164 | } |
| 368 | 165 | ||
| 369 | -class MapArgumentReader { | ||
| 370 | - | ||
| 371 | - let args: [String: Any]? | ||
| 372 | - | ||
| 373 | - init(_ args: [String: Any]?) { | ||
| 374 | - self.args = args | ||
| 375 | - } | ||
| 376 | - | ||
| 377 | - func string(key: String) -> String? { | ||
| 378 | - return args?[key] as? String | ||
| 379 | - } | ||
| 380 | - | ||
| 381 | - func int(key: String) -> Int? { | ||
| 382 | - return (args?[key] as? NSNumber)?.intValue | ||
| 383 | - } | ||
| 384 | - | ||
| 385 | - func bool(key: String) -> Bool? { | ||
| 386 | - return (args?[key] as? NSNumber)?.boolValue | ||
| 387 | - } | ||
| 388 | - | ||
| 389 | - func stringArray(key: String) -> [String]? { | ||
| 390 | - return args?[key] as? [String] | ||
| 391 | - } | ||
| 392 | - | ||
| 393 | - func intArray(key: String) -> [Int]? { | ||
| 394 | - return args?[key] as? [Int] | ||
| 395 | - } | ||
| 396 | - | 166 | +enum DetectionSpeed: Int { |
| 167 | + case noDuplicates = 0 | ||
| 168 | + case normal = 1 | ||
| 169 | + case unrestricted = 2 | ||
| 397 | } | 170 | } |
| @@ -4,7 +4,7 @@ | @@ -4,7 +4,7 @@ | ||
| 4 | # | 4 | # |
| 5 | Pod::Spec.new do |s| | 5 | Pod::Spec.new do |s| |
| 6 | s.name = 'mobile_scanner' | 6 | s.name = 'mobile_scanner' |
| 7 | - s.version = '0.0.1' | 7 | + s.version = '3.0.0' |
| 8 | s.summary = 'An universal scanner for Flutter based on MLKit.' | 8 | s.summary = 'An universal scanner for Flutter based on MLKit.' |
| 9 | s.description = <<-DESC | 9 | s.description = <<-DESC |
| 10 | An universal scanner for Flutter based on MLKit. | 10 | An universal scanner for Flutter based on MLKit. |
| @@ -16,7 +16,7 @@ An universal scanner for Flutter based on MLKit. | @@ -16,7 +16,7 @@ An universal scanner for Flutter based on MLKit. | ||
| 16 | s.source_files = 'Classes/**/*' | 16 | s.source_files = 'Classes/**/*' |
| 17 | s.dependency 'Flutter' | 17 | s.dependency 'Flutter' |
| 18 | s.dependency 'GoogleMLKit/BarcodeScanning', '~> 3.2.0' | 18 | s.dependency 'GoogleMLKit/BarcodeScanning', '~> 3.2.0' |
| 19 | - s.platform = :ios, '10.0' | 19 | + s.platform = :ios, '11.0' |
| 20 | s.static_framework = true | 20 | s.static_framework = true |
| 21 | # Flutter.framework does not contain a i386 slice. | 21 | # Flutter.framework does not contain a i386 slice. |
| 22 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } | 22 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } |
| 1 | library mobile_scanner; | 1 | library mobile_scanner; |
| 2 | 2 | ||
| 3 | +export 'src/barcode.dart'; | ||
| 4 | +export 'src/barcode_capture.dart'; | ||
| 5 | +export 'src/enums/camera_facing.dart'; | ||
| 6 | +export 'src/enums/detection_speed.dart'; | ||
| 7 | +export 'src/enums/mobile_scanner_state.dart'; | ||
| 8 | +export 'src/enums/ratio.dart'; | ||
| 9 | +export 'src/enums/torch_state.dart'; | ||
| 3 | export 'src/mobile_scanner.dart'; | 10 | export 'src/mobile_scanner.dart'; |
| 4 | export 'src/mobile_scanner_arguments.dart'; | 11 | export 'src/mobile_scanner_arguments.dart'; |
| 5 | export 'src/mobile_scanner_controller.dart'; | 12 | export 'src/mobile_scanner_controller.dart'; |
| 6 | -export 'src/objects/barcode.dart'; |
| @@ -5,7 +5,7 @@ import 'dart:ui' as ui; | @@ -5,7 +5,7 @@ import 'dart:ui' as ui; | ||
| 5 | import 'package:flutter/material.dart'; | 5 | import 'package:flutter/material.dart'; |
| 6 | import 'package:flutter/services.dart'; | 6 | import 'package:flutter/services.dart'; |
| 7 | import 'package:flutter_web_plugins/flutter_web_plugins.dart'; | 7 | import 'package:flutter_web_plugins/flutter_web_plugins.dart'; |
| 8 | -import 'package:mobile_scanner/mobile_scanner.dart'; | 8 | +import 'package:mobile_scanner/src/enums/camera_facing.dart'; |
| 9 | import 'package:mobile_scanner/src/web/jsqr.dart'; | 9 | import 'package:mobile_scanner/src/web/jsqr.dart'; |
| 10 | import 'package:mobile_scanner/src/web/media.dart'; | 10 | import 'package:mobile_scanner/src/web/media.dart'; |
| 11 | 11 |
| 1 | import 'dart:typed_data'; | 1 | import 'dart:typed_data'; |
| 2 | import 'dart:ui'; | 2 | import 'dart:ui'; |
| 3 | 3 | ||
| 4 | -import 'package:mobile_scanner/src/objects/barcode_utility.dart'; | 4 | +import 'package:mobile_scanner/src/barcode_utility.dart'; |
| 5 | 5 | ||
| 6 | /// Represents a single recognized barcode and its value. | 6 | /// Represents a single recognized barcode and its value. |
| 7 | class Barcode { | 7 | class Barcode { |
| @@ -97,7 +97,7 @@ class Barcode { | @@ -97,7 +97,7 @@ class Barcode { | ||
| 97 | }); | 97 | }); |
| 98 | 98 | ||
| 99 | /// Create a [Barcode] from native data. | 99 | /// Create a [Barcode] from native data. |
| 100 | - Barcode.fromNative(Map data, this.image) | 100 | + Barcode.fromNative(Map data, {this.image}) |
| 101 | : corners = toCorners(data['corners'] as List?), | 101 | : corners = toCorners(data['corners'] as List?), |
| 102 | format = toFormat(data['format'] as int), | 102 | format = toFormat(data['format'] as int), |
| 103 | rawBytes = data['rawBytes'] as Uint8List?, | 103 | rawBytes = data['rawBytes'] as Uint8List?, |
lib/src/barcode_capture.dart
0 → 100644
lib/src/enums/camera_facing.dart
0 → 100644
lib/src/enums/detection_speed.dart
0 → 100644
| 1 | +/// The detection speed of the scanner. | ||
| 2 | +enum DetectionSpeed { | ||
| 3 | + /// The scanner will only scan a barcode once, and never again until another | ||
| 4 | + /// barcode has been scanned. | ||
| 5 | + noDuplicates, | ||
| 6 | + | ||
| 7 | + /// Front facing camera. | ||
| 8 | + normal, | ||
| 9 | + | ||
| 10 | + /// Back facing camera. | ||
| 11 | + unrestricted, | ||
| 12 | +} |
lib/src/enums/mobile_scanner_state.dart
0 → 100644
| 1 | +enum MobileScannerState { undetermined, authorized, denied } |
lib/src/enums/ratio.dart
0 → 100644
| 1 | +enum Ratio { ratio_4_3, ratio_16_9 } |
lib/src/enums/torch_state.dart
0 → 100644
| 1 | import 'package:flutter/foundation.dart'; | 1 | import 'package:flutter/foundation.dart'; |
| 2 | import 'package:flutter/material.dart'; | 2 | import 'package:flutter/material.dart'; |
| 3 | -import 'package:mobile_scanner/mobile_scanner.dart'; | ||
| 4 | - | ||
| 5 | -enum Ratio { ratio_4_3, ratio_16_9 } | 3 | +import 'package:mobile_scanner/src/barcode_capture.dart'; |
| 4 | +import 'package:mobile_scanner/src/mobile_scanner_arguments.dart'; | ||
| 5 | +import 'package:mobile_scanner/src/mobile_scanner_controller.dart'; | ||
| 6 | 6 | ||
| 7 | /// A widget showing a live camera preview. | 7 | /// A widget showing a live camera preview. |
| 8 | class MobileScanner extends StatefulWidget { | 8 | class MobileScanner extends StatefulWidget { |
| @@ -15,28 +15,23 @@ class MobileScanner extends StatefulWidget { | @@ -15,28 +15,23 @@ class MobileScanner extends StatefulWidget { | ||
| 15 | /// Function that gets called when a Barcode is detected. | 15 | /// Function that gets called when a Barcode is detected. |
| 16 | /// | 16 | /// |
| 17 | /// [barcode] The barcode object with all information about the scanned code. | 17 | /// [barcode] The barcode object with all information about the scanned code. |
| 18 | - /// [args] Information about the state of the MobileScanner widget | ||
| 19 | - final Function(Barcode barcode, MobileScannerArguments? args) onDetect; | ||
| 20 | - | ||
| 21 | - /// TODO: Function that gets called when the Widget is initialized. Can be usefull | ||
| 22 | - /// to check wether the device has a torch(flash) or not. | ||
| 23 | - /// | ||
| 24 | - /// [args] Information about the state of the MobileScanner widget | ||
| 25 | - // final Function(MobileScannerArguments args)? onInitialize; | 18 | + /// [startArguments] Information about the state of the MobileScanner widget |
| 19 | + final Function(BarcodeCapture capture, MobileScannerArguments? arguments) | ||
| 20 | + onDetect; | ||
| 26 | 21 | ||
| 27 | /// Handles how the widget should fit the screen. | 22 | /// Handles how the widget should fit the screen. |
| 28 | final BoxFit fit; | 23 | final BoxFit fit; |
| 29 | 24 | ||
| 30 | - /// Set to false if you don't want duplicate scans. | ||
| 31 | - final bool allowDuplicates; | 25 | + /// Whether to automatically resume the camera when the application is resumed |
| 26 | + final bool autoResume; | ||
| 32 | 27 | ||
| 33 | /// Create a [MobileScanner] with a [controller], the [controller] must has been initialized. | 28 | /// Create a [MobileScanner] with a [controller], the [controller] must has been initialized. |
| 34 | const MobileScanner({ | 29 | const MobileScanner({ |
| 35 | super.key, | 30 | super.key, |
| 36 | required this.onDetect, | 31 | required this.onDetect, |
| 37 | this.controller, | 32 | this.controller, |
| 33 | + this.autoResume = true, | ||
| 38 | this.fit = BoxFit.cover, | 34 | this.fit = BoxFit.cover, |
| 39 | - this.allowDuplicates = false, | ||
| 40 | this.onPermissionSet, | 35 | this.onPermissionSet, |
| 41 | }); | 36 | }); |
| 42 | 37 | ||
| @@ -57,40 +52,37 @@ class _MobileScannerState extends State<MobileScanner> | @@ -57,40 +52,37 @@ class _MobileScannerState extends State<MobileScanner> | ||
| 57 | if (!controller.isStarting) controller.start(); | 52 | if (!controller.isStarting) controller.start(); |
| 58 | } | 53 | } |
| 59 | 54 | ||
| 55 | + AppLifecycleState? _lastState; | ||
| 56 | + | ||
| 60 | @override | 57 | @override |
| 61 | void didChangeAppLifecycleState(AppLifecycleState state) { | 58 | void didChangeAppLifecycleState(AppLifecycleState state) { |
| 62 | switch (state) { | 59 | switch (state) { |
| 63 | case AppLifecycleState.resumed: | 60 | case AppLifecycleState.resumed: |
| 64 | - if (!controller.isStarting && controller.autoResume) controller.start(); | 61 | + if (!controller.isStarting && |
| 62 | + widget.autoResume && | ||
| 63 | + _lastState != AppLifecycleState.inactive) controller.start(); | ||
| 65 | break; | 64 | break; |
| 66 | - case AppLifecycleState.inactive: | ||
| 67 | case AppLifecycleState.paused: | 65 | case AppLifecycleState.paused: |
| 68 | case AppLifecycleState.detached: | 66 | case AppLifecycleState.detached: |
| 69 | controller.stop(); | 67 | controller.stop(); |
| 70 | break; | 68 | break; |
| 69 | + default: | ||
| 70 | + break; | ||
| 71 | } | 71 | } |
| 72 | + _lastState = state; | ||
| 72 | } | 73 | } |
| 73 | 74 | ||
| 74 | - Uint8List? lastScanned; | ||
| 75 | - | ||
| 76 | @override | 75 | @override |
| 77 | Widget build(BuildContext context) { | 76 | Widget build(BuildContext context) { |
| 78 | return ValueListenableBuilder( | 77 | return ValueListenableBuilder( |
| 79 | - valueListenable: controller.args, | 78 | + valueListenable: controller.startArguments, |
| 80 | builder: (context, value, child) { | 79 | builder: (context, value, child) { |
| 81 | value = value as MobileScannerArguments?; | 80 | value = value as MobileScannerArguments?; |
| 82 | if (value == null) { | 81 | if (value == null) { |
| 83 | return const ColoredBox(color: Colors.black); | 82 | return const ColoredBox(color: Colors.black); |
| 84 | } else { | 83 | } else { |
| 85 | controller.barcodes.listen((barcode) { | 84 | controller.barcodes.listen((barcode) { |
| 86 | - if (!widget.allowDuplicates) { | ||
| 87 | - if (lastScanned != barcode.rawBytes) { | ||
| 88 | - lastScanned = barcode.rawBytes; | ||
| 89 | - widget.onDetect(barcode, value! as MobileScannerArguments); | ||
| 90 | - } | ||
| 91 | - } else { | ||
| 92 | - widget.onDetect(barcode, value! as MobileScannerArguments); | ||
| 93 | - } | 85 | + widget.onDetect(barcode, value! as MobileScannerArguments); |
| 94 | }); | 86 | }); |
| 95 | return ClipRect( | 87 | return ClipRect( |
| 96 | child: SizedBox( | 88 | child: SizedBox( |
| @@ -5,192 +5,155 @@ import 'package:flutter/cupertino.dart'; | @@ -5,192 +5,155 @@ import 'package:flutter/cupertino.dart'; | ||
| 5 | import 'package:flutter/foundation.dart'; | 5 | import 'package:flutter/foundation.dart'; |
| 6 | import 'package:flutter/services.dart'; | 6 | import 'package:flutter/services.dart'; |
| 7 | import 'package:mobile_scanner/mobile_scanner.dart'; | 7 | import 'package:mobile_scanner/mobile_scanner.dart'; |
| 8 | -import 'package:mobile_scanner/src/objects/barcode_utility.dart'; | 8 | +import 'package:mobile_scanner/src/barcode_utility.dart'; |
| 9 | +import 'package:mobile_scanner/src/mobile_scanner_exception.dart'; | ||
| 9 | 10 | ||
| 10 | -/// The facing of a camera. | ||
| 11 | -enum CameraFacing { | ||
| 12 | - /// Front facing camera. | ||
| 13 | - front, | 11 | +/// The [MobileScannerController] holds all the logic of this plugin, |
| 12 | +/// where as the [MobileScanner] class is the frontend of this plugin. | ||
| 13 | +class MobileScannerController { | ||
| 14 | + MobileScannerController({ | ||
| 15 | + this.facing = CameraFacing.back, | ||
| 16 | + this.detectionSpeed = DetectionSpeed.noDuplicates, | ||
| 17 | + // this.ratio, | ||
| 18 | + this.torchEnabled = false, | ||
| 19 | + this.formats, | ||
| 20 | + // this.autoResume = true, | ||
| 21 | + this.returnImage = false, | ||
| 22 | + this.onPermissionSet, | ||
| 23 | + }) { | ||
| 24 | + // In case a new instance is created before calling dispose() | ||
| 25 | + if (controllerHashcode != null) { | ||
| 26 | + stop(); | ||
| 27 | + } | ||
| 28 | + controllerHashcode = hashCode; | ||
| 29 | + events = _eventChannel | ||
| 30 | + .receiveBroadcastStream() | ||
| 31 | + .listen((data) => _handleEvent(data as Map)); | ||
| 32 | + } | ||
| 14 | 33 | ||
| 15 | - /// Back facing camera. | ||
| 16 | - back, | ||
| 17 | -} | 34 | + //Must be static to keep the same value on new instances |
| 35 | + static int? controllerHashcode; | ||
| 36 | + | ||
| 37 | + /// Select which camera should be used. | ||
| 38 | + /// | ||
| 39 | + /// Default: CameraFacing.back | ||
| 40 | + final CameraFacing facing; | ||
| 18 | 41 | ||
| 19 | -enum MobileScannerState { undetermined, authorized, denied } | 42 | + // /// Analyze the image in 4:3 or 16:9 |
| 43 | + // /// | ||
| 44 | + // /// Only on Android | ||
| 45 | + // final Ratio? ratio; | ||
| 20 | 46 | ||
| 21 | -/// The state of torch. | ||
| 22 | -enum TorchState { | ||
| 23 | - /// Torch is off. | ||
| 24 | - off, | 47 | + /// Enable or disable the torch (Flash) on start |
| 48 | + /// | ||
| 49 | + /// Default: disabled | ||
| 50 | + final bool torchEnabled; | ||
| 25 | 51 | ||
| 26 | - /// Torch is on. | ||
| 27 | - on, | ||
| 28 | -} | 52 | + /// Set to true if you want to return the image buffer with the Barcode event |
| 53 | + /// | ||
| 54 | + /// Only supported on iOS and Android | ||
| 55 | + final bool returnImage; | ||
| 29 | 56 | ||
| 30 | -// enum AnalyzeMode { none, barcode } | 57 | + /// If provided, the scanner will only detect those specific formats |
| 58 | + final List<BarcodeFormat>? formats; | ||
| 31 | 59 | ||
| 32 | -class MobileScannerController { | ||
| 33 | - MethodChannel methodChannel = | ||
| 34 | - const MethodChannel('dev.steenbakker.mobile_scanner/scanner/method'); | ||
| 35 | - EventChannel eventChannel = | ||
| 36 | - const EventChannel('dev.steenbakker.mobile_scanner/scanner/event'); | 60 | + /// Sets the speed of detections. |
| 61 | + /// | ||
| 62 | + /// WARNING: DetectionSpeed.unrestricted can cause memory issues on some devices | ||
| 63 | + final DetectionSpeed detectionSpeed; | ||
| 37 | 64 | ||
| 38 | - //Must be static to keep the same value on new instances | ||
| 39 | - static int? _controllerHashcode; | ||
| 40 | - StreamSubscription? events; | 65 | + /// Sets the barcode stream |
| 66 | + final StreamController<BarcodeCapture> _barcodesController = | ||
| 67 | + StreamController.broadcast(); | ||
| 68 | + Stream<BarcodeCapture> get barcodes => _barcodesController.stream; | ||
| 69 | + | ||
| 70 | + static const MethodChannel _methodChannel = | ||
| 71 | + MethodChannel('dev.steenbakker.mobile_scanner/scanner/method'); | ||
| 72 | + static const EventChannel _eventChannel = | ||
| 73 | + EventChannel('dev.steenbakker.mobile_scanner/scanner/event'); | ||
| 41 | 74 | ||
| 42 | Function(bool permissionGranted)? onPermissionSet; | 75 | Function(bool permissionGranted)? onPermissionSet; |
| 43 | - final ValueNotifier<MobileScannerArguments?> args = ValueNotifier(null); | ||
| 44 | - final ValueNotifier<TorchState> torchState = ValueNotifier(TorchState.off); | ||
| 45 | - late final ValueNotifier<CameraFacing> cameraFacingState; | ||
| 46 | - final Ratio? ratio; | ||
| 47 | - final bool? torchEnabled; | ||
| 48 | - // Whether to return the image buffer with the Barcode event | ||
| 49 | - final bool returnImage; | ||
| 50 | 76 | ||
| 51 | - /// If provided, the scanner will only detect those specific formats. | ||
| 52 | - final List<BarcodeFormat>? formats; | 77 | + /// Listen to events from the platform specific code |
| 78 | + late StreamSubscription events; | ||
| 53 | 79 | ||
| 54 | - CameraFacing facing; | ||
| 55 | - bool hasTorch = false; | ||
| 56 | - late StreamController<Barcode> barcodesController; | 80 | + /// A notifier that provides several arguments about the MobileScanner |
| 81 | + final ValueNotifier<MobileScannerArguments?> startArguments = | ||
| 82 | + ValueNotifier(null); | ||
| 57 | 83 | ||
| 58 | - /// Whether to automatically resume the camera when the application is resumed | ||
| 59 | - bool autoResume; | 84 | + /// A notifier that provides the state of the Torch (Flash) |
| 85 | + final ValueNotifier<TorchState> torchState = ValueNotifier(TorchState.off); | ||
| 60 | 86 | ||
| 61 | - Stream<Barcode> get barcodes => barcodesController.stream; | 87 | + /// A notifier that provides the state of which camera is being used |
| 88 | + late final ValueNotifier<CameraFacing> cameraFacingState = | ||
| 89 | + ValueNotifier(facing); | ||
| 62 | 90 | ||
| 63 | - MobileScannerController({ | ||
| 64 | - this.facing = CameraFacing.back, | ||
| 65 | - this.ratio, | ||
| 66 | - this.torchEnabled, | ||
| 67 | - this.formats, | ||
| 68 | - this.onPermissionSet, | ||
| 69 | - this.autoResume = true, | ||
| 70 | - this.returnImage = false, | ||
| 71 | - }) { | ||
| 72 | - // In case a new instance is created before calling dispose() | ||
| 73 | - if (_controllerHashcode != null) { | ||
| 74 | - stop(); | ||
| 75 | - } | ||
| 76 | - _controllerHashcode = hashCode; | 91 | + bool isStarting = false; |
| 92 | + bool? _hasTorch; | ||
| 77 | 93 | ||
| 78 | - cameraFacingState = ValueNotifier(facing); | 94 | + /// Set the starting arguments for the camera |
| 95 | + Map<String, dynamic> _argumentsToMap({CameraFacing? cameraFacingOverride}) { | ||
| 96 | + final Map<String, dynamic> arguments = {}; | ||
| 79 | 97 | ||
| 80 | - // Sets analyze mode and barcode stream | ||
| 81 | - barcodesController = StreamController.broadcast( | ||
| 82 | - // onListen: () => setAnalyzeMode(AnalyzeMode.barcode.index), | ||
| 83 | - // onCancel: () => setAnalyzeMode(AnalyzeMode.none.index), | ||
| 84 | - ); | 98 | + cameraFacingState.value = cameraFacingOverride ?? facing; |
| 99 | + arguments['facing'] = cameraFacingState.value.index; | ||
| 85 | 100 | ||
| 86 | - // Listen to events from the platform specific code | ||
| 87 | - events = eventChannel | ||
| 88 | - .receiveBroadcastStream() | ||
| 89 | - .listen((data) => handleEvent(data as Map)); | ||
| 90 | - } | 101 | + // if (ratio != null) arguments['ratio'] = ratio; |
| 102 | + arguments['torch'] = torchEnabled; | ||
| 103 | + arguments['speed'] = detectionSpeed.index; | ||
| 91 | 104 | ||
| 92 | - void handleEvent(Map event) { | ||
| 93 | - final name = event['name']; | ||
| 94 | - final data = event['data']; | ||
| 95 | - final binaryData = event['binaryData']; | ||
| 96 | - switch (name) { | ||
| 97 | - case 'torchState': | ||
| 98 | - final state = TorchState.values[data as int? ?? 0]; | ||
| 99 | - torchState.value = state; | ||
| 100 | - break; | ||
| 101 | - case 'barcode': | ||
| 102 | - final image = returnImage ? event['image'] as Uint8List : null; | ||
| 103 | - final barcode = Barcode.fromNative(data as Map? ?? {}, image); | ||
| 104 | - barcodesController.add(barcode); | ||
| 105 | - break; | ||
| 106 | - case 'barcodeMac': | ||
| 107 | - barcodesController.add( | ||
| 108 | - Barcode( | ||
| 109 | - rawValue: (data as Map)['payload'] as String?, | ||
| 110 | - ), | ||
| 111 | - ); | ||
| 112 | - break; | ||
| 113 | - case 'barcodeWeb': | ||
| 114 | - final bytes = (binaryData as List).cast<int>(); | ||
| 115 | - barcodesController.add( | ||
| 116 | - Barcode( | ||
| 117 | - rawValue: data as String?, | ||
| 118 | - rawBytes: Uint8List.fromList(bytes), | ||
| 119 | - ), | ||
| 120 | - ); | ||
| 121 | - break; | ||
| 122 | - default: | ||
| 123 | - throw UnimplementedError(); | 105 | + if (formats != null) { |
| 106 | + if (Platform.isAndroid) { | ||
| 107 | + arguments['formats'] = formats!.map((e) => e.index).toList(); | ||
| 108 | + } else if (Platform.isIOS || Platform.isMacOS) { | ||
| 109 | + arguments['formats'] = formats!.map((e) => e.rawValue).toList(); | ||
| 110 | + } | ||
| 124 | } | 111 | } |
| 112 | + arguments['returnImage'] = true; | ||
| 113 | + return arguments; | ||
| 125 | } | 114 | } |
| 126 | 115 | ||
| 127 | - // TODO: Add more analyzers like text analyzer | ||
| 128 | - // void setAnalyzeMode(int mode) { | ||
| 129 | - // if (hashCode != _controllerHashcode) { | ||
| 130 | - // return; | ||
| 131 | - // } | ||
| 132 | - // methodChannel.invokeMethod('analyze', mode); | ||
| 133 | - // } | ||
| 134 | - | ||
| 135 | - // List<BarcodeFormats>? formats = _defaultBarcodeFormats, | ||
| 136 | - bool isStarting = false; | ||
| 137 | - | ||
| 138 | /// Start barcode scanning. This will first check if the required permissions | 116 | /// Start barcode scanning. This will first check if the required permissions |
| 139 | /// are set. | 117 | /// are set. |
| 140 | - Future<void> start() async { | ||
| 141 | - ensure('startAsync'); | 118 | + Future<MobileScannerArguments?> start({ |
| 119 | + CameraFacing? cameraFacingOverride, | ||
| 120 | + }) async { | ||
| 121 | + debugPrint('Hashcode controller: $hashCode'); | ||
| 142 | if (isStarting) { | 122 | if (isStarting) { |
| 143 | - throw Exception('mobile_scanner: Called start() while already starting.'); | 123 | + debugPrint("Called start() while starting."); |
| 144 | } | 124 | } |
| 145 | isStarting = true; | 125 | isStarting = true; |
| 146 | - // setAnalyzeMode(AnalyzeMode.barcode.index); | ||
| 147 | 126 | ||
| 148 | // Check authorization status | 127 | // Check authorization status |
| 149 | if (!kIsWeb) { | 128 | if (!kIsWeb) { |
| 150 | - MobileScannerState state = MobileScannerState | ||
| 151 | - .values[await methodChannel.invokeMethod('state') as int? ?? 0]; | 129 | + final MobileScannerState state = MobileScannerState |
| 130 | + .values[await _methodChannel.invokeMethod('state') as int? ?? 0]; | ||
| 152 | switch (state) { | 131 | switch (state) { |
| 153 | case MobileScannerState.undetermined: | 132 | case MobileScannerState.undetermined: |
| 154 | final bool result = | 133 | final bool result = |
| 155 | - await methodChannel.invokeMethod('request') as bool? ?? false; | ||
| 156 | - state = result | ||
| 157 | - ? MobileScannerState.authorized | ||
| 158 | - : MobileScannerState.denied; | ||
| 159 | - onPermissionSet?.call(result); | 134 | + await _methodChannel.invokeMethod('request') as bool? ?? false; |
| 135 | + if (!result) { | ||
| 136 | + isStarting = false; | ||
| 137 | + onPermissionSet?.call(result); | ||
| 138 | + throw MobileScannerException('User declined camera permission.'); | ||
| 139 | + } | ||
| 160 | break; | 140 | break; |
| 161 | case MobileScannerState.denied: | 141 | case MobileScannerState.denied: |
| 162 | isStarting = false; | 142 | isStarting = false; |
| 163 | onPermissionSet?.call(false); | 143 | onPermissionSet?.call(false); |
| 164 | - throw PlatformException(code: 'NO ACCESS'); | 144 | + throw MobileScannerException('User declined camera permission.'); |
| 165 | case MobileScannerState.authorized: | 145 | case MobileScannerState.authorized: |
| 166 | onPermissionSet?.call(true); | 146 | onPermissionSet?.call(true); |
| 167 | break; | 147 | break; |
| 168 | } | 148 | } |
| 169 | } | 149 | } |
| 170 | 150 | ||
| 171 | - cameraFacingState.value = facing; | ||
| 172 | - | ||
| 173 | - // Set the starting arguments for the camera | ||
| 174 | - final Map arguments = {}; | ||
| 175 | - arguments['facing'] = facing.index; | ||
| 176 | - if (ratio != null) arguments['ratio'] = ratio; | ||
| 177 | - if (torchEnabled != null) arguments['torch'] = torchEnabled; | ||
| 178 | - | ||
| 179 | - if (formats != null) { | ||
| 180 | - if (Platform.isAndroid) { | ||
| 181 | - arguments['formats'] = formats!.map((e) => e.index).toList(); | ||
| 182 | - } else if (Platform.isIOS || Platform.isMacOS) { | ||
| 183 | - arguments['formats'] = formats!.map((e) => e.rawValue).toList(); | ||
| 184 | - } | ||
| 185 | - } | ||
| 186 | - arguments['returnImage'] = returnImage; | ||
| 187 | - | ||
| 188 | // Start the camera with arguments | 151 | // Start the camera with arguments |
| 189 | Map<String, dynamic>? startResult = {}; | 152 | Map<String, dynamic>? startResult = {}; |
| 190 | try { | 153 | try { |
| 191 | - startResult = await methodChannel.invokeMapMethod<String, dynamic>( | 154 | + startResult = await _methodChannel.invokeMapMethod<String, dynamic>( |
| 192 | 'start', | 155 | 'start', |
| 193 | - arguments, | 156 | + _argumentsToMap(cameraFacingOverride: cameraFacingOverride), |
| 194 | ); | 157 | ); |
| 195 | } on PlatformException catch (error) { | 158 | } on PlatformException catch (error) { |
| 196 | debugPrint('${error.code}: ${error.message}'); | 159 | debugPrint('${error.code}: ${error.message}'); |
| @@ -198,85 +161,78 @@ class MobileScannerController { | @@ -198,85 +161,78 @@ class MobileScannerController { | ||
| 198 | if (error.code == "MobileScannerWeb") { | 161 | if (error.code == "MobileScannerWeb") { |
| 199 | onPermissionSet?.call(false); | 162 | onPermissionSet?.call(false); |
| 200 | } | 163 | } |
| 201 | - // setAnalyzeMode(AnalyzeMode.none.index); | ||
| 202 | - return; | 164 | + return null; |
| 203 | } | 165 | } |
| 204 | 166 | ||
| 205 | if (startResult == null) { | 167 | if (startResult == null) { |
| 206 | isStarting = false; | 168 | isStarting = false; |
| 207 | - throw PlatformException(code: 'INITIALIZATION ERROR'); | 169 | + throw MobileScannerException( |
| 170 | + 'Failed to start mobileScanner, no response from platform side', | ||
| 171 | + ); | ||
| 208 | } | 172 | } |
| 209 | 173 | ||
| 210 | - hasTorch = startResult['torchable'] as bool? ?? false; | 174 | + _hasTorch = startResult['torchable'] as bool? ?? false; |
| 175 | + if (_hasTorch! && torchEnabled) { | ||
| 176 | + torchState.value = TorchState.on; | ||
| 177 | + } | ||
| 211 | 178 | ||
| 212 | if (kIsWeb) { | 179 | if (kIsWeb) { |
| 213 | onPermissionSet?.call( | 180 | onPermissionSet?.call( |
| 214 | true, | 181 | true, |
| 215 | ); // If we reach this line, it means camera permission has been granted | 182 | ); // If we reach this line, it means camera permission has been granted |
| 216 | 183 | ||
| 217 | - args.value = MobileScannerArguments( | 184 | + startArguments.value = MobileScannerArguments( |
| 218 | webId: startResult['ViewID'] as String?, | 185 | webId: startResult['ViewID'] as String?, |
| 219 | size: Size( | 186 | size: Size( |
| 220 | startResult['videoWidth'] as double? ?? 0, | 187 | startResult['videoWidth'] as double? ?? 0, |
| 221 | startResult['videoHeight'] as double? ?? 0, | 188 | startResult['videoHeight'] as double? ?? 0, |
| 222 | ), | 189 | ), |
| 223 | - hasTorch: hasTorch, | 190 | + hasTorch: _hasTorch!, |
| 224 | ); | 191 | ); |
| 225 | } else { | 192 | } else { |
| 226 | - args.value = MobileScannerArguments( | 193 | + startArguments.value = MobileScannerArguments( |
| 227 | textureId: startResult['textureId'] as int?, | 194 | textureId: startResult['textureId'] as int?, |
| 228 | size: toSize(startResult['size'] as Map? ?? {}), | 195 | size: toSize(startResult['size'] as Map? ?? {}), |
| 229 | - hasTorch: hasTorch, | 196 | + hasTorch: _hasTorch!, |
| 230 | ); | 197 | ); |
| 231 | } | 198 | } |
| 232 | - | ||
| 233 | isStarting = false; | 199 | isStarting = false; |
| 200 | + return startArguments.value!; | ||
| 234 | } | 201 | } |
| 235 | 202 | ||
| 203 | + /// Stops the camera, but does not dispose this controller. | ||
| 236 | Future<void> stop() async { | 204 | Future<void> stop() async { |
| 237 | - try { | ||
| 238 | - await methodChannel.invokeMethod('stop'); | ||
| 239 | - } on PlatformException catch (error) { | ||
| 240 | - debugPrint('${error.code}: ${error.message}'); | ||
| 241 | - } | 205 | + await _methodChannel.invokeMethod('stop'); |
| 242 | } | 206 | } |
| 243 | 207 | ||
| 244 | /// Switches the torch on or off. | 208 | /// Switches the torch on or off. |
| 245 | /// | 209 | /// |
| 246 | /// Only works if torch is available. | 210 | /// Only works if torch is available. |
| 247 | Future<void> toggleTorch() async { | 211 | Future<void> toggleTorch() async { |
| 248 | - ensure('toggleTorch'); | ||
| 249 | - if (!hasTorch) { | ||
| 250 | - debugPrint('Device has no torch/flash.'); | ||
| 251 | - return; | 212 | + if (_hasTorch == null) { |
| 213 | + throw MobileScannerException( | ||
| 214 | + 'Cannot toggle torch if start() has never been called', | ||
| 215 | + ); | ||
| 216 | + } else if (!_hasTorch!) { | ||
| 217 | + throw MobileScannerException('Device has no torch'); | ||
| 252 | } | 218 | } |
| 253 | 219 | ||
| 254 | - final TorchState state = | 220 | + torchState.value = |
| 255 | torchState.value == TorchState.off ? TorchState.on : TorchState.off; | 221 | torchState.value == TorchState.off ? TorchState.on : TorchState.off; |
| 256 | 222 | ||
| 257 | - try { | ||
| 258 | - await methodChannel.invokeMethod('torch', state.index); | ||
| 259 | - } on PlatformException catch (error) { | ||
| 260 | - debugPrint('${error.code}: ${error.message}'); | ||
| 261 | - } | 223 | + await _methodChannel.invokeMethod('torch', torchState.value.index); |
| 262 | } | 224 | } |
| 263 | 225 | ||
| 264 | /// Switches the torch on or off. | 226 | /// Switches the torch on or off. |
| 265 | /// | 227 | /// |
| 266 | /// Only works if torch is available. | 228 | /// Only works if torch is available. |
| 267 | Future<void> switchCamera() async { | 229 | Future<void> switchCamera() async { |
| 268 | - ensure('switchCamera'); | ||
| 269 | - try { | ||
| 270 | - await methodChannel.invokeMethod('stop'); | ||
| 271 | - } on PlatformException catch (error) { | ||
| 272 | - debugPrint( | ||
| 273 | - '${error.code}: camera is stopped! Please start before switching camera.', | ||
| 274 | - ); | ||
| 275 | - return; | ||
| 276 | - } | ||
| 277 | - facing = | ||
| 278 | - facing == CameraFacing.back ? CameraFacing.front : CameraFacing.back; | ||
| 279 | - await start(); | 230 | + await _methodChannel.invokeMethod('stop'); |
| 231 | + final CameraFacing facingToUse = | ||
| 232 | + cameraFacingState.value == CameraFacing.back | ||
| 233 | + ? CameraFacing.front | ||
| 234 | + : CameraFacing.back; | ||
| 235 | + await start(cameraFacingOverride: facingToUse); | ||
| 280 | } | 236 | } |
| 281 | 237 | ||
| 282 | /// Handles a local image file. | 238 | /// Handles a local image file. |
| @@ -285,28 +241,72 @@ class MobileScannerController { | @@ -285,28 +241,72 @@ class MobileScannerController { | ||
| 285 | /// | 241 | /// |
| 286 | /// [path] The path of the image on the devices | 242 | /// [path] The path of the image on the devices |
| 287 | Future<bool> analyzeImage(String path) async { | 243 | Future<bool> analyzeImage(String path) async { |
| 288 | - return methodChannel | 244 | + return _methodChannel |
| 289 | .invokeMethod<bool>('analyzeImage', path) | 245 | .invokeMethod<bool>('analyzeImage', path) |
| 290 | .then<bool>((bool? value) => value ?? false); | 246 | .then<bool>((bool? value) => value ?? false); |
| 291 | } | 247 | } |
| 292 | 248 | ||
| 293 | /// Disposes the MobileScannerController and closes all listeners. | 249 | /// Disposes the MobileScannerController and closes all listeners. |
| 250 | + /// | ||
| 251 | + /// If you call this, you cannot use this controller object anymore. | ||
| 294 | void dispose() { | 252 | void dispose() { |
| 295 | - if (hashCode == _controllerHashcode) { | ||
| 296 | - stop(); | ||
| 297 | - events?.cancel(); | ||
| 298 | - events = null; | ||
| 299 | - _controllerHashcode = null; | 253 | + stop(); |
| 254 | + events.cancel(); | ||
| 255 | + _barcodesController.close(); | ||
| 256 | + if (hashCode == controllerHashcode) { | ||
| 257 | + controllerHashcode = null; | ||
| 300 | onPermissionSet = null; | 258 | onPermissionSet = null; |
| 301 | } | 259 | } |
| 302 | - barcodesController.close(); | ||
| 303 | } | 260 | } |
| 304 | 261 | ||
| 305 | - /// Checks if the MobileScannerController is bound to the correct MobileScanner object. | ||
| 306 | - void ensure(String name) { | ||
| 307 | - final message = | ||
| 308 | - 'MobileScannerController.$name called after MobileScannerController.dispose\n' | ||
| 309 | - 'MobileScannerController methods should not be used after calling dispose.'; | ||
| 310 | - assert(hashCode == _controllerHashcode, message); | 262 | + /// Handles a returning event from the platform side |
| 263 | + void _handleEvent(Map event) { | ||
| 264 | + final name = event['name']; | ||
| 265 | + final data = event['data']; | ||
| 266 | + | ||
| 267 | + switch (name) { | ||
| 268 | + case 'torchState': | ||
| 269 | + final state = TorchState.values[data as int? ?? 0]; | ||
| 270 | + torchState.value = state; | ||
| 271 | + break; | ||
| 272 | + case 'barcode': | ||
| 273 | + if (data == null) return; | ||
| 274 | + final parsed = (data as List) | ||
| 275 | + .map((value) => Barcode.fromNative(value as Map)) | ||
| 276 | + .toList(); | ||
| 277 | + _barcodesController.add( | ||
| 278 | + BarcodeCapture( | ||
| 279 | + barcodes: parsed, | ||
| 280 | + image: event['image'] as Uint8List, | ||
| 281 | + ), | ||
| 282 | + ); | ||
| 283 | + break; | ||
| 284 | + case 'barcodeMac': | ||
| 285 | + _barcodesController.add( | ||
| 286 | + BarcodeCapture( | ||
| 287 | + barcodes: [ | ||
| 288 | + Barcode( | ||
| 289 | + rawValue: (data as Map)['payload'] as String?, | ||
| 290 | + ) | ||
| 291 | + ], | ||
| 292 | + ), | ||
| 293 | + ); | ||
| 294 | + break; | ||
| 295 | + case 'barcodeWeb': | ||
| 296 | + _barcodesController.add( | ||
| 297 | + BarcodeCapture( | ||
| 298 | + barcodes: [ | ||
| 299 | + Barcode( | ||
| 300 | + rawValue: data as String?, | ||
| 301 | + ) | ||
| 302 | + ], | ||
| 303 | + ), | ||
| 304 | + ); | ||
| 305 | + break; | ||
| 306 | + case 'error': | ||
| 307 | + throw MobileScannerException(data as String); | ||
| 308 | + default: | ||
| 309 | + throw UnimplementedError(name as String?); | ||
| 310 | + } | ||
| 311 | } | 311 | } |
| 312 | } | 312 | } |
lib/src/mobile_scanner_exception.dart
0 → 100644
-
Please register or login to post a comment