Julian Steenbakker
Committed by GitHub

Merge branch 'master' into dependabot/gradle/android/com.android.tools.build-gradle-7.3.1

... ... @@ -11,7 +11,7 @@ jobs:
analysis:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.0.2
- uses: actions/checkout@v3.1.0
- uses: actions/setup-java@v3.5.1
with:
java-version: 11
... ... @@ -28,7 +28,7 @@ jobs:
formatting:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.0.2
- uses: actions/checkout@v3.1.0
- uses: actions/setup-java@v3.5.1
with:
java-version: 11
... ...
## NEXT
Breaking changes:
* [iOS] The minimum deployment target is now 11.0 or higher.
* [iOS] Updated POD dependencies
## 3.0.0-beta.1
Breaking changes:
* [Android] SDK updated to SDK 33.
... ...
... ... @@ -2,7 +2,7 @@ group 'dev.steenbakker.mobile_scanner'
version '1.0-SNAPSHOT'
buildscript {
ext.kotlin_version = '1.7.10'
ext.kotlin_version = '1.7.20'
repositories {
google()
mavenCentral()
... ... @@ -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)
... ...
... ... @@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>9.0</string>
<string>11.0</string>
</dict>
</plist>
... ...
# Uncomment this line to define a global platform for your project
platform :ios, '10.0'
platform :ios, '11.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
... ... @@ -37,5 +37,9 @@ end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['ENABLE_BITCODE'] = 'NO'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0'
end
end
end
... ...
... ... @@ -339,7 +339,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
... ... @@ -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 = (
... ... @@ -417,7 +417,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
... ... @@ -466,7 +466,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
... ... @@ -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,
),
);
}
}
},
),
],
),
),
),
],
);
},
),
);
}
}
... ...
... ... @@ -13,10 +13,10 @@ class BarcodeScannerWithController extends StatefulWidget {
class _BarcodeScannerWithControllerState
extends State<BarcodeScannerWithController>
with SingleTickerProviderStateMixin {
String? barcode;
BarcodeCapture? barcode;
MobileScannerController controller = MobileScannerController(
torchEnabled: true,
torchEnabled: true, detectionSpeed: DetectionSpeed.unrestricted,
// formats: [BarcodeFormat.qrCode]
// facing: CameraFacing.front,
);
... ... @@ -41,7 +41,7 @@ class _BarcodeScannerWithControllerState
// ),
onDetect: (barcode, args) {
setState(() {
this.barcode = barcode.rawValue;
this.barcode = barcode;
});
},
),
... ... @@ -99,7 +99,8 @@ 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:typed_data';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
... ... @@ -14,11 +14,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,
... ... @@ -32,7 +32,34 @@ class _BarcodeScannerReturningImageState
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,
),
)
: const ColoredBox(
color: Colors.white,
child: 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 +69,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 +85,12 @@ class _BarcodeScannerReturningImageState
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
color: Colors.white,
ColoredBox(
color: arguments != null && !arguments!.hasTorch
? Colors.red
: Colors.white,
child: IconButton(
// color: ,
icon: ValueListenableBuilder(
valueListenable: controller.torchState,
builder: (context, state, child) {
... ... @@ -93,6 +117,7 @@ class _BarcodeScannerReturningImageState
iconSize: 32.0,
onPressed: () => controller.toggleTorch(),
),
),
IconButton(
color: Colors.white,
icon: isStarted
... ... @@ -100,7 +125,9 @@ class _BarcodeScannerReturningImageState
: const Icon(Icons.play_arrow),
iconSize: 32.0,
onPressed: () => setState(() {
isStarted ? controller.stop() : controller.start();
isStarted
? controller.stop()
: controller.start();
isStarted = !isStarted;
}),
),
... ... @@ -110,7 +137,8 @@ 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 +167,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,8 @@ 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,17 @@ 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(),
),
);
... ...
//
// BarcodeHandler.swift
// mobile_scanner
//
// Created by Julian Steenbakker on 24/08/2022.
//
import Foundation
public class BarcodeHandler: NSObject, FlutterStreamHandler {
var event: [String: Any?] = [:]
private var eventSink: FlutterEventSink?
private let eventChannel: FlutterEventChannel
init(registrar: FlutterPluginRegistrar) {
eventChannel = FlutterEventChannel(name:
"dev.steenbakker.mobile_scanner/scanner/event", binaryMessenger: registrar.messenger())
super.init()
eventChannel.setStreamHandler(self)
}
func publishEvent(_ event: [String: Any?]) {
self.event = event
eventSink?(event)
}
public func onListen(withArguments arguments: Any?,
eventSink: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = eventSink
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
eventSink = nil
return nil
}
}
... ...
//
// SwiftMobileScanner.swift
// mobile_scanner
//
// Created by Julian Steenbakker on 15/02/2022.
//
import Foundation
import AVFoundation
import MLKitVision
import MLKitBarcodeScanning
typealias MobileScannerCallback = ((Array<Barcode>?, Error?, UIImage) -> ())
public class MobileScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, FlutterTexture {
/// Capture session of the camera
var captureSession: AVCaptureSession!
/// The selected camera
var device: AVCaptureDevice!
/// Barcode scanner for results
var scanner = BarcodeScanner.barcodeScanner()
/// Return image buffer with the Barcode event
var returnImage: Bool = false
/// Default position of camera
var videoPosition: AVCaptureDevice.Position = AVCaptureDevice.Position.back
/// When results are found, this callback will be called
let mobileScannerCallback: MobileScannerCallback
/// If provided, the Flutter registry will be used to send the output of the CaptureOutput to a Flutter texture.
private let registry: FlutterTextureRegistry?
/// Image to be sent to the texture
var latestBuffer: CVImageBuffer!
/// Texture id of the camera preview for Flutter
private var textureId: Int64!
var detectionSpeed: DetectionSpeed = DetectionSpeed.noDuplicates
init(registry: FlutterTextureRegistry?, mobileScannerCallback: @escaping MobileScannerCallback) {
self.registry = registry
self.mobileScannerCallback = mobileScannerCallback
super.init()
}
/// Check permissions for video
func checkPermission() -> Int {
let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status {
case .notDetermined:
return 0
case .authorized:
return 1
default:
return 2
}
}
/// Request permissions for video
func requestPermission(_ result: @escaping FlutterResult) {
AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) })
}
/// Start scanning for barcodes
func start(barcodeScannerOptions: BarcodeScannerOptions?, returnImage: Bool, cameraPosition: AVCaptureDevice.Position, torch: AVCaptureDevice.TorchMode, detectionSpeed: DetectionSpeed) throws -> MobileScannerStartParameters {
self.detectionSpeed = detectionSpeed
if (device != nil) {
throw MobileScannerError.alreadyStarted
}
scanner = barcodeScannerOptions != nil ? BarcodeScanner.barcodeScanner(options: barcodeScannerOptions!) : BarcodeScanner.barcodeScanner()
captureSession = AVCaptureSession()
textureId = registry?.register(self)
// Open the camera device
if #available(iOS 10.0, *) {
device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: cameraPosition).devices.first
} else {
device = AVCaptureDevice.devices(for: .video).filter({$0.position == cameraPosition}).first
}
if (device == nil) {
throw MobileScannerError.noCamera
}
// Enable the torch if parameter is set and torch is available
if (device.hasTorch && device.isTorchAvailable) {
do {
try device.lockForConfiguration()
device.torchMode = torch
device.unlockForConfiguration()
} catch {
throw MobileScannerError.torchError(error)
}
}
device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil)
captureSession.beginConfiguration()
// Add device input
do {
let input = try AVCaptureDeviceInput(device: device)
captureSession.addInput(input)
} catch {
throw MobileScannerError.cameraError(error)
}
captureSession.sessionPreset = AVCaptureSession.Preset.photo;
// Add video output.
let videoOutput = AVCaptureVideoDataOutput()
videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]
videoOutput.alwaysDiscardsLateVideoFrames = true
videoPosition = cameraPosition
// calls captureOutput()
videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main)
captureSession.addOutput(videoOutput)
for connection in videoOutput.connections {
connection.videoOrientation = .portrait
if cameraPosition == .front && connection.isVideoMirroringSupported {
connection.isVideoMirrored = true
}
}
captureSession.commitConfiguration()
captureSession.startRunning()
let dimensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription)
return MobileScannerStartParameters(width: Double(dimensions.height), height: Double(dimensions.width), hasTorch: device.hasTorch, textureId: textureId)
}
struct MobileScannerStartParameters {
var width: Double = 0.0
var height: Double = 0.0
var hasTorch = false
var textureId: Int64 = 0
}
/// Stop scanning for barcodes
func stop() throws {
if (device == nil) {
throw MobileScannerError.alreadyStopped
}
captureSession.stopRunning()
for input in captureSession.inputs {
captureSession.removeInput(input)
}
for output in captureSession.outputs {
captureSession.removeOutput(output)
}
device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode))
registry?.unregisterTexture(textureId)
textureId = nil
captureSession = nil
device = nil
}
/// Toggle the flashlight between on and off
func toggleTorch(_ torch: AVCaptureDevice.TorchMode) throws {
if (device == nil) {
throw MobileScannerError.torchWhenStopped
}
do {
try device.lockForConfiguration()
device.torchMode = torch
device.unlockForConfiguration()
} catch {
throw MobileScannerError.torchError(error)
}
}
/// Analyze a single image
func analyzeImage(image: UIImage, position: AVCaptureDevice.Position, callback: @escaping BarcodeScanningCallback) {
let image = VisionImage(image: image)
image.orientation = imageOrientation(
deviceOrientation: UIDevice.current.orientation,
defaultOrientation: .portrait,
position: position
)
scanner.process(image, completion: callback)
}
var i = 0
var barcodesString: Array<String?>?
/// Gets called when a new image is added to the buffer
public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
print("Failed to get image buffer from sample buffer.")
return
}
latestBuffer = imageBuffer
registry?.textureFrameAvailable(textureId)
if ((detectionSpeed == DetectionSpeed.normal || detectionSpeed == DetectionSpeed.noDuplicates) && i > 10 || detectionSpeed == DetectionSpeed.unrestricted) {
i = 0
let ciImage = latestBuffer.image
let image = VisionImage(image: ciImage)
image.orientation = imageOrientation(
deviceOrientation: UIDevice.current.orientation,
defaultOrientation: .portrait,
position: videoPosition
)
scanner.process(image) { [self] barcodes, error in
if (detectionSpeed == DetectionSpeed.noDuplicates) {
let newScannedBarcodes = barcodes?.map { barcode in
return barcode.rawValue
}
if (error == nil && barcodesString != nil && newScannedBarcodes != nil && barcodesString!.elementsEqual(newScannedBarcodes!)) {
return
} else {
barcodesString = newScannedBarcodes
}
}
mobileScannerCallback(barcodes, error, ciImage)
}
} else {
i+=1
}
}
/// Convert image buffer to jpeg
private func ciImageToJpeg(ciImage: CIImage) -> Data {
// let ciImage = CIImage(cvPixelBuffer: latestBuffer)
let context:CIContext = CIContext.init(options: nil)
let cgImage:CGImage = context.createCGImage(ciImage, from: ciImage.extent)!
let uiImage:UIImage = UIImage(cgImage: cgImage, scale: 1, orientation: UIImage.Orientation.up)
return uiImage.jpegData(compressionQuality: 0.8)!;
}
/// Rotates images accordingly
func imageOrientation(
deviceOrientation: UIDeviceOrientation,
defaultOrientation: UIDeviceOrientation,
position: AVCaptureDevice.Position
) -> UIImage.Orientation {
switch deviceOrientation {
case .portrait:
return position == .front ? .leftMirrored : .right
case .landscapeLeft:
return position == .front ? .downMirrored : .up
case .portraitUpsideDown:
return position == .front ? .rightMirrored : .left
case .landscapeRight:
return position == .front ? .upMirrored : .down
case .faceDown, .faceUp, .unknown:
return .up
@unknown default:
return imageOrientation(deviceOrientation: defaultOrientation, defaultOrientation: .portrait, position: .back)
}
}
/// Sends output of OutputBuffer to a Flutter texture
public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
if latestBuffer == nil {
return nil
}
return Unmanaged<CVPixelBuffer>.passRetained(latestBuffer)
}
}
... ...
//
// MobileScannerError.swift
// mobile_scanner
//
// Created by Julian Steenbakker on 24/08/2022.
//
import Foundation
enum MobileScannerError: Error {
case noCamera
case alreadyStarted
case alreadyStopped
case torchError(_ error: Error)
case cameraError(_ error: Error)
case torchWhenStopped
case analyzerError(_ error: Error)
}
... ...
//
// SwiftMobileScanner.swift
// mobile_scanner
//
// Created by Julian Steenbakker on 15/02/2022.
//
import Foundation
import AVFoundation
import Flutter
import MLKitVision
import MLKitBarcodeScanning
import AVFoundation
public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, FlutterTexture, AVCaptureVideoDataOutputSampleBufferDelegate {
let registry: FlutterTextureRegistry
// Sink for publishing event changes
var sink: FlutterEventSink!
// Texture id of the camera preview
var textureId: Int64!
// Capture session of the camera
var captureSession: AVCaptureSession!
// The selected camera
var device: AVCaptureDevice!
// Image to be sent to the texture
var latestBuffer: CVImageBuffer!
// Return image buffer with the Barcode event
var returnImage: Bool = false
// var analyzeMode: Int = 0
var analyzing: Bool = false
var position = AVCaptureDevice.Position.back
public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin {
var scanner = BarcodeScanner.barcodeScanner()
/// The mobile scanner object that handles all logic
private let mobileScanner: MobileScanner
public static func register(with registrar: FlutterPluginRegistrar) {
let instance = SwiftMobileScannerPlugin(registrar.textures())
/// The handler sends all information via an event channel back to Flutter
private let barcodeHandler: BarcodeHandler
let method = FlutterMethodChannel(name:
"dev.steenbakker.mobile_scanner/scanner/method", binaryMessenger: registrar.messenger())
let event = FlutterEventChannel(name:
"dev.steenbakker.mobile_scanner/scanner/event", binaryMessenger: registrar.messenger())
registrar.addMethodCallDelegate(instance, channel: method)
event.setStreamHandler(instance)
init(barcodeHandler: BarcodeHandler, registry: FlutterTextureRegistry) {
self.mobileScanner = MobileScanner(registry: registry, mobileScannerCallback: { barcodes, error, image in
if barcodes != nil {
let barcodesMap = barcodes!.map { barcode in
return barcode.data
}
init(_ registry: FlutterTextureRegistry) {
self.registry = registry
if (!barcodesMap.isEmpty) {
barcodeHandler.publishEvent(["name": "barcode", "data": barcodesMap, "image": FlutterStandardTypedData(bytes: image.jpegData(compressionQuality: 0.8)!)])
}
} else if (error != nil){
barcodeHandler.publishEvent(["name": "error", "data": error!.localizedDescription])
}
})
self.barcodeHandler = barcodeHandler
super.init()
}
public static func register(with registrar: FlutterPluginRegistrar) {
let instance = SwiftMobileScannerPlugin(barcodeHandler: BarcodeHandler(registrar: registrar), registry: registrar.textures())
let methodChannel = FlutterMethodChannel(name:
"dev.steenbakker.mobile_scanner/scanner/method", binaryMessenger: registrar.messenger())
registrar.addMethodCallDelegate(instance, channel: methodChannel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "state":
checkPermission(call, result)
result(mobileScanner.checkPermission())
case "request":
requestPermission(call, result)
AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) })
case "start":
start(call, result)
case "torch":
toggleTorch(call, result)
// case "analyze":
// switchAnalyzeMode(call, result)
case "stop":
stop(result)
case "torch":
toggleTorch(call, result)
case "analyzeImage":
analyzeImage(call, result)
default:
result(FlutterMethodNotImplemented)
}
}
// FlutterStreamHandler
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
sink = events
return nil
}
// FlutterStreamHandler
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
sink = nil
return nil
}
// FlutterTexture
public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
if latestBuffer == nil {
return nil
}
return Unmanaged<CVPixelBuffer>.passRetained(latestBuffer)
}
private func ciImageToJpeg(ciImage: CIImage) -> Data {
// let ciImage = CIImage(cvPixelBuffer: latestBuffer)
let context:CIContext = CIContext.init(options: nil)
let cgImage:CGImage = context.createCGImage(ciImage, from: ciImage.extent)!
let uiImage:UIImage = UIImage(cgImage: cgImage, scale: 1, orientation: UIImage.Orientation.up)
return uiImage.jpegData(compressionQuality: 0.8)!;
}
// Gets called when a new image is added to the buffer
public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
latestBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)
registry.textureFrameAvailable(textureId)
// switch analyzeMode {
// case 1: // barcode
if analyzing {
return
}
analyzing = true
let buffer = CMSampleBufferGetImageBuffer(sampleBuffer)
let image = VisionImage(image: buffer!.image)
image.orientation = imageOrientation(
deviceOrientation: UIDevice.current.orientation,
defaultOrientation: .portrait
)
scanner.process(image) { [self] barcodes, error in
if error == nil && barcodes != nil {
for barcode in barcodes! {
var event: [String: Any?] = ["name": "barcode", "data": barcode.data]
if (returnImage && latestBuffer != nil) {
let image: CIImage = CIImage(cvPixelBuffer: latestBuffer)
event["image"] = FlutterStandardTypedData(bytes: ciImageToJpeg(ciImage: image))
}
sink?(event)
}
}
analyzing = false
}
// default: // none
// break
// }
}
func imageOrientation(
deviceOrientation: UIDeviceOrientation,
defaultOrientation: UIDeviceOrientation
) -> UIImage.Orientation {
switch deviceOrientation {
case .portrait:
return position == .front ? .leftMirrored : .right
case .landscapeLeft:
return position == .front ? .downMirrored : .up
case .portraitUpsideDown:
return position == .front ? .rightMirrored : .left
case .landscapeRight:
return position == .front ? .upMirrored : .down
case .faceDown, .faceUp, .unknown:
return .up
@unknown default:
return imageOrientation(deviceOrientation: defaultOrientation, defaultOrientation: .portrait)
}
}
func checkPermission(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status {
case .notDetermined:
result(0)
case .authorized:
result(1)
default:
result(2)
}
}
func requestPermission(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) })
}
func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
if (device != nil) {
result(FlutterError(code: "MobileScanner",
message: "Called start() while already started!",
details: nil))
return
}
/// Parses all parameters and starts the mobileScanner
private func start(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
// let ratio: Int = (call.arguments as! Dictionary<String, Any?>)["ratio"] as! Int
let torch: Bool = (call.arguments as! Dictionary<String, Any?>)["torch"] as? Bool ?? false
let facing: Int = (call.arguments as! Dictionary<String, Any?>)["facing"] as? Int ?? 1
let formats: Array<Int> = (call.arguments as! Dictionary<String, Any?>)["formats"] as? Array ?? []
let returnImage: Bool = (call.arguments as! Dictionary<String, Any?>)["returnImage"] as? Bool ?? false
textureId = registry.register(self)
captureSession = AVCaptureSession()
let formatList = formats.map { format in return BarcodeFormat(rawValue: format)}
var barcodeOptions: BarcodeScannerOptions? = nil
let argReader = MapArgumentReader(call.arguments as? [String: Any])
returnImage = argReader.bool(key: "returnImage") ?? false
// let ratio: Int = argReader.int(key: "ratio")
let torch: Bool = argReader.bool(key: "torch") ?? false
let facing: Int = argReader.int(key: "facing") ?? 1
let formats: Array = argReader.intArray(key: "formats") ?? []
if (formats.count != 0) {
if (formatList.count != 0) {
var barcodeFormats: BarcodeFormat = []
for index in formats {
barcodeFormats.insert(BarcodeFormat(rawValue: index))
}
let barcodeOptions = BarcodeScannerOptions(formats: barcodeFormats)
scanner = BarcodeScanner.barcodeScanner(options: barcodeOptions)
barcodeOptions = BarcodeScannerOptions(formats: barcodeFormats)
}
// Set the camera to use
position = facing == 0 ? AVCaptureDevice.Position.front : .back
// Open the camera device
if #available(iOS 10.0, *) {
device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: position).devices.first
} else {
device = AVCaptureDevice.devices(for: .video).filter({$0.position == position}).first
}
let position = facing == 0 ? AVCaptureDevice.Position.front : .back
let speed: DetectionSpeed = DetectionSpeed(rawValue: (call.arguments as! Dictionary<String, Any?>)["speed"] as? Int ?? 0)!
if (device == nil) {
do {
let parameters = try mobileScanner.start(barcodeScannerOptions: barcodeOptions, returnImage: returnImage, cameraPosition: position, torch: torch ? .on : .off, detectionSpeed: speed)
result(["textureId": parameters.textureId, "size": ["width": parameters.width, "height": parameters.height], "torchable": parameters.hasTorch])
} catch MobileScannerError.alreadyStarted {
result(FlutterError(code: "MobileScanner",
message: "Called start() while already started!",
details: nil))
} catch MobileScannerError.noCamera {
result(FlutterError(code: "MobileScanner",
message: "No camera found or failed to open camera!",
details: nil))
return
}
// Enable the torch if parameter is set and torch is available
if (device.hasTorch && device.isTorchAvailable) {
do {
try device.lockForConfiguration()
device.torchMode = torch ? .on : .off
device.unlockForConfiguration()
} catch MobileScannerError.torchError(let error) {
result(FlutterError(code: "MobileScanner",
message: "Error occured when setting toch!",
details: error))
} catch MobileScannerError.cameraError(let error) {
result(FlutterError(code: "MobileScanner",
message: "Error occured when setting up camera!",
details: error))
} catch {
error.throwNative(result)
result(FlutterError(code: "MobileScanner",
message: "Unknown error occured..",
details: nil))
}
}
device.addObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode), options: .new, context: nil)
captureSession.beginConfiguration()
// Add device input
/// Stops the mobileScanner and closes the texture
private func stop(_ result: @escaping FlutterResult) {
do {
let input = try AVCaptureDeviceInput(device: device)
captureSession.addInput(input)
try mobileScanner.stop()
} catch {
error.throwNative(result)
}
captureSession.sessionPreset = AVCaptureSession.Preset.photo;
// Add video output.
let videoOutput = AVCaptureVideoDataOutput()
videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]
videoOutput.alwaysDiscardsLateVideoFrames = true
videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main)
captureSession.addOutput(videoOutput)
for connection in videoOutput.connections {
connection.videoOrientation = .portrait
if position == .front && connection.isVideoMirroringSupported {
connection.isVideoMirrored = true
}
result(FlutterError(code: "MobileScanner",
message: "Called stop() while already stopped!",
details: nil))
}
captureSession.commitConfiguration()
captureSession.startRunning()
let demensions = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription)
let width = Double(demensions.height)
let height = Double(demensions.width)
let size = ["width": width, "height": height]
let answer: [String : Any?] = ["textureId": textureId, "size": size, "torchable": device.hasTorch]
result(answer)
result(nil)
}
func toggleTorch(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
if (device == nil) {
/// Toggles the torch
private func toggleTorch(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
do {
try mobileScanner.toggleTorch(call.arguments as? Int == 1 ? .on : .off)
} catch {
result(FlutterError(code: "MobileScanner",
message: "Called toggleTorch() while stopped!",
details: nil))
return
}
do {
try device.lockForConfiguration()
device.torchMode = call.arguments as! Int == 1 ? .on : .off
device.unlockForConfiguration()
result(nil)
} catch {
error.throwNative(result)
}
}
// func switchAnalyzeMode(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
// analyzeMode = call.arguments as! Int
// result(nil)
// }
func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
let uiImage = UIImage(contentsOfFile: call.arguments as! String)
/// Analyzes a single image
private func analyzeImage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
let uiImage = UIImage(contentsOfFile: call.arguments as? String ?? "")
if (uiImage == nil) {
result(FlutterError(code: "MobileScanner",
... ... @@ -299,99 +137,34 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
details: nil))
return
}
let image = VisionImage(image: uiImage!)
image.orientation = imageOrientation(
deviceOrientation: UIDevice.current.orientation,
defaultOrientation: .portrait
)
var barcodeFound = false
scanner.process(image) { [self] barcodes, error in
mobileScanner.analyzeImage(image: uiImage!, position: AVCaptureDevice.Position.back, callback: { [self] barcodes, error in
if error == nil && barcodes != nil {
for barcode in barcodes! {
barcodeFound = true
let event: [String: Any?] = ["name": "barcode", "data": barcode.data]
sink?(event)
barcodeHandler.publishEvent(event)
}
} else if error != nil {
result(FlutterError(code: "MobileScanner",
message: error?.localizedDescription,
details: "analyzeImage()"))
}
analyzing = false
result(barcodeFound)
}
}
func stop(_ result: FlutterResult) {
if (device == nil) {
result(FlutterError(code: "MobileScanner",
message: "Called stop() while already stopped!",
details: nil))
return
}
captureSession.stopRunning()
for input in captureSession.inputs {
captureSession.removeInput(input)
}
for output in captureSession.outputs {
captureSession.removeOutput(output)
barcodeHandler.publishEvent(["name": "error", "message": error?.localizedDescription])
}
device.removeObserver(self, forKeyPath: #keyPath(AVCaptureDevice.torchMode))
registry.unregisterTexture(textureId)
// analyzeMode = 0
latestBuffer = nil
captureSession = nil
device = nil
textureId = nil
})
result(nil)
}
// Observer for torch state
/// Observer for torch state
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
switch keyPath {
case "torchMode":
// off = 0; on = 1; auto = 2;
let state = change?[.newKey] as? Int
let event: [String: Any?] = ["name": "torchState", "data": state]
sink?(event)
barcodeHandler.publishEvent(["name": "torchState", "data": state])
default:
break
}
}
}
class MapArgumentReader {
let args: [String: Any]?
init(_ args: [String: Any]?) {
self.args = args
}
func string(key: String) -> String? {
return args?[key] as? String
}
func int(key: String) -> Int? {
return (args?[key] as? NSNumber)?.intValue
}
func bool(key: String) -> Bool? {
return (args?[key] as? NSNumber)?.boolValue
}
func stringArray(key: String) -> [String]? {
return args?[key] as? [String]
}
func intArray(key: String) -> [Int]? {
return args?[key] as? [Int]
}
enum DetectionSpeed: Int {
case noDuplicates = 0
case normal = 1
case unrestricted = 2
}
... ...
... ... @@ -4,7 +4,7 @@
#
Pod::Spec.new do |s|
s.name = 'mobile_scanner'
s.version = '0.0.1'
s.version = '3.0.0'
s.summary = 'An universal scanner for Flutter based on MLKit.'
s.description = <<-DESC
An universal scanner for Flutter based on MLKit.
... ... @@ -16,7 +16,7 @@ An universal scanner for Flutter based on MLKit.
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.dependency 'GoogleMLKit/BarcodeScanning', '~> 3.2.0'
s.platform = :ios, '10.0'
s.platform = :ios, '11.0'
s.static_framework = true
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
... ...
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';
export 'src/enums/ratio.dart';
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';
... ...
... ... @@ -5,7 +5,7 @@ import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:mobile_scanner/src/enums/camera_facing.dart';
import 'package:mobile_scanner/src/web/jsqr.dart';
import 'package:mobile_scanner/src/web/media.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,
});
}
... ...
/// The facing of a camera.
enum CameraFacing {
/// Front facing camera.
front,
/// Back facing camera.
back,
}
... ...
/// The detection speed of the scanner.
enum DetectionSpeed {
/// The scanner will only scan a barcode once, and never again until another
/// barcode has been scanned.
noDuplicates,
/// Front facing camera.
normal,
/// Back facing camera.
unrestricted,
}
... ...
enum MobileScannerState { undetermined, authorized, denied }
... ...
enum Ratio { ratio_4_3, ratio_16_9 }
... ...
/// The state of torch.
enum TorchState {
/// Torch is off.
off,
/// Torch is on.
on,
}
... ...
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
enum Ratio { ratio_4_3, ratio_16_9 }
import 'package:mobile_scanner/src/barcode_capture.dart';
import 'package:mobile_scanner/src/mobile_scanner_arguments.dart';
import 'package:mobile_scanner/src/mobile_scanner_controller.dart';
/// A widget showing a live camera preview.
class MobileScanner extends StatefulWidget {
... ... @@ -15,28 +15,23 @@ 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,
});
... ... @@ -57,40 +52,37 @@ 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,192 +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_utility.dart';
import 'package:mobile_scanner/src/mobile_scanner_exception.dart';
/// The facing of a camera.
enum CameraFacing {
/// Front facing camera.
front,
/// The [MobileScannerController] holds all the logic of this plugin,
/// where as the [MobileScanner] class is the frontend of this plugin.
class MobileScannerController {
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));
}
/// Back facing camera.
back,
}
//Must be static to keep the same value on new instances
static int? controllerHashcode;
enum MobileScannerState { undetermined, authorized, denied }
/// Select which camera should be used.
///
/// Default: CameraFacing.back
final CameraFacing facing;
/// The state of torch.
enum TorchState {
/// Torch is off.
off,
// /// Analyze the image in 4:3 or 16:9
// ///
// /// Only on Android
// final Ratio? ratio;
/// Torch is on.
on,
}
/// Enable or disable the torch (Flash) on start
///
/// Default: disabled
final bool torchEnabled;
// enum AnalyzeMode { none, barcode }
/// Set to true if you want to return the image buffer with the Barcode event
///
/// Only supported on iOS and Android
final bool returnImage;
class MobileScannerController {
MethodChannel methodChannel =
const MethodChannel('dev.steenbakker.mobile_scanner/scanner/method');
EventChannel eventChannel =
const EventChannel('dev.steenbakker.mobile_scanner/scanner/event');
/// If provided, the scanner will only detect those specific formats
final List<BarcodeFormat>? formats;
//Must be static to keep the same value on new instances
static int? _controllerHashcode;
StreamSubscription? events;
/// Sets the speed of detections.
///
/// WARNING: DetectionSpeed.unrestricted can cause memory issues on some devices
final DetectionSpeed detectionSpeed;
/// Sets the barcode stream
final StreamController<BarcodeCapture> _barcodesController =
StreamController.broadcast();
Stream<BarcodeCapture> get barcodes => _barcodesController.stream;
static const MethodChannel _methodChannel =
MethodChannel('dev.steenbakker.mobile_scanner/scanner/method');
static const EventChannel _eventChannel =
EventChannel('dev.steenbakker.mobile_scanner/scanner/event');
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
final bool returnImage;
/// If provided, the scanner will only detect those specific formats.
final List<BarcodeFormat>? formats;
/// Listen to events from the platform specific code
late StreamSubscription events;
CameraFacing facing;
bool hasTorch = false;
late StreamController<Barcode> barcodesController;
/// A notifier that provides several arguments about the MobileScanner
final ValueNotifier<MobileScannerArguments?> startArguments =
ValueNotifier(null);
/// Whether to automatically resume the camera when the application is resumed
bool autoResume;
/// A notifier that provides the state of the Torch (Flash)
final ValueNotifier<TorchState> torchState = ValueNotifier(TorchState.off);
Stream<Barcode> get barcodes => barcodesController.stream;
/// A notifier that provides the state of which camera is being used
late final ValueNotifier<CameraFacing> cameraFacingState =
ValueNotifier(facing);
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;
bool isStarting = false;
bool? _hasTorch;
cameraFacingState = ValueNotifier(facing);
/// Set the starting arguments for the camera
Map<String, dynamic> _argumentsToMap({CameraFacing? cameraFacingOverride}) {
final Map<String, dynamic> arguments = {};
// Sets analyze mode and barcode stream
barcodesController = StreamController.broadcast(
// onListen: () => setAnalyzeMode(AnalyzeMode.barcode.index),
// onCancel: () => setAnalyzeMode(AnalyzeMode.none.index),
);
cameraFacingState.value = cameraFacingOverride ?? facing;
arguments['facing'] = cameraFacingState.value.index;
// Listen to events from the platform specific code
events = eventChannel
.receiveBroadcastStream()
.listen((data) => handleEvent(data as Map));
}
// if (ratio != null) arguments['ratio'] = ratio;
arguments['torch'] = torchEnabled;
arguments['speed'] = detectionSpeed.index;
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();
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();
}
}
// TODO: Add more analyzers like text analyzer
// void setAnalyzeMode(int mode) {
// if (hashCode != _controllerHashcode) {
// return;
// }
// methodChannel.invokeMethod('analyze', mode);
// }
// List<BarcodeFormats>? formats = _defaultBarcodeFormats,
bool isStarting = false;
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}');
... ... @@ -198,85 +161,78 @@ 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.
... ... @@ -285,28 +241,72 @@ 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?);
}
}
}
... ...
class MobileScannerException implements Exception {
String message;
MobileScannerException(this.message);
}
... ...
... ... @@ -14,6 +14,7 @@ dependencies:
sdk: flutter
js: ^0.6.3
dev_dependencies:
flutter_test:
sdk: flutter
... ...