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,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 +
  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
  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,13 +27,34 @@ class _BarcodeScannerReturningImageState @@ -26,13 +27,34 @@ 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(
  38 + children: [
  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!'))),
  53 + ),
  54 + Container(
  55 + height: 0.66 * MediaQuery.of(context).size.height,
  56 + color: Colors.grey,
  57 + child: Stack(
36 children: [ 58 children: [
37 MobileScanner( 59 MobileScanner(
38 controller: controller, 60 controller: controller,
@@ -42,17 +64,10 @@ class _BarcodeScannerReturningImageState @@ -42,17 +64,10 @@ class _BarcodeScannerReturningImageState
42 // torchEnabled: true, 64 // torchEnabled: true,
43 // facing: CameraFacing.front, 65 // facing: CameraFacing.front,
44 // ), 66 // ),
45 - onDetect: (barcode, args) { 67 + onDetect: (barcode, arguments) {
46 setState(() { 68 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; 69 + this.arguments = arguments;
  70 + this.barcode = barcode;
56 }); 71 });
57 }, 72 },
58 ), 73 ),
@@ -65,8 +80,10 @@ class _BarcodeScannerReturningImageState @@ -65,8 +80,10 @@ class _BarcodeScannerReturningImageState
65 child: Row( 80 child: Row(
66 mainAxisAlignment: MainAxisAlignment.spaceEvenly, 81 mainAxisAlignment: MainAxisAlignment.spaceEvenly,
67 children: [ 82 children: [
68 - IconButton(  
69 - color: Colors.white, 83 + Container(
  84 + color: arguments != null && !arguments!.hasTorch ? Colors.red : Colors.white,
  85 + child: IconButton(
  86 + // color: ,
70 icon: ValueListenableBuilder( 87 icon: ValueListenableBuilder(
71 valueListenable: controller.torchState, 88 valueListenable: controller.torchState,
72 builder: (context, state, child) { 89 builder: (context, state, child) {
@@ -93,6 +110,7 @@ class _BarcodeScannerReturningImageState @@ -93,6 +110,7 @@ class _BarcodeScannerReturningImageState
93 iconSize: 32.0, 110 iconSize: 32.0,
94 onPressed: () => controller.toggleTorch(), 111 onPressed: () => controller.toggleTorch(),
95 ), 112 ),
  113 + ),
96 IconButton( 114 IconButton(
97 color: Colors.white, 115 color: Colors.white,
98 icon: isStarted 116 icon: isStarted
@@ -110,7 +128,7 @@ class _BarcodeScannerReturningImageState @@ -110,7 +128,7 @@ class _BarcodeScannerReturningImageState
110 height: 50, 128 height: 50,
111 child: FittedBox( 129 child: FittedBox(
112 child: Text( 130 child: Text(
113 - barcode ?? 'Scan something!', 131 + barcode?.barcodes.first.rawValue ?? 'Scan something!',
114 overflow: TextOverflow.fade, 132 overflow: TextOverflow.fade,
115 style: Theme.of(context) 133 style: Theme.of(context)
116 .textTheme 134 .textTheme
@@ -139,21 +157,14 @@ class _BarcodeScannerReturningImageState @@ -139,21 +157,14 @@ class _BarcodeScannerReturningImageState
139 iconSize: 32.0, 157 iconSize: 32.0,
140 onPressed: () => controller.switchCamera(), 158 onPressed: () => controller.switchCamera(),
141 ), 159 ),
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 ], 160 ],
153 ), 161 ),
154 ), 162 ),
155 ), 163 ),
156 ], 164 ],
  165 + ),
  166 + ),
  167 + ],
157 ); 168 );
158 }, 169 },
159 ), 170 ),
@@ -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
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 );
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?,
  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); 84 widget.onDetect(barcode, value! as MobileScannerArguments);
88 - }  
89 - } else {  
90 - widget.onDetect(barcode, value! as MobileScannerArguments);  
91 - }  
92 }); 85 });
93 return ClipRect( 86 return ClipRect(
94 child: SizedBox( 87 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_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; 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,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 = 218 + torchState.value =
233 torchState.value == TorchState.off ? TorchState.on : TorchState.off; 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(); 251 stop();
275 - events?.cancel();  
276 - events = null;  
277 - _controllerHashcode = null; 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: