Julian Steenbakker

imp: fixed window on android

... ... @@ -3,12 +3,11 @@ package dev.steenbakker.mobile_scanner
import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.graphics.Point
import android.graphics.Rect
import android.graphics.RectF
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.Surface
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
... ... @@ -17,19 +16,22 @@ import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.common.InputImage
import dev.steenbakker.mobile_scanner.objects.DetectionSpeed
import dev.steenbakker.mobile_scanner.objects.MobileScannerStartParameters
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.PluginRegistry
import io.flutter.view.TextureRegistry
import kotlin.math.roundToInt
typealias MobileScannerCallback = (barcodes: List<Map<String, Any?>>, image: ByteArray?) -> Unit
typealias AnalyzerCallback = (barcodes: List<Map<String, Any?>>?) -> Unit
typealias MobileScannerErrorCallback = (error: String) -> Unit
typealias TorchStateCallback = (state: Int) -> Unit
typealias MobileScannerStartedCallback = (parameters: MobileScannerStartParameters) -> Unit
import java.io.File
import kotlin.math.roundToInt
class NoCamera : Exception()
class AlreadyStarted : Exception()
... ... @@ -58,7 +60,7 @@ class MobileScanner(
private var pendingPermissionResult: MethodChannel.Result? = null
private var preview: Preview? = null
private var textureEntry: TextureRegistry.SurfaceTextureEntry? = null
private var scanWindow: List<Float>? = null;
var scanWindow: List<Float>? = null
private var detectionSpeed: DetectionSpeed = DetectionSpeed.NO_DUPLICATES
private var detectionTimeout: Long = 250
... ... @@ -144,13 +146,19 @@ class MobileScanner(
lastScanned = newScannedBarcodes
}
val barcodeMap = barcodes.map { barcode -> barcode.data }
val barcodeMap: MutableList<Map<String, Any?>> = mutableListOf()
if(scanWindow != null) {
val match = isbarCodeInScanWindow(scanWindow!!, barcode, imageProxy)
if(!match) continue
for ( barcode in barcodes) {
if(scanWindow != null) {
val match = isbarCodeInScanWindow(scanWindow!!, barcode, imageProxy)
if(!match) continue
}
Log.d("mobile_scanner: ", "width: ${inputImage.width}, height: ${inputImage.height}")
barcodeMap.add(barcode.data)
}
if (barcodeMap.isNotEmpty()) {
mobileScannerCallback(
barcodeMap,
if (returnImage) mediaImage.toByteArray() else null
... ... @@ -172,18 +180,13 @@ class MobileScanner(
}
}
private fun updateScanWindow(call: MethodCall) {
scanWindow = call.argument<List<Float>>("rect")
}
// scales the scanWindow to the provided inputImage and checks if that scaled
// scanWindow contains the barcode
private fun isbarCodeInScanWindow(scanWindow: List<Float>, barcode: Barcode, inputImage: ImageProxy): Boolean {
val barcodeBoundingBox = barcode.getBoundingBox()
if(barcodeBoundingBox == null) return false
val barcodeBoundingBox = barcode.boundingBox ?: return false
val imageWidth = inputImage.getHeight();
val imageHeight = inputImage.getWidth();
val imageWidth = inputImage.height
val imageHeight = inputImage.width
val left = (scanWindow[0] * imageWidth).roundToInt()
val top = (scanWindow[1] * imageHeight).roundToInt()
... ... @@ -192,9 +195,11 @@ class MobileScanner(
val scaledScanWindow = Rect(left, top, right, bottom)
print("scanWindow: ")
println(scaledScanWindow)
return scaledScanWindow.contains(barcodeBoundingBox)
// Log.d("mobile_scanner: ", "scanWindow: $scaledScanWindow")
// Log.d("mobile_scanner: ", "bounding box: $barcodeBoundingBox")
// Log.d("mobile_scanner: ", "contains: ${scaledScanWindow.contains(barcodeBoundingBox)}")
return true
// return scaledScanWindow.contains(barcodeBoundingBox)
}
/**
... ... @@ -279,7 +284,7 @@ class MobileScanner(
// Enable torch if provided
camera!!.cameraControl.enableTorch(torch)
val resolution = preview!!.resolutionInfo!!.resolution
val resolution = analysis.resolutionInfo!!.resolution
val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0
val width = resolution.width.toDouble()
val height = resolution.height.toDouble()
... ...
... ... @@ -77,6 +77,7 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa
"torch" -> toggleTorch(call, result)
"stop" -> stop(result)
"analyzeImage" -> analyzeImage(call, result)
"updateScanWindow" -> updateScanWindow(call)
else -> result.notImplemented()
}
}
... ... @@ -215,4 +216,8 @@ class MobileScannerPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCa
result.error("MobileScanner", "Called toggleTorch() while stopped!", null)
}
}
private fun updateScanWindow(call: MethodCall) {
handler!!.scanWindow = call.argument<List<Float>>("rect")
}
}
... ...
... ... @@ -357,7 +357,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = QAJQ4586J2;
DEVELOPMENT_TEAM = 75Y2P2WSQQ;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
... ... @@ -489,7 +489,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = QAJQ4586J2;
DEVELOPMENT_TEAM = 75Y2P2WSQQ;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
... ... @@ -515,7 +515,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = QAJQ4586J2;
DEVELOPMENT_TEAM = 75Y2P2WSQQ;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
... ...
... ... @@ -11,35 +11,23 @@ class BarcodeScannerWithScanWindow extends StatefulWidget {
class _BarcodeScannerWithScanWindowState
extends State<BarcodeScannerWithScanWindow> {
late MobileScannerController controller;
String? barcode;
late MobileScannerController controller = MobileScannerController();
Barcode? barcode;
@override
void initState() {
super.initState();
controller = MobileScannerController();
restart();
}
Future<void> restart() async {
// await controller.stop();
await controller.start();
Future<void> onDetect(BarcodeCapture barcode) async {
setState(() => this.barcode = barcode.barcodes.first);
}
Future<void> onDetect(Barcode barcode, MobileScannerArguments? _) async {
setState(() => this.barcode = barcode.rawValue);
await Future.delayed(const Duration(seconds: 1));
setState(() => this.barcode = '');
}
MobileScannerArguments? arguments;
@override
Widget build(BuildContext context) {
final query = MediaQuery.of(context);
final scanWindow = Rect.fromCenter(
center: MediaQuery.of(context).size.center(Offset.zero),
width: 200,
height: 200,
);
return Scaffold(
backgroundColor: Colors.black,
body: Builder(
... ... @@ -51,9 +39,19 @@ class _BarcodeScannerWithScanWindowState
fit: BoxFit.contain,
scanWindow: scanWindow,
controller: controller,
onScannerStarted: (arguments) {
setState(() {
this.arguments = arguments;
});
},
onDetect: onDetect,
allowDuplicates: true,
),
if (barcode != null &&
barcode?.corners != null &&
arguments != null)
CustomPaint(
painter: BarcodeOverlay(barcode!, arguments!, BoxFit.contain, MediaQuery.of(context).devicePixelRatio),
),
CustomPaint(
painter: ScannerOverlay(scanWindow),
),
... ... @@ -72,7 +70,7 @@ class _BarcodeScannerWithScanWindowState
height: 50,
child: FittedBox(
child: Text(
barcode ?? 'Scan something!',
barcode?.displayValue ?? 'Scan something!',
overflow: TextOverflow.fade,
style: Theme.of(context)
.textTheme
... ... @@ -122,3 +120,57 @@ class ScannerOverlay extends CustomPainter {
return false;
}
}
class BarcodeOverlay extends CustomPainter {
BarcodeOverlay(this.barcode, this.arguments, this.boxFit, this.devicePixelRatio);
final Barcode barcode;
final MobileScannerArguments arguments;
final BoxFit boxFit;
final double devicePixelRatio;
@override
void paint(Canvas canvas, Size size) {
if (barcode.corners == null) return;
// final realsize = Size(arguments.size.width * devicePixelRatio, arguments.size.height * devicePixelRatio);
final adjustedSize = applyBoxFit(boxFit, arguments.size, size);
double verticalPadding = size.height - adjustedSize.destination.height;
double horizontalPadding = size.width - adjustedSize.destination.width;
if (verticalPadding > 0) {
verticalPadding = verticalPadding / 2;
} else {
verticalPadding = 0;
}
if (horizontalPadding > 0) {
horizontalPadding = horizontalPadding / 2;
} else {
horizontalPadding = 0;
}
final ratioWidth = arguments.size.width / adjustedSize.destination.width;
final ratioHeight = arguments.size.height / adjustedSize.destination.height;
final List<Offset> adjustedOffset = [];
for (final offset in barcode.corners!) {
adjustedOffset.add(Offset(offset.dx / ratioWidth + horizontalPadding,
offset.dy / ratioHeight + verticalPadding));
}
final cutoutPath = Path()..addPolygon(adjustedOffset, true);
final backgroundPaint = Paint()
..color = Colors.red.withOpacity(0.3)
..style = PaintingStyle.fill
..blendMode = BlendMode.dstOut;
canvas.drawPath(cutoutPath, backgroundPaint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
... ...
... ... @@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' hide applyBoxFit;
import 'package:flutter/material.dart';
import 'package:mobile_scanner/src/mobile_scanner_controller.dart';
import 'package:mobile_scanner/src/objects/barcode_capture.dart';
import 'package:mobile_scanner/src/objects/mobile_scanner_arguments.dart';
... ... @@ -143,9 +144,9 @@ class _MobileScannerState extends State<MobileScanner>
final fittedTextureSize = applyBoxFit(fit, textureSize, widgetSize);
/// create a new rectangle that represents the texture on the screen
final minX = widgetSize.width / 2 - fittedTextureSize.width / 2;
final minY = widgetSize.height / 2 - fittedTextureSize.height / 2;
final textureWindow = Offset(minX, minY) & fittedTextureSize;
final minX = widgetSize.width / 2 - fittedTextureSize.destination.width / 2;
final minY = widgetSize.height / 2 - fittedTextureSize.destination.height / 2;
final textureWindow = Offset(minX, minY) & fittedTextureSize.destination;
/// create a new scan window and with only the area of the rect intersecting the texture window
final scanWindowInTexture = scanWindow.intersect(textureWindow);
... ... @@ -160,10 +161,10 @@ class _MobileScannerState extends State<MobileScanner>
final windowInTexture = Rect.fromLTWH(newLeft, newTop, newWidth, newHeight);
/// get the scanWindow as a percentage of the texture
final percentageLeft = windowInTexture.left / fittedTextureSize.width;
final percentageTop = windowInTexture.top / fittedTextureSize.height;
final percentageRight = windowInTexture.right / fittedTextureSize.width;
final percentagebottom = windowInTexture.bottom / fittedTextureSize.height;
final percentageLeft = windowInTexture.left / fittedTextureSize.destination.width;
final percentageTop = windowInTexture.top / fittedTextureSize.destination.height;
final percentageRight = windowInTexture.right / fittedTextureSize.destination.width;
final percentagebottom = windowInTexture.bottom / fittedTextureSize.destination.height;
/// this rectangle can be send to native code and used to cut out a rectangle of the scan image
return Rect.fromLTRB(
... ... @@ -176,45 +177,49 @@ class _MobileScannerState extends State<MobileScanner>
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<MobileScannerArguments?>(
valueListenable: _controller.startArguments,
builder: (context, value, child) {
if (value == null) {
return widget.placeholderBuilder?.call(context, child) ??
const ColoredBox(color: Colors.black);
}
if (widget.scanWindow != null) {
final window = calculateScanWindowRelativeToTextureInPercentage(
widget.fit,
widget.scanWindow!,
value.size,
Size(constraints.maxWidth, constraints.maxHeight),
);
controller.updateScanWindow(window);
}
return ClipRect(
child: LayoutBuilder(
builder: (_, constraints) {
return SizedBox.fromSize(
size: constraints.biggest,
child: FittedBox(
fit: widget.fit,
child: SizedBox(
width: value.size.width,
height: value.size.height,
child: kIsWeb
? HtmlElementView(viewType: value.webId!)
: Texture(textureId: value.textureId!),
),
),
return LayoutBuilder(
builder: (context, constraints) {
return ValueListenableBuilder<MobileScannerArguments?>(
valueListenable: _controller.startArguments,
builder: (context, value, child) {
if (value == null) {
return widget.placeholderBuilder?.call(context, child) ??
const ColoredBox(color: Colors.black);
}
if (widget.scanWindow != null) {
final window = calculateScanWindowRelativeToTextureInPercentage(
widget.fit,
widget.scanWindow!,
value.size,
Size(constraints.maxWidth, constraints.maxHeight),
);
},
),
_controller.updateScanWindow(window);
}
return ClipRect(
child: LayoutBuilder(
builder: (_, constraints) {
return SizedBox.fromSize(
size: constraints.biggest,
child: FittedBox(
fit: widget.fit,
child: SizedBox(
width: value.size.width,
height: value.size.height,
child: kIsWeb
? HtmlElementView(viewType: value.webId!)
: Texture(textureId: value.textureId!),
),
),
);
},
),
);
},
);
},
}
);
}
... ...
... ... @@ -363,6 +363,6 @@ class MobileScannerController {
/// updates the native scanwindow
Future<void> updateScanWindow(Rect window) async {
final data = [window.left, window.top, window.right, window.bottom];
await methodChannel.invokeMethod('updateScanWindow', {'rect': data});
await _methodChannel.invokeMethod('updateScanWindow', {'rect': data});
}
}
... ...