Committed by
GitHub
Merge pull request #349 from juliansteenbakker/feature/return-image-dart
feat: add return image and refactor existing functions
Showing
16 changed files
with
631 additions
and
275 deletions
| @@ -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,14 +76,22 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -70,14 +76,22 @@ 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 | 90 | + if (ContextCompat.checkSelfPermission( |
| 91 | + activity, | ||
| 92 | + Manifest.permission.CAMERA | ||
| 93 | + ) == PackageManager.PERMISSION_GRANTED | ||
| 94 | + ) 1 | ||
| 81 | else 0 | 95 | else 0 |
| 82 | result.success(state) | 96 | result.success(state) |
| 83 | } | 97 | } |
| @@ -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 -> { | 118 | + |
| 105 | val mediaImage = imageProxy.image ?: return@Analyzer | 119 | val mediaImage = imageProxy.image ?: return@Analyzer |
| 106 | val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) | 120 | val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) |
| 107 | 121 | ||
| 108 | scanner.process(inputImage) | 122 | scanner.process(inputImage) |
| 109 | .addOnSuccessListener { barcodes -> | 123 | .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() } | 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 | ||
| 117 | // } | 151 | // } |
| 118 | -// else -> imageProxy.close() | 152 | +// |
| 153 | +// Log.d("scanner", "SCANNED IMAGE: $byteArray") | ||
| 154 | +// lastScanned = barcodes; | ||
| 155 | +// | ||
| 156 | +// | ||
| 119 | // } | 157 | // } |
| 158 | +// | ||
| 159 | +// } | ||
| 160 | +// isAnalyzing = false | ||
| 120 | } | 161 | } |
| 162 | + .addOnFailureListener { e -> sink?.success(mapOf( | ||
| 163 | + "name" to "error", | ||
| 164 | + "data" to e.localizedMessage | ||
| 165 | + )) } | ||
| 166 | + .addOnCompleteListener { imageProxy.close() } | ||
| 167 | + } | ||
| 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) | ||
| 121 | 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 | } |
| @@ -186,12 +271,19 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -186,12 +271,19 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 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, | 381 | + get() = mapOf( |
| 382 | + "corners" to cornerPoints?.map { corner -> corner.data }, "format" to format, | ||
| 281 | "rawBytes" to rawBytes, "rawValue" to rawValue, "type" to valueType, | 383 | "rawBytes" to rawBytes, "rawValue" to rawValue, "type" to valueType, |
| 282 | "calendarEvent" to calendarEvent?.data, "contactInfo" to contactInfo?.data, | 384 | "calendarEvent" to calendarEvent?.data, "contactInfo" to contactInfo?.data, |
| 283 | "driverLicense" to driverLicense?.data, "email" to email?.data, | 385 | "driverLicense" to driverLicense?.data, "email" to email?.data, |
| 284 | "geoPoint" to geoPoint?.data, "phone" to phone?.data, "sms" to sms?.data, | 386 | "geoPoint" to geoPoint?.data, "phone" to phone?.data, "sms" to sms?.data, |
| 285 | - "url" to url?.data, "wifi" to wifi?.data, "displayValue" to displayValue) | 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, | 394 | + get() = mapOf( |
| 395 | + "description" to description, "end" to end?.rawValue, "location" to location, | ||
| 292 | "organizer" to organizer, "start" to start?.rawValue, "status" to status, | 396 | "organizer" to organizer, "start" to start?.rawValue, "status" to status, |
| 293 | - "summary" to summary) | 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 }, | 401 | + get() = mapOf( |
| 402 | + "addresses" to addresses.map { address -> address.data }, | ||
| 297 | "emails" to emails.map { email -> email.data }, "name" to name?.data, | 403 | "emails" to emails.map { email -> email.data }, "name" to name?.data, |
| 298 | "organization" to organization, "phones" to phones.map { phone -> phone.data }, | 404 | "organization" to organization, "phones" to phones.map { phone -> phone.data }, |
| 299 | - "title" to title, "urls" to urls) | 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, | 415 | + get() = mapOf( |
| 416 | + "first" to first, "formattedName" to formattedName, "last" to last, | ||
| 306 | "middle" to middle, "prefix" to prefix, "pronunciation" to pronunciation, | 417 | "middle" to middle, "prefix" to prefix, "pronunciation" to pronunciation, |
| 307 | - "suffix" to suffix) | 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, | 422 | + get() = mapOf( |
| 423 | + "addressCity" to addressCity, "addressState" to addressState, | ||
| 311 | "addressStreet" to addressStreet, "addressZip" to addressZip, "birthDate" to birthDate, | 424 | "addressStreet" to addressStreet, "addressZip" to addressZip, "birthDate" to birthDate, |
| 312 | "documentType" to documentType, "expiryDate" to expiryDate, "firstName" to firstName, | 425 | "documentType" to documentType, "expiryDate" to expiryDate, "firstName" to firstName, |
| 313 | "gender" to gender, "issueDate" to issueDate, "issuingCountry" to issuingCountry, | 426 | "gender" to gender, "issueDate" to issueDate, "issuingCountry" to issuingCountry, |
| 314 | - "lastName" to lastName, "licenseNumber" to licenseNumber, "middleName" to middleName) | 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) |
| @@ -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 = ( |
| @@ -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,7 +32,34 @@ class _BarcodeScannerReturningImageState | @@ -32,7 +32,34 @@ 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: [ | ||
| 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, | ||
| 48 | + ), | ||
| 49 | + ) | ||
| 50 | + : const ColoredBox( | ||
| 51 | + color: Colors.white, | ||
| 52 | + child: Center( | ||
| 53 | + child: Text( | ||
| 54 | + 'Your scanned barcode will appear here!', | ||
| 55 | + ), | ||
| 56 | + ), | ||
| 57 | + ), | ||
| 58 | + ), | ||
| 59 | + Container( | ||
| 60 | + height: 0.66 * MediaQuery.of(context).size.height, | ||
| 61 | + color: Colors.grey, | ||
| 62 | + child: Stack( | ||
| 36 | children: [ | 63 | children: [ |
| 37 | MobileScanner( | 64 | MobileScanner( |
| 38 | controller: controller, | 65 | controller: controller, |
| @@ -42,17 +69,10 @@ class _BarcodeScannerReturningImageState | @@ -42,17 +69,10 @@ class _BarcodeScannerReturningImageState | ||
| 42 | // torchEnabled: true, | 69 | // torchEnabled: true, |
| 43 | // facing: CameraFacing.front, | 70 | // facing: CameraFacing.front, |
| 44 | // ), | 71 | // ), |
| 45 | - onDetect: (barcode, args) { | 72 | + onDetect: (barcode, arguments) { |
| 46 | setState(() { | 73 | 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; | 74 | + this.arguments = arguments; |
| 75 | + this.barcode = barcode; | ||
| 56 | }); | 76 | }); |
| 57 | }, | 77 | }, |
| 58 | ), | 78 | ), |
| @@ -65,8 +85,12 @@ class _BarcodeScannerReturningImageState | @@ -65,8 +85,12 @@ class _BarcodeScannerReturningImageState | ||
| 65 | child: Row( | 85 | child: Row( |
| 66 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, | 86 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, |
| 67 | children: [ | 87 | children: [ |
| 68 | - IconButton( | ||
| 69 | - color: Colors.white, | 88 | + ColoredBox( |
| 89 | + color: arguments != null && !arguments!.hasTorch | ||
| 90 | + ? Colors.red | ||
| 91 | + : Colors.white, | ||
| 92 | + child: IconButton( | ||
| 93 | + // color: , | ||
| 70 | icon: ValueListenableBuilder( | 94 | icon: ValueListenableBuilder( |
| 71 | valueListenable: controller.torchState, | 95 | valueListenable: controller.torchState, |
| 72 | builder: (context, state, child) { | 96 | builder: (context, state, child) { |
| @@ -93,6 +117,7 @@ class _BarcodeScannerReturningImageState | @@ -93,6 +117,7 @@ class _BarcodeScannerReturningImageState | ||
| 93 | iconSize: 32.0, | 117 | iconSize: 32.0, |
| 94 | onPressed: () => controller.toggleTorch(), | 118 | onPressed: () => controller.toggleTorch(), |
| 95 | ), | 119 | ), |
| 120 | + ), | ||
| 96 | IconButton( | 121 | IconButton( |
| 97 | color: Colors.white, | 122 | color: Colors.white, |
| 98 | icon: isStarted | 123 | icon: isStarted |
| @@ -100,7 +125,9 @@ class _BarcodeScannerReturningImageState | @@ -100,7 +125,9 @@ class _BarcodeScannerReturningImageState | ||
| 100 | : const Icon(Icons.play_arrow), | 125 | : const Icon(Icons.play_arrow), |
| 101 | iconSize: 32.0, | 126 | iconSize: 32.0, |
| 102 | onPressed: () => setState(() { | 127 | onPressed: () => setState(() { |
| 103 | - isStarted ? controller.stop() : controller.start(); | 128 | + isStarted |
| 129 | + ? controller.stop() | ||
| 130 | + : controller.start(); | ||
| 104 | isStarted = !isStarted; | 131 | isStarted = !isStarted; |
| 105 | }), | 132 | }), |
| 106 | ), | 133 | ), |
| @@ -110,7 +137,8 @@ class _BarcodeScannerReturningImageState | @@ -110,7 +137,8 @@ class _BarcodeScannerReturningImageState | ||
| 110 | height: 50, | 137 | height: 50, |
| 111 | child: FittedBox( | 138 | child: FittedBox( |
| 112 | child: Text( | 139 | child: Text( |
| 113 | - barcode ?? 'Scan something!', | 140 | + barcode?.barcodes.first.rawValue ?? |
| 141 | + 'Scan something!', | ||
| 114 | overflow: TextOverflow.fade, | 142 | overflow: TextOverflow.fade, |
| 115 | style: Theme.of(context) | 143 | style: Theme.of(context) |
| 116 | .textTheme | 144 | .textTheme |
| @@ -139,21 +167,14 @@ class _BarcodeScannerReturningImageState | @@ -139,21 +167,14 @@ class _BarcodeScannerReturningImageState | ||
| 139 | iconSize: 32.0, | 167 | iconSize: 32.0, |
| 140 | onPressed: () => controller.switchCamera(), | 168 | onPressed: () => controller.switchCamera(), |
| 141 | ), | 169 | ), |
| 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 | - ), | ||
| 152 | ], | 170 | ], |
| 153 | ), | 171 | ), |
| 154 | ), | 172 | ), |
| 155 | ), | 173 | ), |
| 156 | ], | 174 | ], |
| 175 | + ), | ||
| 176 | + ), | ||
| 177 | + ], | ||
| 157 | ); | 178 | ); |
| 158 | }, | 179 | }, |
| 159 | ), | 180 | ), |
| @@ -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 | ); |
| 1 | library mobile_scanner; | 1 | library mobile_scanner; |
| 2 | 2 | ||
| 3 | +export 'src/barcode.dart'; | ||
| 4 | +export 'src/barcode_capture.dart'; | ||
| 3 | export 'src/enums/camera_facing.dart'; | 5 | export 'src/enums/camera_facing.dart'; |
| 4 | export 'src/enums/detection_speed.dart'; | 6 | export 'src/enums/detection_speed.dart'; |
| 5 | export 'src/enums/mobile_scanner_state.dart'; | 7 | export 'src/enums/mobile_scanner_state.dart'; |
| @@ -8,4 +10,3 @@ export 'src/enums/torch_state.dart'; | @@ -8,4 +10,3 @@ export 'src/enums/torch_state.dart'; | ||
| 8 | export 'src/mobile_scanner.dart'; | 10 | export 'src/mobile_scanner.dart'; |
| 9 | export 'src/mobile_scanner_arguments.dart'; | 11 | export 'src/mobile_scanner_arguments.dart'; |
| 10 | export 'src/mobile_scanner_controller.dart'; | 12 | export 'src/mobile_scanner_controller.dart'; |
| 11 | -export 'src/objects/barcode.dart'; |
| 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
| 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'; | 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'; | ||
| 4 | 6 | ||
| 5 | /// A widget showing a live camera preview. | 7 | /// A widget showing a live camera preview. |
| 6 | class MobileScanner extends StatefulWidget { | 8 | class MobileScanner extends StatefulWidget { |
| @@ -13,28 +15,23 @@ class MobileScanner extends StatefulWidget { | @@ -13,28 +15,23 @@ class MobileScanner extends StatefulWidget { | ||
| 13 | /// Function that gets called when a Barcode is detected. | 15 | /// Function that gets called when a Barcode is detected. |
| 14 | /// | 16 | /// |
| 15 | /// [barcode] The barcode object with all information about the scanned code. | 17 | /// [barcode] The barcode object with all information about the scanned code. |
| 16 | - /// [args] Information about the state of the MobileScanner widget | ||
| 17 | - final Function(Barcode barcode, MobileScannerArguments? args) onDetect; | ||
| 18 | - | ||
| 19 | - /// TODO: Function that gets called when the Widget is initialized. Can be usefull | ||
| 20 | - /// to check wether the device has a torch(flash) or not. | ||
| 21 | - /// | ||
| 22 | - /// [args] Information about the state of the MobileScanner widget | ||
| 23 | - // final Function(MobileScannerArguments args)? onInitialize; | 18 | + /// [startArguments] Information about the state of the MobileScanner widget |
| 19 | + final Function(BarcodeCapture capture, MobileScannerArguments? arguments) | ||
| 20 | + onDetect; | ||
| 24 | 21 | ||
| 25 | /// Handles how the widget should fit the screen. | 22 | /// Handles how the widget should fit the screen. |
| 26 | final BoxFit fit; | 23 | final BoxFit fit; |
| 27 | 24 | ||
| 28 | - /// Set to false if you don't want duplicate scans. | ||
| 29 | - final bool allowDuplicates; | 25 | + /// Whether to automatically resume the camera when the application is resumed |
| 26 | + final bool autoResume; | ||
| 30 | 27 | ||
| 31 | /// 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. |
| 32 | const MobileScanner({ | 29 | const MobileScanner({ |
| 33 | super.key, | 30 | super.key, |
| 34 | required this.onDetect, | 31 | required this.onDetect, |
| 35 | this.controller, | 32 | this.controller, |
| 33 | + this.autoResume = true, | ||
| 36 | this.fit = BoxFit.cover, | 34 | this.fit = BoxFit.cover, |
| 37 | - this.allowDuplicates = false, | ||
| 38 | this.onPermissionSet, | 35 | this.onPermissionSet, |
| 39 | }); | 36 | }); |
| 40 | 37 | ||
| @@ -55,40 +52,37 @@ class _MobileScannerState extends State<MobileScanner> | @@ -55,40 +52,37 @@ class _MobileScannerState extends State<MobileScanner> | ||
| 55 | if (!controller.isStarting) controller.start(); | 52 | if (!controller.isStarting) controller.start(); |
| 56 | } | 53 | } |
| 57 | 54 | ||
| 55 | + AppLifecycleState? _lastState; | ||
| 56 | + | ||
| 58 | @override | 57 | @override |
| 59 | void didChangeAppLifecycleState(AppLifecycleState state) { | 58 | void didChangeAppLifecycleState(AppLifecycleState state) { |
| 60 | switch (state) { | 59 | switch (state) { |
| 61 | case AppLifecycleState.resumed: | 60 | case AppLifecycleState.resumed: |
| 62 | - if (!controller.isStarting && controller.autoResume) controller.start(); | 61 | + if (!controller.isStarting && |
| 62 | + widget.autoResume && | ||
| 63 | + _lastState != AppLifecycleState.inactive) controller.start(); | ||
| 63 | break; | 64 | break; |
| 64 | - case AppLifecycleState.inactive: | ||
| 65 | case AppLifecycleState.paused: | 65 | case AppLifecycleState.paused: |
| 66 | case AppLifecycleState.detached: | 66 | case AppLifecycleState.detached: |
| 67 | controller.stop(); | 67 | controller.stop(); |
| 68 | break; | 68 | break; |
| 69 | + default: | ||
| 70 | + break; | ||
| 69 | } | 71 | } |
| 72 | + _lastState = state; | ||
| 70 | } | 73 | } |
| 71 | 74 | ||
| 72 | - Uint8List? lastScanned; | ||
| 73 | - | ||
| 74 | @override | 75 | @override |
| 75 | Widget build(BuildContext context) { | 76 | Widget build(BuildContext context) { |
| 76 | return ValueListenableBuilder( | 77 | return ValueListenableBuilder( |
| 77 | - valueListenable: controller.args, | 78 | + valueListenable: controller.startArguments, |
| 78 | builder: (context, value, child) { | 79 | builder: (context, value, child) { |
| 79 | value = value as MobileScannerArguments?; | 80 | value = value as MobileScannerArguments?; |
| 80 | if (value == null) { | 81 | if (value == null) { |
| 81 | return const ColoredBox(color: Colors.black); | 82 | return const ColoredBox(color: Colors.black); |
| 82 | } else { | 83 | } else { |
| 83 | controller.barcodes.listen((barcode) { | 84 | controller.barcodes.listen((barcode) { |
| 84 | - if (!widget.allowDuplicates) { | ||
| 85 | - if (lastScanned != barcode.rawBytes) { | ||
| 86 | - lastScanned = barcode.rawBytes; | ||
| 87 | widget.onDetect(barcode, value! as MobileScannerArguments); | 85 | widget.onDetect(barcode, value! as MobileScannerArguments); |
| 88 | - } | ||
| 89 | - } else { | ||
| 90 | - widget.onDetect(barcode, value! as MobileScannerArguments); | ||
| 91 | - } | ||
| 92 | }); | 86 | }); |
| 93 | return ClipRect( | 87 | return ClipRect( |
| 94 | child: SizedBox( | 88 | child: SizedBox( |
| @@ -5,170 +5,155 @@ import 'package:flutter/cupertino.dart'; | @@ -5,170 +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 | ||
| 11 | +/// The [MobileScannerController] holds all the logic of this plugin, | ||
| 12 | +/// where as the [MobileScanner] class is the frontend of this plugin. | ||
| 10 | class MobileScannerController { | 13 | class MobileScannerController { |
| 11 | - MethodChannel methodChannel = | ||
| 12 | - const MethodChannel('dev.steenbakker.mobile_scanner/scanner/method'); | ||
| 13 | - EventChannel eventChannel = | ||
| 14 | - const EventChannel('dev.steenbakker.mobile_scanner/scanner/event'); | 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 | + } | ||
| 15 | 33 | ||
| 16 | //Must be static to keep the same value on new instances | 34 | //Must be static to keep the same value on new instances |
| 17 | - static int? _controllerHashcode; | ||
| 18 | - StreamSubscription? events; | 35 | + static int? controllerHashcode; |
| 19 | 36 | ||
| 20 | - Function(bool permissionGranted)? onPermissionSet; | ||
| 21 | - final ValueNotifier<MobileScannerArguments?> args = ValueNotifier(null); | ||
| 22 | - final ValueNotifier<TorchState> torchState = ValueNotifier(TorchState.off); | ||
| 23 | - late final ValueNotifier<CameraFacing> cameraFacingState; | ||
| 24 | - final Ratio? ratio; | ||
| 25 | - final bool? torchEnabled; | ||
| 26 | - // Whether to return the image buffer with the Barcode event | 37 | + /// Select which camera should be used. |
| 38 | + /// | ||
| 39 | + /// Default: CameraFacing.back | ||
| 40 | + final CameraFacing facing; | ||
| 41 | + | ||
| 42 | + // /// Analyze the image in 4:3 or 16:9 | ||
| 43 | + // /// | ||
| 44 | + // /// Only on Android | ||
| 45 | + // final Ratio? ratio; | ||
| 46 | + | ||
| 47 | + /// Enable or disable the torch (Flash) on start | ||
| 48 | + /// | ||
| 49 | + /// Default: disabled | ||
| 50 | + final bool torchEnabled; | ||
| 51 | + | ||
| 52 | + /// Set to true if you want to return the image buffer with the Barcode event | ||
| 53 | + /// | ||
| 54 | + /// Only supported on iOS and Android | ||
| 27 | final bool returnImage; | 55 | final bool returnImage; |
| 28 | 56 | ||
| 29 | - /// If provided, the scanner will only detect those specific formats. | 57 | + /// If provided, the scanner will only detect those specific formats |
| 30 | final List<BarcodeFormat>? formats; | 58 | final List<BarcodeFormat>? formats; |
| 31 | 59 | ||
| 32 | - CameraFacing facing; | ||
| 33 | - bool hasTorch = false; | ||
| 34 | - late StreamController<Barcode> barcodesController; | 60 | + /// Sets the speed of detections. |
| 61 | + /// | ||
| 62 | + /// WARNING: DetectionSpeed.unrestricted can cause memory issues on some devices | ||
| 63 | + final DetectionSpeed detectionSpeed; | ||
| 35 | 64 | ||
| 36 | - /// Whether to automatically resume the camera when the application is resumed | ||
| 37 | - bool autoResume; | 65 | + /// Sets the barcode stream |
| 66 | + final StreamController<BarcodeCapture> _barcodesController = | ||
| 67 | + StreamController.broadcast(); | ||
| 68 | + Stream<BarcodeCapture> get barcodes => _barcodesController.stream; | ||
| 38 | 69 | ||
| 39 | - Stream<Barcode> get barcodes => barcodesController.stream; | 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'); | ||
| 40 | 74 | ||
| 41 | - MobileScannerController({ | ||
| 42 | - this.facing = CameraFacing.back, | ||
| 43 | - this.ratio, | ||
| 44 | - this.torchEnabled, | ||
| 45 | - this.formats, | ||
| 46 | - this.onPermissionSet, | ||
| 47 | - this.autoResume = true, | ||
| 48 | - this.returnImage = false, | ||
| 49 | - }) { | ||
| 50 | - // In case a new instance is created before calling dispose() | ||
| 51 | - if (_controllerHashcode != null) { | ||
| 52 | - stop(); | ||
| 53 | - } | ||
| 54 | - _controllerHashcode = hashCode; | 75 | + Function(bool permissionGranted)? onPermissionSet; |
| 55 | 76 | ||
| 56 | - cameraFacingState = ValueNotifier(facing); | 77 | + /// Listen to events from the platform specific code |
| 78 | + late StreamSubscription events; | ||
| 57 | 79 | ||
| 58 | - // Sets analyze mode and barcode stream | ||
| 59 | - barcodesController = StreamController.broadcast( | ||
| 60 | - // onListen: () => setAnalyzeMode(AnalyzeMode.barcode.index), | ||
| 61 | - // onCancel: () => setAnalyzeMode(AnalyzeMode.none.index), | ||
| 62 | - ); | 80 | + /// A notifier that provides several arguments about the MobileScanner |
| 81 | + final ValueNotifier<MobileScannerArguments?> startArguments = | ||
| 82 | + ValueNotifier(null); | ||
| 63 | 83 | ||
| 64 | - // Listen to events from the platform specific code | ||
| 65 | - events = eventChannel | ||
| 66 | - .receiveBroadcastStream() | ||
| 67 | - .listen((data) => handleEvent(data as Map)); | ||
| 68 | - } | ||
| 69 | - | ||
| 70 | - void handleEvent(Map event) { | ||
| 71 | - final name = event['name']; | ||
| 72 | - final data = event['data']; | ||
| 73 | - final binaryData = event['binaryData']; | ||
| 74 | - switch (name) { | ||
| 75 | - case 'torchState': | ||
| 76 | - final state = TorchState.values[data as int? ?? 0]; | ||
| 77 | - torchState.value = state; | ||
| 78 | - break; | ||
| 79 | - case 'barcode': | ||
| 80 | - final image = returnImage ? event['image'] as Uint8List : null; | ||
| 81 | - final barcode = Barcode.fromNative(data as Map? ?? {}, image); | ||
| 82 | - barcodesController.add(barcode); | ||
| 83 | - break; | ||
| 84 | - case 'barcodeMac': | ||
| 85 | - barcodesController.add( | ||
| 86 | - Barcode( | ||
| 87 | - rawValue: (data as Map)['payload'] as String?, | ||
| 88 | - ), | ||
| 89 | - ); | ||
| 90 | - break; | ||
| 91 | - case 'barcodeWeb': | ||
| 92 | - final bytes = (binaryData as List).cast<int>(); | ||
| 93 | - barcodesController.add( | ||
| 94 | - Barcode( | ||
| 95 | - rawValue: data as String?, | ||
| 96 | - rawBytes: Uint8List.fromList(bytes), | ||
| 97 | - ), | ||
| 98 | - ); | ||
| 99 | - break; | ||
| 100 | - default: | ||
| 101 | - throw UnimplementedError(); | ||
| 102 | - } | ||
| 103 | - } | 84 | + /// A notifier that provides the state of the Torch (Flash) |
| 85 | + final ValueNotifier<TorchState> torchState = ValueNotifier(TorchState.off); | ||
| 104 | 86 | ||
| 105 | - // TODO: Add more analyzers like text analyzer | ||
| 106 | - // void setAnalyzeMode(int mode) { | ||
| 107 | - // if (hashCode != _controllerHashcode) { | ||
| 108 | - // return; | ||
| 109 | - // } | ||
| 110 | - // methodChannel.invokeMethod('analyze', mode); | ||
| 111 | - // } | 87 | + /// A notifier that provides the state of which camera is being used |
| 88 | + late final ValueNotifier<CameraFacing> cameraFacingState = | ||
| 89 | + ValueNotifier(facing); | ||
| 112 | 90 | ||
| 113 | - // List<BarcodeFormats>? formats = _defaultBarcodeFormats, | ||
| 114 | bool isStarting = false; | 91 | bool isStarting = false; |
| 92 | + bool? _hasTorch; | ||
| 93 | + | ||
| 94 | + /// Set the starting arguments for the camera | ||
| 95 | + Map<String, dynamic> _argumentsToMap({CameraFacing? cameraFacingOverride}) { | ||
| 96 | + final Map<String, dynamic> arguments = {}; | ||
| 97 | + | ||
| 98 | + cameraFacingState.value = cameraFacingOverride ?? facing; | ||
| 99 | + arguments['facing'] = cameraFacingState.value.index; | ||
| 100 | + | ||
| 101 | + // if (ratio != null) arguments['ratio'] = ratio; | ||
| 102 | + arguments['torch'] = torchEnabled; | ||
| 103 | + arguments['speed'] = detectionSpeed.index; | ||
| 104 | + | ||
| 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 | + } | ||
| 111 | + } | ||
| 112 | + arguments['returnImage'] = true; | ||
| 113 | + return arguments; | ||
| 114 | + } | ||
| 115 | 115 | ||
| 116 | /// Start barcode scanning. This will first check if the required permissions | 116 | /// Start barcode scanning. This will first check if the required permissions |
| 117 | /// are set. | 117 | /// are set. |
| 118 | - Future<void> start() async { | ||
| 119 | - ensure('startAsync'); | 118 | + Future<MobileScannerArguments?> start({ |
| 119 | + CameraFacing? cameraFacingOverride, | ||
| 120 | + }) async { | ||
| 121 | + debugPrint('Hashcode controller: $hashCode'); | ||
| 120 | if (isStarting) { | 122 | if (isStarting) { |
| 121 | - throw Exception('mobile_scanner: Called start() while already starting.'); | 123 | + debugPrint("Called start() while starting."); |
| 122 | } | 124 | } |
| 123 | isStarting = true; | 125 | isStarting = true; |
| 124 | - // setAnalyzeMode(AnalyzeMode.barcode.index); | ||
| 125 | 126 | ||
| 126 | // Check authorization status | 127 | // Check authorization status |
| 127 | if (!kIsWeb) { | 128 | if (!kIsWeb) { |
| 128 | - MobileScannerState state = MobileScannerState | ||
| 129 | - .values[await methodChannel.invokeMethod('state') as int? ?? 0]; | 129 | + final MobileScannerState state = MobileScannerState |
| 130 | + .values[await _methodChannel.invokeMethod('state') as int? ?? 0]; | ||
| 130 | switch (state) { | 131 | switch (state) { |
| 131 | case MobileScannerState.undetermined: | 132 | case MobileScannerState.undetermined: |
| 132 | final bool result = | 133 | final bool result = |
| 133 | - await methodChannel.invokeMethod('request') as bool? ?? false; | ||
| 134 | - state = result | ||
| 135 | - ? MobileScannerState.authorized | ||
| 136 | - : MobileScannerState.denied; | 134 | + await _methodChannel.invokeMethod('request') as bool? ?? false; |
| 135 | + if (!result) { | ||
| 136 | + isStarting = false; | ||
| 137 | onPermissionSet?.call(result); | 137 | onPermissionSet?.call(result); |
| 138 | + throw MobileScannerException('User declined camera permission.'); | ||
| 139 | + } | ||
| 138 | break; | 140 | break; |
| 139 | case MobileScannerState.denied: | 141 | case MobileScannerState.denied: |
| 140 | isStarting = false; | 142 | isStarting = false; |
| 141 | onPermissionSet?.call(false); | 143 | onPermissionSet?.call(false); |
| 142 | - throw PlatformException(code: 'NO ACCESS'); | 144 | + throw MobileScannerException('User declined camera permission.'); |
| 143 | case MobileScannerState.authorized: | 145 | case MobileScannerState.authorized: |
| 144 | onPermissionSet?.call(true); | 146 | onPermissionSet?.call(true); |
| 145 | break; | 147 | break; |
| 146 | } | 148 | } |
| 147 | } | 149 | } |
| 148 | 150 | ||
| 149 | - cameraFacingState.value = facing; | ||
| 150 | - | ||
| 151 | - // Set the starting arguments for the camera | ||
| 152 | - final Map arguments = {}; | ||
| 153 | - arguments['facing'] = facing.index; | ||
| 154 | - if (ratio != null) arguments['ratio'] = ratio; | ||
| 155 | - if (torchEnabled != null) arguments['torch'] = torchEnabled; | ||
| 156 | - | ||
| 157 | - if (formats != null) { | ||
| 158 | - if (Platform.isAndroid) { | ||
| 159 | - arguments['formats'] = formats!.map((e) => e.index).toList(); | ||
| 160 | - } else if (Platform.isIOS || Platform.isMacOS) { | ||
| 161 | - arguments['formats'] = formats!.map((e) => e.rawValue).toList(); | ||
| 162 | - } | ||
| 163 | - } | ||
| 164 | - arguments['returnImage'] = returnImage; | ||
| 165 | - | ||
| 166 | // Start the camera with arguments | 151 | // Start the camera with arguments |
| 167 | Map<String, dynamic>? startResult = {}; | 152 | Map<String, dynamic>? startResult = {}; |
| 168 | try { | 153 | try { |
| 169 | - startResult = await methodChannel.invokeMapMethod<String, dynamic>( | 154 | + startResult = await _methodChannel.invokeMapMethod<String, dynamic>( |
| 170 | 'start', | 155 | 'start', |
| 171 | - arguments, | 156 | + _argumentsToMap(cameraFacingOverride: cameraFacingOverride), |
| 172 | ); | 157 | ); |
| 173 | } on PlatformException catch (error) { | 158 | } on PlatformException catch (error) { |
| 174 | debugPrint('${error.code}: ${error.message}'); | 159 | debugPrint('${error.code}: ${error.message}'); |
| @@ -176,85 +161,78 @@ class MobileScannerController { | @@ -176,85 +161,78 @@ class MobileScannerController { | ||
| 176 | if (error.code == "MobileScannerWeb") { | 161 | if (error.code == "MobileScannerWeb") { |
| 177 | onPermissionSet?.call(false); | 162 | onPermissionSet?.call(false); |
| 178 | } | 163 | } |
| 179 | - // setAnalyzeMode(AnalyzeMode.none.index); | ||
| 180 | - return; | 164 | + return null; |
| 181 | } | 165 | } |
| 182 | 166 | ||
| 183 | if (startResult == null) { | 167 | if (startResult == null) { |
| 184 | isStarting = false; | 168 | isStarting = false; |
| 185 | - throw PlatformException(code: 'INITIALIZATION ERROR'); | 169 | + throw MobileScannerException( |
| 170 | + 'Failed to start mobileScanner, no response from platform side', | ||
| 171 | + ); | ||
| 186 | } | 172 | } |
| 187 | 173 | ||
| 188 | - hasTorch = startResult['torchable'] as bool? ?? false; | 174 | + _hasTorch = startResult['torchable'] as bool? ?? false; |
| 175 | + if (_hasTorch! && torchEnabled) { | ||
| 176 | + torchState.value = TorchState.on; | ||
| 177 | + } | ||
| 189 | 178 | ||
| 190 | if (kIsWeb) { | 179 | if (kIsWeb) { |
| 191 | onPermissionSet?.call( | 180 | onPermissionSet?.call( |
| 192 | true, | 181 | true, |
| 193 | ); // 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 |
| 194 | 183 | ||
| 195 | - args.value = MobileScannerArguments( | 184 | + startArguments.value = MobileScannerArguments( |
| 196 | webId: startResult['ViewID'] as String?, | 185 | webId: startResult['ViewID'] as String?, |
| 197 | size: Size( | 186 | size: Size( |
| 198 | startResult['videoWidth'] as double? ?? 0, | 187 | startResult['videoWidth'] as double? ?? 0, |
| 199 | startResult['videoHeight'] as double? ?? 0, | 188 | startResult['videoHeight'] as double? ?? 0, |
| 200 | ), | 189 | ), |
| 201 | - hasTorch: hasTorch, | 190 | + hasTorch: _hasTorch!, |
| 202 | ); | 191 | ); |
| 203 | } else { | 192 | } else { |
| 204 | - args.value = MobileScannerArguments( | 193 | + startArguments.value = MobileScannerArguments( |
| 205 | textureId: startResult['textureId'] as int?, | 194 | textureId: startResult['textureId'] as int?, |
| 206 | size: toSize(startResult['size'] as Map? ?? {}), | 195 | size: toSize(startResult['size'] as Map? ?? {}), |
| 207 | - hasTorch: hasTorch, | 196 | + hasTorch: _hasTorch!, |
| 208 | ); | 197 | ); |
| 209 | } | 198 | } |
| 210 | - | ||
| 211 | isStarting = false; | 199 | isStarting = false; |
| 200 | + return startArguments.value!; | ||
| 212 | } | 201 | } |
| 213 | 202 | ||
| 203 | + /// Stops the camera, but does not dispose this controller. | ||
| 214 | Future<void> stop() async { | 204 | Future<void> stop() async { |
| 215 | - try { | ||
| 216 | - await methodChannel.invokeMethod('stop'); | ||
| 217 | - } on PlatformException catch (error) { | ||
| 218 | - debugPrint('${error.code}: ${error.message}'); | ||
| 219 | - } | 205 | + await _methodChannel.invokeMethod('stop'); |
| 220 | } | 206 | } |
| 221 | 207 | ||
| 222 | /// Switches the torch on or off. | 208 | /// Switches the torch on or off. |
| 223 | /// | 209 | /// |
| 224 | /// Only works if torch is available. | 210 | /// Only works if torch is available. |
| 225 | Future<void> toggleTorch() async { | 211 | Future<void> toggleTorch() async { |
| 226 | - ensure('toggleTorch'); | ||
| 227 | - if (!hasTorch) { | ||
| 228 | - debugPrint('Device has no torch/flash.'); | ||
| 229 | - 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'); | ||
| 230 | } | 218 | } |
| 231 | 219 | ||
| 232 | - final TorchState state = | 220 | + torchState.value = |
| 233 | torchState.value == TorchState.off ? TorchState.on : TorchState.off; | 221 | torchState.value == TorchState.off ? TorchState.on : TorchState.off; |
| 234 | 222 | ||
| 235 | - try { | ||
| 236 | - await methodChannel.invokeMethod('torch', state.index); | ||
| 237 | - } on PlatformException catch (error) { | ||
| 238 | - debugPrint('${error.code}: ${error.message}'); | ||
| 239 | - } | 223 | + await _methodChannel.invokeMethod('torch', torchState.value.index); |
| 240 | } | 224 | } |
| 241 | 225 | ||
| 242 | /// Switches the torch on or off. | 226 | /// Switches the torch on or off. |
| 243 | /// | 227 | /// |
| 244 | /// Only works if torch is available. | 228 | /// Only works if torch is available. |
| 245 | Future<void> switchCamera() async { | 229 | Future<void> switchCamera() async { |
| 246 | - ensure('switchCamera'); | ||
| 247 | - try { | ||
| 248 | - await methodChannel.invokeMethod('stop'); | ||
| 249 | - } on PlatformException catch (error) { | ||
| 250 | - debugPrint( | ||
| 251 | - '${error.code}: camera is stopped! Please start before switching camera.', | ||
| 252 | - ); | ||
| 253 | - return; | ||
| 254 | - } | ||
| 255 | - facing = | ||
| 256 | - facing == CameraFacing.back ? CameraFacing.front : CameraFacing.back; | ||
| 257 | - 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); | ||
| 258 | } | 236 | } |
| 259 | 237 | ||
| 260 | /// Handles a local image file. | 238 | /// Handles a local image file. |
| @@ -263,28 +241,72 @@ class MobileScannerController { | @@ -263,28 +241,72 @@ class MobileScannerController { | ||
| 263 | /// | 241 | /// |
| 264 | /// [path] The path of the image on the devices | 242 | /// [path] The path of the image on the devices |
| 265 | Future<bool> analyzeImage(String path) async { | 243 | Future<bool> analyzeImage(String path) async { |
| 266 | - return methodChannel | 244 | + return _methodChannel |
| 267 | .invokeMethod<bool>('analyzeImage', path) | 245 | .invokeMethod<bool>('analyzeImage', path) |
| 268 | .then<bool>((bool? value) => value ?? false); | 246 | .then<bool>((bool? value) => value ?? false); |
| 269 | } | 247 | } |
| 270 | 248 | ||
| 271 | /// 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. | ||
| 272 | void dispose() { | 252 | void dispose() { |
| 273 | - if (hashCode == _controllerHashcode) { | ||
| 274 | stop(); | 253 | stop(); |
| 275 | - events?.cancel(); | ||
| 276 | - events = null; | ||
| 277 | - _controllerHashcode = null; | 254 | + events.cancel(); |
| 255 | + _barcodesController.close(); | ||
| 256 | + if (hashCode == controllerHashcode) { | ||
| 257 | + controllerHashcode = null; | ||
| 278 | onPermissionSet = null; | 258 | onPermissionSet = null; |
| 279 | } | 259 | } |
| 280 | - barcodesController.close(); | ||
| 281 | } | 260 | } |
| 282 | 261 | ||
| 283 | - /// Checks if the MobileScannerController is bound to the correct MobileScanner object. | ||
| 284 | - void ensure(String name) { | ||
| 285 | - final message = | ||
| 286 | - 'MobileScannerController.$name called after MobileScannerController.dispose\n' | ||
| 287 | - 'MobileScannerController methods should not be used after calling dispose.'; | ||
| 288 | - 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 | + } | ||
| 289 | } | 311 | } |
| 290 | } | 312 | } |
lib/src/mobile_scanner_exception.dart
0 → 100644
-
Please register or login to post a comment