Julian Steenbakker

feat: add return image and refactor existing functions

... ... @@ -50,7 +50,7 @@ dependencies {
// Use this dependency to bundle the model with your app
implementation 'com.google.mlkit:barcode-scanning:17.0.2'
// Use this dependency to use the dynamically downloaded model in Google Play Services
// implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.0.0'
// implementation 'com.google.android.gms:play-services-mlkit-barcode-scanning:18.1.0'
implementation 'androidx.camera:camera-camera2:1.1.0'
implementation 'androidx.camera:camera-lifecycle:1.1.0'
... ...
... ... @@ -3,7 +3,11 @@ package dev.steenbakker.mobile_scanner
import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.graphics.ImageFormat
import android.graphics.Point
import android.graphics.Rect
import android.graphics.YuvImage
import android.media.Image
import android.net.Uri
import android.util.Log
import android.util.Size
... ... @@ -23,11 +27,13 @@ import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.PluginRegistry
import io.flutter.view.TextureRegistry
import java.io.ByteArrayOutputStream
import java.io.File
class MobileScanner(private val activity: Activity, private val textureRegistry: TextureRegistry)
: MethodChannel.MethodCallHandler, EventChannel.StreamHandler, PluginRegistry.RequestPermissionsResultListener {
class MobileScanner(private val activity: Activity, private val textureRegistry: TextureRegistry) :
MethodChannel.MethodCallHandler, EventChannel.StreamHandler,
PluginRegistry.RequestPermissionsResultListener {
companion object {
/**
* When the application's activity is [androidx.fragment.app.FragmentActivity], requestCode can only use the lower 16 bits.
... ... @@ -70,14 +76,22 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
sink = null
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray): Boolean {
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
): Boolean {
return listener?.onRequestPermissionsResult(requestCode, permissions, grantResults) ?: false
}
private fun checkPermission(result: MethodChannel.Result) {
// Can't get exact denied or not_determined state without request. Just return not_determined when state isn't authorized
val state =
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) 1
if (ContextCompat.checkSelfPermission(
activity,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
) 1
else 0
result.success(state)
}
... ... @@ -96,32 +110,83 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
val permissions = arrayOf(Manifest.permission.CAMERA)
ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE)
}
// var lastScanned: List<Barcode>? = null
// var isAnalyzing: Boolean = false
@ExperimentalGetImage
val analyzer = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format
// when (analyzeMode) {
// AnalyzeMode.BARCODE -> {
val mediaImage = imageProxy.image ?: return@Analyzer
val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
scanner.process(inputImage)
.addOnSuccessListener { barcodes ->
for (barcode in barcodes) {
val event = mapOf("name" to "barcode", "data" to barcode.data)
sink?.success(event)
}
}
.addOnFailureListener { e -> Log.e(TAG, e.message, e) }
.addOnCompleteListener { imageProxy.close() }
// if (isAnalyzing) {
// Log.d("scanner", "SKIPPING" )
// return@addOnSuccessListener
// }
// isAnalyzing = true
val barcodeMap = barcodes.map { barcode -> barcode.data }
if (barcodeMap.isNotEmpty()) {
sink?.success(mapOf(
"name" to "barcode",
"data" to barcodeMap,
"image" to mediaImage.toByteArray()
))
}
// for (barcode in barcodes) {
//// if (lastScanned?.contains(barcodes.first) == true) continue;
// if (lastScanned == null) {
// lastScanned = barcodes
// } else if (lastScanned!!.contains(barcode)) {
// // Duplicate, don't send image
// sink?.success(mapOf(
// "name" to "barcode",
// "data" to barcode.data,
// ))
// } else {
// if (byteArray.isEmpty()) {
// Log.d("scanner", "EMPTY" )
// return@addOnSuccessListener
// }
// else -> imageProxy.close()
//
// Log.d("scanner", "SCANNED IMAGE: $byteArray")
// lastScanned = barcodes;
//
//
// }
//
// }
// isAnalyzing = false
}
.addOnFailureListener { e -> sink?.success(mapOf(
"name" to "error",
"data" to e.localizedMessage
)) }
.addOnCompleteListener { imageProxy.close() }
}
private fun Image.toByteArray(): ByteArray {
val yBuffer = planes[0].buffer // Y
val vuBuffer = planes[2].buffer // VU
val ySize = yBuffer.remaining()
val vuSize = vuBuffer.remaining()
val nv21 = ByteArray(ySize + vuSize)
yBuffer.get(nv21, 0, ySize)
vuBuffer.get(nv21, ySize, vuSize)
val yuvImage = YuvImage(nv21, ImageFormat.NV21, this.width, this.height, null)
val out = ByteArrayOutputStream()
yuvImage.compressToJpeg(Rect(0, 0, yuvImage.width, yuvImage.height), 50, out)
return out.toByteArray()
}
private var scanner = BarcodeScanning.getClient()
@ExperimentalGetImage
private fun start(call: MethodCall, result: MethodChannel.Result) {
if (camera?.cameraInfo != null && preview != null && textureEntry != null) {
... ... @@ -129,14 +194,23 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0
val width = resolution.width.toDouble()
val height = resolution.height.toDouble()
val size = if (portrait) mapOf("width" to width, "height" to height) else mapOf("width" to height, "height" to width)
val answer = mapOf("textureId" to textureEntry!!.id(), "size" to size, "torchable" to camera!!.cameraInfo.hasFlashUnit())
val size = if (portrait) mapOf(
"width" to width,
"height" to height
) else mapOf("width" to height, "height" to width)
val answer = mapOf(
"textureId" to textureEntry!!.id(),
"size" to size,
"torchable" to camera!!.cameraInfo.hasFlashUnit()
)
result.success(answer)
} else {
val facing: Int = call.argument<Int>("facing") ?: 0
val ratio: Int? = call.argument<Int>("ratio")
val torch: Boolean = call.argument<Boolean>("torch") ?: false
val formats: List<Int>? = call.argument<List<Int>>("formats")
// val analyzerWidth = call.argument<Int>("ratio")
// val analyzeRHEIG = call.argument<Int>("ratio")
if (formats != null) {
val formatsList: MutableList<Int> = mutableListOf()
... ... @@ -144,9 +218,17 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
formatsList.add(BarcodeFormats.values()[index].intValue)
}
scanner = if (formatsList.size == 1) {
BarcodeScanning.getClient(BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first()).build())
BarcodeScanning.getClient(
BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first())
.build()
)
} else {
BarcodeScanning.getClient(BarcodeScannerOptions.Builder().setBarcodeFormats(formatsList.first(), *formatsList.subList(1, formatsList.size).toIntArray()).build())
BarcodeScanning.getClient(
BarcodeScannerOptions.Builder().setBarcodeFormats(
formatsList.first(),
*formatsList.subList(1, formatsList.size).toIntArray()
).build()
)
}
}
... ... @@ -168,7 +250,10 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
// Preview
val surfaceProvider = Preview.SurfaceProvider { request ->
val texture = textureEntry!!.surfaceTexture()
texture.setDefaultBufferSize(request.resolution.width, request.resolution.height)
texture.setDefaultBufferSize(
request.resolution.width,
request.resolution.height
)
val surface = Surface(texture)
request.provideSurface(surface, executor) { }
}
... ... @@ -186,12 +271,19 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
if (ratio != null) {
analysisBuilder.setTargetAspectRatio(ratio)
}
// analysisBuilder.setTargetResolution(Size(1440, 1920))
val analysis = analysisBuilder.build().apply { setAnalyzer(executor, analyzer) }
// Select the correct camera
val selector = if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA
val selector =
if (facing == 0) CameraSelector.DEFAULT_FRONT_CAMERA else CameraSelector.DEFAULT_BACK_CAMERA
camera = cameraProvider!!.bindToLifecycle(activity as LifecycleOwner, selector, preview, analysis)
camera = cameraProvider!!.bindToLifecycle(
activity as LifecycleOwner,
selector,
preview,
analysis
)
val analysisSize = analysis.resolutionInfo?.resolution ?: Size(0, 0)
val previewSize = preview!!.resolutionInfo?.resolution ?: Size(0, 0)
... ... @@ -216,8 +308,15 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0
val width = resolution.width.toDouble()
val height = resolution.height.toDouble()
val size = if (portrait) mapOf("width" to width, "height" to height) else mapOf("width" to height, "height" to width)
val answer = mapOf("textureId" to textureEntry!!.id(), "size" to size, "torchable" to camera!!.cameraInfo.hasFlashUnit())
val size = if (portrait) mapOf(
"width" to width,
"height" to height
) else mapOf("width" to height, "height" to width)
val answer = mapOf(
"textureId" to textureEntry!!.id(),
"size" to size,
"torchable" to camera!!.cameraInfo.hasFlashUnit()
)
result.success(answer)
}, executor)
}
... ... @@ -225,7 +324,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
private fun toggleTorch(call: MethodCall, result: MethodChannel.Result) {
if (camera == null) {
result.error(TAG,"Called toggleTorch() while stopped!", null)
result.error(TAG, "Called toggleTorch() while stopped!", null)
return
}
camera!!.cameraControl.enableTorch(call.arguments == 1)
... ... @@ -238,7 +337,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
// }
private fun analyzeImage(call: MethodCall, result: MethodChannel.Result) {
val uri = Uri.fromFile( File(call.arguments.toString()))
val uri = Uri.fromFile(File(call.arguments.toString()))
val inputImage = InputImage.fromFilePath(activity, uri)
var barcodeFound = false
... ... @@ -249,15 +348,17 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
sink?.success(mapOf("name" to "barcode", "data" to barcode.data))
}
}
.addOnFailureListener { e -> Log.e(TAG, e.message, e)
result.error(TAG, e.message, e)}
.addOnFailureListener { e ->
Log.e(TAG, e.message, e)
result.error(TAG, e.message, e)
}
.addOnCompleteListener { result.success(barcodeFound) }
}
private fun stop(result: MethodChannel.Result) {
if (camera == null && preview == null) {
result.error(TAG,"Called stop() while already stopped!", null)
result.error(TAG, "Called stop() while already stopped!", null)
return
}
... ... @@ -277,41 +378,54 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
private val Barcode.data: Map<String, Any?>
get() = mapOf("corners" to cornerPoints?.map { corner -> corner.data }, "format" to format,
get() = mapOf(
"corners" to cornerPoints?.map { corner -> corner.data }, "format" to format,
"rawBytes" to rawBytes, "rawValue" to rawValue, "type" to valueType,
"calendarEvent" to calendarEvent?.data, "contactInfo" to contactInfo?.data,
"driverLicense" to driverLicense?.data, "email" to email?.data,
"geoPoint" to geoPoint?.data, "phone" to phone?.data, "sms" to sms?.data,
"url" to url?.data, "wifi" to wifi?.data, "displayValue" to displayValue)
"url" to url?.data, "wifi" to wifi?.data, "displayValue" to displayValue
)
private val Point.data: Map<String, Double>
get() = mapOf("x" to x.toDouble(), "y" to y.toDouble())
private val Barcode.CalendarEvent.data: Map<String, Any?>
get() = mapOf("description" to description, "end" to end?.rawValue, "location" to location,
get() = mapOf(
"description" to description, "end" to end?.rawValue, "location" to location,
"organizer" to organizer, "start" to start?.rawValue, "status" to status,
"summary" to summary)
"summary" to summary
)
private val Barcode.ContactInfo.data: Map<String, Any?>
get() = mapOf("addresses" to addresses.map { address -> address.data },
get() = mapOf(
"addresses" to addresses.map { address -> address.data },
"emails" to emails.map { email -> email.data }, "name" to name?.data,
"organization" to organization, "phones" to phones.map { phone -> phone.data },
"title" to title, "urls" to urls)
"title" to title, "urls" to urls
)
private val Barcode.Address.data: Map<String, Any?>
get() = mapOf("addressLines" to addressLines.map { addressLine -> addressLine.toString() }, "type" to type)
get() = mapOf(
"addressLines" to addressLines.map { addressLine -> addressLine.toString() },
"type" to type
)
private val Barcode.PersonName.data: Map<String, Any?>
get() = mapOf("first" to first, "formattedName" to formattedName, "last" to last,
get() = mapOf(
"first" to first, "formattedName" to formattedName, "last" to last,
"middle" to middle, "prefix" to prefix, "pronunciation" to pronunciation,
"suffix" to suffix)
"suffix" to suffix
)
private val Barcode.DriverLicense.data: Map<String, Any?>
get() = mapOf("addressCity" to addressCity, "addressState" to addressState,
get() = mapOf(
"addressCity" to addressCity, "addressState" to addressState,
"addressStreet" to addressStreet, "addressZip" to addressZip, "birthDate" to birthDate,
"documentType" to documentType, "expiryDate" to expiryDate, "firstName" to firstName,
"gender" to gender, "issueDate" to issueDate, "issuingCountry" to issuingCountry,
"lastName" to lastName, "licenseNumber" to licenseNumber, "middleName" to middleName)
"lastName" to lastName, "licenseNumber" to licenseNumber, "middleName" to middleName
)
private val Barcode.Email.data: Map<String, Any?>
get() = mapOf("address" to address, "body" to body, "subject" to subject, "type" to type)
... ...
... ... @@ -355,7 +355,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = RCH2VG82SH;
DEVELOPMENT_TEAM = 75Y2P2WSQQ;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
... ... @@ -484,7 +484,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = RCH2VG82SH;
DEVELOPMENT_TEAM = 75Y2P2WSQQ;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
... ... @@ -507,7 +507,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = RCH2VG82SH;
DEVELOPMENT_TEAM = 75Y2P2WSQQ;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
... ...
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class BarcodeListScannerWithController extends StatefulWidget {
const BarcodeListScannerWithController({Key? key}) : super(key: key);
@override
_BarcodeListScannerWithControllerState createState() =>
_BarcodeListScannerWithControllerState();
}
class _BarcodeListScannerWithControllerState
extends State<BarcodeListScannerWithController>
with SingleTickerProviderStateMixin {
BarcodeCapture? barcodeCapture;
MobileScannerController controller = MobileScannerController(
torchEnabled: true,
// formats: [BarcodeFormat.qrCode]
// facing: CameraFacing.front,
);
bool isStarted = true;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Builder(
builder: (context) {
return Stack(
children: [
MobileScanner(
controller: controller,
fit: BoxFit.contain,
// allowDuplicates: true,
// controller: MobileScannerController(
// torchEnabled: true,
// facing: CameraFacing.front,
// ),
onDetect: (barcodeCapture, arguments) {
setState(() {
this.barcodeCapture = barcodeCapture;
});
},
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
alignment: Alignment.bottomCenter,
height: 100,
color: Colors.black.withOpacity(0.4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
color: Colors.white,
icon: ValueListenableBuilder(
valueListenable: controller.torchState,
builder: (context, state, child) {
if (state == null) {
return const Icon(
Icons.flash_off,
color: Colors.grey,
);
}
switch (state as TorchState) {
case TorchState.off:
return const Icon(
Icons.flash_off,
color: Colors.grey,
);
case TorchState.on:
return const Icon(
Icons.flash_on,
color: Colors.yellow,
);
}
},
),
iconSize: 32.0,
onPressed: () => controller.toggleTorch(),
),
IconButton(
color: Colors.white,
icon: isStarted
? const Icon(Icons.stop)
: const Icon(Icons.play_arrow),
iconSize: 32.0,
onPressed: () => setState(() {
isStarted ? controller.stop() : controller.start();
isStarted = !isStarted;
}),
),
Center(
child: SizedBox(
width: MediaQuery.of(context).size.width - 200,
height: 50,
child: FittedBox(
child: Text(
'${barcodeCapture?.barcodes.map((e) => e.rawValue)}',
overflow: TextOverflow.fade,
style: Theme.of(context)
.textTheme
.headline4!
.copyWith(color: Colors.white),
),
),
),
),
IconButton(
color: Colors.white,
icon: ValueListenableBuilder(
valueListenable: controller.cameraFacingState,
builder: (context, state, child) {
if (state == null) {
return const Icon(Icons.camera_front);
}
switch (state as CameraFacing) {
case CameraFacing.front:
return const Icon(Icons.camera_front);
case CameraFacing.back:
return const Icon(Icons.camera_rear);
}
},
),
iconSize: 32.0,
onPressed: () => controller.switchCamera(),
),
IconButton(
color: Colors.white,
icon: const Icon(Icons.image),
iconSize: 32.0,
onPressed: () async {
final ImagePicker picker = ImagePicker();
// Pick an image
final XFile? image = await picker.pickImage(
source: ImageSource.gallery,
);
if (image != null) {
if (await controller.analyzeImage(image.path)) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Barcode found!'),
backgroundColor: Colors.green,
),
);
} else {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('No barcode found!'),
backgroundColor: Colors.red,
),
);
}
}
},
),
],
),
),
),
],
);
},
),
);
}
}
\ No newline at end of file
... ...
... ... @@ -13,10 +13,12 @@ class BarcodeScannerWithController extends StatefulWidget {
class _BarcodeScannerWithControllerState
extends State<BarcodeScannerWithController>
with SingleTickerProviderStateMixin {
String? barcode;
BarcodeCapture? barcode;
MobileScannerController controller = MobileScannerController(
torchEnabled: true,
detectionSpeed: DetectionSpeed.unrestricted
// formats: [BarcodeFormat.qrCode]
// facing: CameraFacing.front,
);
... ... @@ -41,7 +43,7 @@ class _BarcodeScannerWithControllerState
// ),
onDetect: (barcode, args) {
setState(() {
this.barcode = barcode.rawValue;
this.barcode = barcode;
});
},
),
... ... @@ -99,7 +101,7 @@ class _BarcodeScannerWithControllerState
height: 50,
child: FittedBox(
child: Text(
barcode ?? 'Scan something!',
barcode?.barcodes.first.rawValue ?? 'Scan something!',
overflow: TextOverflow.fade,
style: Theme.of(context)
.textTheme
... ...
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter/material.dart';
... ... @@ -14,11 +15,11 @@ class BarcodeScannerReturningImage extends StatefulWidget {
class _BarcodeScannerReturningImageState
extends State<BarcodeScannerReturningImage>
with SingleTickerProviderStateMixin {
String? barcode;
Uint8List? image;
BarcodeCapture? barcode;
MobileScannerArguments? arguments;
MobileScannerController controller = MobileScannerController(
torchEnabled: true,
// torchEnabled: true,
returnImage: true,
// formats: [BarcodeFormat.qrCode]
// facing: CameraFacing.front,
... ... @@ -26,13 +27,34 @@ class _BarcodeScannerReturningImageState
bool isStarted = true;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Builder(
builder: (context) {
return Stack(
return Column(
children: [
Container(
color: Colors.blueGrey,
width: double.infinity,
height: 0.33 * MediaQuery.of(context).size.height,
child: barcode?.image != null
? Transform.rotate(
angle: 90 * pi/180,
child: Image(
gaplessPlayback: true,
image: MemoryImage(barcode!.image!),
fit: BoxFit.contain,
),
)
: Container(color: Colors.white, child: const Center(child: Text('Your scanned barcode will appear here!'))),
),
Container(
height: 0.66 * MediaQuery.of(context).size.height,
color: Colors.grey,
child: Stack(
children: [
MobileScanner(
controller: controller,
... ... @@ -42,17 +64,10 @@ class _BarcodeScannerReturningImageState
// torchEnabled: true,
// facing: CameraFacing.front,
// ),
onDetect: (barcode, args) {
onDetect: (barcode, arguments) {
setState(() {
this.barcode = barcode.rawValue;
showDialog(
context: context,
builder: (context) => Image(
image: MemoryImage(image!),
fit: BoxFit.contain,
),
);
image = barcode.image;
this.arguments = arguments;
this.barcode = barcode;
});
},
),
... ... @@ -65,8 +80,10 @@ class _BarcodeScannerReturningImageState
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
color: Colors.white,
Container(
color: arguments != null && !arguments!.hasTorch ? Colors.red : Colors.white,
child: IconButton(
// color: ,
icon: ValueListenableBuilder(
valueListenable: controller.torchState,
builder: (context, state, child) {
... ... @@ -93,6 +110,7 @@ class _BarcodeScannerReturningImageState
iconSize: 32.0,
onPressed: () => controller.toggleTorch(),
),
),
IconButton(
color: Colors.white,
icon: isStarted
... ... @@ -110,7 +128,7 @@ class _BarcodeScannerReturningImageState
height: 50,
child: FittedBox(
child: Text(
barcode ?? 'Scan something!',
barcode?.barcodes.first.rawValue ?? 'Scan something!',
overflow: TextOverflow.fade,
style: Theme.of(context)
.textTheme
... ... @@ -139,21 +157,14 @@ class _BarcodeScannerReturningImageState
iconSize: 32.0,
onPressed: () => controller.switchCamera(),
),
SizedBox(
width: 50,
height: 50,
child: image != null
? Image(
image: MemoryImage(image!),
fit: BoxFit.contain,
)
: Container(),
),
],
),
),
),
],
),
),
],
);
},
),
... ...
... ... @@ -12,7 +12,7 @@ class BarcodeScannerWithoutController extends StatefulWidget {
class _BarcodeScannerWithoutControllerState
extends State<BarcodeScannerWithoutController>
with SingleTickerProviderStateMixin {
String? barcode;
BarcodeCapture? capture;
@override
Widget build(BuildContext context) {
... ... @@ -25,9 +25,9 @@ class _BarcodeScannerWithoutControllerState
MobileScanner(
fit: BoxFit.contain,
// allowDuplicates: false,
onDetect: (barcode, args) {
onDetect: (capture, arguments) {
setState(() {
this.barcode = barcode.rawValue;
this.capture = capture;
});
},
),
... ... @@ -46,7 +46,7 @@ class _BarcodeScannerWithoutControllerState
height: 50,
child: FittedBox(
child: Text(
barcode ?? 'Scan something!',
capture?.barcodes.first.rawValue ?? 'Scan something!',
overflow: TextOverflow.fade,
style: Theme.of(context)
.textTheme
... ...
import 'package:flutter/material.dart';
import 'package:mobile_scanner_example/barcode_list_scanner_controller.dart';
import 'package:mobile_scanner_example/barcode_scanner_controller.dart';
import 'package:mobile_scanner_example/barcode_scanner_returning_image.dart';
import 'package:mobile_scanner_example/barcode_scanner_without_controller.dart';
... ... @@ -22,6 +23,16 @@ class MyHome extends StatelessWidget {
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const BarcodeListScannerWithController(),
),
);
},
child: const Text('MobileScanner with List Controller'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const BarcodeScannerWithController(),
),
);
... ...
library mobile_scanner;
export 'src/barcode.dart';
export 'src/barcode_capture.dart';
export 'src/enums/camera_facing.dart';
export 'src/enums/detection_speed.dart';
export 'src/enums/mobile_scanner_state.dart';
... ... @@ -8,4 +10,3 @@ export 'src/enums/torch_state.dart';
export 'src/mobile_scanner.dart';
export 'src/mobile_scanner_arguments.dart';
export 'src/mobile_scanner_controller.dart';
export 'src/objects/barcode.dart';
... ...
import 'dart:typed_data';
import 'dart:ui';
import 'package:mobile_scanner/src/objects/barcode_utility.dart';
import 'package:mobile_scanner/src/barcode_utility.dart';
/// Represents a single recognized barcode and its value.
class Barcode {
... ... @@ -97,7 +97,7 @@ class Barcode {
});
/// Create a [Barcode] from native data.
Barcode.fromNative(Map data, this.image)
Barcode.fromNative(Map data, {this.image})
: corners = toCorners(data['corners'] as List?),
format = toFormat(data['format'] as int),
rawBytes = data['rawBytes'] as Uint8List?,
... ...
import 'dart:typed_data';
import 'package:mobile_scanner/src/barcode.dart';
class BarcodeCapture {
List<Barcode> barcodes;
Uint8List? image;
BarcodeCapture({
required this.barcodes,
this.image,
});
}
... ...
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:mobile_scanner/src/mobile_scanner_arguments.dart';
import 'package:mobile_scanner/src/mobile_scanner_controller.dart';
import 'package:mobile_scanner/src/barcode_capture.dart';
/// A widget showing a live camera preview.
class MobileScanner extends StatefulWidget {
... ... @@ -13,28 +15,24 @@ class MobileScanner extends StatefulWidget {
/// Function that gets called when a Barcode is detected.
///
/// [barcode] The barcode object with all information about the scanned code.
/// [args] Information about the state of the MobileScanner widget
final Function(Barcode barcode, MobileScannerArguments? args) onDetect;
/// TODO: Function that gets called when the Widget is initialized. Can be usefull
/// to check wether the device has a torch(flash) or not.
///
/// [args] Information about the state of the MobileScanner widget
// final Function(MobileScannerArguments args)? onInitialize;
/// [startArguments] Information about the state of the MobileScanner widget
final Function(
BarcodeCapture capture, MobileScannerArguments? arguments)
onDetect;
/// Handles how the widget should fit the screen.
final BoxFit fit;
/// Set to false if you don't want duplicate scans.
final bool allowDuplicates;
/// Whether to automatically resume the camera when the application is resumed
final bool autoResume;
/// Create a [MobileScanner] with a [controller], the [controller] must has been initialized.
const MobileScanner({
super.key,
required this.onDetect,
this.controller,
this.autoResume = true,
this.fit = BoxFit.cover,
this.allowDuplicates = false,
this.onPermissionSet,
});
... ... @@ -55,40 +53,35 @@ class _MobileScannerState extends State<MobileScanner>
if (!controller.isStarting) controller.start();
}
AppLifecycleState? _lastState;
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.resumed:
if (!controller.isStarting && controller.autoResume) controller.start();
if (!controller.isStarting && widget.autoResume && _lastState != AppLifecycleState.inactive) controller.start();
break;
case AppLifecycleState.inactive:
case AppLifecycleState.paused:
case AppLifecycleState.detached:
controller.stop();
break;
default:
break;
}
_lastState = state;
}
Uint8List? lastScanned;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: controller.args,
valueListenable: controller.startArguments,
builder: (context, value, child) {
value = value as MobileScannerArguments?;
if (value == null) {
return const ColoredBox(color: Colors.black);
} else {
controller.barcodes.listen((barcode) {
if (!widget.allowDuplicates) {
if (lastScanned != barcode.rawBytes) {
lastScanned = barcode.rawBytes;
widget.onDetect(barcode, value! as MobileScannerArguments);
}
} else {
widget.onDetect(barcode, value! as MobileScannerArguments);
}
});
return ClipRect(
child: SizedBox(
... ...
... ... @@ -5,170 +5,155 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:mobile_scanner/src/objects/barcode_utility.dart';
import 'package:mobile_scanner/src/barcode_capture.dart';
import 'package:mobile_scanner/src/barcode_utility.dart';
import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
/// The [MobileScannerController] holds all the logic of this plugin,
/// where as the [MobileScanner] class is the frontend of this plugin.
class MobileScannerController {
MethodChannel methodChannel =
const MethodChannel('dev.steenbakker.mobile_scanner/scanner/method');
EventChannel eventChannel =
const EventChannel('dev.steenbakker.mobile_scanner/scanner/event');
MobileScannerController({
this.facing = CameraFacing.back,
this.detectionSpeed = DetectionSpeed.noDuplicates,
// this.ratio,
this.torchEnabled = false,
this.formats,
// this.autoResume = true,
this.returnImage = false,
this.onPermissionSet,
}) {
// In case a new instance is created before calling dispose()
if (controllerHashcode != null) {
stop();
}
controllerHashcode = hashCode;
events = _eventChannel
.receiveBroadcastStream()
.listen((data) => _handleEvent(data as Map));
}
//Must be static to keep the same value on new instances
static int? _controllerHashcode;
StreamSubscription? events;
static int? controllerHashcode;
Function(bool permissionGranted)? onPermissionSet;
final ValueNotifier<MobileScannerArguments?> args = ValueNotifier(null);
final ValueNotifier<TorchState> torchState = ValueNotifier(TorchState.off);
late final ValueNotifier<CameraFacing> cameraFacingState;
final Ratio? ratio;
final bool? torchEnabled;
// Whether to return the image buffer with the Barcode event
/// Select which camera should be used.
///
/// Default: CameraFacing.back
final CameraFacing facing;
// /// Analyze the image in 4:3 or 16:9
// ///
// /// Only on Android
// final Ratio? ratio;
/// Enable or disable the torch (Flash) on start
///
/// Default: disabled
final bool torchEnabled;
/// Set to true if you want to return the image buffer with the Barcode event
///
/// Only supported on iOS and Android
final bool returnImage;
/// If provided, the scanner will only detect those specific formats.
/// If provided, the scanner will only detect those specific formats
final List<BarcodeFormat>? formats;
CameraFacing facing;
bool hasTorch = false;
late StreamController<Barcode> barcodesController;
/// Whether to automatically resume the camera when the application is resumed
bool autoResume;
/// Sets the speed of detections.
///
/// WARNING: DetectionSpeed.unrestricted can cause memory issues on some devices
final DetectionSpeed detectionSpeed;
Stream<Barcode> get barcodes => barcodesController.stream;
/// Sets the barcode stream
final StreamController<BarcodeCapture> _barcodesController =
StreamController.broadcast();
Stream<BarcodeCapture> get barcodes => _barcodesController.stream;
MobileScannerController({
this.facing = CameraFacing.back,
this.ratio,
this.torchEnabled,
this.formats,
this.onPermissionSet,
this.autoResume = true,
this.returnImage = false,
}) {
// In case a new instance is created before calling dispose()
if (_controllerHashcode != null) {
stop();
}
_controllerHashcode = hashCode;
static const MethodChannel _methodChannel =
MethodChannel('dev.steenbakker.mobile_scanner/scanner/method');
static const EventChannel _eventChannel =
EventChannel('dev.steenbakker.mobile_scanner/scanner/event');
cameraFacingState = ValueNotifier(facing);
Function(bool permissionGranted)? onPermissionSet;
// Sets analyze mode and barcode stream
barcodesController = StreamController.broadcast(
// onListen: () => setAnalyzeMode(AnalyzeMode.barcode.index),
// onCancel: () => setAnalyzeMode(AnalyzeMode.none.index),
);
/// Listen to events from the platform specific code
late StreamSubscription events;
// Listen to events from the platform specific code
events = eventChannel
.receiveBroadcastStream()
.listen((data) => handleEvent(data as Map));
}
/// A notifier that provides several arguments about the MobileScanner
final ValueNotifier<MobileScannerArguments?> startArguments = ValueNotifier(null);
void handleEvent(Map event) {
final name = event['name'];
final data = event['data'];
final binaryData = event['binaryData'];
switch (name) {
case 'torchState':
final state = TorchState.values[data as int? ?? 0];
torchState.value = state;
break;
case 'barcode':
final image = returnImage ? event['image'] as Uint8List : null;
final barcode = Barcode.fromNative(data as Map? ?? {}, image);
barcodesController.add(barcode);
break;
case 'barcodeMac':
barcodesController.add(
Barcode(
rawValue: (data as Map)['payload'] as String?,
),
);
break;
case 'barcodeWeb':
final bytes = (binaryData as List).cast<int>();
barcodesController.add(
Barcode(
rawValue: data as String?,
rawBytes: Uint8List.fromList(bytes),
),
);
break;
default:
throw UnimplementedError();
}
}
/// A notifier that provides the state of the Torch (Flash)
final ValueNotifier<TorchState> torchState = ValueNotifier(TorchState.off);
// TODO: Add more analyzers like text analyzer
// void setAnalyzeMode(int mode) {
// if (hashCode != _controllerHashcode) {
// return;
// }
// methodChannel.invokeMethod('analyze', mode);
// }
/// A notifier that provides the state of which camera is being used
late final ValueNotifier<CameraFacing> cameraFacingState =
ValueNotifier(facing);
// List<BarcodeFormats>? formats = _defaultBarcodeFormats,
bool isStarting = false;
bool? _hasTorch;
/// Set the starting arguments for the camera
Map<String, dynamic> _argumentsToMap({CameraFacing? cameraFacingOverride}) {
final Map<String, dynamic> arguments = {};
cameraFacingState.value = cameraFacingOverride ?? facing;
arguments['facing'] = cameraFacingState.value.index;
// if (ratio != null) arguments['ratio'] = ratio;
arguments['torch'] = torchEnabled;
arguments['speed'] = detectionSpeed.index;
if (formats != null) {
if (Platform.isAndroid) {
arguments['formats'] = formats!.map((e) => e.index).toList();
} else if (Platform.isIOS || Platform.isMacOS) {
arguments['formats'] = formats!.map((e) => e.rawValue).toList();
}
}
arguments['returnImage'] = true;
return arguments;
}
/// Start barcode scanning. This will first check if the required permissions
/// are set.
Future<void> start() async {
ensure('startAsync');
Future<MobileScannerArguments?> start({
CameraFacing? cameraFacingOverride,
}) async {
debugPrint('Hashcode controller: $hashCode');
if (isStarting) {
throw Exception('mobile_scanner: Called start() while already starting.');
debugPrint("Called start() while starting.");
}
isStarting = true;
// setAnalyzeMode(AnalyzeMode.barcode.index);
// Check authorization status
if (!kIsWeb) {
MobileScannerState state = MobileScannerState
.values[await methodChannel.invokeMethod('state') as int? ?? 0];
final MobileScannerState state = MobileScannerState
.values[await _methodChannel.invokeMethod('state') as int? ?? 0];
switch (state) {
case MobileScannerState.undetermined:
final bool result =
await methodChannel.invokeMethod('request') as bool? ?? false;
state = result
? MobileScannerState.authorized
: MobileScannerState.denied;
await _methodChannel.invokeMethod('request') as bool? ?? false;
if (!result) {
isStarting = false;
onPermissionSet?.call(result);
throw MobileScannerException('User declined camera permission.');
}
break;
case MobileScannerState.denied:
isStarting = false;
onPermissionSet?.call(false);
throw PlatformException(code: 'NO ACCESS');
throw MobileScannerException('User declined camera permission.');
case MobileScannerState.authorized:
onPermissionSet?.call(true);
break;
}
}
cameraFacingState.value = facing;
// Set the starting arguments for the camera
final Map arguments = {};
arguments['facing'] = facing.index;
if (ratio != null) arguments['ratio'] = ratio;
if (torchEnabled != null) arguments['torch'] = torchEnabled;
if (formats != null) {
if (Platform.isAndroid) {
arguments['formats'] = formats!.map((e) => e.index).toList();
} else if (Platform.isIOS || Platform.isMacOS) {
arguments['formats'] = formats!.map((e) => e.rawValue).toList();
}
}
arguments['returnImage'] = returnImage;
// Start the camera with arguments
Map<String, dynamic>? startResult = {};
try {
startResult = await methodChannel.invokeMapMethod<String, dynamic>(
startResult = await _methodChannel.invokeMapMethod<String, dynamic>(
'start',
arguments,
_argumentsToMap(cameraFacingOverride: cameraFacingOverride),
);
} on PlatformException catch (error) {
debugPrint('${error.code}: ${error.message}');
... ... @@ -176,85 +161,76 @@ class MobileScannerController {
if (error.code == "MobileScannerWeb") {
onPermissionSet?.call(false);
}
// setAnalyzeMode(AnalyzeMode.none.index);
return;
return null;
}
if (startResult == null) {
isStarting = false;
throw PlatformException(code: 'INITIALIZATION ERROR');
throw MobileScannerException(
'Failed to start mobileScanner, no response from platform side');
}
hasTorch = startResult['torchable'] as bool? ?? false;
_hasTorch = startResult['torchable'] as bool? ?? false;
if (_hasTorch! && torchEnabled) {
torchState.value = TorchState.on;
}
if (kIsWeb) {
onPermissionSet?.call(
true,
); // If we reach this line, it means camera permission has been granted
args.value = MobileScannerArguments(
startArguments.value = MobileScannerArguments(
webId: startResult['ViewID'] as String?,
size: Size(
startResult['videoWidth'] as double? ?? 0,
startResult['videoHeight'] as double? ?? 0,
),
hasTorch: hasTorch,
hasTorch: _hasTorch!,
);
} else {
args.value = MobileScannerArguments(
startArguments.value = MobileScannerArguments(
textureId: startResult['textureId'] as int?,
size: toSize(startResult['size'] as Map? ?? {}),
hasTorch: hasTorch,
hasTorch: _hasTorch!,
);
}
isStarting = false;
return startArguments.value!;
}
/// Stops the camera, but does not dispose this controller.
Future<void> stop() async {
try {
await methodChannel.invokeMethod('stop');
} on PlatformException catch (error) {
debugPrint('${error.code}: ${error.message}');
}
await _methodChannel.invokeMethod('stop');
}
/// Switches the torch on or off.
///
/// Only works if torch is available.
Future<void> toggleTorch() async {
ensure('toggleTorch');
if (!hasTorch) {
debugPrint('Device has no torch/flash.');
return;
if (_hasTorch == null) {
throw MobileScannerException(
'Cannot toggle torch if start() has never been called');
} else if (!_hasTorch!) {
throw MobileScannerException('Device has no torch');
}
final TorchState state =
torchState.value =
torchState.value == TorchState.off ? TorchState.on : TorchState.off;
try {
await methodChannel.invokeMethod('torch', state.index);
} on PlatformException catch (error) {
debugPrint('${error.code}: ${error.message}');
}
await _methodChannel.invokeMethod('torch', torchState.value.index);
}
/// Switches the torch on or off.
///
/// Only works if torch is available.
Future<void> switchCamera() async {
ensure('switchCamera');
try {
await methodChannel.invokeMethod('stop');
} on PlatformException catch (error) {
debugPrint(
'${error.code}: camera is stopped! Please start before switching camera.',
);
return;
}
facing =
facing == CameraFacing.back ? CameraFacing.front : CameraFacing.back;
await start();
await _methodChannel.invokeMethod('stop');
final CameraFacing facingToUse =
cameraFacingState.value == CameraFacing.back
? CameraFacing.front
: CameraFacing.back;
await start(cameraFacingOverride: facingToUse);
}
/// Handles a local image file.
... ... @@ -263,28 +239,66 @@ class MobileScannerController {
///
/// [path] The path of the image on the devices
Future<bool> analyzeImage(String path) async {
return methodChannel
return _methodChannel
.invokeMethod<bool>('analyzeImage', path)
.then<bool>((bool? value) => value ?? false);
}
/// Disposes the MobileScannerController and closes all listeners.
///
/// If you call this, you cannot use this controller object anymore.
void dispose() {
if (hashCode == _controllerHashcode) {
stop();
events?.cancel();
events = null;
_controllerHashcode = null;
events.cancel();
_barcodesController.close();
if (hashCode == controllerHashcode) {
controllerHashcode = null;
onPermissionSet = null;
}
barcodesController.close();
}
/// Checks if the MobileScannerController is bound to the correct MobileScanner object.
void ensure(String name) {
final message =
'MobileScannerController.$name called after MobileScannerController.dispose\n'
'MobileScannerController methods should not be used after calling dispose.';
assert(hashCode == _controllerHashcode, message);
/// Handles a returning event from the platform side
void _handleEvent(Map event) {
final name = event['name'];
final data = event['data'];
switch (name) {
case 'torchState':
final state = TorchState.values[data as int? ?? 0];
torchState.value = state;
break;
case 'barcode':
if (data == null) return;
final parsed = (data as List)
.map((value) => Barcode.fromNative(value as Map))
.toList();
_barcodesController.add(BarcodeCapture(
barcodes: parsed,
image: event['image'] as Uint8List,
));
break;
case 'barcodeMac':
_barcodesController.add(
BarcodeCapture(
barcodes: [
Barcode(
rawValue: (data as Map)['payload'] as String?,
)
],
),
);
break;
case 'barcodeWeb':
_barcodesController.add(BarcodeCapture(barcodes: [
Barcode(
rawValue: data as String?,
)
]));
break;
case 'error':
throw MobileScannerException(data as String);
default:
throw UnimplementedError(name as String?);
}
}
}
\ No newline at end of file
... ...
class MobileScannerException implements Exception {
String message;
MobileScannerException(this.message);
}
... ...
... ... @@ -12,11 +12,13 @@ dependencies:
sdk: flutter
flutter_web_plugins:
sdk: flutter
js: ^0.6.3
json_serializable: ^6.3.1
dev_dependencies:
build_runner: ^2.2.0
flutter_test:
sdk: flutter
json_annotation: ^4.6.0
lint: ^1.10.0
flutter:
... ...