casvanluijtelaar

android implementation

@@ -4,9 +4,11 @@ import android.Manifest @@ -4,9 +4,11 @@ import android.Manifest
4 import android.app.Activity 4 import android.app.Activity
5 import android.content.pm.PackageManager 5 import android.content.pm.PackageManager
6 import android.graphics.Point 6 import android.graphics.Point
  7 +import android.graphics.Rect
7 import android.net.Uri 8 import android.net.Uri
8 import android.util.Log 9 import android.util.Log
9 import android.util.Size 10 import android.util.Size
  11 +import android.media.Image
10 import android.view.Surface 12 import android.view.Surface
11 import androidx.annotation.NonNull 13 import androidx.annotation.NonNull
12 import androidx.camera.core.* 14 import androidx.camera.core.*
@@ -18,6 +20,7 @@ import com.google.mlkit.vision.barcode.BarcodeScannerOptions @@ -18,6 +20,7 @@ import com.google.mlkit.vision.barcode.BarcodeScannerOptions
18 import com.google.mlkit.vision.barcode.BarcodeScanning 20 import com.google.mlkit.vision.barcode.BarcodeScanning
19 import com.google.mlkit.vision.barcode.common.Barcode 21 import com.google.mlkit.vision.barcode.common.Barcode
20 import com.google.mlkit.vision.common.InputImage 22 import com.google.mlkit.vision.common.InputImage
  23 +import com.google.mlkit.vision.common.InputImage.IMAGE_FORMAT_NV21
21 import io.flutter.plugin.common.EventChannel 24 import io.flutter.plugin.common.EventChannel
22 import io.flutter.plugin.common.MethodCall 25 import io.flutter.plugin.common.MethodCall
23 import io.flutter.plugin.common.MethodChannel 26 import io.flutter.plugin.common.MethodChannel
@@ -40,6 +43,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -40,6 +43,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
40 private var camera: Camera? = null 43 private var camera: Camera? = null
41 private var preview: Preview? = null 44 private var preview: Preview? = null
42 private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null 45 private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null
  46 + private var scanWindow: Rect? = null;
43 47
44 // @AnalyzeMode 48 // @AnalyzeMode
45 // private var analyzeMode: Int = AnalyzeMode.NONE 49 // private var analyzeMode: Int = AnalyzeMode.NONE
@@ -99,7 +103,20 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -99,7 +103,20 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
99 // when (analyzeMode) { 103 // when (analyzeMode) {
100 // AnalyzeMode.BARCODE -> { 104 // AnalyzeMode.BARCODE -> {
101 val mediaImage = imageProxy.image ?: return@Analyzer 105 val mediaImage = imageProxy.image ?: return@Analyzer
102 - val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) 106 + var inputImage: InputImage?;
  107 +
  108 + if(scanWindow != null) {
  109 + val croppedImage = croppedNV21(mediaImage, scanWindow!!)
  110 + inputImage = InputImage.fromByteArray(
  111 + croppedImage,
  112 + scanWindow!!.width(),
  113 + scanWindow!!.height(),
  114 + imageProxy.imageInfo.rotationDegrees,
  115 + IMAGE_FORMAT_NV21,
  116 + )
  117 + } else {
  118 + inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
  119 + }
103 120
104 scanner.process(inputImage) 121 scanner.process(inputImage)
105 .addOnSuccessListener { barcodes -> 122 .addOnSuccessListener { barcodes ->
@@ -115,6 +132,50 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -115,6 +132,50 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
115 // } 132 // }
116 } 133 }
117 134
  135 + private fun croppedNV21(mediaImage: Image, cropRect: Rect): ByteArray {
  136 + val yBuffer = mediaImage.planes[0].buffer // Y
  137 + val vuBuffer = mediaImage.planes[2].buffer // VU
  138 +
  139 + val ySize = yBuffer.remaining()
  140 + val vuSize = vuBuffer.remaining()
  141 +
  142 + val nv21 = ByteArray(ySize + vuSize)
  143 +
  144 + yBuffer.get(nv21, 0, ySize)
  145 + vuBuffer.get(nv21, ySize, vuSize)
  146 +
  147 + return cropByteArray(nv21, mediaImage.width, mediaImage.height, cropRect)
  148 + }
  149 +
  150 + private fun cropByteArray(src: ByteArray, width: Int, height: Int, cropRect: Rect, ): ByteArray {
  151 + val x = cropRect.left * 2 / 2
  152 + val y = cropRect.top * 2 / 2
  153 + val w = cropRect.width() * 2 / 2
  154 + val h = cropRect.height() * 2 / 2
  155 + val yUnit = w * h
  156 + val uv = yUnit / 2
  157 + val nData = ByteArray(yUnit + uv)
  158 + val uvIndexDst = w * h - y / 2 * w
  159 + val uvIndexSrc = width * height + x
  160 + var srcPos0 = y * width
  161 + var destPos0 = 0
  162 + var uvSrcPos0 = uvIndexSrc
  163 + var uvDestPos0 = uvIndexDst
  164 + for (i in y until y + h) {
  165 + System.arraycopy(src, srcPos0 + x, nData, destPos0, w) //y memory block copy
  166 + srcPos0 += width
  167 + destPos0 += w
  168 + if (i and 1 == 0) {
  169 + System.arraycopy(src, uvSrcPos0, nData, uvDestPos0, w) //uv memory block copy
  170 + uvSrcPos0 += width
  171 + uvDestPos0 += w
  172 + }
  173 + }
  174 + return nData
  175 + }
  176 +
  177 +
  178 +
118 179
119 private var scanner = BarcodeScanning.getClient() 180 private var scanner = BarcodeScanning.getClient()
120 181
@@ -134,6 +195,9 @@ class MobileScanner(private val activity: Activity, private val textureRegistry: @@ -134,6 +195,9 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
134 val torch: Boolean = call.argument<Boolean>("torch") ?: false 195 val torch: Boolean = call.argument<Boolean>("torch") ?: false
135 val formats: List<Int>? = call.argument<List<Int>>("formats") 196 val formats: List<Int>? = call.argument<List<Int>>("formats")
136 197
  198 + val scanWindowData: List<Int>? = call.argument("scanWindow")
  199 + if(scanWindowData != null) scanWindow = Rect(scanWindowData!![0], scanWindowData!![1], scanWindowData!![2], scanWindowData!![3])
  200 +
137 if (formats != null) { 201 if (formats != null) {
138 val formatsList: MutableList<Int> = mutableListOf() 202 val formatsList: MutableList<Int> = mutableListOf()
139 for (index in formats) { 203 for (index in formats) {
  1 +import 'dart:ui';
  2 +
  3 +import 'package:flutter/material.dart';
  4 +import 'package:mobile_scanner/mobile_scanner.dart';
  5 +
  6 +class BarcodeScannerWithScanWindow extends StatefulWidget {
  7 + const BarcodeScannerWithScanWindow({Key? key}) : super(key: key);
  8 +
  9 + @override
  10 + _BarcodeScannerWithScanWindowState createState() =>
  11 + _BarcodeScannerWithScanWindowState();
  12 +}
  13 +
  14 +class _BarcodeScannerWithScanWindowState
  15 + extends State<BarcodeScannerWithScanWindow>
  16 + with SingleTickerProviderStateMixin {
  17 + String? barcode;
  18 +
  19 + late Rect scanWindow;
  20 + late MobileScannerController controller;
  21 +
  22 + @override
  23 + void initState() {
  24 + super.initState();
  25 +
  26 + final screenWidth =
  27 + window.physicalSize.shortestSide / window.devicePixelRatio;
  28 + final screenHeight =
  29 + window.physicalSize.longestSide / window.devicePixelRatio;
  30 +
  31 + scanWindow = Rect.fromCenter(
  32 + center: Offset(screenWidth / 2, screenHeight / 2),
  33 + width: 200,
  34 + height: 200,
  35 + );
  36 +
  37 + controller = MobileScannerController(
  38 + torchEnabled: true,
  39 + scanWindow: scanWindow,
  40 + );
  41 + }
  42 +
  43 + bool isStarted = true;
  44 +
  45 + @override
  46 + Widget build(BuildContext context) {
  47 + return Scaffold(
  48 + backgroundColor: Colors.black,
  49 + body: Builder(
  50 + builder: (context) {
  51 + return Stack(
  52 + children: [
  53 + MobileScanner(
  54 + fit: BoxFit.contain,
  55 + controller: controller,
  56 + onDetect: (barcode, args) {
  57 + setState(() {
  58 + this.barcode = barcode.rawValue;
  59 + });
  60 + },
  61 + ),
  62 + CustomPaint(
  63 + painter: ScannerOverlay(scanWindow),
  64 + ),
  65 + Align(
  66 + alignment: Alignment.bottomCenter,
  67 + child: Container(
  68 + alignment: Alignment.bottomCenter,
  69 + height: 100,
  70 + color: Colors.black.withOpacity(0.4),
  71 + child: Row(
  72 + mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  73 + children: [
  74 + Center(
  75 + child: SizedBox(
  76 + width: MediaQuery.of(context).size.width - 120,
  77 + height: 50,
  78 + child: FittedBox(
  79 + child: Text(
  80 + barcode ?? 'Scan something!',
  81 + overflow: TextOverflow.fade,
  82 + style: Theme.of(context)
  83 + .textTheme
  84 + .headline4!
  85 + .copyWith(color: Colors.white),
  86 + ),
  87 + ),
  88 + ),
  89 + ),
  90 + ],
  91 + ),
  92 + ),
  93 + ),
  94 + ],
  95 + );
  96 + },
  97 + ),
  98 + );
  99 + }
  100 +}
  101 +
  102 +class ScannerOverlay extends CustomPainter {
  103 + ScannerOverlay(this.scanWindow);
  104 +
  105 + final Rect scanWindow;
  106 +
  107 + @override
  108 + void paint(Canvas canvas, Size size) {
  109 + final backgroundPath = Path()..addRect(Rect.largest);
  110 + final cutoutPath = Path()..addRect(scanWindow);
  111 +
  112 + final backgroundPaint = Paint()
  113 + ..color = Colors.black.withOpacity(0.5)
  114 + ..style = PaintingStyle.fill
  115 + ..blendMode = BlendMode.dstOut;
  116 +
  117 + final backgroundWithCutout = Path.combine(
  118 + PathOperation.difference,
  119 + backgroundPath,
  120 + cutoutPath,
  121 + );
  122 + canvas.drawPath(backgroundWithCutout, backgroundPaint);
  123 + }
  124 +
  125 + @override
  126 + bool shouldRepaint(covariant CustomPainter oldDelegate) {
  127 + return false;
  128 + }
  129 +}
1 import 'package:flutter/material.dart'; 1 import 'package:flutter/material.dart';
2 import 'package:mobile_scanner_example/barcode_scanner_controller.dart'; 2 import 'package:mobile_scanner_example/barcode_scanner_controller.dart';
  3 +import 'package:mobile_scanner_example/barcode_scanner_window.dart';
3 import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart'; 4 import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart';
4 5
  6 +
5 void main() => runApp(const MaterialApp(home: MyHome())); 7 void main() => runApp(const MaterialApp(home: MyHome()));
6 8
7 class MyHome extends StatelessWidget { 9 class MyHome extends StatelessWidget {
@@ -27,6 +29,17 @@ class MyHome extends StatelessWidget { @@ -27,6 +29,17 @@ class MyHome extends StatelessWidget {
27 }, 29 },
28 child: const Text('MobileScanner with Controller'), 30 child: const Text('MobileScanner with Controller'),
29 ), 31 ),
  32 +
  33 + ElevatedButton(
  34 + onPressed: () {
  35 + Navigator.of(context).push(
  36 + MaterialPageRoute(
  37 + builder: (context) => const BarcodeScannerWithScanWindow(),
  38 + ),
  39 + );
  40 + },
  41 + child: const Text('MobileScanner with ScanWindow'),
  42 + ),
30 ElevatedButton( 43 ElevatedButton(
31 onPressed: () { 44 onPressed: () {
32 Navigator.of(context).push( 45 Navigator.of(context).push(
@@ -27,6 +27,7 @@ class MobileScanner extends StatefulWidget { @@ -27,6 +27,7 @@ class MobileScanner extends StatefulWidget {
27 /// Set to false if you don't want duplicate scans. 27 /// Set to false if you don't want duplicate scans.
28 final bool allowDuplicates; 28 final bool allowDuplicates;
29 29
  30 +
30 /// Create a [MobileScanner] with a [controller], the [controller] must has been initialized. 31 /// Create a [MobileScanner] with a [controller], the [controller] must has been initialized.
31 const MobileScanner({ 32 const MobileScanner({
32 Key? key, 33 Key? key,
@@ -49,6 +49,9 @@ class MobileScannerController { @@ -49,6 +49,9 @@ class MobileScannerController {
49 /// WARNING: On iOS, only 1 format is supported. 49 /// WARNING: On iOS, only 1 format is supported.
50 final List<BarcodeFormat>? formats; 50 final List<BarcodeFormat>? formats;
51 51
  52 + /// can be used to limit the scan area to a portion of the screen
  53 + final Rect? scanWindow;
  54 +
52 CameraFacing facing; 55 CameraFacing facing;
53 bool hasTorch = false; 56 bool hasTorch = false;
54 late StreamController<Barcode> barcodesController; 57 late StreamController<Barcode> barcodesController;
@@ -60,6 +63,7 @@ class MobileScannerController { @@ -60,6 +63,7 @@ class MobileScannerController {
60 this.ratio, 63 this.ratio,
61 this.torchEnabled, 64 this.torchEnabled,
62 this.formats, 65 this.formats,
  66 + this.scanWindow,
63 }) { 67 }) {
64 // In case a new instance is created before calling dispose() 68 // In case a new instance is created before calling dispose()
65 if (_controllerHashcode != null) { 69 if (_controllerHashcode != null) {
@@ -156,6 +160,14 @@ class MobileScannerController { @@ -156,6 +160,14 @@ class MobileScannerController {
156 // Set the starting arguments for the camera 160 // Set the starting arguments for the camera
157 final Map arguments = {}; 161 final Map arguments = {};
158 arguments['facing'] = facing.index; 162 arguments['facing'] = facing.index;
  163 + if (scanWindow != null) {
  164 + arguments['scanWindow'] = [
  165 + scanWindow!.left,
  166 + scanWindow!.top,
  167 + scanWindow!.right,
  168 + scanWindow!.bottom,
  169 + ];
  170 + }
159 if (ratio != null) arguments['ratio'] = ratio; 171 if (ratio != null) arguments['ratio'] = ratio;
160 if (torchEnabled != null) arguments['torch'] = torchEnabled; 172 if (torchEnabled != null) arguments['torch'] = torchEnabled;
161 173