feat: fix android scanner, torch, switch camera and much more
Showing
14 changed files
with
596 additions
and
787 deletions
| 1 | +package dev.steenbakker.mobile_scanner | ||
| 2 | + | ||
| 3 | +import androidx.annotation.IntDef | ||
| 4 | + | ||
| 5 | +@IntDef(AnalyzeMode.NONE, AnalyzeMode.BARCODE) | ||
| 6 | +@Target(AnnotationTarget.FIELD) | ||
| 7 | +@Retention(AnnotationRetention.SOURCE) | ||
| 8 | +annotation class AnalyzeMode { | ||
| 9 | + companion object { | ||
| 10 | + const val NONE = 0 | ||
| 11 | + const val BARCODE = 1 | ||
| 12 | + } | ||
| 13 | +} |
| 1 | -package dev.steenbakker.mobile_scanner.old | 1 | +package dev.steenbakker.mobile_scanner |
| 2 | 2 | ||
| 3 | import com.google.mlkit.vision.barcode.BarcodeScannerOptions | 3 | import com.google.mlkit.vision.barcode.BarcodeScannerOptions |
| 4 | import java.util.ArrayList | 4 | import java.util.ArrayList |
| @@ -81,12 +81,12 @@ enum class BarcodeFormats(val intValue: Int) { | @@ -81,12 +81,12 @@ enum class BarcodeFormats(val intValue: Int) { | ||
| 81 | 81 | ||
| 82 | init { | 82 | init { |
| 83 | val values = values() | 83 | val values = values() |
| 84 | -// formatsMap = | ||
| 85 | -// HashMap<String, Int>(com.github.rmtmckenzie.qrmobilevision.values.size * 4 / 3) | ||
| 86 | -// for (value in com.github.rmtmckenzie.qrmobilevision.values) { | ||
| 87 | -// formatsMap!![com.github.rmtmckenzie.qrmobilevision.value.name] = | ||
| 88 | -// com.github.rmtmckenzie.qrmobilevision.value.intValue | ||
| 89 | -// } | 84 | + formatsMap = |
| 85 | + HashMap<String, Int>(values.size * 4 / 3) | ||
| 86 | + for (value in values) { | ||
| 87 | + formatsMap!![value.name] = | ||
| 88 | + value.intValue | ||
| 89 | + } | ||
| 90 | } | 90 | } |
| 91 | } | 91 | } |
| 92 | } | 92 | } |
| 1 | package dev.steenbakker.mobile_scanner | 1 | package dev.steenbakker.mobile_scanner |
| 2 | 2 | ||
| 3 | import android.Manifest | 3 | import android.Manifest |
| 4 | -import android.R.attr.height | ||
| 5 | -import android.R.attr.width | ||
| 6 | -import android.annotation.SuppressLint | ||
| 7 | import android.app.Activity | 4 | import android.app.Activity |
| 8 | -import android.content.Context | ||
| 9 | import android.content.pm.PackageManager | 5 | import android.content.pm.PackageManager |
| 10 | -import android.graphics.ImageFormat | ||
| 11 | -import android.graphics.SurfaceTexture | ||
| 12 | -import android.hardware.camera2.CameraCharacteristics | ||
| 13 | -import android.hardware.camera2.CameraManager | ||
| 14 | -import android.hardware.camera2.params.StreamConfigurationMap | 6 | +import android.graphics.Point |
| 15 | import android.util.Log | 7 | import android.util.Log |
| 16 | -import android.util.Rational | ||
| 17 | import android.util.Size | 8 | import android.util.Size |
| 18 | import android.view.Surface | 9 | import android.view.Surface |
| 19 | -import android.view.Surface.ROTATION_0 | ||
| 20 | -import android.view.Surface.ROTATION_180 | ||
| 21 | -import androidx.annotation.IntDef | ||
| 22 | import androidx.annotation.NonNull | 10 | import androidx.annotation.NonNull |
| 23 | import androidx.camera.core.* | 11 | import androidx.camera.core.* |
| 24 | -import androidx.camera.core.impl.PreviewConfig | ||
| 25 | import androidx.camera.lifecycle.ProcessCameraProvider | 12 | import androidx.camera.lifecycle.ProcessCameraProvider |
| 26 | import androidx.core.app.ActivityCompat | 13 | import androidx.core.app.ActivityCompat |
| 27 | import androidx.core.content.ContextCompat | 14 | import androidx.core.content.ContextCompat |
| 28 | import androidx.lifecycle.LifecycleOwner | 15 | import androidx.lifecycle.LifecycleOwner |
| 16 | +import com.google.mlkit.vision.barcode.BarcodeScannerOptions | ||
| 29 | import com.google.mlkit.vision.barcode.BarcodeScanning | 17 | import com.google.mlkit.vision.barcode.BarcodeScanning |
| 18 | +import com.google.mlkit.vision.barcode.common.Barcode | ||
| 30 | import com.google.mlkit.vision.common.InputImage | 19 | import com.google.mlkit.vision.common.InputImage |
| 31 | import io.flutter.plugin.common.EventChannel | 20 | import io.flutter.plugin.common.EventChannel |
| 32 | import io.flutter.plugin.common.MethodCall | 21 | import io.flutter.plugin.common.MethodCall |
| @@ -38,7 +27,8 @@ import io.flutter.view.TextureRegistry | @@ -38,7 +27,8 @@ import io.flutter.view.TextureRegistry | ||
| 38 | class MobileScanner(private val activity: Activity, private val textureRegistry: TextureRegistry) | 27 | class MobileScanner(private val activity: Activity, private val textureRegistry: TextureRegistry) |
| 39 | : MethodChannel.MethodCallHandler, EventChannel.StreamHandler, PluginRegistry.RequestPermissionsResultListener { | 28 | : MethodChannel.MethodCallHandler, EventChannel.StreamHandler, PluginRegistry.RequestPermissionsResultListener { |
| 40 | companion object { | 29 | companion object { |
| 41 | - private const val REQUEST_CODE = 19930430 | 30 | + private const val REQUEST_CODE = 22022022 |
| 31 | + private val TAG = MobileScanner::class.java.simpleName | ||
| 42 | } | 32 | } |
| 43 | 33 | ||
| 44 | private var sink: EventChannel.EventSink? = null | 34 | private var sink: EventChannel.EventSink? = null |
| @@ -54,12 +44,12 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -54,12 +44,12 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 54 | @ExperimentalGetImage | 44 | @ExperimentalGetImage |
| 55 | override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: MethodChannel.Result) { | 45 | override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: MethodChannel.Result) { |
| 56 | when (call.method) { | 46 | when (call.method) { |
| 57 | - "state" -> stateNative(result) | ||
| 58 | - "request" -> requestNative(result) | ||
| 59 | - "start" -> startNative(call, result) | ||
| 60 | - "torch" -> torchNative(call, result) | ||
| 61 | - "analyze" -> analyzeNative(call, result) | ||
| 62 | - "stop" -> stopNative(result) | 47 | + "state" -> checkPermission(result) |
| 48 | + "request" -> requestPermission(result) | ||
| 49 | + "start" -> start(call, result) | ||
| 50 | + "torch" -> switchTorch(call, result) | ||
| 51 | + "analyze" -> switchAnalyzeMode(call, result) | ||
| 52 | + "stop" -> stop(result) | ||
| 63 | else -> result.notImplemented() | 53 | else -> result.notImplemented() |
| 64 | } | 54 | } |
| 65 | } | 55 | } |
| @@ -76,7 +66,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -76,7 +66,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 76 | return listener?.onRequestPermissionsResult(requestCode, permissions, grantResults) ?: false | 66 | return listener?.onRequestPermissionsResult(requestCode, permissions, grantResults) ?: false |
| 77 | } | 67 | } |
| 78 | 68 | ||
| 79 | - private fun stateNative(result: MethodChannel.Result) { | 69 | + private fun checkPermission(result: MethodChannel.Result) { |
| 80 | // Can't get exact denied or not_determined state without request. Just return not_determined when state isn't authorized | 70 | // Can't get exact denied or not_determined state without request. Just return not_determined when state isn't authorized |
| 81 | val state = | 71 | val state = |
| 82 | if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) 1 | 72 | if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) 1 |
| @@ -84,7 +74,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -84,7 +74,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 84 | result.success(state) | 74 | result.success(state) |
| 85 | } | 75 | } |
| 86 | 76 | ||
| 87 | - private fun requestNative(result: MethodChannel.Result) { | 77 | + private fun requestPermission(result: MethodChannel.Result) { |
| 88 | listener = PluginRegistry.RequestPermissionsResultListener { requestCode, _, grantResults -> | 78 | listener = PluginRegistry.RequestPermissionsResultListener { requestCode, _, grantResults -> |
| 89 | if (requestCode != REQUEST_CODE) { | 79 | if (requestCode != REQUEST_CODE) { |
| 90 | false | 80 | false |
| @@ -99,113 +89,115 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -99,113 +89,115 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 99 | ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE) | 89 | ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE) |
| 100 | } | 90 | } |
| 101 | 91 | ||
| 102 | - private var sensorOrientation = 0 | ||
| 103 | 92 | ||
| 104 | @ExperimentalGetImage | 93 | @ExperimentalGetImage |
| 105 | -// @androidx.camera.camera2.interop.ExperimentalCamera2Interop | ||
| 106 | - private fun startNative(call: MethodCall, result: MethodChannel.Result) { | 94 | + val analyzer = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format |
| 95 | + when (analyzeMode) { | ||
| 96 | + AnalyzeMode.BARCODE -> { | ||
| 97 | + val mediaImage = imageProxy.image ?: return@Analyzer | ||
| 98 | + val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) | ||
| 107 | 99 | ||
| 108 | - val targetWidth: Int? = call.argument<Int>("targetWidth") | ||
| 109 | - val targetHeight: Int? = call.argument<Int>("targetHeight") | ||
| 110 | - val facing: Int? = call.argument<Int>("facing") | ||
| 111 | - | ||
| 112 | - if (targetWidth == null || targetHeight == null) { | ||
| 113 | - result.error("INVALID_ARGUMENT", "Missing a required argument", "Expecting targetWidth, targetHeight") | ||
| 114 | - return | 100 | + scanner.process(inputImage) |
| 101 | + .addOnSuccessListener { barcodes -> | ||
| 102 | + for (barcode in barcodes) { | ||
| 103 | + val event = mapOf("name" to "barcode", "data" to barcode.data) | ||
| 104 | + sink?.success(event) | ||
| 105 | + } | ||
| 106 | + } | ||
| 107 | + .addOnFailureListener { e -> Log.e(TAG, e.message, e) } | ||
| 108 | + .addOnCompleteListener { imageProxy.close() } | ||
| 109 | + } | ||
| 110 | + else -> imageProxy.close() | ||
| 115 | } | 111 | } |
| 112 | + } | ||
| 113 | + | ||
| 114 | + | ||
| 115 | + private var scanner = BarcodeScanning.getClient() | ||
| 116 | 116 | ||
| 117 | - Log.i("LOG", "Target resolution : $targetWidth, $targetHeight") | 117 | + @ExperimentalGetImage |
| 118 | + private fun start(call: MethodCall, result: MethodChannel.Result) { | ||
| 119 | + | ||
| 120 | + val facing: Int = call.argument<Int>("facing") ?: 0 | ||
| 121 | + val ratio: Int? = call.argument<Int>("ratio") | ||
| 122 | + val torch: Boolean = call.argument<Boolean>("torch") ?: false | ||
| 123 | + val formatStrings: List<String>? = call.argument<List<String>>("formats") | ||
| 124 | + | ||
| 125 | + if (formatStrings != null) { | ||
| 126 | + val options: BarcodeScannerOptions = BarcodeFormats.optionsFromStringList(formatStrings) | ||
| 127 | + scanner = BarcodeScanning.getClient(options) | ||
| 128 | + } | ||
| 118 | 129 | ||
| 119 | val future = ProcessCameraProvider.getInstance(activity) | 130 | val future = ProcessCameraProvider.getInstance(activity) |
| 120 | val executor = ContextCompat.getMainExecutor(activity) | 131 | val executor = ContextCompat.getMainExecutor(activity) |
| 132 | + | ||
| 121 | future.addListener({ | 133 | future.addListener({ |
| 122 | cameraProvider = future.get() | 134 | cameraProvider = future.get() |
| 123 | textureEntry = textureRegistry.createSurfaceTexture() | 135 | textureEntry = textureRegistry.createSurfaceTexture() |
| 124 | - val textureId = textureEntry!!.id() | 136 | + |
| 125 | // Preview | 137 | // Preview |
| 126 | val surfaceProvider = Preview.SurfaceProvider { request -> | 138 | val surfaceProvider = Preview.SurfaceProvider { request -> |
| 127 | val texture = textureEntry!!.surfaceTexture() | 139 | val texture = textureEntry!!.surfaceTexture() |
| 128 | - val resolution = request.resolution | ||
| 129 | - texture.setDefaultBufferSize(resolution.width, resolution.height) | ||
| 130 | - Log.i("LOG", "Image resolution : ${request.resolution}") | 140 | + texture.setDefaultBufferSize(request.resolution.width, request.resolution.height) |
| 131 | val surface = Surface(texture) | 141 | val surface = Surface(texture) |
| 132 | request.provideSurface(surface, executor) { } | 142 | request.provideSurface(surface, executor) { } |
| 133 | } | 143 | } |
| 134 | -// PreviewConfig().apply { } | ||
| 135 | -// val previewConfig = PreviewConfig.Builder().apply { | ||
| 136 | -// setTargetAspectRatio(SQUARE_ASPECT_RATIO) | ||
| 137 | -// setTargetRotation(viewFinder.display.rotation) | ||
| 138 | -// }.build() | ||
| 139 | - | ||
| 140 | - | ||
| 141 | - val preview = Preview.Builder() | ||
| 142 | - .setTargetResolution(Size(targetWidth, targetHeight)) | ||
| 143 | - .build().apply { setSurfaceProvider(surfaceProvider) } | ||
| 144 | - // Analyzer | ||
| 145 | - val analyzer = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format | ||
| 146 | - when (analyzeMode) { | ||
| 147 | - AnalyzeMode.BARCODE -> { | ||
| 148 | - val mediaImage = imageProxy.image ?: return@Analyzer | ||
| 149 | - val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) | ||
| 150 | - val scanner = BarcodeScanning.getClient() | ||
| 151 | - scanner.process(inputImage) | ||
| 152 | - .addOnSuccessListener { barcodes -> | ||
| 153 | - for (barcode in barcodes) { | ||
| 154 | - val event = mapOf("name" to "barcode", "data" to barcode.data) | ||
| 155 | - sink?.success(event) | ||
| 156 | - } | ||
| 157 | - } | ||
| 158 | - .addOnFailureListener { e -> Log.e(TAG, e.message, e) } | ||
| 159 | - .addOnCompleteListener { imageProxy.close() } | ||
| 160 | - } | ||
| 161 | - else -> imageProxy.close() | ||
| 162 | - } | 144 | + |
| 145 | + // Build the preview to be shown on the Flutter texture | ||
| 146 | + val previewBuilder = Preview.Builder() | ||
| 147 | + if (ratio != null) { | ||
| 148 | + previewBuilder.setTargetAspectRatio(ratio) | ||
| 163 | } | 149 | } |
| 164 | - val analysis = ImageAnalysis.Builder() | 150 | + val preview = previewBuilder.build().apply { setSurfaceProvider(surfaceProvider) } |
| 151 | + | ||
| 152 | + // Build the analyzer to be passed on to MLKit | ||
| 153 | + val analysisBuilder = ImageAnalysis.Builder() | ||
| 165 | .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) | 154 | .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) |
| 166 | - .setTargetResolution(Size(targetWidth, targetHeight)) | ||
| 167 | - .build().apply { setAnalyzer(executor, analyzer) } | ||
| 168 | - // Bind to lifecycle. | ||
| 169 | - val owner = activity as LifecycleOwner | ||
| 170 | - val selector = | ||
| 171 | - if (call.arguments == 0) CameraSelector.DEFAULT_FRONT_CAMERA | ||
| 172 | - else CameraSelector.DEFAULT_BACK_CAMERA | ||
| 173 | - camera = cameraProvider!!.bindToLifecycle(owner, selector, preview, analysis) | ||
| 174 | - | ||
| 175 | - val analysisSize = analysis.resolutionInfo?.resolution ?: Size(0, 0) | ||
| 176 | - val previewSize = preview.resolutionInfo?.resolution ?: Size(0, 0) | ||
| 177 | - Log.i("LOG", "Analyzer: $analysisSize") | ||
| 178 | - Log.i("LOG", "Preview: $previewSize") | ||
| 179 | - | ||
| 180 | - camera!!.cameraInfo.torchState.observe(owner) { state -> | 155 | + if (ratio != null) { |
| 156 | + analysisBuilder.setTargetAspectRatio(ratio) | ||
| 157 | + } | ||
| 158 | + val analysis = analysisBuilder.build().apply { setAnalyzer(executor, analyzer) } | ||
| 159 | + | ||
| 160 | + // Select the correct camera | ||
| 161 | + val selector = if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA | ||
| 162 | + | ||
| 163 | + camera = cameraProvider!!.bindToLifecycle(activity as LifecycleOwner, selector, preview, analysis) | ||
| 164 | + | ||
| 165 | + val analysisSize = analysis.resolutionInfo?.resolution ?: Size(0, 0) | ||
| 166 | + val previewSize = preview.resolutionInfo?.resolution ?: Size(0, 0) | ||
| 167 | + Log.i("LOG", "Analyzer: $analysisSize") | ||
| 168 | + Log.i("LOG", "Preview: $previewSize") | ||
| 169 | + | ||
| 170 | + // Register the torch listener | ||
| 171 | + camera!!.cameraInfo.torchState.observe(activity) { state -> | ||
| 181 | // TorchState.OFF = 0; TorchState.ON = 1 | 172 | // TorchState.OFF = 0; TorchState.ON = 1 |
| 182 | - val event = mapOf("name" to "torchState", "data" to state) | ||
| 183 | - sink?.success(event) | 173 | + sink?.success(mapOf("name" to "torchState", "data" to state)) |
| 184 | } | 174 | } |
| 185 | 175 | ||
| 176 | + // Enable torch if provided | ||
| 177 | + camera!!.cameraControl.enableTorch(torch) | ||
| 178 | + | ||
| 186 | val resolution = preview.resolutionInfo!!.resolution | 179 | val resolution = preview.resolutionInfo!!.resolution |
| 187 | val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0 | 180 | val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0 |
| 188 | val width = resolution.width.toDouble() | 181 | val width = resolution.width.toDouble() |
| 189 | val height = resolution.height.toDouble() | 182 | val height = resolution.height.toDouble() |
| 190 | -// val size = mapOf("width" to 1920.0, "height" to 1080.0) | ||
| 191 | val size = if (portrait) mapOf("width" to width, "height" to height) else mapOf("width" to height, "height" to width) | 183 | val size = if (portrait) mapOf("width" to width, "height" to height) else mapOf("width" to height, "height" to width) |
| 192 | - val answer = mapOf("textureId" to textureId, "size" to size, "torchable" to camera!!.torchable) | 184 | + val answer = mapOf("textureId" to textureEntry!!.id(), "size" to size, "torchable" to camera!!.cameraInfo.hasFlashUnit()) |
| 193 | result.success(answer) | 185 | result.success(answer) |
| 194 | }, executor) | 186 | }, executor) |
| 195 | } | 187 | } |
| 196 | 188 | ||
| 197 | - private fun torchNative(call: MethodCall, result: MethodChannel.Result) { | 189 | + private fun switchTorch(call: MethodCall, result: MethodChannel.Result) { |
| 198 | val state = call.arguments == 1 | 190 | val state = call.arguments == 1 |
| 199 | camera!!.cameraControl.enableTorch(state) | 191 | camera!!.cameraControl.enableTorch(state) |
| 200 | result.success(null) | 192 | result.success(null) |
| 201 | } | 193 | } |
| 202 | 194 | ||
| 203 | - private fun analyzeNative(call: MethodCall, result: MethodChannel.Result) { | 195 | + private fun switchAnalyzeMode(call: MethodCall, result: MethodChannel.Result) { |
| 204 | analyzeMode = call.arguments as Int | 196 | analyzeMode = call.arguments as Int |
| 205 | result.success(null) | 197 | result.success(null) |
| 206 | } | 198 | } |
| 207 | 199 | ||
| 208 | - private fun stopNative(result: MethodChannel.Result) { | 200 | + private fun stop(result: MethodChannel.Result) { |
| 209 | val owner = activity as LifecycleOwner | 201 | val owner = activity as LifecycleOwner |
| 210 | camera!!.cameraInfo.torchState.removeObservers(owner) | 202 | camera!!.cameraInfo.torchState.removeObservers(owner) |
| 211 | cameraProvider!!.unbindAll() | 203 | cameraProvider!!.unbindAll() |
| @@ -218,14 +210,60 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -218,14 +210,60 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 218 | 210 | ||
| 219 | result.success(null) | 211 | result.success(null) |
| 220 | } | 212 | } |
| 221 | -} | ||
| 222 | 213 | ||
| 223 | -@IntDef(AnalyzeMode.NONE, AnalyzeMode.BARCODE) | ||
| 224 | -@Target(AnnotationTarget.FIELD) | ||
| 225 | -@Retention(AnnotationRetention.SOURCE) | ||
| 226 | -annotation class AnalyzeMode { | ||
| 227 | - companion object { | ||
| 228 | - const val NONE = 0 | ||
| 229 | - const val BARCODE = 1 | ||
| 230 | - } | 214 | + |
| 215 | + private val Barcode.data: Map<String, Any?> | ||
| 216 | + get() = mapOf("corners" to cornerPoints?.map { corner -> corner.data }, "format" to format, | ||
| 217 | + "rawBytes" to rawBytes, "rawValue" to rawValue, "type" to valueType, | ||
| 218 | + "calendarEvent" to calendarEvent?.data, "contactInfo" to contactInfo?.data, | ||
| 219 | + "driverLicense" to driverLicense?.data, "email" to email?.data, | ||
| 220 | + "geoPoint" to geoPoint?.data, "phone" to phone?.data, "sms" to sms?.data, | ||
| 221 | + "url" to url?.data, "wifi" to wifi?.data) | ||
| 222 | + | ||
| 223 | + private val Point.data: Map<String, Double> | ||
| 224 | + get() = mapOf("x" to x.toDouble(), "y" to y.toDouble()) | ||
| 225 | + | ||
| 226 | + private val Barcode.CalendarEvent.data: Map<String, Any?> | ||
| 227 | + get() = mapOf("description" to description, "end" to end?.rawValue, "location" to location, | ||
| 228 | + "organizer" to organizer, "start" to start?.rawValue, "status" to status, | ||
| 229 | + "summary" to summary) | ||
| 230 | + | ||
| 231 | + private val Barcode.ContactInfo.data: Map<String, Any?> | ||
| 232 | + get() = mapOf("addresses" to addresses.map { address -> address.data }, | ||
| 233 | + "emails" to emails.map { email -> email.data }, "name" to name?.data, | ||
| 234 | + "organization" to organization, "phones" to phones.map { phone -> phone.data }, | ||
| 235 | + "title" to title, "urls" to urls) | ||
| 236 | + | ||
| 237 | + private val Barcode.Address.data: Map<String, Any?> | ||
| 238 | + get() = mapOf("addressLines" to addressLines, "type" to type) | ||
| 239 | + | ||
| 240 | + private val Barcode.PersonName.data: Map<String, Any?> | ||
| 241 | + get() = mapOf("first" to first, "formattedName" to formattedName, "last" to last, | ||
| 242 | + "middle" to middle, "prefix" to prefix, "pronunciation" to pronunciation, | ||
| 243 | + "suffix" to suffix) | ||
| 244 | + | ||
| 245 | + private val Barcode.DriverLicense.data: Map<String, Any?> | ||
| 246 | + get() = mapOf("addressCity" to addressCity, "addressState" to addressState, | ||
| 247 | + "addressStreet" to addressStreet, "addressZip" to addressZip, "birthDate" to birthDate, | ||
| 248 | + "documentType" to documentType, "expiryDate" to expiryDate, "firstName" to firstName, | ||
| 249 | + "gender" to gender, "issueDate" to issueDate, "issuingCountry" to issuingCountry, | ||
| 250 | + "lastName" to lastName, "licenseNumber" to licenseNumber, "middleName" to middleName) | ||
| 251 | + | ||
| 252 | + private val Barcode.Email.data: Map<String, Any?> | ||
| 253 | + get() = mapOf("address" to address, "body" to body, "subject" to subject, "type" to type) | ||
| 254 | + | ||
| 255 | + private val Barcode.GeoPoint.data: Map<String, Any?> | ||
| 256 | + get() = mapOf("latitude" to lat, "longitude" to lng) | ||
| 257 | + | ||
| 258 | + private val Barcode.Phone.data: Map<String, Any?> | ||
| 259 | + get() = mapOf("number" to number, "type" to type) | ||
| 260 | + | ||
| 261 | + private val Barcode.Sms.data: Map<String, Any?> | ||
| 262 | + get() = mapOf("message" to message, "phoneNumber" to phoneNumber) | ||
| 263 | + | ||
| 264 | + private val Barcode.UrlBookmark.data: Map<String, Any?> | ||
| 265 | + get() = mapOf("title" to title, "url" to url) | ||
| 266 | + | ||
| 267 | + private val Barcode.WiFi.data: Map<String, Any?> | ||
| 268 | + get() = mapOf("encryptionType" to encryptionType, "password" to password, "ssid" to ssid) | ||
| 231 | } | 269 | } |
| @@ -7,7 +7,7 @@ import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding | @@ -7,7 +7,7 @@ import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding | ||
| 7 | import io.flutter.plugin.common.EventChannel | 7 | import io.flutter.plugin.common.EventChannel |
| 8 | import io.flutter.plugin.common.MethodChannel | 8 | import io.flutter.plugin.common.MethodChannel |
| 9 | 9 | ||
| 10 | -/** CameraXPlugin */ | 10 | +/** MobileScannerPlugin */ |
| 11 | class MobileScannerPlugin : FlutterPlugin, ActivityAware { | 11 | class MobileScannerPlugin : FlutterPlugin, ActivityAware { |
| 12 | private var flutter: FlutterPlugin.FlutterPluginBinding? = null | 12 | private var flutter: FlutterPlugin.FlutterPluginBinding? = null |
| 13 | private var activity: ActivityPluginBinding? = null | 13 | private var activity: ActivityPluginBinding? = null |
| 1 | -package dev.steenbakker.mobile_scanner | ||
| 2 | - | ||
| 3 | -import android.graphics.Point | ||
| 4 | -import androidx.camera.core.Camera | ||
| 5 | -import androidx.camera.core.ImageProxy | ||
| 6 | -import com.google.mlkit.vision.barcode.common.Barcode | ||
| 7 | - | ||
| 8 | -val Any.TAG: String | ||
| 9 | - get() = javaClass.simpleName | ||
| 10 | - | ||
| 11 | -val Camera.torchable: Boolean | ||
| 12 | - get() = cameraInfo.hasFlashUnit() | ||
| 13 | -// | ||
| 14 | -//val ImageProxy.yuv: ByteArray | ||
| 15 | -// get() { | ||
| 16 | -// val ySize = y.buffer.remaining() | ||
| 17 | -// val uSize = u.buffer.remaining() | ||
| 18 | -// val vSize = v.buffer.remaining() | ||
| 19 | -// | ||
| 20 | -// val size = ySize + uSize + vSize | ||
| 21 | -// val data = ByteArray(size) | ||
| 22 | -// | ||
| 23 | -// var offset = 0 | ||
| 24 | -// y.buffer.get(data, offset, ySize) | ||
| 25 | -// offset += ySize | ||
| 26 | -// u.buffer.get(data, offset, uSize) | ||
| 27 | -// offset += uSize | ||
| 28 | -// v.buffer.get(data, offset, vSize) | ||
| 29 | -// | ||
| 30 | -// return data | ||
| 31 | -// } | ||
| 32 | -// | ||
| 33 | -//val ImageProxy.nv21: ByteArray | ||
| 34 | -// get() { | ||
| 35 | -// if (BuildConfig.DEBUG) { | ||
| 36 | -// if (y.pixelStride != 1 || u.rowStride != v.rowStride || u.pixelStride != v.pixelStride) { | ||
| 37 | -// error("Assertion failed") | ||
| 38 | -// } | ||
| 39 | -// } | ||
| 40 | -// | ||
| 41 | -// val ySize = width * height | ||
| 42 | -// val uvSize = ySize / 2 | ||
| 43 | -// val size = ySize + uvSize | ||
| 44 | -// val data = ByteArray(size) | ||
| 45 | -// | ||
| 46 | -// var offset = 0 | ||
| 47 | -// // Y Plane | ||
| 48 | -// if (y.rowStride == width) { | ||
| 49 | -// y.buffer.get(data, offset, ySize) | ||
| 50 | -// offset += ySize | ||
| 51 | -// } else { | ||
| 52 | -// for (row in 0 until height) { | ||
| 53 | -// y.buffer.get(data, offset, width) | ||
| 54 | -// offset += width | ||
| 55 | -// } | ||
| 56 | -// | ||
| 57 | -// if (BuildConfig.DEBUG && offset != ySize) { | ||
| 58 | -// error("Assertion failed") | ||
| 59 | -// } | ||
| 60 | -// } | ||
| 61 | -// // U,V Planes | ||
| 62 | -// if (v.rowStride == width && v.pixelStride == 2) { | ||
| 63 | -// if (BuildConfig.DEBUG && v.size != uvSize - 1) { | ||
| 64 | -// error("Assertion failed") | ||
| 65 | -// } | ||
| 66 | -// | ||
| 67 | -// v.buffer.get(data, offset, 1) | ||
| 68 | -// offset += 1 | ||
| 69 | -// u.buffer.get(data, offset, u.size) | ||
| 70 | -// if (BuildConfig.DEBUG) { | ||
| 71 | -// val value = v.buffer.get() | ||
| 72 | -// if (data[offset] != value) { | ||
| 73 | -// error("Assertion failed") | ||
| 74 | -// } | ||
| 75 | -// } | ||
| 76 | -// } else { | ||
| 77 | -// for (row in 0 until height / 2) | ||
| 78 | -// for (col in 0 until width / 2) { | ||
| 79 | -// val index = row * v.rowStride + col * v.pixelStride | ||
| 80 | -// data[offset++] = v.buffer.get(index) | ||
| 81 | -// data[offset++] = u.buffer.get(index) | ||
| 82 | -// } | ||
| 83 | -// | ||
| 84 | -// if (BuildConfig.DEBUG && offset != size) { | ||
| 85 | -// error("Assertion failed") | ||
| 86 | -// } | ||
| 87 | -// } | ||
| 88 | -// | ||
| 89 | -// return data | ||
| 90 | -// } | ||
| 91 | - | ||
| 92 | -val ImageProxy.PlaneProxy.size | ||
| 93 | - get() = buffer.remaining() | ||
| 94 | - | ||
| 95 | -val ImageProxy.y: ImageProxy.PlaneProxy | ||
| 96 | - get() = planes[0] | ||
| 97 | - | ||
| 98 | -val ImageProxy.u: ImageProxy.PlaneProxy | ||
| 99 | - get() = planes[1] | ||
| 100 | - | ||
| 101 | -val ImageProxy.v: ImageProxy.PlaneProxy | ||
| 102 | - get() = planes[2] | ||
| 103 | - | ||
| 104 | -val Barcode.data: Map<String, Any?> | ||
| 105 | - get() = mapOf("corners" to cornerPoints?.map { corner -> corner.data }, "format" to format, | ||
| 106 | - "rawBytes" to rawBytes, "rawValue" to rawValue, "type" to valueType, | ||
| 107 | - "calendarEvent" to calendarEvent?.data, "contactInfo" to contactInfo?.data, | ||
| 108 | - "driverLicense" to driverLicense?.data, "email" to email?.data, | ||
| 109 | - "geoPoint" to geoPoint?.data, "phone" to phone?.data, "sms" to sms?.data, | ||
| 110 | - "url" to url?.data, "wifi" to wifi?.data) | ||
| 111 | - | ||
| 112 | -val Point.data: Map<String, Double> | ||
| 113 | - get() = mapOf("x" to x.toDouble(), "y" to y.toDouble()) | ||
| 114 | - | ||
| 115 | -val Barcode.CalendarEvent.data: Map<String, Any?> | ||
| 116 | - get() = mapOf("description" to description, "end" to end?.rawValue, "location" to location, | ||
| 117 | - "organizer" to organizer, "start" to start?.rawValue, "status" to status, | ||
| 118 | - "summary" to summary) | ||
| 119 | - | ||
| 120 | -val Barcode.ContactInfo.data: Map<String, Any?> | ||
| 121 | - get() = mapOf("addresses" to addresses.map { address -> address.data }, | ||
| 122 | - "emails" to emails.map { email -> email.data }, "name" to name?.data, | ||
| 123 | - "organization" to organization, "phones" to phones.map { phone -> phone.data }, | ||
| 124 | - "title" to title, "urls" to urls) | ||
| 125 | - | ||
| 126 | -val Barcode.Address.data: Map<String, Any?> | ||
| 127 | - get() = mapOf("addressLines" to addressLines, "type" to type) | ||
| 128 | - | ||
| 129 | -val Barcode.PersonName.data: Map<String, Any?> | ||
| 130 | - get() = mapOf("first" to first, "formattedName" to formattedName, "last" to last, | ||
| 131 | - "middle" to middle, "prefix" to prefix, "pronunciation" to pronunciation, | ||
| 132 | - "suffix" to suffix) | ||
| 133 | - | ||
| 134 | -val Barcode.DriverLicense.data: Map<String, Any?> | ||
| 135 | - get() = mapOf("addressCity" to addressCity, "addressState" to addressState, | ||
| 136 | - "addressStreet" to addressStreet, "addressZip" to addressZip, "birthDate" to birthDate, | ||
| 137 | - "documentType" to documentType, "expiryDate" to expiryDate, "firstName" to firstName, | ||
| 138 | - "gender" to gender, "issueDate" to issueDate, "issuingCountry" to issuingCountry, | ||
| 139 | - "lastName" to lastName, "licenseNumber" to licenseNumber, "middleName" to middleName) | ||
| 140 | - | ||
| 141 | -val Barcode.Email.data: Map<String, Any?> | ||
| 142 | - get() = mapOf("address" to address, "body" to body, "subject" to subject, "type" to type) | ||
| 143 | - | ||
| 144 | -val Barcode.GeoPoint.data: Map<String, Any?> | ||
| 145 | - get() = mapOf("latitude" to lat, "longitude" to lng) | ||
| 146 | - | ||
| 147 | -val Barcode.Phone.data: Map<String, Any?> | ||
| 148 | - get() = mapOf("number" to number, "type" to type) | ||
| 149 | - | ||
| 150 | -val Barcode.Sms.data: Map<String, Any?> | ||
| 151 | - get() = mapOf("message" to message, "phoneNumber" to phoneNumber) | ||
| 152 | - | ||
| 153 | -val Barcode.UrlBookmark.data: Map<String, Any?> | ||
| 154 | - get() = mapOf("title" to title, "url" to url) | ||
| 155 | - | ||
| 156 | -val Barcode.WiFi.data: Map<String, Any?> | ||
| 157 | - get() = mapOf("encryptionType" to encryptionType, "password" to password, "ssid" to ssid) |
| 1 | package dev.steenbakker.mobile_scanner.exceptions | 1 | package dev.steenbakker.mobile_scanner.exceptions |
| 2 | 2 | ||
| 3 | -internal class NoPermissionException : RuntimeException() | ||
| 3 | +internal class NoPermissionException : RuntimeException() | ||
| 4 | + | ||
| 5 | +//internal class Exception(val reason: Reason) : | ||
| 6 | +// java.lang.Exception("Mobile Scanner failed because $reason") { | ||
| 7 | +// | ||
| 8 | +// internal enum class Reason { | ||
| 9 | +// noHardware, noPermissions, noBackCamera | ||
| 10 | +// } | ||
| 11 | +//} |
| 1 | -package dev.steenbakker.mobile_scanner.old | ||
| 2 | - | ||
| 3 | -import android.Manifest | ||
| 4 | -import android.annotation.SuppressLint | ||
| 5 | -import android.app.Activity | ||
| 6 | -import android.content.Context | ||
| 7 | -import android.content.pm.PackageManager | ||
| 8 | -import android.util.Log | ||
| 9 | -import android.view.Surface | ||
| 10 | -import androidx.camera.core.* | ||
| 11 | -import androidx.camera.lifecycle.ProcessCameraProvider | ||
| 12 | -import androidx.core.content.ContextCompat | ||
| 13 | -import androidx.lifecycle.LifecycleOwner | ||
| 14 | -import com.google.mlkit.vision.barcode.BarcodeScannerOptions | ||
| 15 | -import com.google.mlkit.vision.barcode.BarcodeScanning | ||
| 16 | -import com.google.mlkit.vision.common.InputImage | ||
| 17 | -import dev.steenbakker.mobile_scanner.exceptions.NoPermissionException | ||
| 18 | -import io.flutter.plugin.common.MethodChannel | ||
| 19 | -import io.flutter.view.TextureRegistry | ||
| 20 | -import java.io.IOException | ||
| 21 | - | ||
| 22 | -internal class MobileScanner( | ||
| 23 | - private val context: Activity, | ||
| 24 | - private val texture: TextureRegistry | ||
| 25 | -) { | ||
| 26 | - | ||
| 27 | - private var cameraProvider: ProcessCameraProvider? = null | ||
| 28 | - private var camera: Camera? = null | ||
| 29 | - private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null | ||
| 30 | - | ||
| 31 | - @ExperimentalGetImage | ||
| 32 | - @Throws(IOException::class, NoPermissionException::class, Exception::class) | ||
| 33 | - fun start( | ||
| 34 | - result: MethodChannel.Result, | ||
| 35 | - options: BarcodeScannerOptions?, | ||
| 36 | - channel: MethodChannel | ||
| 37 | - ) { | ||
| 38 | - if (!hasCameraHardware(context)) { | ||
| 39 | - throw Exception(Exception.Reason.noHardware) | ||
| 40 | - } | ||
| 41 | - if (!checkCameraPermission(context)) { | ||
| 42 | - throw NoPermissionException() | ||
| 43 | - } | ||
| 44 | - | ||
| 45 | - textureEntry = texture.createSurfaceTexture() | ||
| 46 | - val textureId = textureEntry!!.id() | ||
| 47 | - | ||
| 48 | - val future = ProcessCameraProvider.getInstance(context) | ||
| 49 | - val executor = ContextCompat.getMainExecutor(context) | ||
| 50 | - future.addListener({ | ||
| 51 | - | ||
| 52 | - // Preview | ||
| 53 | - val surfaceProvider = Preview.SurfaceProvider { request -> | ||
| 54 | - val resolution = request.resolution | ||
| 55 | - val texture = textureEntry!!.surfaceTexture() | ||
| 56 | - texture.setDefaultBufferSize(resolution.width, resolution.height) | ||
| 57 | - val surface = Surface(texture) | ||
| 58 | - request.provideSurface(surface, executor, { }) | ||
| 59 | - } | ||
| 60 | - | ||
| 61 | - val preview = Preview.Builder().build().apply { setSurfaceProvider(surfaceProvider) } | ||
| 62 | - val analyzer = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format | ||
| 63 | - val mediaImage = imageProxy.image ?: return@Analyzer | ||
| 64 | - val inputImage = | ||
| 65 | - InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) | ||
| 66 | - val scanner = if (options != null) { | ||
| 67 | - BarcodeScanning.getClient(options) | ||
| 68 | - } else { | ||
| 69 | - BarcodeScanning.getClient() | ||
| 70 | - } | ||
| 71 | - | ||
| 72 | - scanner.process(inputImage) | ||
| 73 | - .addOnSuccessListener { barcodes -> | ||
| 74 | - val barcodeList: MutableList<Map<String, Any?>> = mutableListOf() | ||
| 75 | - for (barcode in barcodes) { | ||
| 76 | - barcodeList.add( | ||
| 77 | - mapOf( | ||
| 78 | - "value" to barcode.rawValue, | ||
| 79 | - "bytes" to barcode.rawBytes | ||
| 80 | - ) | ||
| 81 | - ) | ||
| 82 | - | ||
| 83 | - } | ||
| 84 | - channel.invokeMethod("qrRead", barcodeList) | ||
| 85 | - } | ||
| 86 | - .addOnFailureListener { e -> Log.e("Camera", e.message, e) } | ||
| 87 | - .addOnCompleteListener { imageProxy.close() } | ||
| 88 | - } | ||
| 89 | - val analysis = ImageAnalysis.Builder() | ||
| 90 | - .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) | ||
| 91 | - .build().apply { setAnalyzer(executor, analyzer) } | ||
| 92 | - // Bind to lifecycle. | ||
| 93 | - val owner = context as LifecycleOwner | ||
| 94 | -// val selector = | ||
| 95 | -// if (call.arguments == 0) CameraSelector.DEFAULT_FRONT_CAMERA | ||
| 96 | -// else CameraSelector.DEFAULT_BACK_CAMERA | ||
| 97 | - camera = cameraProvider!!.bindToLifecycle( | ||
| 98 | - owner, | ||
| 99 | - CameraSelector.DEFAULT_BACK_CAMERA, | ||
| 100 | - preview, | ||
| 101 | - analysis | ||
| 102 | - ) | ||
| 103 | - camera!!.cameraInfo.torchState.observe(owner, { state -> | ||
| 104 | - // TorchState.OFF = 0; TorchState.ON = 1 | ||
| 105 | -// val event = mapOf("name" to "torchState", "data" to state) | ||
| 106 | -// sink?.success(event) | ||
| 107 | - }) | ||
| 108 | - // TODO: seems there's not a better way to get the final resolution | ||
| 109 | - @SuppressLint("RestrictedApi") | ||
| 110 | - val resolution = preview.attachedSurfaceResolution!! | ||
| 111 | - val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0 | ||
| 112 | - val width = resolution.width.toDouble() | ||
| 113 | - val height = resolution.height.toDouble() | ||
| 114 | - val size = if (portrait) mapOf( | ||
| 115 | - "width" to width, | ||
| 116 | - "height" to height | ||
| 117 | - ) else mapOf("width" to height, "height" to width) | ||
| 118 | - result.success(mapOf("textureId" to textureId, "size" to size)) | ||
| 119 | - }, executor) | ||
| 120 | - } | ||
| 121 | - | ||
| 122 | - fun stop() { | ||
| 123 | - val owner = context as LifecycleOwner | ||
| 124 | - camera!!.cameraInfo.torchState.removeObservers(owner) | ||
| 125 | - cameraProvider!!.unbindAll() | ||
| 126 | - textureEntry!!.release() | ||
| 127 | - | ||
| 128 | - camera = null | ||
| 129 | - textureEntry = null | ||
| 130 | - cameraProvider = null | ||
| 131 | - } | ||
| 132 | - | ||
| 133 | - private fun hasCameraHardware(context: Context): Boolean { | ||
| 134 | - return context.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) | ||
| 135 | - } | ||
| 136 | - | ||
| 137 | - private fun checkCameraPermission(context: Context): Boolean { | ||
| 138 | - val permissions = arrayOf(Manifest.permission.CAMERA) | ||
| 139 | - val res = context.checkCallingOrSelfPermission(permissions[0]) | ||
| 140 | - return res == PackageManager.PERMISSION_GRANTED | ||
| 141 | - } | ||
| 142 | - | ||
| 143 | - internal class Exception(val reason: Reason) : | ||
| 144 | - java.lang.Exception("Mobile Scanner failed because $reason") { | ||
| 145 | - | ||
| 146 | - internal enum class Reason { | ||
| 147 | - noHardware, noPermissions, noBackCamera | ||
| 148 | - } | ||
| 149 | - } | ||
| 150 | -} |
android/src/main/kotlin/dev/steenbakker/mobile_scanner/old/MobileScannerPlugin.kt
deleted
100644 → 0
| 1 | -package dev.steenbakker.mobile_scanner.old | ||
| 2 | - | ||
| 3 | -import android.Manifest | ||
| 4 | -import android.app.Activity | ||
| 5 | -import android.content.pm.PackageManager | ||
| 6 | -import android.util.Log | ||
| 7 | -import androidx.annotation.NonNull | ||
| 8 | -import androidx.camera.core.ExperimentalGetImage | ||
| 9 | -import androidx.core.app.ActivityCompat | ||
| 10 | -import dev.steenbakker.mobile_scanner.exceptions.NoPermissionException | ||
| 11 | - | ||
| 12 | -import io.flutter.embedding.engine.plugins.FlutterPlugin | ||
| 13 | -import io.flutter.embedding.engine.plugins.activity.ActivityAware | ||
| 14 | -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding | ||
| 15 | -import io.flutter.plugin.common.EventChannel | ||
| 16 | -import io.flutter.plugin.common.MethodCall | ||
| 17 | -import io.flutter.plugin.common.MethodChannel | ||
| 18 | -import io.flutter.plugin.common.MethodChannel.MethodCallHandler | ||
| 19 | -import io.flutter.plugin.common.MethodChannel.Result | ||
| 20 | -import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener | ||
| 21 | -import io.flutter.view.TextureRegistry | ||
| 22 | -import java.io.IOException | ||
| 23 | - | ||
| 24 | -/** MobileScannerPlugin */ | ||
| 25 | -class MobileScannerPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, | ||
| 26 | - RequestPermissionsResultListener, EventChannel.StreamHandler { | ||
| 27 | - | ||
| 28 | - private lateinit var textures: TextureRegistry | ||
| 29 | - private lateinit var channel : MethodChannel | ||
| 30 | - private lateinit var event : EventChannel | ||
| 31 | - private var activity: Activity? = null | ||
| 32 | - private var waitingForPermissionResult = false | ||
| 33 | - private var sink: EventChannel.EventSink? = null | ||
| 34 | - | ||
| 35 | - private var mobileScanner: MobileScanner? = null | ||
| 36 | - | ||
| 37 | - override fun onAttachedToActivity(binding: ActivityPluginBinding) { | ||
| 38 | - activity = binding.activity | ||
| 39 | - } | ||
| 40 | - | ||
| 41 | - override fun onDetachedFromActivityForConfigChanges() { | ||
| 42 | - onDetachedFromActivity() | ||
| 43 | - } | ||
| 44 | - | ||
| 45 | - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { | ||
| 46 | - onAttachedToActivity(binding) | ||
| 47 | - } | ||
| 48 | - | ||
| 49 | - override fun onDetachedFromActivity() { | ||
| 50 | - activity = null | ||
| 51 | - } | ||
| 52 | - | ||
| 53 | - | ||
| 54 | - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { | ||
| 55 | - channel.setMethodCallHandler(null) | ||
| 56 | - } | ||
| 57 | - | ||
| 58 | - | ||
| 59 | - override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { | ||
| 60 | - textures = flutterPluginBinding.textureRegistry | ||
| 61 | - channel = MethodChannel(flutterPluginBinding.binaryMessenger, "dev.steenbakker.mobile_scanner/scanner/method") | ||
| 62 | - event = EventChannel(flutterPluginBinding.binaryMessenger, "dev.steenbakker.mobile_scanner/scanner/event") | ||
| 63 | - channel.setMethodCallHandler(this) | ||
| 64 | - event.setStreamHandler(this) | ||
| 65 | - } | ||
| 66 | - | ||
| 67 | - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { | ||
| 68 | - this.sink = events | ||
| 69 | - } | ||
| 70 | - | ||
| 71 | - override fun onCancel(arguments: Any?) { | ||
| 72 | - sink = null | ||
| 73 | - } | ||
| 74 | - | ||
| 75 | - | ||
| 76 | - override fun onRequestPermissionsResult( | ||
| 77 | - requestCode: Int, | ||
| 78 | - permissions: Array<String?>?, | ||
| 79 | - grantResults: IntArray | ||
| 80 | - ): Boolean { | ||
| 81 | - if (requestCode == 105505) { | ||
| 82 | - waitingForPermissionResult = false | ||
| 83 | - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { | ||
| 84 | - Log.i( | ||
| 85 | - "mobile_scanner", | ||
| 86 | - "Permissions request granted." | ||
| 87 | - ) | ||
| 88 | - mobileScanner?.stop() | ||
| 89 | - } else { | ||
| 90 | - Log.i( | ||
| 91 | - "mobile_scanner", | ||
| 92 | - "Permissions request denied." | ||
| 93 | - ) | ||
| 94 | - mobileScanner?.stop() | ||
| 95 | - } | ||
| 96 | - return true | ||
| 97 | - } | ||
| 98 | - return false | ||
| 99 | - } | ||
| 100 | - | ||
| 101 | - @ExperimentalGetImage | ||
| 102 | - override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { | ||
| 103 | - when (call.method) { | ||
| 104 | - "start" -> { | ||
| 105 | -// val targetWidth: Int? = call.argument<Int>("targetWidth") | ||
| 106 | -// val targetHeight: Int? = call.argument<Int>("targetHeight") | ||
| 107 | -// val formatStrings: List<String>? = call.argument<List<String>>("formats") | ||
| 108 | -// if (targetWidth == null || targetHeight == null) { | ||
| 109 | -// result.error( | ||
| 110 | -// "INVALID_ARGUMENT", | ||
| 111 | -// "Missing a required argument", | ||
| 112 | -// "Expecting targetWidth, targetHeight" | ||
| 113 | -// ) | ||
| 114 | -// return | ||
| 115 | -// } | ||
| 116 | - | ||
| 117 | -// val options: BarcodeScannerOptions = BarcodeFormats.optionsFromStringList(formatStrings) | ||
| 118 | - | ||
| 119 | -// mobileScanner ?: | ||
| 120 | - | ||
| 121 | - try { | ||
| 122 | - MobileScanner(activity!!, textures).start(result, null, channel) | ||
| 123 | - } catch (e: IOException) { | ||
| 124 | - e.printStackTrace() | ||
| 125 | - result.error( | ||
| 126 | - "IOException", | ||
| 127 | - "Error starting camera because of IOException: " + e.localizedMessage, | ||
| 128 | - null | ||
| 129 | - ) | ||
| 130 | - } catch (e: MobileScanner.Exception) { | ||
| 131 | - e.printStackTrace() | ||
| 132 | - result.error( | ||
| 133 | - e.reason.name, | ||
| 134 | - "Error starting camera for reason: " + e.reason.name, | ||
| 135 | - null | ||
| 136 | - ) | ||
| 137 | - } catch (e: NoPermissionException) { | ||
| 138 | - waitingForPermissionResult = true | ||
| 139 | - ActivityCompat.requestPermissions( | ||
| 140 | - activity!!, | ||
| 141 | - arrayOf(Manifest.permission.CAMERA), | ||
| 142 | - 105505 | ||
| 143 | - ) | ||
| 144 | - } | ||
| 145 | - } | ||
| 146 | - "stop" -> { | ||
| 147 | - if (mobileScanner != null && !waitingForPermissionResult) { | ||
| 148 | - mobileScanner!!.stop() | ||
| 149 | - } | ||
| 150 | - result.success(null) | ||
| 151 | - } | ||
| 152 | - else -> result.notImplemented() | ||
| 153 | - } | ||
| 154 | - } | ||
| 155 | - | ||
| 156 | -} |
| @@ -16,57 +16,95 @@ class AnalyzeView extends StatefulWidget { | @@ -16,57 +16,95 @@ class AnalyzeView extends StatefulWidget { | ||
| 16 | 16 | ||
| 17 | class _AnalyzeViewState extends State<AnalyzeView> | 17 | class _AnalyzeViewState extends State<AnalyzeView> |
| 18 | with SingleTickerProviderStateMixin { | 18 | with SingleTickerProviderStateMixin { |
| 19 | - List<Offset> points = []; | 19 | + String? barcode; |
| 20 | 20 | ||
| 21 | - // CameraController cameraController = CameraController(context, width: 320, height: 150); | ||
| 22 | - | ||
| 23 | - String? barcode = null; | 21 | + MobileScannerController controller = MobileScannerController(torchEnabled: true, |
| 22 | + facing: CameraFacing.front,); | ||
| 24 | 23 | ||
| 25 | @override | 24 | @override |
| 26 | Widget build(BuildContext context) { | 25 | Widget build(BuildContext context) { |
| 27 | return MaterialApp( | 26 | return MaterialApp( |
| 28 | home: Scaffold( | 27 | home: Scaffold( |
| 28 | + backgroundColor: Colors.black, | ||
| 29 | body: Builder(builder: (context) { | 29 | body: Builder(builder: (context) { |
| 30 | return Stack( | 30 | return Stack( |
| 31 | children: [ | 31 | children: [ |
| 32 | MobileScanner( | 32 | MobileScanner( |
| 33 | - // fitScreen: false, | ||
| 34 | - // controller: cameraController, | 33 | + controller: controller, |
| 34 | + fit: BoxFit.contain, | ||
| 35 | + // controller: MobileScannerController( | ||
| 36 | + // torchEnabled: true, | ||
| 37 | + // facing: CameraFacing.front, | ||
| 38 | + // ), | ||
| 35 | onDetect: (barcode, args) { | 39 | onDetect: (barcode, args) { |
| 36 | if (this.barcode != barcode.rawValue) { | 40 | if (this.barcode != barcode.rawValue) { |
| 37 | - this.barcode = barcode.rawValue; | ||
| 38 | - if (barcode.corners != null) { | ||
| 39 | - ScaffoldMessenger.of(context).showSnackBar(SnackBar( | ||
| 40 | - content: Text('${barcode.rawValue}'), | ||
| 41 | - duration: const Duration(milliseconds: 200), | ||
| 42 | - animation: null, | ||
| 43 | - )); | ||
| 44 | - setState(() { | ||
| 45 | - final List<Offset> points = []; | ||
| 46 | - double factorWidth = args.size.width / 520; | ||
| 47 | - // double factorHeight = wanted / args.size.height; | ||
| 48 | - final size = MediaQuery.of(context).devicePixelRatio; | ||
| 49 | - debugPrint('Size: ${barcode.corners}'); | ||
| 50 | - for (var point in barcode.corners!) { | ||
| 51 | - final adjustedWith = point.dx ; | ||
| 52 | - final adjustedHeight= point.dy ; | ||
| 53 | - points.add(Offset(adjustedWith / size, adjustedHeight / size)); | ||
| 54 | - // points.add(Offset((point.dx ) / size, | ||
| 55 | - // (point.dy) / size)); | ||
| 56 | - // final differenceWidth = (args.wantedSize!.width - args.size.width) / 2; | ||
| 57 | - // final differenceHeight = (args.wantedSize!.height - args.size.height) / 2; | ||
| 58 | - // points.add(Offset((point.dx + differenceWidth) / size, | ||
| 59 | - // (point.dy + differenceHeight) / size)); | ||
| 60 | - } | ||
| 61 | - this.points = points; | ||
| 62 | - }); | ||
| 63 | - } | 41 | + setState(() { |
| 42 | + this.barcode = barcode.rawValue; | ||
| 43 | + }); | ||
| 64 | } | 44 | } |
| 65 | - // Default 640 x480 | ||
| 66 | }), | 45 | }), |
| 67 | - CustomPaint( | ||
| 68 | - painter: OpenPainter(points), | 46 | + Align( |
| 47 | + alignment: Alignment.bottomCenter, | ||
| 48 | + child: Container( | ||
| 49 | + alignment: Alignment.bottomCenter, | ||
| 50 | + height: 100, | ||
| 51 | + color: Colors.black.withOpacity(0.4), | ||
| 52 | + child: Row( | ||
| 53 | + crossAxisAlignment: CrossAxisAlignment.center, | ||
| 54 | + mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||
| 55 | + children: [ | ||
| 56 | + IconButton( | ||
| 57 | + color: Colors.white, | ||
| 58 | + icon: ValueListenableBuilder( | ||
| 59 | + valueListenable: controller.torchState, | ||
| 60 | + builder: (context, state, child) { | ||
| 61 | + switch (state as TorchState) { | ||
| 62 | + case TorchState.off: | ||
| 63 | + return const Icon(Icons.flash_off, color: Colors.grey); | ||
| 64 | + case TorchState.on: | ||
| 65 | + return const Icon(Icons.flash_on, color: Colors.yellow); | ||
| 66 | + } | ||
| 67 | + }, | ||
| 68 | + ), | ||
| 69 | + iconSize: 32.0, | ||
| 70 | + onPressed: () => controller.toggleTorch(), | ||
| 71 | + ), | ||
| 72 | + Center( | ||
| 73 | + child: SizedBox( | ||
| 74 | + width: MediaQuery.of(context).size.width - 120, | ||
| 75 | + height: 50, | ||
| 76 | + child: FittedBox( | ||
| 77 | + child: Text( | ||
| 78 | + barcode ?? 'Scan something!', | ||
| 79 | + overflow: TextOverflow.fade, | ||
| 80 | + style: Theme.of(context) | ||
| 81 | + .textTheme | ||
| 82 | + .headline4! | ||
| 83 | + .copyWith(color: Colors.white), | ||
| 84 | + ), | ||
| 85 | + ), | ||
| 86 | + ), | ||
| 87 | + ), | ||
| 88 | + IconButton( | ||
| 89 | + color: Colors.white, | ||
| 90 | + icon: ValueListenableBuilder( | ||
| 91 | + valueListenable: controller.cameraFacingState, | ||
| 92 | + builder: (context, state, child) { | ||
| 93 | + if (state == CameraFacing.front) { | ||
| 94 | + return const Icon(Icons.camera_front); | ||
| 95 | + } else { | ||
| 96 | + return const Icon(Icons.camera_rear); | ||
| 97 | + } | ||
| 98 | + }, | ||
| 99 | + ), | ||
| 100 | + iconSize: 32.0, | ||
| 101 | + onPressed: () => controller.switchCamera(), | ||
| 102 | + ), | ||
| 103 | + ], | ||
| 104 | + ), | ||
| 105 | + ), | ||
| 69 | ), | 106 | ), |
| 107 | + | ||
| 70 | // Container( | 108 | // Container( |
| 71 | // alignment: Alignment.bottomCenter, | 109 | // alignment: Alignment.bottomCenter, |
| 72 | // margin: EdgeInsets.only(bottom: 80.0), | 110 | // margin: EdgeInsets.only(bottom: 80.0), |
| @@ -101,36 +139,6 @@ class _AnalyzeViewState extends State<AnalyzeView> | @@ -101,36 +139,6 @@ class _AnalyzeViewState extends State<AnalyzeView> | ||
| 101 | } | 139 | } |
| 102 | } | 140 | } |
| 103 | 141 | ||
| 104 | -class OpenPainter extends CustomPainter { | ||
| 105 | - final List<Offset> points; | ||
| 106 | - | ||
| 107 | - OpenPainter(this.points); | ||
| 108 | - @override | ||
| 109 | - void paint(Canvas canvas, Size size) { | ||
| 110 | - var paint1 = Paint() | ||
| 111 | - ..color = Color(0xff63aa65) | ||
| 112 | - ..strokeWidth = 10; | ||
| 113 | - //draw points on canvas | ||
| 114 | - canvas.drawPoints(PointMode.points, points, paint1); | ||
| 115 | - } | ||
| 116 | - | ||
| 117 | - @override | ||
| 118 | - bool shouldRepaint(CustomPainter oldDelegate) => true; | ||
| 119 | -} | ||
| 120 | - | ||
| 121 | -class OpacityCurve extends Curve { | ||
| 122 | - @override | ||
| 123 | - double transform(double t) { | ||
| 124 | - if (t < 0.1) { | ||
| 125 | - return t * 10; | ||
| 126 | - } else if (t <= 0.9) { | ||
| 127 | - return 1.0; | ||
| 128 | - } else { | ||
| 129 | - return (1.0 - t) * 10; | ||
| 130 | - } | ||
| 131 | - } | ||
| 132 | -} | ||
| 133 | - | ||
| 134 | // import 'package:flutter/material.dart'; | 142 | // import 'package:flutter/material.dart'; |
| 135 | // import 'package:flutter/rendering.dart'; | 143 | // import 'package:flutter/rendering.dart'; |
| 136 | // import 'package:mobile_scanner/mobile_scanner.dart'; | 144 | // import 'package:mobile_scanner/mobile_scanner.dart'; |
example/lib/mobile_scanner_overlay.dart
0 → 100644
| 1 | +import 'dart:ui'; | ||
| 2 | + | ||
| 3 | +import 'package:flutter/material.dart'; | ||
| 4 | +import 'package:mobile_scanner/mobile_scanner.dart'; | ||
| 5 | + | ||
| 6 | +void main() { | ||
| 7 | + runApp(const AnalyzeView()); | ||
| 8 | +} | ||
| 9 | + | ||
| 10 | +class AnalyzeView extends StatefulWidget { | ||
| 11 | + const AnalyzeView({Key? key}) : super(key: key); | ||
| 12 | + | ||
| 13 | + @override | ||
| 14 | + _AnalyzeViewState createState() => _AnalyzeViewState(); | ||
| 15 | +} | ||
| 16 | + | ||
| 17 | +class _AnalyzeViewState extends State<AnalyzeView> | ||
| 18 | + with SingleTickerProviderStateMixin { | ||
| 19 | + List<Offset> points = []; | ||
| 20 | + | ||
| 21 | + // CameraController cameraController = CameraController(context, width: 320, height: 150); | ||
| 22 | + | ||
| 23 | + String? barcode = null; | ||
| 24 | + | ||
| 25 | + @override | ||
| 26 | + Widget build(BuildContext context) { | ||
| 27 | + return MaterialApp( | ||
| 28 | + home: Scaffold( | ||
| 29 | + body: Builder(builder: (context) { | ||
| 30 | + return Stack( | ||
| 31 | + children: [ | ||
| 32 | + MobileScanner( | ||
| 33 | + // fitScreen: false, | ||
| 34 | + // controller: cameraController, | ||
| 35 | + onDetect: (barcode, args) { | ||
| 36 | + if (this.barcode != barcode.rawValue) { | ||
| 37 | + this.barcode = barcode.rawValue; | ||
| 38 | + if (barcode.corners != null) { | ||
| 39 | + ScaffoldMessenger.of(context).showSnackBar(SnackBar( | ||
| 40 | + content: Text('${barcode.rawValue}'), | ||
| 41 | + duration: const Duration(milliseconds: 200), | ||
| 42 | + animation: null, | ||
| 43 | + )); | ||
| 44 | + setState(() { | ||
| 45 | + final List<Offset> points = []; | ||
| 46 | + double factorWidth = args.size.width / 520; | ||
| 47 | + // double factorHeight = wanted / args.size.height; | ||
| 48 | + final size = MediaQuery.of(context).devicePixelRatio; | ||
| 49 | + debugPrint('Size: ${barcode.corners}'); | ||
| 50 | + for (var point in barcode.corners!) { | ||
| 51 | + final adjustedWith = point.dx ; | ||
| 52 | + final adjustedHeight= point.dy ; | ||
| 53 | + points.add(Offset(adjustedWith / size, adjustedHeight / size)); | ||
| 54 | + // points.add(Offset((point.dx ) / size, | ||
| 55 | + // (point.dy) / size)); | ||
| 56 | + // final differenceWidth = (args.wantedSize!.width - args.size.width) / 2; | ||
| 57 | + // final differenceHeight = (args.wantedSize!.height - args.size.height) / 2; | ||
| 58 | + // points.add(Offset((point.dx + differenceWidth) / size, | ||
| 59 | + // (point.dy + differenceHeight) / size)); | ||
| 60 | + } | ||
| 61 | + this.points = points; | ||
| 62 | + }); | ||
| 63 | + } | ||
| 64 | + } | ||
| 65 | + // Default 640 x480 | ||
| 66 | + }), | ||
| 67 | + CustomPaint( | ||
| 68 | + painter: OpenPainter(points), | ||
| 69 | + ), | ||
| 70 | + // Container( | ||
| 71 | + // alignment: Alignment.bottomCenter, | ||
| 72 | + // margin: EdgeInsets.only(bottom: 80.0), | ||
| 73 | + // child: IconButton( | ||
| 74 | + // icon: ValueListenableBuilder( | ||
| 75 | + // valueListenable: cameraController.torchState, | ||
| 76 | + // builder: (context, state, child) { | ||
| 77 | + // final color = | ||
| 78 | + // state == TorchState.off ? Colors.grey : Colors.white; | ||
| 79 | + // return Icon(Icons.bolt, color: color); | ||
| 80 | + // }, | ||
| 81 | + // ), | ||
| 82 | + // iconSize: 32.0, | ||
| 83 | + // onPressed: () => cameraController.torch(), | ||
| 84 | + // ), | ||
| 85 | + // ), | ||
| 86 | + ], | ||
| 87 | + ); | ||
| 88 | + }), | ||
| 89 | + ), | ||
| 90 | + ); | ||
| 91 | + } | ||
| 92 | + | ||
| 93 | + @override | ||
| 94 | + void dispose() { | ||
| 95 | + // cameraController.dispose(); | ||
| 96 | + super.dispose(); | ||
| 97 | + } | ||
| 98 | + | ||
| 99 | + void display(Barcode barcode) { | ||
| 100 | + Navigator.of(context).popAndPushNamed('display', arguments: barcode); | ||
| 101 | + } | ||
| 102 | +} | ||
| 103 | + | ||
| 104 | +class OpenPainter extends CustomPainter { | ||
| 105 | + final List<Offset> points; | ||
| 106 | + | ||
| 107 | + OpenPainter(this.points); | ||
| 108 | + @override | ||
| 109 | + void paint(Canvas canvas, Size size) { | ||
| 110 | + var paint1 = Paint() | ||
| 111 | + ..color = Color(0xff63aa65) | ||
| 112 | + ..strokeWidth = 10; | ||
| 113 | + //draw points on canvas | ||
| 114 | + canvas.drawPoints(PointMode.points, points, paint1); | ||
| 115 | + } | ||
| 116 | + | ||
| 117 | + @override | ||
| 118 | + bool shouldRepaint(CustomPainter oldDelegate) => true; | ||
| 119 | +} | ||
| 120 | + | ||
| 121 | +class OpacityCurve extends Curve { | ||
| 122 | + @override | ||
| 123 | + double transform(double t) { | ||
| 124 | + if (t < 0.1) { | ||
| 125 | + return t * 10; | ||
| 126 | + } else if (t <= 0.9) { | ||
| 127 | + return 1.0; | ||
| 128 | + } else { | ||
| 129 | + return (1.0 - t) * 10; | ||
| 130 | + } | ||
| 131 | + } | ||
| 132 | +} | ||
| 133 | + | ||
| 134 | +// import 'package:flutter/material.dart'; | ||
| 135 | +// import 'package:flutter/rendering.dart'; | ||
| 136 | +// import 'package:mobile_scanner/mobile_scanner.dart'; | ||
| 137 | +// | ||
| 138 | +// void main() { | ||
| 139 | +// debugPaintSizeEnabled = false; | ||
| 140 | +// runApp(HomePage()); | ||
| 141 | +// } | ||
| 142 | +// | ||
| 143 | +// class HomePage extends StatefulWidget { | ||
| 144 | +// @override | ||
| 145 | +// HomeState createState() => HomeState(); | ||
| 146 | +// } | ||
| 147 | +// | ||
| 148 | +// class HomeState extends State<HomePage> { | ||
| 149 | +// @override | ||
| 150 | +// Widget build(BuildContext context) { | ||
| 151 | +// return MaterialApp(home: MyApp()); | ||
| 152 | +// } | ||
| 153 | +// } | ||
| 154 | +// | ||
| 155 | +// class MyApp extends StatefulWidget { | ||
| 156 | +// @override | ||
| 157 | +// _MyAppState createState() => _MyAppState(); | ||
| 158 | +// } | ||
| 159 | +// | ||
| 160 | +// class _MyAppState extends State<MyApp> { | ||
| 161 | +// String? qr; | ||
| 162 | +// bool camState = false; | ||
| 163 | +// | ||
| 164 | +// @override | ||
| 165 | +// initState() { | ||
| 166 | +// super.initState(); | ||
| 167 | +// } | ||
| 168 | +// | ||
| 169 | +// @override | ||
| 170 | +// Widget build(BuildContext context) { | ||
| 171 | +// return Scaffold( | ||
| 172 | +// appBar: AppBar( | ||
| 173 | +// title: Text('Plugin example app'), | ||
| 174 | +// ), | ||
| 175 | +// body: Center( | ||
| 176 | +// child: Column( | ||
| 177 | +// crossAxisAlignment: CrossAxisAlignment.center, | ||
| 178 | +// mainAxisAlignment: MainAxisAlignment.center, | ||
| 179 | +// children: <Widget>[ | ||
| 180 | +// Expanded( | ||
| 181 | +// child: camState | ||
| 182 | +// ? Center( | ||
| 183 | +// child: SizedBox( | ||
| 184 | +// width: 300.0, | ||
| 185 | +// height: 600.0, | ||
| 186 | +// child: MobileScanner( | ||
| 187 | +// onError: (context, error) => Text( | ||
| 188 | +// error.toString(), | ||
| 189 | +// style: TextStyle(color: Colors.red), | ||
| 190 | +// ), | ||
| 191 | +// qrCodeCallback: (code) { | ||
| 192 | +// setState(() { | ||
| 193 | +// qr = code; | ||
| 194 | +// }); | ||
| 195 | +// }, | ||
| 196 | +// child: Container( | ||
| 197 | +// decoration: BoxDecoration( | ||
| 198 | +// color: Colors.transparent, | ||
| 199 | +// border: Border.all( | ||
| 200 | +// color: Colors.orange, | ||
| 201 | +// width: 10.0, | ||
| 202 | +// style: BorderStyle.solid), | ||
| 203 | +// ), | ||
| 204 | +// ), | ||
| 205 | +// ), | ||
| 206 | +// ), | ||
| 207 | +// ) | ||
| 208 | +// : Center(child: Text("Camera inactive"))), | ||
| 209 | +// Text("QRCODE: $qr"), | ||
| 210 | +// ], | ||
| 211 | +// ), | ||
| 212 | +// ), | ||
| 213 | +// floatingActionButton: FloatingActionButton( | ||
| 214 | +// child: Text( | ||
| 215 | +// "press me", | ||
| 216 | +// textAlign: TextAlign.center, | ||
| 217 | +// ), | ||
| 218 | +// onPressed: () { | ||
| 219 | +// setState(() { | ||
| 220 | +// camState = !camState; | ||
| 221 | +// }); | ||
| 222 | +// }), | ||
| 223 | +// ); | ||
| 224 | +// } | ||
| 225 | +// } |
| @@ -3,18 +3,22 @@ import 'package:mobile_scanner/mobile_scanner.dart'; | @@ -3,18 +3,22 @@ import 'package:mobile_scanner/mobile_scanner.dart'; | ||
| 3 | 3 | ||
| 4 | import 'mobile_scanner_arguments.dart'; | 4 | import 'mobile_scanner_arguments.dart'; |
| 5 | 5 | ||
| 6 | +enum Ratio { | ||
| 7 | + ratio_4_3, | ||
| 8 | + ratio_16_9 | ||
| 9 | +} | ||
| 10 | + | ||
| 6 | /// A widget showing a live camera preview. | 11 | /// A widget showing a live camera preview. |
| 7 | class MobileScanner extends StatefulWidget { | 12 | class MobileScanner extends StatefulWidget { |
| 8 | /// The controller of the camera. | 13 | /// The controller of the camera. |
| 9 | final MobileScannerController? controller; | 14 | final MobileScannerController? controller; |
| 10 | final Function(Barcode barcode, MobileScannerArguments args)? onDetect; | 15 | final Function(Barcode barcode, MobileScannerArguments args)? onDetect; |
| 11 | - final bool fitScreen; | ||
| 12 | - final bool fitWidth; | 16 | + final BoxFit fit; |
| 13 | 17 | ||
| 14 | /// Create a [MobileScanner] with a [controller], the [controller] must has been initialized. | 18 | /// Create a [MobileScanner] with a [controller], the [controller] must has been initialized. |
| 15 | const MobileScanner( | 19 | const MobileScanner( |
| 16 | - {Key? key, this.onDetect, this.controller, this.fitScreen = true, this.fitWidth = true}) | ||
| 17 | - : super(key: key); | 20 | + {Key? key, this.onDetect, this.controller, this.fit = BoxFit.cover}) |
| 21 | + : assert((controller != null )), super(key: key); | ||
| 18 | 22 | ||
| 19 | @override | 23 | @override |
| 20 | State<MobileScanner> createState() => _MobileScannerState(); | 24 | State<MobileScanner> createState() => _MobileScannerState(); |
| @@ -23,19 +27,28 @@ class MobileScanner extends StatefulWidget { | @@ -23,19 +27,28 @@ class MobileScanner extends StatefulWidget { | ||
| 23 | class _MobileScannerState extends State<MobileScanner> | 27 | class _MobileScannerState extends State<MobileScanner> |
| 24 | with WidgetsBindingObserver { | 28 | with WidgetsBindingObserver { |
| 25 | bool onScreen = true; | 29 | bool onScreen = true; |
| 26 | - MobileScannerController? controller; | 30 | + late MobileScannerController controller; |
| 31 | + | ||
| 32 | + @override | ||
| 33 | + void initState() { | ||
| 34 | + super.initState(); | ||
| 35 | + if (widget.controller == null) { | ||
| 36 | + controller = MobileScannerController(); | ||
| 37 | + } else { | ||
| 38 | + controller = widget.controller!; | ||
| 39 | + } | ||
| 40 | + } | ||
| 27 | 41 | ||
| 28 | @override | 42 | @override |
| 29 | void didChangeAppLifecycleState(AppLifecycleState state) { | 43 | void didChangeAppLifecycleState(AppLifecycleState state) { |
| 30 | if (state == AppLifecycleState.resumed) { | 44 | if (state == AppLifecycleState.resumed) { |
| 31 | setState(() => onScreen = true); | 45 | setState(() => onScreen = true); |
| 32 | } else { | 46 | } else { |
| 33 | - if (controller != null && onScreen) { | ||
| 34 | - controller!.stop(); | 47 | + if (onScreen) { |
| 48 | + controller.stop(); | ||
| 35 | } | 49 | } |
| 36 | setState(() { | 50 | setState(() { |
| 37 | onScreen = false; | 51 | onScreen = false; |
| 38 | - controller = null; | ||
| 39 | }); | 52 | }); |
| 40 | } | 53 | } |
| 41 | } | 54 | } |
| @@ -43,28 +56,28 @@ class _MobileScannerState extends State<MobileScanner> | @@ -43,28 +56,28 @@ class _MobileScannerState extends State<MobileScanner> | ||
| 43 | @override | 56 | @override |
| 44 | Widget build(BuildContext context) { | 57 | Widget build(BuildContext context) { |
| 45 | return LayoutBuilder(builder: (context, BoxConstraints constraints) { | 58 | return LayoutBuilder(builder: (context, BoxConstraints constraints) { |
| 46 | - final media = MediaQuery.of(context); | ||
| 47 | - | ||
| 48 | - controller ??= MobileScannerController(context, | ||
| 49 | - width: constraints.maxWidth, height: constraints.maxHeight); | ||
| 50 | if (!onScreen) return const Text("Camera Paused."); | 59 | if (!onScreen) return const Text("Camera Paused."); |
| 51 | return ValueListenableBuilder( | 60 | return ValueListenableBuilder( |
| 52 | - valueListenable: controller!.args, | 61 | + valueListenable: controller.args, |
| 53 | builder: (context, value, child) { | 62 | builder: (context, value, child) { |
| 54 | value = value as MobileScannerArguments?; | 63 | value = value as MobileScannerArguments?; |
| 55 | if (value == null) { | 64 | if (value == null) { |
| 56 | return Container(color: Colors.black); | 65 | return Container(color: Colors.black); |
| 57 | } else { | 66 | } else { |
| 58 | - controller!.barcodes.listen( | 67 | + controller.barcodes.listen( |
| 59 | (a) => widget.onDetect!(a, value as MobileScannerArguments)); | 68 | (a) => widget.onDetect!(a, value as MobileScannerArguments)); |
| 60 | - // Texture(textureId: value.textureId) | 69 | + debugPrint(' size MediaQuery ${MediaQuery.of(context).size}'); |
| 61 | return ClipRect( | 70 | return ClipRect( |
| 62 | - child: FittedBox( | ||
| 63 | - fit: BoxFit.cover, | ||
| 64 | - child: SizedBox( | ||
| 65 | - width: value.size.width, | ||
| 66 | - height: value.size.height, | ||
| 67 | - child: Texture(textureId: value.textureId), | 71 | + child: SizedBox( |
| 72 | + width: MediaQuery.of(context).size.width, | ||
| 73 | + height: MediaQuery.of(context).size.height, | ||
| 74 | + child: FittedBox( | ||
| 75 | + fit: widget.fit, | ||
| 76 | + child: SizedBox( | ||
| 77 | + width: value.size.width, | ||
| 78 | + height: value.size.height, | ||
| 79 | + child: Texture(textureId: value.textureId), | ||
| 80 | + ), | ||
| 68 | ), | 81 | ), |
| 69 | ), | 82 | ), |
| 70 | ); | 83 | ); |
| @@ -75,17 +88,7 @@ class _MobileScannerState extends State<MobileScanner> | @@ -75,17 +88,7 @@ class _MobileScannerState extends State<MobileScanner> | ||
| 75 | 88 | ||
| 76 | @override | 89 | @override |
| 77 | void dispose() { | 90 | void dispose() { |
| 78 | - controller?.dispose(); | 91 | + controller.dispose(); |
| 79 | super.dispose(); | 92 | super.dispose(); |
| 80 | } | 93 | } |
| 81 | -} | ||
| 82 | - | ||
| 83 | -extension on Size { | ||
| 84 | - double fill(Size targetSize) { | ||
| 85 | - if (targetSize.aspectRatio < aspectRatio) { | ||
| 86 | - return targetSize.height * aspectRatio / targetSize.width; | ||
| 87 | - } else { | ||
| 88 | - return targetSize.width / aspectRatio / targetSize.height; | ||
| 89 | - } | ||
| 90 | - } | ||
| 91 | -} | 94 | +} |
| @@ -8,9 +8,8 @@ class MobileScannerArguments { | @@ -8,9 +8,8 @@ class MobileScannerArguments { | ||
| 8 | /// Size of the texture. | 8 | /// Size of the texture. |
| 9 | final Size size; | 9 | final Size size; |
| 10 | 10 | ||
| 11 | - /// Size of the texture. | ||
| 12 | - final Size? wantedSize; | 11 | + final bool hasTorch; |
| 13 | 12 | ||
| 14 | /// Create a [MobileScannerArguments]. | 13 | /// Create a [MobileScannerArguments]. |
| 15 | - MobileScannerArguments({required this.textureId,required this.size, this.wantedSize}); | 14 | + MobileScannerArguments({required this.textureId,required this.size, required this.hasTorch}); |
| 16 | } | 15 | } |
| @@ -2,9 +2,9 @@ import 'dart:async'; | @@ -2,9 +2,9 @@ import 'dart:async'; | ||
| 2 | 2 | ||
| 3 | import 'package:flutter/cupertino.dart'; | 3 | import 'package:flutter/cupertino.dart'; |
| 4 | import 'package:flutter/services.dart'; | 4 | import 'package:flutter/services.dart'; |
| 5 | +import 'package:mobile_scanner/mobile_scanner.dart'; | ||
| 5 | 6 | ||
| 6 | import 'mobile_scanner_arguments.dart'; | 7 | import 'mobile_scanner_arguments.dart'; |
| 7 | -import 'objects/barcode.dart'; | ||
| 8 | import 'objects/barcode_utility.dart'; | 8 | import 'objects/barcode_utility.dart'; |
| 9 | 9 | ||
| 10 | /// The facing of a camera. | 10 | /// The facing of a camera. |
| @@ -16,11 +16,7 @@ enum CameraFacing { | @@ -16,11 +16,7 @@ enum CameraFacing { | ||
| 16 | back, | 16 | back, |
| 17 | } | 17 | } |
| 18 | 18 | ||
| 19 | -enum MobileScannerState { | ||
| 20 | - undetermined, | ||
| 21 | - authorized, | ||
| 22 | - denied | ||
| 23 | -} | 19 | +enum MobileScannerState { undetermined, authorized, denied } |
| 24 | 20 | ||
| 25 | /// The state of torch. | 21 | /// The state of torch. |
| 26 | enum TorchState { | 22 | enum TorchState { |
| @@ -31,85 +27,52 @@ enum TorchState { | @@ -31,85 +27,52 @@ enum TorchState { | ||
| 31 | on, | 27 | on, |
| 32 | } | 28 | } |
| 33 | 29 | ||
| 34 | - | ||
| 35 | - | ||
| 36 | -// /// A camera controller. | ||
| 37 | -// abstract class CameraController { | ||
| 38 | -// /// Arguments for [CameraView]. | ||
| 39 | -// ValueNotifier<CameraArgs?> get args; | ||
| 40 | -// | ||
| 41 | -// /// Torch state of the camera. | ||
| 42 | -// ValueNotifier<TorchState> get torchState; | ||
| 43 | -// | ||
| 44 | -// /// A stream of barcodes. | ||
| 45 | -// Stream<Barcode> get barcodes; | ||
| 46 | -// | ||
| 47 | -// /// Create a [CameraController]. | ||
| 48 | -// /// | ||
| 49 | -// /// [facing] target facing used to select camera. | ||
| 50 | -// /// | ||
| 51 | -// /// [formats] the barcode formats for image analyzer. | ||
| 52 | -// factory CameraController([CameraFacing facing = CameraFacing.back] ) => | ||
| 53 | -// _CameraController(facing); | ||
| 54 | -// | ||
| 55 | -// /// Start the camera asynchronously. | ||
| 56 | -// Future<void> start(); | ||
| 57 | -// | ||
| 58 | -// /// Switch the torch's state. | ||
| 59 | -// void torch(); | ||
| 60 | -// | ||
| 61 | -// /// Release the resources of the camera. | ||
| 62 | -// void dispose(); | ||
| 63 | -// } | 30 | +enum AnalyzeMode { none, barcode } |
| 64 | 31 | ||
| 65 | class MobileScannerController { | 32 | class MobileScannerController { |
| 33 | + MethodChannel methodChannel = | ||
| 34 | + const MethodChannel('dev.steenbakker.mobile_scanner/scanner/method'); | ||
| 35 | + EventChannel eventChannel = | ||
| 36 | + const EventChannel('dev.steenbakker.mobile_scanner/scanner/event'); | ||
| 66 | 37 | ||
| 67 | - static const MethodChannel method = | ||
| 68 | - MethodChannel('dev.steenbakker.mobile_scanner/scanner/method'); | ||
| 69 | - static const EventChannel event = | ||
| 70 | - EventChannel('dev.steenbakker.mobile_scanner/scanner/event'); | ||
| 71 | - | ||
| 72 | - | ||
| 73 | - | ||
| 74 | - static const analyze_none = 0; | ||
| 75 | - static const analyze_barcode = 1; | 38 | + int? _controllerHashcode; |
| 39 | + StreamSubscription? events; | ||
| 76 | 40 | ||
| 77 | - static int? id; | ||
| 78 | - static StreamSubscription? subscription; | ||
| 79 | 41 | ||
| 80 | - final CameraFacing facing; | ||
| 81 | - final ValueNotifier<MobileScannerArguments?> args; | ||
| 82 | - final ValueNotifier<TorchState> torchState; | 42 | + final ValueNotifier<MobileScannerArguments?> args = ValueNotifier(null); |
| 43 | + final ValueNotifier<TorchState> torchState = ValueNotifier(TorchState.off); | ||
| 44 | + late final ValueNotifier<CameraFacing> cameraFacingState; | ||
| 45 | + final Ratio? ratio; | ||
| 46 | + final bool? torchEnabled; | ||
| 83 | 47 | ||
| 84 | - bool torchable; | 48 | + CameraFacing facing; |
| 49 | + bool hasTorch = false; | ||
| 85 | late StreamController<Barcode> barcodesController; | 50 | late StreamController<Barcode> barcodesController; |
| 86 | 51 | ||
| 87 | Stream<Barcode> get barcodes => barcodesController.stream; | 52 | Stream<Barcode> get barcodes => barcodesController.stream; |
| 88 | 53 | ||
| 89 | - MobileScannerController(BuildContext context, {required num width, required num height, this.facing = CameraFacing.back}) | ||
| 90 | - : args = ValueNotifier(null), | ||
| 91 | - torchState = ValueNotifier(TorchState.off), | ||
| 92 | - torchable = false { | ||
| 93 | - // In case new instance before dispose. | ||
| 94 | - if (id != null) { | 54 | + MobileScannerController( |
| 55 | + {this.facing = CameraFacing.back, this.ratio, this.torchEnabled}) { | ||
| 56 | + // In case a new instance is created before calling dispose() | ||
| 57 | + if (_controllerHashcode != null) { | ||
| 95 | stop(); | 58 | stop(); |
| 96 | } | 59 | } |
| 97 | - id = hashCode; | ||
| 98 | - // Create barcode stream controller. | 60 | + _controllerHashcode = hashCode; |
| 61 | + | ||
| 62 | + cameraFacingState = ValueNotifier(facing); | ||
| 63 | + | ||
| 64 | + // Sets analyze mode and barcode stream | ||
| 99 | barcodesController = StreamController.broadcast( | 65 | barcodesController = StreamController.broadcast( |
| 100 | - onListen: () => tryAnalyze(analyze_barcode), | ||
| 101 | - onCancel: () => tryAnalyze(analyze_none), | 66 | + onListen: () => setAnalyzeMode(AnalyzeMode.barcode.index), |
| 67 | + onCancel: () => setAnalyzeMode(AnalyzeMode.none.index), | ||
| 102 | ); | 68 | ); |
| 103 | 69 | ||
| 104 | - final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; | ||
| 105 | - | 70 | + start(); |
| 106 | 71 | ||
| 107 | - start( | ||
| 108 | - width: (devicePixelRatio * width.toInt()).ceil(), | ||
| 109 | - height: (devicePixelRatio * height.toInt()).ceil()); | ||
| 110 | - // Listen event handler. | ||
| 111 | - subscription = | ||
| 112 | - event.receiveBroadcastStream().listen((data) => handleEvent(data)); | 72 | + // Listen to events from the platform specific code |
| 73 | + events = eventChannel | ||
| 74 | + .receiveBroadcastStream() | ||
| 75 | + .listen((data) => handleEvent(data)); | ||
| 113 | } | 76 | } |
| 114 | 77 | ||
| 115 | void handleEvent(Map<dynamic, dynamic> event) { | 78 | void handleEvent(Map<dynamic, dynamic> event) { |
| @@ -129,76 +92,91 @@ class MobileScannerController { | @@ -129,76 +92,91 @@ class MobileScannerController { | ||
| 129 | } | 92 | } |
| 130 | } | 93 | } |
| 131 | 94 | ||
| 132 | - void tryAnalyze(int mode) { | ||
| 133 | - if (hashCode != id) { | 95 | + void setAnalyzeMode(int mode) { |
| 96 | + if (hashCode != _controllerHashcode) { | ||
| 134 | return; | 97 | return; |
| 135 | } | 98 | } |
| 136 | - method.invokeMethod('analyze', mode); | 99 | + methodChannel.invokeMethod('analyze', mode); |
| 137 | } | 100 | } |
| 138 | 101 | ||
| 139 | - Future<void> start({ | ||
| 140 | - int? width, | ||
| 141 | - int? height, | ||
| 142 | - // List<BarcodeFormats>? formats = _defaultBarcodeFormats, | ||
| 143 | - }) async { | 102 | + // List<BarcodeFormats>? formats = _defaultBarcodeFormats, |
| 103 | + /// Start barcode scanning. This will first check if the required permissions | ||
| 104 | + /// are set. | ||
| 105 | + Future<void> start() async { | ||
| 144 | ensure('startAsync'); | 106 | ensure('startAsync'); |
| 145 | - // Check authorization state. | ||
| 146 | - MobileScannerState state = MobileScannerState.values[await method.invokeMethod('state')]; | 107 | + |
| 108 | + setAnalyzeMode(AnalyzeMode.barcode.index); | ||
| 109 | + // Check authorization status | ||
| 110 | + MobileScannerState state = MobileScannerState.values[await methodChannel.invokeMethod('state')]; | ||
| 147 | switch (state) { | 111 | switch (state) { |
| 148 | case MobileScannerState.undetermined: | 112 | case MobileScannerState.undetermined: |
| 149 | - final bool result = await method.invokeMethod('request'); | ||
| 150 | - state = result ? MobileScannerState.authorized : MobileScannerState.denied; | ||
| 151 | - break; | ||
| 152 | - case MobileScannerState.authorized: | 113 | + final bool result = await methodChannel.invokeMethod('request'); |
| 114 | + state = | ||
| 115 | + result ? MobileScannerState.authorized : MobileScannerState.denied; | ||
| 153 | break; | 116 | break; |
| 154 | case MobileScannerState.denied: | 117 | case MobileScannerState.denied: |
| 155 | throw PlatformException(code: 'NO ACCESS'); | 118 | throw PlatformException(code: 'NO ACCESS'); |
| 119 | + case MobileScannerState.authorized: | ||
| 120 | + break; | ||
| 156 | } | 121 | } |
| 157 | 122 | ||
| 158 | - debugPrint('TARGET RESOLUTION $width, $height'); | ||
| 159 | - // Start camera. | ||
| 160 | - final answer = | ||
| 161 | - await method.invokeMapMethod<String, dynamic>('start', { | ||
| 162 | - 'targetWidth': width, | ||
| 163 | - 'targetHeight': height, | ||
| 164 | - 'facing': facing.index | ||
| 165 | - }); | ||
| 166 | - final textureId = answer?['textureId']; | ||
| 167 | - final Size size = toSize(answer?['size']); | ||
| 168 | - debugPrint('RECEIVED SIZE: ${size.width} ${size.height}'); | ||
| 169 | - if (width != null && height != null) { | ||
| 170 | - args.value = MobileScannerArguments(textureId: textureId, size: size, wantedSize: Size(width.toDouble(), height.toDouble())); | ||
| 171 | - } else { | ||
| 172 | - args.value = MobileScannerArguments(textureId: textureId, size: size); | ||
| 173 | - } | 123 | + cameraFacingState.value = facing; |
| 124 | + | ||
| 125 | + // Set the starting arguments for the camera | ||
| 126 | + Map arguments = {}; | ||
| 127 | + arguments['facing'] = facing.index; | ||
| 128 | + if (ratio != null) arguments['ratio'] = ratio; | ||
| 129 | + if (torchEnabled != null) arguments['torch'] = torchEnabled; | ||
| 130 | + | ||
| 131 | + // Start the camera with arguments | ||
| 132 | + final Map<String, dynamic>? startResult = await methodChannel.invokeMapMethod<String, dynamic>( | ||
| 133 | + 'start', arguments); | ||
| 174 | 134 | ||
| 175 | - torchable = answer?['torchable']; | 135 | + if (startResult == null) throw PlatformException(code: 'INITIALIZATION ERROR'); |
| 136 | + | ||
| 137 | + hasTorch = startResult['torchable']; | ||
| 138 | + args.value = MobileScannerArguments(textureId: startResult['textureId'], size: toSize(startResult['size']), hasTorch: hasTorch); | ||
| 176 | } | 139 | } |
| 177 | 140 | ||
| 178 | - void torch() { | ||
| 179 | - ensure('torch'); | ||
| 180 | - if (!torchable) return; | ||
| 181 | - var state = | 141 | + Future<void> stop() async => await methodChannel.invokeMethod('stop'); |
| 142 | + | ||
| 143 | + /// Switches the torch on or off. | ||
| 144 | + /// | ||
| 145 | + /// Only works if torch is available. | ||
| 146 | + void toggleTorch() { | ||
| 147 | + ensure('toggleTorch'); | ||
| 148 | + if (!hasTorch) return; | ||
| 149 | + TorchState state = | ||
| 182 | torchState.value == TorchState.off ? TorchState.on : TorchState.off; | 150 | torchState.value == TorchState.off ? TorchState.on : TorchState.off; |
| 183 | - method.invokeMethod('torch', state.index); | 151 | + methodChannel.invokeMethod('torch', state.index); |
| 184 | } | 152 | } |
| 185 | 153 | ||
| 154 | + /// Switches the torch on or off. | ||
| 155 | + /// | ||
| 156 | + /// Only works if torch is available. | ||
| 157 | + Future<void> switchCamera() async { | ||
| 158 | + ensure('switchCamera'); | ||
| 159 | + await stop(); | ||
| 160 | + facing = facing == CameraFacing.back ? CameraFacing.front : CameraFacing.back; | ||
| 161 | + start(); | ||
| 162 | + } | ||
| 163 | + | ||
| 164 | + /// Disposes the controller and closes all listeners. | ||
| 186 | void dispose() { | 165 | void dispose() { |
| 187 | - if (hashCode == id) { | 166 | + if (hashCode == _controllerHashcode) { |
| 188 | stop(); | 167 | stop(); |
| 189 | - subscription?.cancel(); | ||
| 190 | - subscription = null; | ||
| 191 | - id = null; | 168 | + events?.cancel(); |
| 169 | + events = null; | ||
| 170 | + _controllerHashcode = null; | ||
| 192 | } | 171 | } |
| 193 | barcodesController.close(); | 172 | barcodesController.close(); |
| 194 | } | 173 | } |
| 195 | 174 | ||
| 196 | - void stop() => method.invokeMethod('stop'); | ||
| 197 | - | 175 | + /// Checks if the controller is bound to the correct MobileScanner object. |
| 198 | void ensure(String name) { | 176 | void ensure(String name) { |
| 199 | final message = | 177 | final message = |
| 200 | 'CameraController.$name called after CameraController.dispose\n' | 178 | 'CameraController.$name called after CameraController.dispose\n' |
| 201 | 'CameraController methods should not be used after calling dispose.'; | 179 | 'CameraController methods should not be used after calling dispose.'; |
| 202 | - assert(hashCode == id, message); | 180 | + assert(hashCode == _controllerHashcode, message); |
| 203 | } | 181 | } |
| 204 | } | 182 | } |
| 1 | name: mobile_scanner | 1 | name: mobile_scanner |
| 2 | -description: An universal scanner for Flutter based on MLKit. | 2 | +description: An universal scanner for Flutter based on MLKit. Uses CameraX on Android and AVFoundation on iOS. |
| 3 | version: 0.0.1 | 3 | version: 0.0.1 |
| 4 | -homepage: | 4 | +repository: https://github.com/juliansteenbakker/mobile_scanner |
| 5 | 5 | ||
| 6 | environment: | 6 | environment: |
| 7 | sdk: ">=2.16.0 <3.0.0" | 7 | sdk: ">=2.16.0 <3.0.0" |
-
Please register or login to post a comment