Julian Steenbakker

feat: add return image and refactor existing functions

@@ -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 +}
@@ -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 +
  17 + BarcodeCapture? barcodeCapture;
  18 +
  19 + MobileScannerController controller = MobileScannerController(
  20 + torchEnabled: true,
  21 + // formats: [BarcodeFormat.qrCode]
  22 + // facing: CameraFacing.front,
  23 + );
  24 +
  25 + bool isStarted = true;
  26 +
  27 + @override
  28 + Widget build(BuildContext context) {
  29 + return Scaffold(
  30 + backgroundColor: Colors.black,
  31 + body: Builder(
  32 + builder: (context) {
  33 + return Stack(
  34 + children: [
  35 + MobileScanner(
  36 + controller: controller,
  37 + fit: BoxFit.contain,
  38 + // allowDuplicates: true,
  39 + // controller: MobileScannerController(
  40 + // torchEnabled: true,
  41 + // facing: CameraFacing.front,
  42 + // ),
  43 + onDetect: (barcodeCapture, arguments) {
  44 + setState(() {
  45 + this.barcodeCapture = barcodeCapture;
  46 + });
  47 + },
  48 + ),
  49 + Align(
  50 + alignment: Alignment.bottomCenter,
  51 + child: Container(
  52 + alignment: Alignment.bottomCenter,
  53 + height: 100,
  54 + color: Colors.black.withOpacity(0.4),
  55 + child: Row(
  56 + mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  57 + children: [
  58 + IconButton(
  59 + color: Colors.white,
  60 + icon: ValueListenableBuilder(
  61 + valueListenable: controller.torchState,
  62 + builder: (context, state, child) {
  63 + if (state == null) {
  64 + return const Icon(
  65 + Icons.flash_off,
  66 + color: Colors.grey,
  67 + );
  68 + }
  69 + switch (state as TorchState) {
  70 + case TorchState.off:
  71 + return const Icon(
  72 + Icons.flash_off,
  73 + color: Colors.grey,
  74 + );
  75 + case TorchState.on:
  76 + return const Icon(
  77 + Icons.flash_on,
  78 + color: Colors.yellow,
  79 + );
  80 + }
  81 + },
  82 + ),
  83 + iconSize: 32.0,
  84 + onPressed: () => controller.toggleTorch(),
  85 + ),
  86 + IconButton(
  87 + color: Colors.white,
  88 + icon: isStarted
  89 + ? const Icon(Icons.stop)
  90 + : const Icon(Icons.play_arrow),
  91 + iconSize: 32.0,
  92 + onPressed: () => setState(() {
  93 + isStarted ? controller.stop() : controller.start();
  94 + isStarted = !isStarted;
  95 + }),
  96 + ),
  97 + Center(
  98 + child: SizedBox(
  99 + width: MediaQuery.of(context).size.width - 200,
  100 + height: 50,
  101 + child: FittedBox(
  102 + child: Text(
  103 + '${barcodeCapture?.barcodes.map((e) => e.rawValue)}',
  104 + overflow: TextOverflow.fade,
  105 + style: Theme.of(context)
  106 + .textTheme
  107 + .headline4!
  108 + .copyWith(color: Colors.white),
  109 + ),
  110 + ),
  111 + ),
  112 + ),
  113 + IconButton(
  114 + color: Colors.white,
  115 + icon: ValueListenableBuilder(
  116 + valueListenable: controller.cameraFacingState,
  117 + builder: (context, state, child) {
  118 + if (state == null) {
  119 + return const Icon(Icons.camera_front);
  120 + }
  121 + switch (state as CameraFacing) {
  122 + case CameraFacing.front:
  123 + return const Icon(Icons.camera_front);
  124 + case CameraFacing.back:
  125 + return const Icon(Icons.camera_rear);
  126 + }
  127 + },
  128 + ),
  129 + iconSize: 32.0,
  130 + onPressed: () => controller.switchCamera(),
  131 + ),
  132 + IconButton(
  133 + color: Colors.white,
  134 + icon: const Icon(Icons.image),
  135 + iconSize: 32.0,
  136 + onPressed: () async {
  137 + final ImagePicker picker = ImagePicker();
  138 + // Pick an image
  139 + final XFile? image = await picker.pickImage(
  140 + source: ImageSource.gallery,
  141 + );
  142 + if (image != null) {
  143 + if (await controller.analyzeImage(image.path)) {
  144 + if (!mounted) return;
  145 + ScaffoldMessenger.of(context).showSnackBar(
  146 + const SnackBar(
  147 + content: Text('Barcode found!'),
  148 + backgroundColor: Colors.green,
  149 + ),
  150 + );
  151 + } else {
  152 + if (!mounted) return;
  153 + ScaffoldMessenger.of(context).showSnackBar(
  154 + const SnackBar(
  155 + content: Text('No barcode found!'),
  156 + backgroundColor: Colors.red,
  157 + ),
  158 + );
  159 + }
  160 + }
  161 + },
  162 + ),
  163 + ],
  164 + ),
  165 + ),
  166 + ),
  167 + ],
  168 + );
  169 + },
  170 + ),
  171 + );
  172 + }
  173 +}
@@ -13,10 +13,12 @@ class BarcodeScannerWithController extends StatefulWidget { @@ -13,10 +13,12 @@ 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 +
  17 + BarcodeCapture? barcode;
17 18
18 MobileScannerController controller = MobileScannerController( 19 MobileScannerController controller = MobileScannerController(
19 - torchEnabled: true, 20 + torchEnabled: true,
  21 + detectionSpeed: DetectionSpeed.unrestricted
20 // formats: [BarcodeFormat.qrCode] 22 // formats: [BarcodeFormat.qrCode]
21 // facing: CameraFacing.front, 23 // facing: CameraFacing.front,
22 ); 24 );
@@ -41,7 +43,7 @@ class _BarcodeScannerWithControllerState @@ -41,7 +43,7 @@ class _BarcodeScannerWithControllerState
41 // ), 43 // ),
42 onDetect: (barcode, args) { 44 onDetect: (barcode, args) {
43 setState(() { 45 setState(() {
44 - this.barcode = barcode.rawValue; 46 + this.barcode = barcode;
45 }); 47 });
46 }, 48 },
47 ), 49 ),
@@ -99,7 +101,7 @@ class _BarcodeScannerWithControllerState @@ -99,7 +101,7 @@ class _BarcodeScannerWithControllerState
99 height: 50, 101 height: 50,
100 child: FittedBox( 102 child: FittedBox(
101 child: Text( 103 child: Text(
102 - barcode ?? 'Scan something!', 104 + barcode?.barcodes.first.rawValue ?? 'Scan something!',
103 overflow: TextOverflow.fade, 105 overflow: TextOverflow.fade,
104 style: Theme.of(context) 106 style: Theme.of(context)
105 .textTheme 107 .textTheme
@@ -169,4 +171,4 @@ class _BarcodeScannerWithControllerState @@ -169,4 +171,4 @@ class _BarcodeScannerWithControllerState
169 ), 171 ),
170 ); 172 );
171 } 173 }
172 -} 174 +}
  1 +import 'dart:math';
1 import 'dart:typed_data'; 2 import 'dart:typed_data';
2 3
3 import 'package:flutter/material.dart'; 4 import 'package:flutter/material.dart';
@@ -14,11 +15,11 @@ class BarcodeScannerReturningImage extends StatefulWidget { @@ -14,11 +15,11 @@ class BarcodeScannerReturningImage extends StatefulWidget {
14 class _BarcodeScannerReturningImageState 15 class _BarcodeScannerReturningImageState
15 extends State<BarcodeScannerReturningImage> 16 extends State<BarcodeScannerReturningImage>
16 with SingleTickerProviderStateMixin { 17 with SingleTickerProviderStateMixin {
17 - String? barcode;  
18 - Uint8List? image; 18 + BarcodeCapture? barcode;
  19 + MobileScannerArguments? arguments;
19 20
20 MobileScannerController controller = MobileScannerController( 21 MobileScannerController controller = MobileScannerController(
21 - torchEnabled: true, 22 + // torchEnabled: true,
22 returnImage: true, 23 returnImage: true,
23 // formats: [BarcodeFormat.qrCode] 24 // formats: [BarcodeFormat.qrCode]
24 // facing: CameraFacing.front, 25 // facing: CameraFacing.front,
@@ -26,131 +27,141 @@ class _BarcodeScannerReturningImageState @@ -26,131 +27,141 @@ class _BarcodeScannerReturningImageState
26 27
27 bool isStarted = true; 28 bool isStarted = true;
28 29
  30 +
29 @override 31 @override
30 Widget build(BuildContext context) { 32 Widget build(BuildContext context) {
31 return Scaffold( 33 return Scaffold(
32 backgroundColor: Colors.black, 34 backgroundColor: Colors.black,
33 body: Builder( 35 body: Builder(
34 builder: (context) { 36 builder: (context) {
35 - return Stack( 37 + return Column(
36 children: [ 38 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 - }, 39 + Container(
  40 + color: Colors.blueGrey,
  41 + width: double.infinity,
  42 + height: 0.33 * MediaQuery.of(context).size.height,
  43 + child: barcode?.image != null
  44 + ? Transform.rotate(
  45 + angle: 90 * pi/180,
  46 + child: Image(
  47 + gaplessPlayback: true,
  48 + image: MemoryImage(barcode!.image!),
  49 + fit: BoxFit.contain,
  50 + ),
  51 + )
  52 + : Container(color: Colors.white, child: const Center(child: Text('Your scanned barcode will appear here!'))),
58 ), 53 ),
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 - },  
92 - ),  
93 - iconSize: 32.0,  
94 - onPressed: () => controller.toggleTorch(),  
95 - ),  
96 - IconButton(  
97 - 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), 54 + Container(
  55 + height: 0.66 * MediaQuery.of(context).size.height,
  56 + color: Colors.grey,
  57 + child: Stack(
  58 + children: [
  59 + MobileScanner(
  60 + controller: controller,
  61 + fit: BoxFit.contain,
  62 + // allowDuplicates: true,
  63 + // controller: MobileScannerController(
  64 + // torchEnabled: true,
  65 + // facing: CameraFacing.front,
  66 + // ),
  67 + onDetect: (barcode, arguments) {
  68 + setState(() {
  69 + this.arguments = arguments;
  70 + this.barcode = barcode;
  71 + });
  72 + },
  73 + ),
  74 + Align(
  75 + alignment: Alignment.bottomCenter,
  76 + child: Container(
  77 + alignment: Alignment.bottomCenter,
  78 + height: 100,
  79 + color: Colors.black.withOpacity(0.4),
  80 + child: Row(
  81 + mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  82 + children: [
  83 + Container(
  84 + color: arguments != null && !arguments!.hasTorch ? Colors.red : Colors.white,
  85 + child: IconButton(
  86 + // color: ,
  87 + icon: ValueListenableBuilder(
  88 + valueListenable: controller.torchState,
  89 + builder: (context, state, child) {
  90 + if (state == null) {
  91 + return const Icon(
  92 + Icons.flash_off,
  93 + color: Colors.grey,
  94 + );
  95 + }
  96 + switch (state as TorchState) {
  97 + case TorchState.off:
  98 + return const Icon(
  99 + Icons.flash_off,
  100 + color: Colors.grey,
  101 + );
  102 + case TorchState.on:
  103 + return const Icon(
  104 + Icons.flash_on,
  105 + color: Colors.yellow,
  106 + );
  107 + }
  108 + },
  109 + ),
  110 + iconSize: 32.0,
  111 + onPressed: () => controller.toggleTorch(),
  112 + ),
119 ), 113 ),
120 - ),  
121 - ),  
122 - ),  
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 - }, 114 + IconButton(
  115 + color: Colors.white,
  116 + icon: isStarted
  117 + ? const Icon(Icons.stop)
  118 + : const Icon(Icons.play_arrow),
  119 + iconSize: 32.0,
  120 + onPressed: () => setState(() {
  121 + isStarted ? controller.stop() : controller.start();
  122 + isStarted = !isStarted;
  123 + }),
  124 + ),
  125 + Center(
  126 + child: SizedBox(
  127 + width: MediaQuery.of(context).size.width - 200,
  128 + height: 50,
  129 + child: FittedBox(
  130 + child: Text(
  131 + barcode?.barcodes.first.rawValue ?? 'Scan something!',
  132 + overflow: TextOverflow.fade,
  133 + style: Theme.of(context)
  134 + .textTheme
  135 + .headline4!
  136 + .copyWith(color: Colors.white),
  137 + ),
  138 + ),
  139 + ),
  140 + ),
  141 + IconButton(
  142 + color: Colors.white,
  143 + icon: ValueListenableBuilder(
  144 + valueListenable: controller.cameraFacingState,
  145 + builder: (context, state, child) {
  146 + if (state == null) {
  147 + return const Icon(Icons.camera_front);
  148 + }
  149 + switch (state as CameraFacing) {
  150 + case CameraFacing.front:
  151 + return const Icon(Icons.camera_front);
  152 + case CameraFacing.back:
  153 + return const Icon(Icons.camera_rear);
  154 + }
  155 + },
  156 + ),
  157 + iconSize: 32.0,
  158 + onPressed: () => controller.switchCamera(),
  159 + ),
  160 + ],
138 ), 161 ),
139 - iconSize: 32.0,  
140 - onPressed: () => controller.switchCamera(),  
141 ), 162 ),
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 - ],  
153 - ), 163 + ),
  164 + ],
154 ), 165 ),
155 ), 166 ),
156 ], 167 ],
@@ -159,4 +170,4 @@ class _BarcodeScannerReturningImageState @@ -159,4 +170,4 @@ class _BarcodeScannerReturningImageState
159 ), 170 ),
160 ); 171 );
161 } 172 }
162 -} 173 +}
@@ -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,7 @@ class _BarcodeScannerWithoutControllerState @@ -46,7 +46,7 @@ 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 ?? 'Scan something!',
50 overflow: TextOverflow.fade, 50 overflow: TextOverflow.fade,
51 style: Theme.of(context) 51 style: Theme.of(context)
52 .textTheme 52 .textTheme
@@ -66,4 +66,4 @@ class _BarcodeScannerWithoutControllerState @@ -66,4 +66,4 @@ class _BarcodeScannerWithoutControllerState
66 ), 66 ),
67 ); 67 );
68 } 68 }
69 -} 69 +}
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,16 @@ class MyHome extends StatelessWidget { @@ -22,6 +23,16 @@ 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) => const BarcodeListScannerWithController(),
  27 + ),
  28 + );
  29 + },
  30 + child: const Text('MobileScanner with List Controller'),
  31 + ),
  32 + ElevatedButton(
  33 + onPressed: () {
  34 + Navigator.of(context).push(
  35 + MaterialPageRoute(
25 builder: (context) => const BarcodeScannerWithController(), 36 builder: (context) => const BarcodeScannerWithController(),
26 ), 37 ),
27 ); 38 );
@@ -37,14 +48,14 @@ class MyHome extends StatelessWidget { @@ -37,14 +48,14 @@ class MyHome extends StatelessWidget {
37 ); 48 );
38 }, 49 },
39 child: 50 child:
40 - const Text('MobileScanner with Controller (returning image)'), 51 + const Text('MobileScanner with Controller (returning image)'),
41 ), 52 ),
42 ElevatedButton( 53 ElevatedButton(
43 onPressed: () { 54 onPressed: () {
44 Navigator.of(context).push( 55 Navigator.of(context).push(
45 MaterialPageRoute( 56 MaterialPageRoute(
46 builder: (context) => 57 builder: (context) =>
47 - const BarcodeScannerWithoutController(), 58 + const BarcodeScannerWithoutController(),
48 ), 59 ),
49 ); 60 );
50 }, 61 },
@@ -55,4 +66,4 @@ class MyHome extends StatelessWidget { @@ -55,4 +66,4 @@ class MyHome extends StatelessWidget {
55 ), 66 ),
56 ); 67 );
57 } 68 }
58 -} 69 +}
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?,
@@ -207,8 +207,8 @@ class ContactInfo { @@ -207,8 +207,8 @@ class ContactInfo {
207 /// Create a [ContactInfo] from native data. 207 /// Create a [ContactInfo] from native data.
208 ContactInfo.fromNative(Map data) 208 ContactInfo.fromNative(Map data)
209 : addresses = List.unmodifiable( 209 : addresses = List.unmodifiable(
210 - (data['addresses'] as List).map((e) => Address.fromNative(e as Map)),  
211 - ), 210 + (data['addresses'] as List).map((e) => Address.fromNative(e as Map)),
  211 + ),
212 emails = List.unmodifiable( 212 emails = List.unmodifiable(
213 (data['emails'] as List).map((e) => Email.fromNative(e as Map)), 213 (data['emails'] as List).map((e) => Email.fromNative(e as Map)),
214 ), 214 ),
@@ -768,4 +768,4 @@ enum EncryptionType { @@ -768,4 +768,4 @@ enum EncryptionType {
768 /// 768 ///
769 /// Constant Value: 3 769 /// Constant Value: 3
770 wep, 770 wep,
771 -} 771 +}
  1 +import 'dart:typed_data';
  2 +
  3 +import 'package:mobile_scanner/src/barcode.dart';
  4 +
  5 +class BarcodeCapture {
  6 + List<Barcode> barcodes;
  7 + Uint8List? image;
  8 +
  9 + BarcodeCapture({
  10 + required this.barcodes,
  11 + this.image,
  12 + });
  13 +
  14 +}
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/mobile_scanner_arguments.dart';
  4 +import 'package:mobile_scanner/src/mobile_scanner_controller.dart';
  5 +import 'package:mobile_scanner/src/barcode_capture.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,24 @@ class MobileScanner extends StatefulWidget { @@ -13,28 +15,24 @@ 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(
  20 + BarcodeCapture capture, MobileScannerArguments? arguments)
  21 + onDetect;
24 22
25 /// Handles how the widget should fit the screen. 23 /// Handles how the widget should fit the screen.
26 final BoxFit fit; 24 final BoxFit fit;
27 25
28 - /// Set to false if you don't want duplicate scans.  
29 - final bool allowDuplicates; 26 + /// Whether to automatically resume the camera when the application is resumed
  27 + final bool autoResume;
30 28
31 /// Create a [MobileScanner] with a [controller], the [controller] must has been initialized. 29 /// Create a [MobileScanner] with a [controller], the [controller] must has been initialized.
32 const MobileScanner({ 30 const MobileScanner({
33 super.key, 31 super.key,
34 required this.onDetect, 32 required this.onDetect,
35 this.controller, 33 this.controller,
  34 + this.autoResume = true,
36 this.fit = BoxFit.cover, 35 this.fit = BoxFit.cover,
37 - this.allowDuplicates = false,  
38 this.onPermissionSet, 36 this.onPermissionSet,
39 }); 37 });
40 38
@@ -55,40 +53,35 @@ class _MobileScannerState extends State<MobileScanner> @@ -55,40 +53,35 @@ class _MobileScannerState extends State<MobileScanner>
55 if (!controller.isStarting) controller.start(); 53 if (!controller.isStarting) controller.start();
56 } 54 }
57 55
  56 + AppLifecycleState? _lastState;
  57 +
58 @override 58 @override
59 void didChangeAppLifecycleState(AppLifecycleState state) { 59 void didChangeAppLifecycleState(AppLifecycleState state) {
60 switch (state) { 60 switch (state) {
61 case AppLifecycleState.resumed: 61 case AppLifecycleState.resumed:
62 - if (!controller.isStarting && controller.autoResume) controller.start(); 62 + if (!controller.isStarting && widget.autoResume && _lastState != AppLifecycleState.inactive) controller.start();
63 break; 63 break;
64 - case AppLifecycleState.inactive:  
65 case AppLifecycleState.paused: 64 case AppLifecycleState.paused:
66 case AppLifecycleState.detached: 65 case AppLifecycleState.detached:
67 controller.stop(); 66 controller.stop();
68 break; 67 break;
  68 + default:
  69 + break;
69 } 70 }
  71 + _lastState = state;
70 } 72 }
71 73
72 - Uint8List? lastScanned;  
73 -  
74 @override 74 @override
75 Widget build(BuildContext context) { 75 Widget build(BuildContext context) {
76 return ValueListenableBuilder( 76 return ValueListenableBuilder(
77 - valueListenable: controller.args, 77 + valueListenable: controller.startArguments,
78 builder: (context, value, child) { 78 builder: (context, value, child) {
79 value = value as MobileScannerArguments?; 79 value = value as MobileScannerArguments?;
80 if (value == null) { 80 if (value == null) {
81 return const ColoredBox(color: Colors.black); 81 return const ColoredBox(color: Colors.black);
82 } else { 82 } else {
83 controller.barcodes.listen((barcode) { 83 controller.barcodes.listen((barcode) {
84 - if (!widget.allowDuplicates) {  
85 - if (lastScanned != barcode.rawBytes) {  
86 - lastScanned = barcode.rawBytes;  
87 - widget.onDetect(barcode, value! as MobileScannerArguments);  
88 - }  
89 - } else {  
90 - widget.onDetect(barcode, value! as MobileScannerArguments);  
91 - } 84 + widget.onDetect(barcode, value! as MobileScannerArguments);
92 }); 85 });
93 return ClipRect( 86 return ClipRect(
94 child: SizedBox( 87 child: SizedBox(
@@ -135,4 +128,4 @@ class _MobileScannerState extends State<MobileScanner> @@ -135,4 +128,4 @@ class _MobileScannerState extends State<MobileScanner>
135 WidgetsBinding.instance.removeObserver(this); 128 WidgetsBinding.instance.removeObserver(this);
136 super.dispose(); 129 super.dispose();
137 } 130 }
138 -} 131 +}
@@ -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_capture.dart';
  9 +import 'package:mobile_scanner/src/barcode_utility.dart';
  10 +import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
9 11
  12 +/// The [MobileScannerController] holds all the logic of this plugin,
  13 +/// where as the [MobileScanner] class is the frontend of this plugin.
10 class MobileScannerController { 14 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'); 15 + MobileScannerController({
  16 + this.facing = CameraFacing.back,
  17 + this.detectionSpeed = DetectionSpeed.noDuplicates,
  18 + // this.ratio,
  19 + this.torchEnabled = false,
  20 + this.formats,
  21 + // this.autoResume = true,
  22 + this.returnImage = false,
  23 + this.onPermissionSet,
  24 + }) {
  25 + // In case a new instance is created before calling dispose()
  26 + if (controllerHashcode != null) {
  27 + stop();
  28 + }
  29 + controllerHashcode = hashCode;
  30 + events = _eventChannel
  31 + .receiveBroadcastStream()
  32 + .listen((data) => _handleEvent(data as Map));
  33 + }
15 34
16 //Must be static to keep the same value on new instances 35 //Must be static to keep the same value on new instances
17 - static int? _controllerHashcode;  
18 - StreamSubscription? events; 36 + static int? controllerHashcode;
19 37
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 38 + /// Select which camera should be used.
  39 + ///
  40 + /// Default: CameraFacing.back
  41 + final CameraFacing facing;
  42 +
  43 + // /// Analyze the image in 4:3 or 16:9
  44 + // ///
  45 + // /// Only on Android
  46 + // final Ratio? ratio;
  47 +
  48 + /// Enable or disable the torch (Flash) on start
  49 + ///
  50 + /// Default: disabled
  51 + final bool torchEnabled;
  52 +
  53 + /// Set to true if you want to return the image buffer with the Barcode event
  54 + ///
  55 + /// Only supported on iOS and Android
27 final bool returnImage; 56 final bool returnImage;
28 57
29 - /// If provided, the scanner will only detect those specific formats. 58 + /// If provided, the scanner will only detect those specific formats
30 final List<BarcodeFormat>? formats; 59 final List<BarcodeFormat>? formats;
31 60
32 - CameraFacing facing;  
33 - bool hasTorch = false;  
34 - late StreamController<Barcode> barcodesController;  
35 -  
36 - /// Whether to automatically resume the camera when the application is resumed  
37 - bool autoResume; 61 + /// Sets the speed of detections.
  62 + ///
  63 + /// WARNING: DetectionSpeed.unrestricted can cause memory issues on some devices
  64 + final DetectionSpeed detectionSpeed;
38 65
39 - Stream<Barcode> get barcodes => barcodesController.stream; 66 + /// Sets the barcode stream
  67 + final StreamController<BarcodeCapture> _barcodesController =
  68 + StreamController.broadcast();
  69 + Stream<BarcodeCapture> get barcodes => _barcodesController.stream;
40 70
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; 71 + static const MethodChannel _methodChannel =
  72 + MethodChannel('dev.steenbakker.mobile_scanner/scanner/method');
  73 + static const EventChannel _eventChannel =
  74 + EventChannel('dev.steenbakker.mobile_scanner/scanner/event');
55 75
56 - cameraFacingState = ValueNotifier(facing); 76 + Function(bool permissionGranted)? onPermissionSet;
57 77
58 - // Sets analyze mode and barcode stream  
59 - barcodesController = StreamController.broadcast(  
60 - // onListen: () => setAnalyzeMode(AnalyzeMode.barcode.index),  
61 - // onCancel: () => setAnalyzeMode(AnalyzeMode.none.index),  
62 - ); 78 + /// Listen to events from the platform specific code
  79 + late StreamSubscription events;
63 80
64 - // Listen to events from the platform specific code  
65 - events = eventChannel  
66 - .receiveBroadcastStream()  
67 - .listen((data) => handleEvent(data as Map));  
68 - } 81 + /// A notifier that provides several arguments about the MobileScanner
  82 + final ValueNotifier<MobileScannerArguments?> startArguments = ValueNotifier(null);
69 83
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;  
137 - 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 + }
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,76 @@ class MobileScannerController { @@ -176,85 +161,76 @@ 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');
186 } 171 }
187 172
188 - hasTorch = startResult['torchable'] as bool? ?? false; 173 + _hasTorch = startResult['torchable'] as bool? ?? false;
  174 + if (_hasTorch! && torchEnabled) {
  175 + torchState.value = TorchState.on;
  176 + }
189 177
190 if (kIsWeb) { 178 if (kIsWeb) {
191 onPermissionSet?.call( 179 onPermissionSet?.call(
192 true, 180 true,
193 ); // If we reach this line, it means camera permission has been granted 181 ); // If we reach this line, it means camera permission has been granted
194 182
195 - args.value = MobileScannerArguments( 183 + startArguments.value = MobileScannerArguments(
196 webId: startResult['ViewID'] as String?, 184 webId: startResult['ViewID'] as String?,
197 size: Size( 185 size: Size(
198 startResult['videoWidth'] as double? ?? 0, 186 startResult['videoWidth'] as double? ?? 0,
199 startResult['videoHeight'] as double? ?? 0, 187 startResult['videoHeight'] as double? ?? 0,
200 ), 188 ),
201 - hasTorch: hasTorch, 189 + hasTorch: _hasTorch!,
202 ); 190 );
203 } else { 191 } else {
204 - args.value = MobileScannerArguments( 192 + startArguments.value = MobileScannerArguments(
205 textureId: startResult['textureId'] as int?, 193 textureId: startResult['textureId'] as int?,
206 size: toSize(startResult['size'] as Map? ?? {}), 194 size: toSize(startResult['size'] as Map? ?? {}),
207 - hasTorch: hasTorch, 195 + hasTorch: _hasTorch!,
208 ); 196 );
209 } 197 }
210 -  
211 isStarting = false; 198 isStarting = false;
  199 + return startArguments.value!;
212 } 200 }
213 201
  202 + /// Stops the camera, but does not dispose this controller.
214 Future<void> stop() async { 203 Future<void> stop() async {
215 - try {  
216 - await methodChannel.invokeMethod('stop');  
217 - } on PlatformException catch (error) {  
218 - debugPrint('${error.code}: ${error.message}');  
219 - } 204 + await _methodChannel.invokeMethod('stop');
220 } 205 }
221 206
222 /// Switches the torch on or off. 207 /// Switches the torch on or off.
223 /// 208 ///
224 /// Only works if torch is available. 209 /// Only works if torch is available.
225 Future<void> toggleTorch() async { 210 Future<void> toggleTorch() async {
226 - ensure('toggleTorch');  
227 - if (!hasTorch) {  
228 - debugPrint('Device has no torch/flash.');  
229 - return; 211 + if (_hasTorch == null) {
  212 + throw MobileScannerException(
  213 + 'Cannot toggle torch if start() has never been called');
  214 + } else if (!_hasTorch!) {
  215 + throw MobileScannerException('Device has no torch');
230 } 216 }
231 217
232 - final TorchState state =  
233 - torchState.value == TorchState.off ? TorchState.on : TorchState.off; 218 + torchState.value =
  219 + torchState.value == TorchState.off ? TorchState.on : TorchState.off;
234 220
235 - try {  
236 - await methodChannel.invokeMethod('torch', state.index);  
237 - } on PlatformException catch (error) {  
238 - debugPrint('${error.code}: ${error.message}');  
239 - } 221 + await _methodChannel.invokeMethod('torch', torchState.value.index);
240 } 222 }
241 223
242 /// Switches the torch on or off. 224 /// Switches the torch on or off.
243 /// 225 ///
244 /// Only works if torch is available. 226 /// Only works if torch is available.
245 Future<void> switchCamera() async { 227 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(); 228 + await _methodChannel.invokeMethod('stop');
  229 + final CameraFacing facingToUse =
  230 + cameraFacingState.value == CameraFacing.back
  231 + ? CameraFacing.front
  232 + : CameraFacing.back;
  233 + await start(cameraFacingOverride: facingToUse);
258 } 234 }
259 235
260 /// Handles a local image file. 236 /// Handles a local image file.
@@ -263,28 +239,66 @@ class MobileScannerController { @@ -263,28 +239,66 @@ class MobileScannerController {
263 /// 239 ///
264 /// [path] The path of the image on the devices 240 /// [path] The path of the image on the devices
265 Future<bool> analyzeImage(String path) async { 241 Future<bool> analyzeImage(String path) async {
266 - return methodChannel 242 + return _methodChannel
267 .invokeMethod<bool>('analyzeImage', path) 243 .invokeMethod<bool>('analyzeImage', path)
268 .then<bool>((bool? value) => value ?? false); 244 .then<bool>((bool? value) => value ?? false);
269 } 245 }
270 246
271 /// Disposes the MobileScannerController and closes all listeners. 247 /// Disposes the MobileScannerController and closes all listeners.
  248 + ///
  249 + /// If you call this, you cannot use this controller object anymore.
272 void dispose() { 250 void dispose() {
273 - if (hashCode == _controllerHashcode) {  
274 - stop();  
275 - events?.cancel();  
276 - events = null;  
277 - _controllerHashcode = null; 251 + stop();
  252 + events.cancel();
  253 + _barcodesController.close();
  254 + if (hashCode == controllerHashcode) {
  255 + controllerHashcode = null;
278 onPermissionSet = null; 256 onPermissionSet = null;
279 } 257 }
280 - barcodesController.close();  
281 } 258 }
282 259
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); 260 + /// Handles a returning event from the platform side
  261 + void _handleEvent(Map event) {
  262 + final name = event['name'];
  263 + final data = event['data'];
  264 +
  265 + switch (name) {
  266 + case 'torchState':
  267 + final state = TorchState.values[data as int? ?? 0];
  268 + torchState.value = state;
  269 + break;
  270 + case 'barcode':
  271 + if (data == null) return;
  272 + final parsed = (data as List)
  273 + .map((value) => Barcode.fromNative(value as Map))
  274 + .toList();
  275 + _barcodesController.add(BarcodeCapture(
  276 + barcodes: parsed,
  277 + image: event['image'] as Uint8List,
  278 + ));
  279 + break;
  280 + case 'barcodeMac':
  281 + _barcodesController.add(
  282 + BarcodeCapture(
  283 + barcodes: [
  284 + Barcode(
  285 + rawValue: (data as Map)['payload'] as String?,
  286 + )
  287 + ],
  288 + ),
  289 + );
  290 + break;
  291 + case 'barcodeWeb':
  292 + _barcodesController.add(BarcodeCapture(barcodes: [
  293 + Barcode(
  294 + rawValue: data as String?,
  295 + )
  296 + ]));
  297 + break;
  298 + case 'error':
  299 + throw MobileScannerException(data as String);
  300 + default:
  301 + throw UnimplementedError(name as String?);
  302 + }
289 } 303 }
290 -} 304 +}
  1 +class MobileScannerException implements Exception {
  2 + String message;
  3 + MobileScannerException(this.message);
  4 +}
@@ -12,11 +12,13 @@ dependencies: @@ -12,11 +12,13 @@ dependencies:
12 sdk: flutter 12 sdk: flutter
13 flutter_web_plugins: 13 flutter_web_plugins:
14 sdk: flutter 14 sdk: flutter
15 - js: ^0.6.3 15 + json_serializable: ^6.3.1
16 16
17 dev_dependencies: 17 dev_dependencies:
  18 + build_runner: ^2.2.0
18 flutter_test: 19 flutter_test:
19 sdk: flutter 20 sdk: flutter
  21 + json_annotation: ^4.6.0
20 lint: ^1.10.0 22 lint: ^1.10.0
21 23
22 flutter: 24 flutter: