Showing
16 changed files
with
353 additions
and
608 deletions
| @@ -47,5 +47,16 @@ android { | @@ -47,5 +47,16 @@ android { | ||
| 47 | dependencies { | 47 | dependencies { |
| 48 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" | 48 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" |
| 49 | implementation 'com.google.mlkit:barcode-scanning:17.0.2' | 49 | implementation 'com.google.mlkit:barcode-scanning:17.0.2' |
| 50 | - implementation 'com.google.mlkit:camera:16.0.0-beta3' | 50 | + implementation "androidx.camera:camera-camera2:1.1.0-beta01" |
| 51 | + implementation 'androidx.camera:camera-lifecycle:1.1.0-beta01' | ||
| 52 | + | ||
| 53 | +// // The following line is optional, as the core library is included indirectly by camera-camera2 | ||
| 54 | +// implementation "androidx.camera:camera-core:1.1.0-alpha11" | ||
| 55 | +// implementation "androidx.camera:camera-camera2:1.1.0-alpha11" | ||
| 56 | +// // If you want to additionally use the CameraX Lifecycle library | ||
| 57 | +// implementation "androidx.camera:camera-lifecycle:1.1.0-alpha11" | ||
| 58 | +// // If you want to additionally use the CameraX View class | ||
| 59 | +// implementation "androidx.camera:camera-view:1.0.0-alpha31" | ||
| 60 | +// // If you want to additionally use the CameraX Extensions library | ||
| 61 | +// implementation "androidx.camera:camera-extensions:1.0.0-alpha31" | ||
| 51 | } | 62 | } |
| 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 | ||
| 4 | import android.annotation.SuppressLint | 6 | import android.annotation.SuppressLint |
| 5 | import android.app.Activity | 7 | import android.app.Activity |
| 8 | +import android.content.Context | ||
| 6 | import android.content.pm.PackageManager | 9 | 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 | ||
| 7 | import android.util.Log | 15 | import android.util.Log |
| 16 | +import android.util.Rational | ||
| 17 | +import android.util.Size | ||
| 8 | import android.view.Surface | 18 | import android.view.Surface |
| 19 | +import android.view.Surface.ROTATION_0 | ||
| 20 | +import android.view.Surface.ROTATION_180 | ||
| 9 | import androidx.annotation.IntDef | 21 | import androidx.annotation.IntDef |
| 10 | import androidx.annotation.NonNull | 22 | import androidx.annotation.NonNull |
| 11 | import androidx.camera.core.* | 23 | import androidx.camera.core.* |
| 24 | +import androidx.camera.core.impl.PreviewConfig | ||
| 12 | import androidx.camera.lifecycle.ProcessCameraProvider | 25 | import androidx.camera.lifecycle.ProcessCameraProvider |
| 13 | import androidx.core.app.ActivityCompat | 26 | import androidx.core.app.ActivityCompat |
| 14 | import androidx.core.content.ContextCompat | 27 | import androidx.core.content.ContextCompat |
| 15 | import androidx.lifecycle.LifecycleOwner | 28 | import androidx.lifecycle.LifecycleOwner |
| 16 | import com.google.mlkit.vision.barcode.BarcodeScanning | 29 | import com.google.mlkit.vision.barcode.BarcodeScanning |
| 17 | import com.google.mlkit.vision.common.InputImage | 30 | import com.google.mlkit.vision.common.InputImage |
| 18 | -import io.flutter.plugin.common.* | 31 | +import io.flutter.plugin.common.EventChannel |
| 32 | +import io.flutter.plugin.common.MethodCall | ||
| 33 | +import io.flutter.plugin.common.MethodChannel | ||
| 34 | +import io.flutter.plugin.common.PluginRegistry | ||
| 19 | import io.flutter.view.TextureRegistry | 35 | import io.flutter.view.TextureRegistry |
| 20 | 36 | ||
| 37 | + | ||
| 21 | class MobileScanner(private val activity: Activity, private val textureRegistry: TextureRegistry) | 38 | class MobileScanner(private val activity: Activity, private val textureRegistry: TextureRegistry) |
| 22 | : MethodChannel.MethodCallHandler, EventChannel.StreamHandler, PluginRegistry.RequestPermissionsResultListener { | 39 | : MethodChannel.MethodCallHandler, EventChannel.StreamHandler, PluginRegistry.RequestPermissionsResultListener { |
| 23 | companion object { | 40 | companion object { |
| @@ -34,6 +51,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -34,6 +51,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 34 | @AnalyzeMode | 51 | @AnalyzeMode |
| 35 | private var analyzeMode: Int = AnalyzeMode.NONE | 52 | private var analyzeMode: Int = AnalyzeMode.NONE |
| 36 | 53 | ||
| 54 | + @ExperimentalGetImage | ||
| 37 | override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: MethodChannel.Result) { | 55 | override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: MethodChannel.Result) { |
| 38 | when (call.method) { | 56 | when (call.method) { |
| 39 | "state" -> stateNative(result) | 57 | "state" -> stateNative(result) |
| @@ -81,8 +99,23 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -81,8 +99,23 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 81 | ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE) | 99 | ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE) |
| 82 | } | 100 | } |
| 83 | 101 | ||
| 102 | + private var sensorOrientation = 0 | ||
| 103 | + | ||
| 84 | @ExperimentalGetImage | 104 | @ExperimentalGetImage |
| 105 | +// @androidx.camera.camera2.interop.ExperimentalCamera2Interop | ||
| 85 | private fun startNative(call: MethodCall, result: MethodChannel.Result) { | 106 | private fun startNative(call: MethodCall, result: MethodChannel.Result) { |
| 107 | + | ||
| 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 | ||
| 115 | + } | ||
| 116 | + | ||
| 117 | + Log.i("LOG", "Target resolution : $targetWidth, $targetHeight") | ||
| 118 | + | ||
| 86 | val future = ProcessCameraProvider.getInstance(activity) | 119 | val future = ProcessCameraProvider.getInstance(activity) |
| 87 | val executor = ContextCompat.getMainExecutor(activity) | 120 | val executor = ContextCompat.getMainExecutor(activity) |
| 88 | future.addListener({ | 121 | future.addListener({ |
| @@ -91,13 +124,23 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -91,13 +124,23 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 91 | val textureId = textureEntry!!.id() | 124 | val textureId = textureEntry!!.id() |
| 92 | // Preview | 125 | // Preview |
| 93 | val surfaceProvider = Preview.SurfaceProvider { request -> | 126 | val surfaceProvider = Preview.SurfaceProvider { request -> |
| 94 | - val resolution = request.resolution | ||
| 95 | val texture = textureEntry!!.surfaceTexture() | 127 | val texture = textureEntry!!.surfaceTexture() |
| 128 | + val resolution = request.resolution | ||
| 96 | texture.setDefaultBufferSize(resolution.width, resolution.height) | 129 | texture.setDefaultBufferSize(resolution.width, resolution.height) |
| 130 | + Log.i("LOG", "Image resolution : ${request.resolution}") | ||
| 97 | val surface = Surface(texture) | 131 | val surface = Surface(texture) |
| 98 | - request.provideSurface(surface, executor, { }) | 132 | + request.provideSurface(surface, executor) { } |
| 99 | } | 133 | } |
| 100 | - val preview = Preview.Builder().build().apply { setSurfaceProvider(surfaceProvider) } | 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) } | ||
| 101 | // Analyzer | 144 | // Analyzer |
| 102 | val analyzer = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format | 145 | val analyzer = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format |
| 103 | when (analyzeMode) { | 146 | when (analyzeMode) { |
| @@ -120,6 +163,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -120,6 +163,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 120 | } | 163 | } |
| 121 | val analysis = ImageAnalysis.Builder() | 164 | val analysis = ImageAnalysis.Builder() |
| 122 | .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) | 165 | .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) |
| 166 | + .setTargetResolution(Size(targetWidth, targetHeight)) | ||
| 123 | .build().apply { setAnalyzer(executor, analyzer) } | 167 | .build().apply { setAnalyzer(executor, analyzer) } |
| 124 | // Bind to lifecycle. | 168 | // Bind to lifecycle. |
| 125 | val owner = activity as LifecycleOwner | 169 | val owner = activity as LifecycleOwner |
| @@ -127,17 +171,23 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | @@ -127,17 +171,23 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: | ||
| 127 | if (call.arguments == 0) CameraSelector.DEFAULT_FRONT_CAMERA | 171 | if (call.arguments == 0) CameraSelector.DEFAULT_FRONT_CAMERA |
| 128 | else CameraSelector.DEFAULT_BACK_CAMERA | 172 | else CameraSelector.DEFAULT_BACK_CAMERA |
| 129 | camera = cameraProvider!!.bindToLifecycle(owner, selector, preview, analysis) | 173 | camera = cameraProvider!!.bindToLifecycle(owner, selector, preview, analysis) |
| 130 | - camera!!.cameraInfo.torchState.observe(owner, { state -> | 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 -> | ||
| 131 | // TorchState.OFF = 0; TorchState.ON = 1 | 181 | // TorchState.OFF = 0; TorchState.ON = 1 |
| 132 | val event = mapOf("name" to "torchState", "data" to state) | 182 | val event = mapOf("name" to "torchState", "data" to state) |
| 133 | sink?.success(event) | 183 | sink?.success(event) |
| 134 | - }) | ||
| 135 | - // TODO: seems there's not a better way to get the final resolution | ||
| 136 | - @SuppressLint("RestrictedApi") | ||
| 137 | - val resolution = preview.attachedSurfaceResolution!! | 184 | + } |
| 185 | + | ||
| 186 | + val resolution = preview.resolutionInfo!!.resolution | ||
| 138 | val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0 | 187 | val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0 |
| 139 | val width = resolution.width.toDouble() | 188 | val width = resolution.width.toDouble() |
| 140 | val height = resolution.height.toDouble() | 189 | val height = resolution.height.toDouble() |
| 190 | +// val size = mapOf("width" to 1920.0, "height" to 1080.0) | ||
| 141 | val size = if (portrait) mapOf("width" to width, "height" to height) else mapOf("width" to height, "height" to width) | 191 | val size = if (portrait) mapOf("width" to width, "height" to height) else mapOf("width" to height, "height" to width) |
| 142 | val answer = mapOf("textureId" to textureId, "size" to size, "torchable" to camera!!.torchable) | 192 | val answer = mapOf("textureId" to textureId, "size" to size, "torchable" to camera!!.torchable) |
| 143 | result.success(answer) | 193 | result.success(answer) |
| @@ -18,7 +18,7 @@ class _AnalyzeViewState extends State<AnalyzeView> | @@ -18,7 +18,7 @@ class _AnalyzeViewState extends State<AnalyzeView> | ||
| 18 | with SingleTickerProviderStateMixin { | 18 | with SingleTickerProviderStateMixin { |
| 19 | List<Offset> points = []; | 19 | List<Offset> points = []; |
| 20 | 20 | ||
| 21 | - CameraController cameraController = CameraController(); | 21 | + // CameraController cameraController = CameraController(context, width: 320, height: 150); |
| 22 | 22 | ||
| 23 | String? barcode = null; | 23 | String? barcode = null; |
| 24 | 24 | ||
| @@ -29,25 +29,34 @@ class _AnalyzeViewState extends State<AnalyzeView> | @@ -29,25 +29,34 @@ class _AnalyzeViewState extends State<AnalyzeView> | ||
| 29 | body: Builder(builder: (context) { | 29 | body: Builder(builder: (context) { |
| 30 | return Stack( | 30 | return Stack( |
| 31 | children: [ | 31 | children: [ |
| 32 | - CameraView( | ||
| 33 | - controller: cameraController, | 32 | + MobileScanner( |
| 33 | + // fitScreen: false, | ||
| 34 | + // controller: cameraController, | ||
| 34 | onDetect: (barcode, args) { | 35 | onDetect: (barcode, args) { |
| 35 | if (this.barcode != barcode.rawValue) { | 36 | if (this.barcode != barcode.rawValue) { |
| 36 | this.barcode = barcode.rawValue; | 37 | this.barcode = barcode.rawValue; |
| 37 | if (barcode.corners != null) { | 38 | if (barcode.corners != null) { |
| 38 | - debugPrint('Size: ${MediaQuery.of(context).size}'); | ||
| 39 | ScaffoldMessenger.of(context).showSnackBar(SnackBar( | 39 | ScaffoldMessenger.of(context).showSnackBar(SnackBar( |
| 40 | content: Text('${barcode.rawValue}'), | 40 | content: Text('${barcode.rawValue}'), |
| 41 | - duration: Duration(milliseconds: 200), | 41 | + duration: const Duration(milliseconds: 200), |
| 42 | animation: null, | 42 | animation: null, |
| 43 | )); | 43 | )); |
| 44 | setState(() { | 44 | setState(() { |
| 45 | final List<Offset> points = []; | 45 | final List<Offset> points = []; |
| 46 | double factorWidth = args.size.width / 520; | 46 | double factorWidth = args.size.width / 520; |
| 47 | - double factorHeight = args.size.height / 640; | 47 | + // double factorHeight = wanted / args.size.height; |
| 48 | + final size = MediaQuery.of(context).devicePixelRatio; | ||
| 49 | + debugPrint('Size: ${barcode.corners}'); | ||
| 48 | for (var point in barcode.corners!) { | 50 | for (var point in barcode.corners!) { |
| 49 | - points.add(Offset(point.dx * factorWidth, | ||
| 50 | - point.dy * factorHeight)); | 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)); | ||
| 51 | } | 60 | } |
| 52 | this.points = points; | 61 | this.points = points; |
| 53 | }); | 62 | }); |
| @@ -55,29 +64,25 @@ class _AnalyzeViewState extends State<AnalyzeView> | @@ -55,29 +64,25 @@ class _AnalyzeViewState extends State<AnalyzeView> | ||
| 55 | } | 64 | } |
| 56 | // Default 640 x480 | 65 | // Default 640 x480 |
| 57 | }), | 66 | }), |
| 58 | - Container( | ||
| 59 | - // width: 400, | ||
| 60 | - // height: 400, | ||
| 61 | - child: CustomPaint( | 67 | + CustomPaint( |
| 62 | painter: OpenPainter(points), | 68 | painter: OpenPainter(points), |
| 63 | ), | 69 | ), |
| 64 | - ), | ||
| 65 | - Container( | ||
| 66 | - alignment: Alignment.bottomCenter, | ||
| 67 | - margin: EdgeInsets.only(bottom: 80.0), | ||
| 68 | - child: IconButton( | ||
| 69 | - icon: ValueListenableBuilder( | ||
| 70 | - valueListenable: cameraController.torchState, | ||
| 71 | - builder: (context, state, child) { | ||
| 72 | - final color = | ||
| 73 | - state == TorchState.off ? Colors.grey : Colors.white; | ||
| 74 | - return Icon(Icons.bolt, color: color); | ||
| 75 | - }, | ||
| 76 | - ), | ||
| 77 | - iconSize: 32.0, | ||
| 78 | - onPressed: () => cameraController.torch(), | ||
| 79 | - ), | ||
| 80 | - ), | 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 | + // ), | ||
| 81 | ], | 86 | ], |
| 82 | ); | 87 | ); |
| 83 | }), | 88 | }), |
| @@ -87,7 +92,7 @@ class _AnalyzeViewState extends State<AnalyzeView> | @@ -87,7 +92,7 @@ class _AnalyzeViewState extends State<AnalyzeView> | ||
| 87 | 92 | ||
| 88 | @override | 93 | @override |
| 89 | void dispose() { | 94 | void dispose() { |
| 90 | - cameraController.dispose(); | 95 | + // cameraController.dispose(); |
| 91 | super.dispose(); | 96 | super.dispose(); |
| 92 | } | 97 | } |
| 93 | 98 |
| @@ -80,6 +80,11 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | @@ -80,6 +80,11 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | ||
| 80 | analyzing = true | 80 | analyzing = true |
| 81 | let buffer = CMSampleBufferGetImageBuffer(sampleBuffer) | 81 | let buffer = CMSampleBufferGetImageBuffer(sampleBuffer) |
| 82 | let image = VisionImage(image: buffer!.image) | 82 | let image = VisionImage(image: buffer!.image) |
| 83 | + image.orientation = imageOrientation( | ||
| 84 | + deviceOrientation: UIDevice.current.orientation, | ||
| 85 | + defaultOrientation: .portrait | ||
| 86 | + ) | ||
| 87 | + | ||
| 83 | let scanner = BarcodeScanner.barcodeScanner() | 88 | let scanner = BarcodeScanner.barcodeScanner() |
| 84 | scanner.process(image) { [self] barcodes, error in | 89 | scanner.process(image) { [self] barcodes, error in |
| 85 | if error == nil && barcodes != nil { | 90 | if error == nil && barcodes != nil { |
| @@ -95,6 +100,26 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | @@ -95,6 +100,26 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | ||
| 95 | } | 100 | } |
| 96 | } | 101 | } |
| 97 | 102 | ||
| 103 | + func imageOrientation( | ||
| 104 | + deviceOrientation: UIDeviceOrientation, | ||
| 105 | + defaultOrientation: UIDeviceOrientation | ||
| 106 | + ) -> UIImage.Orientation { | ||
| 107 | + switch deviceOrientation { | ||
| 108 | + case .portrait: | ||
| 109 | + return position == .front ? .leftMirrored : .right | ||
| 110 | + case .landscapeLeft: | ||
| 111 | + return position == .front ? .downMirrored : .up | ||
| 112 | + case .portraitUpsideDown: | ||
| 113 | + return position == .front ? .rightMirrored : .left | ||
| 114 | + case .landscapeRight: | ||
| 115 | + return position == .front ? .upMirrored : .down | ||
| 116 | + case .faceDown, .faceUp, .unknown: | ||
| 117 | + return .up | ||
| 118 | + @unknown default: | ||
| 119 | + return imageOrientation(deviceOrientation: defaultOrientation, defaultOrientation: .portrait) | ||
| 120 | + } | ||
| 121 | + } | ||
| 122 | + | ||
| 98 | func stateNative(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | 123 | func stateNative(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
| 99 | let status = AVCaptureDevice.authorizationStatus(for: .video) | 124 | let status = AVCaptureDevice.authorizationStatus(for: .video) |
| 100 | switch status { | 125 | switch status { |
| @@ -111,10 +136,22 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | @@ -111,10 +136,22 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | ||
| 111 | AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) }) | 136 | AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) }) |
| 112 | } | 137 | } |
| 113 | 138 | ||
| 139 | + var position = AVCaptureDevice.Position.back | ||
| 140 | + | ||
| 114 | func startNative(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { | 141 | func startNative(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
| 115 | textureId = registry.register(self) | 142 | textureId = registry.register(self) |
| 116 | captureSession = AVCaptureSession() | 143 | captureSession = AVCaptureSession() |
| 117 | - let position = call.arguments as! Int == 0 ? AVCaptureDevice.Position.front : .back | 144 | + |
| 145 | + let argReader = MapArgumentReader(call.arguments as? [String: Any]) | ||
| 146 | + | ||
| 147 | + guard let targetWidth = argReader.int(key: "targetWidth"), | ||
| 148 | + let targetHeight = argReader.int(key: "targetHeight"), | ||
| 149 | + let facing = argReader.int(key: "facing") else { | ||
| 150 | + result(FlutterError(code: "INVALID_ARGUMENT", message: "Missing a required argument", details: "Expecting targetWidth, targetHeight, formats, and optionally heartbeatTimeout")) | ||
| 151 | + return | ||
| 152 | + } | ||
| 153 | + | ||
| 154 | + position = facing == 0 ? AVCaptureDevice.Position.front : .back | ||
| 118 | if #available(iOS 10.0, *) { | 155 | if #available(iOS 10.0, *) { |
| 119 | device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: position).devices.first | 156 | device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: position).devices.first |
| 120 | } else { | 157 | } else { |
| @@ -129,10 +166,13 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | @@ -129,10 +166,13 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | ||
| 129 | } catch { | 166 | } catch { |
| 130 | error.throwNative(result) | 167 | error.throwNative(result) |
| 131 | } | 168 | } |
| 169 | + captureSession.sessionPreset = AVCaptureSession.Preset.photo; | ||
| 132 | // Add video output. | 170 | // Add video output. |
| 133 | let videoOutput = AVCaptureVideoDataOutput() | 171 | let videoOutput = AVCaptureVideoDataOutput() |
| 172 | + | ||
| 134 | videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA] | 173 | videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA] |
| 135 | videoOutput.alwaysDiscardsLateVideoFrames = true | 174 | videoOutput.alwaysDiscardsLateVideoFrames = true |
| 175 | + | ||
| 136 | videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main) | 176 | videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main) |
| 137 | captureSession.addOutput(videoOutput) | 177 | captureSession.addOutput(videoOutput) |
| 138 | for connection in videoOutput.connections { | 178 | for connection in videoOutput.connections { |
| @@ -199,3 +239,25 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | @@ -199,3 +239,25 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan | ||
| 199 | } | 239 | } |
| 200 | } | 240 | } |
| 201 | } | 241 | } |
| 242 | + | ||
| 243 | +class MapArgumentReader { | ||
| 244 | + | ||
| 245 | + let args: [String: Any]? | ||
| 246 | + | ||
| 247 | + init(_ args: [String: Any]?) { | ||
| 248 | + self.args = args | ||
| 249 | + } | ||
| 250 | + | ||
| 251 | + func string(key: String) -> String? { | ||
| 252 | + return args?[key] as? String | ||
| 253 | + } | ||
| 254 | + | ||
| 255 | + func int(key: String) -> Int? { | ||
| 256 | + return (args?[key] as? NSNumber)?.intValue | ||
| 257 | + } | ||
| 258 | + | ||
| 259 | + func stringArray(key: String) -> [String]? { | ||
| 260 | + return args?[key] as? [String] | ||
| 261 | + } | ||
| 262 | + | ||
| 263 | +} |
| 1 | library mobile_scanner; | 1 | library mobile_scanner; |
| 2 | 2 | ||
| 3 | export 'src/mobile_scanner.dart'; | 3 | export 'src/mobile_scanner.dart'; |
| 4 | -export 'src/camera_controller.dart'; | ||
| 5 | -export 'src/camera_view.dart'; | ||
| 6 | -export 'src/torch_state.dart'; | 4 | +export 'src/mobile_scanner_controller.dart'; |
| 7 | export 'src/objects/barcode.dart'; | 5 | export 'src/objects/barcode.dart'; |
lib/src/camera_view.dart
deleted
100644 → 0
| 1 | -import 'package:flutter/material.dart'; | ||
| 2 | -import 'package:mobile_scanner/mobile_scanner.dart'; | ||
| 3 | - | ||
| 4 | -import 'camera_args.dart'; | ||
| 5 | - | ||
| 6 | -/// A widget showing a live camera preview. | ||
| 7 | -class CameraView extends StatefulWidget { | ||
| 8 | - /// The controller of the camera. | ||
| 9 | - final CameraController? controller; | ||
| 10 | - final Function(Barcode barcode, CameraArgs args)? onDetect; | ||
| 11 | - | ||
| 12 | - /// Create a [CameraView] with a [controller], the [controller] must has been initialized. | ||
| 13 | - const CameraView({Key? key, this.onDetect, this.controller}) : super(key: key); | ||
| 14 | - | ||
| 15 | - @override | ||
| 16 | - State<CameraView> createState() => _CameraViewState(); | ||
| 17 | -} | ||
| 18 | - | ||
| 19 | -class _CameraViewState extends State<CameraView> { | ||
| 20 | - | ||
| 21 | - late CameraController controller; | ||
| 22 | - @override | ||
| 23 | - initState() { | ||
| 24 | - super.initState(); | ||
| 25 | - controller = widget.controller ?? CameraController(); | ||
| 26 | - } | ||
| 27 | - | ||
| 28 | - @override | ||
| 29 | - Widget build(BuildContext context) { | ||
| 30 | - return ValueListenableBuilder( | ||
| 31 | - valueListenable: controller.args, | ||
| 32 | - builder: (context, value, child) { | ||
| 33 | - value = value as CameraArgs?; | ||
| 34 | - if (value == null) { | ||
| 35 | - return Container(color: Colors.black); | ||
| 36 | - } else { | ||
| 37 | - controller.barcodes | ||
| 38 | - .listen((a) => widget.onDetect!(a, value as CameraArgs)); | ||
| 39 | - | ||
| 40 | - return ClipRect( | ||
| 41 | - child: Transform.scale( | ||
| 42 | - scale: value.size.fill(MediaQuery.of(context).size), | ||
| 43 | - child: Center( | ||
| 44 | - child: AspectRatio( | ||
| 45 | - aspectRatio: value.size.aspectRatio, | ||
| 46 | - child: Texture(textureId: value.textureId), | ||
| 47 | - ), | ||
| 48 | - ), | ||
| 49 | - ), | ||
| 50 | - ); | ||
| 51 | - } | ||
| 52 | - }); | ||
| 53 | - } | ||
| 54 | - | ||
| 55 | - @override | ||
| 56 | - void dispose() { | ||
| 57 | - controller.dispose(); | ||
| 58 | - super.dispose(); | ||
| 59 | - } | ||
| 60 | -} | ||
| 61 | - | ||
| 62 | -extension on Size { | ||
| 63 | - double fill(Size targetSize) { | ||
| 64 | - if (targetSize.aspectRatio < aspectRatio) { | ||
| 65 | - return targetSize.height * aspectRatio / targetSize.width; | ||
| 66 | - } else { | ||
| 67 | - return targetSize.width / aspectRatio / targetSize.height; | ||
| 68 | - } | ||
| 69 | - } | ||
| 70 | -} |
| 1 | -// import 'dart:async'; | ||
| 2 | -// import 'package:flutter/material.dart'; | ||
| 3 | -// import 'package:mobile_scanner/src/mobile_scanner_handler.dart'; | ||
| 4 | -// import 'package:mobile_scanner/src/objects/preview_details.dart'; | ||
| 5 | -// | ||
| 6 | -// import 'mobile_scanner_preview.dart'; | ||
| 7 | -// import 'objects/barcode_formats.dart'; | ||
| 8 | -// | ||
| 9 | -// typedef ErrorCallback = Widget Function(BuildContext context, Object? error); | ||
| 10 | -// | ||
| 11 | -// Text _defaultNotStartedBuilder(context) => const Text("Camera Loading ..."); | ||
| 12 | -// Text _defaultOffscreenBuilder(context) => const Text("Camera Paused."); | ||
| 13 | -// Text _defaultOnError(BuildContext context, Object? error) { | ||
| 14 | -// debugPrint("Error reading from camera: $error"); | ||
| 15 | -// return const Text("Error reading from camera..."); | ||
| 16 | -// } | ||
| 17 | -// | ||
| 18 | -// class MobileScanner extends StatefulWidget { | ||
| 19 | -// const MobileScanner( | ||
| 20 | -// {Key? key, | ||
| 21 | -// required this.qrCodeCallback, | ||
| 22 | -// this.child, | ||
| 23 | -// this.fit = BoxFit.cover, | ||
| 24 | -// WidgetBuilder? notStartedBuilder, | ||
| 25 | -// WidgetBuilder? offscreenBuilder, | ||
| 26 | -// ErrorCallback? onError, | ||
| 27 | -// this.formats, | ||
| 28 | -// this.rearLens = true, | ||
| 29 | -// this.manualFocus = false}) | ||
| 30 | -// : notStartedBuilder = notStartedBuilder ?? _defaultNotStartedBuilder, | ||
| 31 | -// offscreenBuilder = | ||
| 32 | -// offscreenBuilder ?? notStartedBuilder ?? _defaultOffscreenBuilder, | ||
| 33 | -// onError = onError ?? _defaultOnError, | ||
| 34 | -// super(key: key); | ||
| 35 | -// | ||
| 36 | -// final BoxFit fit; | ||
| 37 | -// final ValueChanged<String?> qrCodeCallback; | ||
| 38 | -// final Widget? child; | ||
| 39 | -// final WidgetBuilder notStartedBuilder; | ||
| 40 | -// final WidgetBuilder offscreenBuilder; | ||
| 41 | -// final ErrorCallback onError; | ||
| 42 | -// final List<BarcodeFormats>? formats; | ||
| 43 | -// final bool rearLens; | ||
| 44 | -// final bool manualFocus; | ||
| 45 | -// | ||
| 46 | -// static void toggleFlash() { | ||
| 47 | -// MobileScannerHandler.toggleFlash(); | ||
| 48 | -// } | ||
| 49 | -// | ||
| 50 | -// static void flipCamera() { | ||
| 51 | -// MobileScannerHandler.switchCamera(); | ||
| 52 | -// } | ||
| 53 | -// | ||
| 54 | -// @override | ||
| 55 | -// _MobileScannerState createState() => _MobileScannerState(); | ||
| 56 | -// } | ||
| 57 | -// | ||
| 58 | -// class _MobileScannerState extends State<MobileScanner> | ||
| 59 | -// with WidgetsBindingObserver { | ||
| 60 | -// | ||
| 61 | -// bool onScreen = true; | ||
| 62 | -// Future<PreviewDetails>? _previewDetails; | ||
| 63 | -// | ||
| 64 | -// @override | ||
| 65 | -// void initState() { | ||
| 66 | -// super.initState(); | ||
| 67 | -// WidgetsBinding.instance!.addObserver(this); | ||
| 68 | -// } | ||
| 69 | -// | ||
| 70 | -// @override | ||
| 71 | -// void dispose() { | ||
| 72 | -// WidgetsBinding.instance!.removeObserver(this); | ||
| 73 | -// super.dispose(); | ||
| 74 | -// } | ||
| 75 | -// | ||
| 76 | -// @override | ||
| 77 | -// void didChangeAppLifecycleState(AppLifecycleState state) { | ||
| 78 | -// if (state == AppLifecycleState.resumed) { | ||
| 79 | -// setState(() => onScreen = true); | ||
| 80 | -// } else { | ||
| 81 | -// if (_previewDetails != null && onScreen) { | ||
| 82 | -// MobileScannerHandler.stop(); | ||
| 83 | -// } | ||
| 84 | -// setState(() { | ||
| 85 | -// onScreen = false; | ||
| 86 | -// _previewDetails = null; | ||
| 87 | -// }); | ||
| 88 | -// } | ||
| 89 | -// } | ||
| 90 | -// | ||
| 91 | -// Future<PreviewDetails> _initPreview(num width, num height) async { | ||
| 92 | -// final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; | ||
| 93 | -// return await MobileScannerHandler.start( | ||
| 94 | -// width: (devicePixelRatio * width.toInt()).ceil(), | ||
| 95 | -// height: (devicePixelRatio * height.toInt()).ceil(), | ||
| 96 | -// qrCodeHandler: widget.qrCodeCallback, | ||
| 97 | -// formats: widget.formats, | ||
| 98 | -// ); | ||
| 99 | -// } | ||
| 100 | -// | ||
| 101 | -// void switchCamera() { | ||
| 102 | -// MobileScannerHandler.rearLens = !MobileScannerHandler.rearLens; | ||
| 103 | -// restart(); | ||
| 104 | -// } | ||
| 105 | -// | ||
| 106 | -// | ||
| 107 | -// void switchFocus() { | ||
| 108 | -// MobileScannerHandler.manualFocus = !MobileScannerHandler.manualFocus; | ||
| 109 | -// restart(); | ||
| 110 | -// } | ||
| 111 | -// | ||
| 112 | -// /// This method can be used to restart scanning | ||
| 113 | -// /// the event that it was paused. | ||
| 114 | -// Future<void> restart() async { | ||
| 115 | -// await MobileScannerHandler.stop(); | ||
| 116 | -// setState(() { | ||
| 117 | -// _previewDetails = null; | ||
| 118 | -// }); | ||
| 119 | -// } | ||
| 120 | -// | ||
| 121 | -// /// This method can be used to manually stop the | ||
| 122 | -// /// camera. | ||
| 123 | -// Future<void> stop() async { | ||
| 124 | -// await MobileScannerHandler.stop(); | ||
| 125 | -// } | ||
| 126 | -// | ||
| 127 | -// @override | ||
| 128 | -// deactivate() { | ||
| 129 | -// super.deactivate(); | ||
| 130 | -// MobileScannerHandler.stop(); | ||
| 131 | -// } | ||
| 132 | -// | ||
| 133 | -// @override | ||
| 134 | -// Widget build(BuildContext context) { | ||
| 135 | -// return LayoutBuilder( | ||
| 136 | -// builder: (BuildContext context, BoxConstraints constraints) { | ||
| 137 | -// if (_previewDetails == null && onScreen) { | ||
| 138 | -// _previewDetails = | ||
| 139 | -// _initPreview(constraints.maxWidth, constraints.maxHeight); | ||
| 140 | -// } else if (!onScreen) { | ||
| 141 | -// return widget.offscreenBuilder(context); | ||
| 142 | -// } | ||
| 143 | -// | ||
| 144 | -// return FutureBuilder( | ||
| 145 | -// future: _previewDetails, | ||
| 146 | -// builder: (BuildContext context, AsyncSnapshot<PreviewDetails> details) { | ||
| 147 | -// switch (details.connectionState) { | ||
| 148 | -// case ConnectionState.none: | ||
| 149 | -// case ConnectionState.waiting: | ||
| 150 | -// return widget.notStartedBuilder(context); | ||
| 151 | -// case ConnectionState.done: | ||
| 152 | -// if (details.hasError) { | ||
| 153 | -// debugPrint(details.error.toString()); | ||
| 154 | -// return widget.onError(context, details.error); | ||
| 155 | -// } | ||
| 156 | -// Widget preview = SizedBox( | ||
| 157 | -// width: constraints.maxWidth, | ||
| 158 | -// height: constraints.maxHeight, | ||
| 159 | -// child: Preview( | ||
| 160 | -// previewDetails: details.data!, | ||
| 161 | -// targetWidth: constraints.maxWidth, | ||
| 162 | -// targetHeight: constraints.maxHeight, | ||
| 163 | -// fit: widget.fit, | ||
| 164 | -// ), | ||
| 165 | -// ); | ||
| 166 | -// | ||
| 167 | -// if (widget.child != null) { | ||
| 168 | -// return Stack( | ||
| 169 | -// children: [ | ||
| 170 | -// preview, | ||
| 171 | -// widget.child!, | ||
| 172 | -// ], | ||
| 173 | -// ); | ||
| 174 | -// } | ||
| 175 | -// return preview; | ||
| 176 | -// | ||
| 177 | -// default: | ||
| 178 | -// throw AssertionError("${details.connectionState} not supported."); | ||
| 179 | -// } | ||
| 180 | -// }, | ||
| 181 | -// ); | ||
| 182 | -// }); | ||
| 183 | -// } | ||
| 184 | -// } | 1 | +import 'package:flutter/material.dart'; |
| 2 | +import 'package:mobile_scanner/mobile_scanner.dart'; | ||
| 3 | + | ||
| 4 | +import 'mobile_scanner_arguments.dart'; | ||
| 5 | + | ||
| 6 | +/// A widget showing a live camera preview. | ||
| 7 | +class MobileScanner extends StatefulWidget { | ||
| 8 | + /// The controller of the camera. | ||
| 9 | + final MobileScannerController? controller; | ||
| 10 | + final Function(Barcode barcode, MobileScannerArguments args)? onDetect; | ||
| 11 | + final bool fitScreen; | ||
| 12 | + final bool fitWidth; | ||
| 13 | + | ||
| 14 | + /// Create a [MobileScanner] with a [controller], the [controller] must has been initialized. | ||
| 15 | + const MobileScanner( | ||
| 16 | + {Key? key, this.onDetect, this.controller, this.fitScreen = true, this.fitWidth = true}) | ||
| 17 | + : super(key: key); | ||
| 18 | + | ||
| 19 | + @override | ||
| 20 | + State<MobileScanner> createState() => _MobileScannerState(); | ||
| 21 | +} | ||
| 22 | + | ||
| 23 | +class _MobileScannerState extends State<MobileScanner> | ||
| 24 | + with WidgetsBindingObserver { | ||
| 25 | + bool onScreen = true; | ||
| 26 | + MobileScannerController? controller; | ||
| 27 | + | ||
| 28 | + @override | ||
| 29 | + void didChangeAppLifecycleState(AppLifecycleState state) { | ||
| 30 | + if (state == AppLifecycleState.resumed) { | ||
| 31 | + setState(() => onScreen = true); | ||
| 32 | + } else { | ||
| 33 | + if (controller != null && onScreen) { | ||
| 34 | + controller!.stop(); | ||
| 35 | + } | ||
| 36 | + setState(() { | ||
| 37 | + onScreen = false; | ||
| 38 | + controller = null; | ||
| 39 | + }); | ||
| 40 | + } | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + @override | ||
| 44 | + Widget build(BuildContext context) { | ||
| 45 | + 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."); | ||
| 51 | + return ValueListenableBuilder( | ||
| 52 | + valueListenable: controller!.args, | ||
| 53 | + builder: (context, value, child) { | ||
| 54 | + value = value as MobileScannerArguments?; | ||
| 55 | + if (value == null) { | ||
| 56 | + return Container(color: Colors.black); | ||
| 57 | + } else { | ||
| 58 | + controller!.barcodes.listen( | ||
| 59 | + (a) => widget.onDetect!(a, value as MobileScannerArguments)); | ||
| 60 | + // Texture(textureId: value.textureId) | ||
| 61 | + 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), | ||
| 68 | + ), | ||
| 69 | + ), | ||
| 70 | + ); | ||
| 71 | + } | ||
| 72 | + }); | ||
| 73 | + }); | ||
| 74 | + } | ||
| 75 | + | ||
| 76 | + @override | ||
| 77 | + void dispose() { | ||
| 78 | + controller?.dispose(); | ||
| 79 | + super.dispose(); | ||
| 80 | + } | ||
| 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 | +} |
| 1 | import 'package:flutter/material.dart'; | 1 | import 'package:flutter/material.dart'; |
| 2 | 2 | ||
| 3 | /// Camera args for [CameraView]. | 3 | /// Camera args for [CameraView]. |
| 4 | -class CameraArgs { | 4 | +class MobileScannerArguments { |
| 5 | /// The texture id. | 5 | /// The texture id. |
| 6 | final int textureId; | 6 | final int textureId; |
| 7 | 7 | ||
| 8 | /// Size of the texture. | 8 | /// Size of the texture. |
| 9 | final Size size; | 9 | final Size size; |
| 10 | 10 | ||
| 11 | - /// Create a [CameraArgs]. | ||
| 12 | - CameraArgs(this.textureId, this.size); | 11 | + /// Size of the texture. |
| 12 | + final Size? wantedSize; | ||
| 13 | + | ||
| 14 | + /// Create a [MobileScannerArguments]. | ||
| 15 | + MobileScannerArguments({required this.textureId,required this.size, this.wantedSize}); | ||
| 13 | } | 16 | } |
| @@ -3,10 +3,9 @@ import 'dart:async'; | @@ -3,10 +3,9 @@ import 'dart:async'; | ||
| 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 | 5 | ||
| 6 | -import 'camera_args.dart'; | 6 | +import 'mobile_scanner_arguments.dart'; |
| 7 | import 'objects/barcode.dart'; | 7 | import 'objects/barcode.dart'; |
| 8 | -import 'torch_state.dart'; | ||
| 9 | -import 'util.dart'; | 8 | +import 'objects/barcode_utility.dart'; |
| 10 | 9 | ||
| 11 | /// The facing of a camera. | 10 | /// The facing of a camera. |
| 12 | enum CameraFacing { | 11 | enum CameraFacing { |
| @@ -17,45 +16,60 @@ enum CameraFacing { | @@ -17,45 +16,60 @@ enum CameraFacing { | ||
| 17 | back, | 16 | back, |
| 18 | } | 17 | } |
| 19 | 18 | ||
| 19 | +enum MobileScannerState { | ||
| 20 | + undetermined, | ||
| 21 | + authorized, | ||
| 22 | + denied | ||
| 23 | +} | ||
| 20 | 24 | ||
| 21 | -/// A camera controller. | ||
| 22 | -abstract class CameraController { | ||
| 23 | - /// Arguments for [CameraView]. | ||
| 24 | - ValueNotifier<CameraArgs?> get args; | ||
| 25 | - | ||
| 26 | - /// Torch state of the camera. | ||
| 27 | - ValueNotifier<TorchState> get torchState; | ||
| 28 | - | ||
| 29 | - /// A stream of barcodes. | ||
| 30 | - Stream<Barcode> get barcodes; | 25 | +/// The state of torch. |
| 26 | +enum TorchState { | ||
| 27 | + /// Torch is off. | ||
| 28 | + off, | ||
| 31 | 29 | ||
| 32 | - /// Create a [CameraController]. | ||
| 33 | - /// | ||
| 34 | - /// [facing] target facing used to select camera. | ||
| 35 | - /// | ||
| 36 | - /// [formats] the barcode formats for image analyzer. | ||
| 37 | - factory CameraController([CameraFacing facing = CameraFacing.back]) => | ||
| 38 | - _CameraController(facing); | 30 | + /// Torch is on. |
| 31 | + on, | ||
| 32 | +} | ||
| 39 | 33 | ||
| 40 | - /// Start the camera asynchronously. | ||
| 41 | - Future<void> startAsync(); | ||
| 42 | 34 | ||
| 43 | - /// Switch the torch's state. | ||
| 44 | - void torch(); | ||
| 45 | 35 | ||
| 46 | - /// Release the resources of the camera. | ||
| 47 | - void dispose(); | ||
| 48 | -} | 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 | +// } | ||
| 64 | + | ||
| 65 | +class MobileScannerController { | ||
| 49 | 66 | ||
| 50 | -class _CameraController implements CameraController { | ||
| 51 | static const MethodChannel method = | 67 | static const MethodChannel method = |
| 52 | MethodChannel('dev.steenbakker.mobile_scanner/scanner/method'); | 68 | MethodChannel('dev.steenbakker.mobile_scanner/scanner/method'); |
| 53 | static const EventChannel event = | 69 | static const EventChannel event = |
| 54 | EventChannel('dev.steenbakker.mobile_scanner/scanner/event'); | 70 | EventChannel('dev.steenbakker.mobile_scanner/scanner/event'); |
| 55 | 71 | ||
| 56 | - static const undetermined = 0; | ||
| 57 | - static const authorized = 1; | ||
| 58 | - static const denied = 2; | 72 | + |
| 59 | 73 | ||
| 60 | static const analyze_none = 0; | 74 | static const analyze_none = 0; |
| 61 | static const analyze_barcode = 1; | 75 | static const analyze_barcode = 1; |
| @@ -64,18 +78,15 @@ class _CameraController implements CameraController { | @@ -64,18 +78,15 @@ class _CameraController implements CameraController { | ||
| 64 | static StreamSubscription? subscription; | 78 | static StreamSubscription? subscription; |
| 65 | 79 | ||
| 66 | final CameraFacing facing; | 80 | final CameraFacing facing; |
| 67 | - @override | ||
| 68 | - final ValueNotifier<CameraArgs?> args; | ||
| 69 | - @override | 81 | + final ValueNotifier<MobileScannerArguments?> args; |
| 70 | final ValueNotifier<TorchState> torchState; | 82 | final ValueNotifier<TorchState> torchState; |
| 71 | 83 | ||
| 72 | bool torchable; | 84 | bool torchable; |
| 73 | late StreamController<Barcode> barcodesController; | 85 | late StreamController<Barcode> barcodesController; |
| 74 | 86 | ||
| 75 | - @override | ||
| 76 | Stream<Barcode> get barcodes => barcodesController.stream; | 87 | Stream<Barcode> get barcodes => barcodesController.stream; |
| 77 | 88 | ||
| 78 | - _CameraController(this.facing) | 89 | + MobileScannerController(BuildContext context, {required num width, required num height, this.facing = CameraFacing.back}) |
| 79 | : args = ValueNotifier(null), | 90 | : args = ValueNotifier(null), |
| 80 | torchState = ValueNotifier(TorchState.off), | 91 | torchState = ValueNotifier(TorchState.off), |
| 81 | torchable = false { | 92 | torchable = false { |
| @@ -89,7 +100,13 @@ class _CameraController implements CameraController { | @@ -89,7 +100,13 @@ class _CameraController implements CameraController { | ||
| 89 | onListen: () => tryAnalyze(analyze_barcode), | 100 | onListen: () => tryAnalyze(analyze_barcode), |
| 90 | onCancel: () => tryAnalyze(analyze_none), | 101 | onCancel: () => tryAnalyze(analyze_none), |
| 91 | ); | 102 | ); |
| 92 | - startAsync(); | 103 | + |
| 104 | + final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; | ||
| 105 | + | ||
| 106 | + | ||
| 107 | + start( | ||
| 108 | + width: (devicePixelRatio * width.toInt()).ceil(), | ||
| 109 | + height: (devicePixelRatio * height.toInt()).ceil()); | ||
| 93 | // Listen event handler. | 110 | // Listen event handler. |
| 94 | subscription = | 111 | subscription = |
| 95 | event.receiveBroadcastStream().listen((data) => handleEvent(data)); | 112 | event.receiveBroadcastStream().listen((data) => handleEvent(data)); |
| @@ -119,39 +136,53 @@ class _CameraController implements CameraController { | @@ -119,39 +136,53 @@ class _CameraController implements CameraController { | ||
| 119 | method.invokeMethod('analyze', mode); | 136 | method.invokeMethod('analyze', mode); |
| 120 | } | 137 | } |
| 121 | 138 | ||
| 122 | - @override | ||
| 123 | - Future<void> startAsync() async { | 139 | + Future<void> start({ |
| 140 | + int? width, | ||
| 141 | + int? height, | ||
| 142 | + // List<BarcodeFormats>? formats = _defaultBarcodeFormats, | ||
| 143 | + }) async { | ||
| 124 | ensure('startAsync'); | 144 | ensure('startAsync'); |
| 125 | // Check authorization state. | 145 | // Check authorization state. |
| 126 | - var state = await method.invokeMethod('state'); | ||
| 127 | - if (state == undetermined) { | ||
| 128 | - final result = await method.invokeMethod('request'); | ||
| 129 | - state = result ? authorized : denied; | ||
| 130 | - } | ||
| 131 | - if (state != authorized) { | 146 | + MobileScannerState state = MobileScannerState.values[await method.invokeMethod('state')]; |
| 147 | + switch (state) { | ||
| 148 | + case MobileScannerState.undetermined: | ||
| 149 | + final bool result = await method.invokeMethod('request'); | ||
| 150 | + state = result ? MobileScannerState.authorized : MobileScannerState.denied; | ||
| 151 | + break; | ||
| 152 | + case MobileScannerState.authorized: | ||
| 153 | + break; | ||
| 154 | + case MobileScannerState.denied: | ||
| 132 | throw PlatformException(code: 'NO ACCESS'); | 155 | throw PlatformException(code: 'NO ACCESS'); |
| 133 | } | 156 | } |
| 157 | + | ||
| 158 | + debugPrint('TARGET RESOLUTION $width, $height'); | ||
| 134 | // Start camera. | 159 | // Start camera. |
| 135 | final answer = | 160 | final answer = |
| 136 | - await method.invokeMapMethod<String, dynamic>('start', facing.index); | 161 | + await method.invokeMapMethod<String, dynamic>('start', { |
| 162 | + 'targetWidth': width, | ||
| 163 | + 'targetHeight': height, | ||
| 164 | + 'facing': facing.index | ||
| 165 | + }); | ||
| 137 | final textureId = answer?['textureId']; | 166 | final textureId = answer?['textureId']; |
| 138 | - final size = toSize(answer?['size']); | ||
| 139 | - args.value = CameraArgs(textureId, size); | 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 | + } | ||
| 174 | + | ||
| 140 | torchable = answer?['torchable']; | 175 | torchable = answer?['torchable']; |
| 141 | } | 176 | } |
| 142 | 177 | ||
| 143 | - @override | ||
| 144 | void torch() { | 178 | void torch() { |
| 145 | ensure('torch'); | 179 | ensure('torch'); |
| 146 | - if (!torchable) { | ||
| 147 | - return; | ||
| 148 | - } | 180 | + if (!torchable) return; |
| 149 | var state = | 181 | var state = |
| 150 | torchState.value == TorchState.off ? TorchState.on : TorchState.off; | 182 | torchState.value == TorchState.off ? TorchState.on : TorchState.off; |
| 151 | method.invokeMethod('torch', state.index); | 183 | method.invokeMethod('torch', state.index); |
| 152 | } | 184 | } |
| 153 | 185 | ||
| 154 | - @override | ||
| 155 | void dispose() { | 186 | void dispose() { |
| 156 | if (hashCode == id) { | 187 | if (hashCode == id) { |
| 157 | stop(); | 188 | stop(); |
lib/src/mobile_scanner_handler.dart
deleted
100644 → 0
| 1 | -import 'dart:async'; | ||
| 2 | -import 'package:flutter/material.dart'; | ||
| 3 | -import 'package:flutter/services.dart'; | ||
| 4 | -import 'package:mobile_scanner/src/objects/preview_details.dart'; | ||
| 5 | - | ||
| 6 | -import 'objects/barcode_formats.dart'; | ||
| 7 | - | ||
| 8 | -enum FrameRotation { none, ninetyCC, oneeighty, twoseventyCC } | ||
| 9 | - | ||
| 10 | -const _defaultBarcodeFormats = [ | ||
| 11 | - BarcodeFormats.ALL_FORMATS, | ||
| 12 | -]; | ||
| 13 | - | ||
| 14 | -typedef QRCodeHandler = void Function(String? qr); | ||
| 15 | - | ||
| 16 | -class MobileScannerHandler { | ||
| 17 | - static const MethodChannel _channel = | ||
| 18 | - MethodChannel('dev.steenbakker.mobile_scanner/scanner'); | ||
| 19 | - | ||
| 20 | - static Future<String?> get platformVersion async { | ||
| 21 | - final String? version = await _channel.invokeMethod('getPlatformVersion'); | ||
| 22 | - return version; | ||
| 23 | - } | ||
| 24 | - | ||
| 25 | - static bool rearLens = true; | ||
| 26 | - static bool manualFocus = false; | ||
| 27 | - | ||
| 28 | - //Set target size before starting | ||
| 29 | - static Future<PreviewDetails> start({ | ||
| 30 | - required int width, | ||
| 31 | - required int height, | ||
| 32 | - required QRCodeHandler qrCodeHandler, | ||
| 33 | - List<BarcodeFormats>? formats = _defaultBarcodeFormats, | ||
| 34 | - }) async { | ||
| 35 | - final _formats = formats ?? _defaultBarcodeFormats; | ||
| 36 | - assert(_formats.isNotEmpty); | ||
| 37 | - | ||
| 38 | - List<String> formatStrings = _formats | ||
| 39 | - .map((format) => format.toString().split('.')[1]) | ||
| 40 | - .toList(growable: false); | ||
| 41 | - | ||
| 42 | - _channel.setMethodCallHandler((MethodCall call) async { | ||
| 43 | - switch (call.method) { | ||
| 44 | - case 'qrRead': | ||
| 45 | - assert(call.arguments is String); | ||
| 46 | - qrCodeHandler(call.arguments); | ||
| 47 | - break; | ||
| 48 | - default: | ||
| 49 | - debugPrint("QrChannelHandler: unknown method call received at " | ||
| 50 | - "${call.method}"); | ||
| 51 | - } | ||
| 52 | - }); | ||
| 53 | - | ||
| 54 | - var details = await _channel.invokeMethod('start', { | ||
| 55 | - 'targetWidth': width, | ||
| 56 | - 'targetHeight': height, | ||
| 57 | - 'heartbeatTimeout': 0, | ||
| 58 | - 'formats': formatStrings, | ||
| 59 | - 'rearLens': rearLens, | ||
| 60 | - 'manualFocus': manualFocus | ||
| 61 | - }); | ||
| 62 | - | ||
| 63 | - assert(details is Map<dynamic, dynamic>); | ||
| 64 | - | ||
| 65 | - int? textureId = details["textureId"]; | ||
| 66 | - num? orientation = details["surfaceOrientation"]; | ||
| 67 | - num? surfaceHeight = details["surfaceHeight"]; | ||
| 68 | - num? surfaceWidth = details["surfaceWidth"]; | ||
| 69 | - | ||
| 70 | - return PreviewDetails(surfaceWidth, surfaceHeight, orientation, textureId); | ||
| 71 | - } | ||
| 72 | - | ||
| 73 | - static Future switchCamera() { | ||
| 74 | - return _channel.invokeMethod('switch').catchError(print); | ||
| 75 | - } | ||
| 76 | - | ||
| 77 | - static Future toggleFlash() { | ||
| 78 | - return _channel.invokeMethod('toggleFlash').catchError(print); | ||
| 79 | - } | ||
| 80 | - | ||
| 81 | - static Future stop() { | ||
| 82 | - _channel.setMethodCallHandler(null); | ||
| 83 | - return _channel.invokeMethod('stop').catchError(print); | ||
| 84 | - } | ||
| 85 | - | ||
| 86 | - static Future heartbeat() { | ||
| 87 | - return _channel.invokeMethod('heartbeat').catchError(print); | ||
| 88 | - } | ||
| 89 | - | ||
| 90 | - static Future<List<List<int>>?> getSupportedSizes() { | ||
| 91 | - return _channel.invokeMethod('getSupportedSizes').catchError(print) | ||
| 92 | - as Future<List<List<int>>?>; | ||
| 93 | - } | ||
| 94 | -} |
lib/src/mobile_scanner_preview.dart
deleted
100644 → 0
| 1 | -// import 'dart:async'; | ||
| 2 | -// | ||
| 3 | -// import 'package:flutter/material.dart'; | ||
| 4 | -// import 'package:mobile_scanner/src/objects/preview_details.dart'; | ||
| 5 | -// | ||
| 6 | -// class Preview extends StatefulWidget { | ||
| 7 | -// final double width, height; | ||
| 8 | -// final double targetWidth, targetHeight; | ||
| 9 | -// final int? textureId; | ||
| 10 | -// final int? sensorOrientation; | ||
| 11 | -// final BoxFit fit; | ||
| 12 | -// | ||
| 13 | -// Preview({ | ||
| 14 | -// Key? key, | ||
| 15 | -// required PreviewDetails previewDetails, | ||
| 16 | -// required this.targetWidth, | ||
| 17 | -// required this.targetHeight, | ||
| 18 | -// required this.fit, | ||
| 19 | -// }) : textureId = previewDetails.textureId, | ||
| 20 | -// width = previewDetails.width!.toDouble(), | ||
| 21 | -// height = previewDetails.height!.toDouble(), | ||
| 22 | -// sensorOrientation = previewDetails.sensorOrientation as int?, | ||
| 23 | -// super(key: key); | ||
| 24 | -// | ||
| 25 | -// @override | ||
| 26 | -// State<Preview> createState() => _PreviewState(); | ||
| 27 | -// } | ||
| 28 | -// | ||
| 29 | -// class _PreviewState extends State<Preview> { | ||
| 30 | -// | ||
| 31 | -// final _streamSubscriptions = <StreamSubscription<dynamic>>[]; | ||
| 32 | -// bool landscapeLeft = false; | ||
| 33 | -// | ||
| 34 | -// @override | ||
| 35 | -// void initState() { | ||
| 36 | -// super.initState(); | ||
| 37 | -// _streamSubscriptions.add( | ||
| 38 | -// magnetometerEvents.listen( | ||
| 39 | -// (MagnetometerEvent event) { | ||
| 40 | -// if (event.x <= 0) { | ||
| 41 | -// landscapeLeft = true; | ||
| 42 | -// } else { | ||
| 43 | -// landscapeLeft = false; | ||
| 44 | -// } | ||
| 45 | -// }, | ||
| 46 | -// ), | ||
| 47 | -// ); | ||
| 48 | -// } | ||
| 49 | -// | ||
| 50 | -// @override | ||
| 51 | -// void dispose() { | ||
| 52 | -// super.dispose(); | ||
| 53 | -// for (final subscription in _streamSubscriptions) { | ||
| 54 | -// subscription.cancel(); | ||
| 55 | -// } | ||
| 56 | -// } | ||
| 57 | -// | ||
| 58 | -// | ||
| 59 | -// int _getRotationCompensation(NativeDeviceOrientation nativeOrientation) { | ||
| 60 | -// int nativeRotation = 0; | ||
| 61 | -// switch (nativeOrientation) { | ||
| 62 | -// case NativeDeviceOrientation.portraitUp: | ||
| 63 | -// nativeRotation = 0; | ||
| 64 | -// break; | ||
| 65 | -// case NativeDeviceOrientation.landscapeRight: | ||
| 66 | -// nativeRotation = 90; | ||
| 67 | -// break; | ||
| 68 | -// case NativeDeviceOrientation.portraitDown: | ||
| 69 | -// nativeRotation = 180; | ||
| 70 | -// break; | ||
| 71 | -// case NativeDeviceOrientation.landscapeLeft: | ||
| 72 | -// nativeRotation = 270; | ||
| 73 | -// break; | ||
| 74 | -// case NativeDeviceOrientation.unknown: | ||
| 75 | -// default: | ||
| 76 | -// break; | ||
| 77 | -// } | ||
| 78 | -// | ||
| 79 | -// return ((nativeRotation - widget.sensorOrientation! + 450) % 360) ~/ 90; | ||
| 80 | -// } | ||
| 81 | -// | ||
| 82 | -// @override | ||
| 83 | -// Widget build(BuildContext context) { | ||
| 84 | -// final orientation = MediaQuery.of(context).orientation; | ||
| 85 | -// double frameHeight = widget.width; | ||
| 86 | -// double frameWidth = widget.height; | ||
| 87 | -// | ||
| 88 | -// return ClipRect( | ||
| 89 | -// child: FittedBox( | ||
| 90 | -// fit: widget.fit, | ||
| 91 | -// child: RotatedBox( | ||
| 92 | -// quarterTurns: orientation == Orientation.landscape ? landscapeLeft ? 1 : 3 : 0, | ||
| 93 | -// child: SizedBox( | ||
| 94 | -// width: frameWidth, | ||
| 95 | -// height: frameHeight, | ||
| 96 | -// child: Texture(textureId: widget.textureId!), | ||
| 97 | -// ), | ||
| 98 | -// ), | ||
| 99 | -// ), | ||
| 100 | -// ); | ||
| 101 | -// | ||
| 102 | -// return NativeDeviceOrientationReader( | ||
| 103 | -// builder: (context) { | ||
| 104 | -// var nativeOrientation = | ||
| 105 | -// NativeDeviceOrientationReader.orientation(context); | ||
| 106 | -// | ||
| 107 | -// double frameHeight = widget.width; | ||
| 108 | -// double frameWidth = widget.height; | ||
| 109 | -// | ||
| 110 | -// return ClipRect( | ||
| 111 | -// child: FittedBox( | ||
| 112 | -// fit: widget.fit, | ||
| 113 | -// child: RotatedBox( | ||
| 114 | -// quarterTurns: _getRotationCompensation(nativeOrientation), | ||
| 115 | -// child: SizedBox( | ||
| 116 | -// width: frameWidth, | ||
| 117 | -// height: frameHeight, | ||
| 118 | -// child: Texture(textureId: widget.textureId!), | ||
| 119 | -// ), | ||
| 120 | -// ), | ||
| 121 | -// ), | ||
| 122 | -// ); | ||
| 123 | -// }, | ||
| 124 | -// ); | ||
| 125 | -// } | ||
| 126 | -// } |
| 1 | import 'dart:typed_data'; | 1 | import 'dart:typed_data'; |
| 2 | import 'dart:ui'; | 2 | import 'dart:ui'; |
| 3 | 3 | ||
| 4 | -import '../util.dart'; | 4 | +import '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 { |
lib/src/objects/barcode_formats.dart
deleted
100644 → 0
| 1 | import 'package:flutter/material.dart'; | 1 | import 'package:flutter/material.dart'; |
| 2 | 2 | ||
| 3 | -import 'objects/barcode.dart'; | 3 | +import 'barcode.dart'; |
| 4 | 4 | ||
| 5 | Size toSize(Map<dynamic, dynamic> data) { | 5 | Size toSize(Map<dynamic, dynamic> data) { |
| 6 | final width = data['width']; | 6 | final width = data['width']; |
lib/src/objects/preview_details.dart
deleted
100644 → 0
-
Please register or login to post a comment