Julian Steenbakker

imp: add docs, update flutter side to match native code.

## NEXT
## 3.0.0-beta.2
Breaking changes:
* The arguments parameter of onDetect is removed. The data is now returned by the onStart callback
in the MobileScanner widget.
* allowDuplicates is removed and replaced by MobileScannerSpeed enum.
* onPermissionSet in MobileScanner widget is deprecated and will be removed. Use the onPermissionSet
onPermissionSet callback in MobileScannerController instead.
* [iOS] The minimum deployment target is now 11.0 or higher.
Features:
* The returnImage is working for both iOS and Android. You can enable it in the MobileScannerController.
The image will be returned in the BarcodeCapture object provided by onDetect.
* You can now control the DetectionSpeed, as well as the timeout of the DetectionSpeed. For more
info see the DetectionSpeed documentation. This replaces the allowDuplicates function.
Other improvements:
* Both the [iOS] and [Android] codebases have been refactored completely.
* [iOS] Updated POD dependencies
## 3.0.0-beta.1
... ...
... ... @@ -19,6 +19,12 @@ class _BarcodeListScannerWithControllerState
torchEnabled: true,
// formats: [BarcodeFormat.qrCode]
// facing: CameraFacing.front,
onPermissionSet: (hasPermission) {
// Do something with permission callback
},
// detectionSpeed: DetectionSpeed.normal
// detectionTimeoutMs: 1000,
// returnImage: false,
);
bool isStarted = true;
... ... @@ -34,16 +40,18 @@ class _BarcodeListScannerWithControllerState
MobileScanner(
controller: controller,
fit: BoxFit.contain,
// allowDuplicates: true,
// controller: MobileScannerController(
// torchEnabled: true,
// facing: CameraFacing.front,
// ),
onDetect: (barcodeCapture, arguments) {
onDetect: (barcodeCapture) {
setState(() {
this.barcodeCapture = barcodeCapture;
});
},
onStart: (arguments) {
// Do something with start arguments
},
),
Align(
alignment: Alignment.bottomCenter,
... ...
... ... @@ -16,9 +16,15 @@ class _BarcodeScannerWithControllerState
BarcodeCapture? barcode;
MobileScannerController controller = MobileScannerController(
torchEnabled: true, detectionSpeed: DetectionSpeed.unrestricted,
torchEnabled: true,
// formats: [BarcodeFormat.qrCode]
// facing: CameraFacing.front,
onPermissionSet: (hasPermission) {
// Do something with permission callback
},
// detectionSpeed: DetectionSpeed.normal
// detectionTimeoutMs: 1000,
// returnImage: false,
);
bool isStarted = true;
... ... @@ -34,12 +40,11 @@ class _BarcodeScannerWithControllerState
MobileScanner(
controller: controller,
fit: BoxFit.contain,
// allowDuplicates: true,
// controller: MobileScannerController(
// torchEnabled: true,
// facing: CameraFacing.front,
// ),
onDetect: (barcode, args) {
onDetect: (barcode) {
setState(() {
this.barcode = barcode;
});
... ...
... ... @@ -18,10 +18,15 @@ class _BarcodeScannerReturningImageState
MobileScannerArguments? arguments;
MobileScannerController controller = MobileScannerController(
// torchEnabled: true,
returnImage: true,
torchEnabled: true,
// formats: [BarcodeFormat.qrCode]
// facing: CameraFacing.front,
onPermissionSet: (hasPermission) {
// Do something with permission callback
},
// detectionSpeed: DetectionSpeed.normal
// detectionTimeoutMs: 1000,
returnImage: true,
);
bool isStarted = true;
... ... @@ -29,150 +34,140 @@ class _BarcodeScannerReturningImageState
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Builder(
builder: (context) {
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,
body: SafeArea(
child: Column(
children: [
Expanded(
child: barcode?.image != null
? Transform.rotate(
angle: 90 * pi / 180,
child: Image(
gaplessPlayback: true,
image: MemoryImage(barcode!.image!),
fit: BoxFit.contain,
// allowDuplicates: true,
// controller: MobileScannerController(
// torchEnabled: true,
// facing: CameraFacing.front,
// ),
onDetect: (barcode, arguments) {
setState(() {
this.arguments = arguments;
this.barcode = barcode;
});
},
),
Align(
)
: const Center(
child: Text(
'Your scanned barcode will appear here!',
),
),
),
Expanded(
flex: 2,
child: ColoredBox(
color: Colors.grey,
child: Stack(
children: [
MobileScanner(
controller: controller,
fit: BoxFit.contain,
// controller: MobileScannerController(
// torchEnabled: true,
// facing: CameraFacing.front,
// ),
onDetect: (barcode) {
setState(() {
this.barcode = barcode;
});
},
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
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) {
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,
);
}
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;
}),
case TorchState.on:
return const Icon(
Icons.flash_on,
color: Colors.yellow,
);
}
},
),
Center(
child: SizedBox(
width: MediaQuery.of(context).size.width - 200,
height: 50,
child: FittedBox(
child: Text(
barcode?.barcodes.first.rawValue ??
'Scan something!',
overflow: TextOverflow.fade,
style: Theme.of(context)
.textTheme
.headline4!
.copyWith(color: Colors.white),
),
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(
barcode?.barcodes.first.rawValue ??
'Scan something!',
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) {
),
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);
}
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(),
case CameraFacing.back:
return const Icon(Icons.camera_rear);
}
},
),
],
),
iconSize: 32.0,
onPressed: () => controller.switchCamera(),
),
],
),
),
],
),
),
],
),
],
);
},
),
),
],
),
);
),);
}
}
... ...
... ... @@ -24,8 +24,7 @@ class _BarcodeScannerWithoutControllerState
children: [
MobileScanner(
fit: BoxFit.contain,
// allowDuplicates: false,
onDetect: (capture, arguments) {
onDetect: (capture) {
setState(() {
this.capture = capture;
});
... ...
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';
export 'src/objects/barcode_capture.dart';
export 'src/objects/mobile_scanner_arguments.dart';
... ...
... ... @@ -2,11 +2,19 @@
enum DetectionSpeed {
/// The scanner will only scan a barcode once, and never again until another
/// barcode has been scanned.
///
/// NOTE: This mode does analyze every frame in order to check if the value
/// has changed.
noDuplicates,
/// The barcode scanner will wait
/// The barcode scanner will scan one barcode, and wait 250 Miliseconds before
/// scanning again. This will prevent memory issues on older devices.
///
/// You can change the timeout duration with [detectionTimeout] parameter.
normal,
/// Back facing camera.
/// Let the scanner detect barcodes without restriction.
///
/// NOTE: This can cause memory issues with older devices.
unrestricted,
}
... ...
enum MobileScannerState { undetermined, authorized, denied }
/// The authorization state of the scanner.
enum MobileScannerState {
/// The scanner has yet to request weather it is [authorized] or [denied]
undetermined,
/// The scanner has the required permissions.
authorized,
/// The user denied the required permissions.
denied
}
... ...
enum Ratio { ratio_4_3, ratio_16_9 }
// This enum is not used yet
// enum Ratio { ratio_4_3, ratio_16_9 }
... ...
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
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';
import 'package:mobile_scanner/src/objects/barcode_capture.dart';
import 'package:mobile_scanner/src/objects/mobile_scanner_arguments.dart';
typedef MobileScannerCallback = void Function(BarcodeCapture barcodes);
typedef MobileScannerArgumentsCallback = void Function(
MobileScannerArguments? arguments,
);
/// A widget showing a live camera preview.
class MobileScanner extends StatefulWidget {
... ... @@ -10,14 +15,21 @@ class MobileScanner extends StatefulWidget {
final MobileScannerController? controller;
/// Calls the provided [onPermissionSet] callback when the permission is set.
// @Deprecated('Use the [onPermissionSet] paremeter in the [MobileScannerController] instead.')
// ignore: deprecated_consistency
final Function(bool permissionGranted)? onPermissionSet;
/// Function that gets called when a Barcode is detected.
///
/// [barcode] The barcode object with all information about the scanned code.
/// [startArguments] Information about the state of the MobileScanner widget
final Function(BarcodeCapture capture, MobileScannerArguments? arguments)
onDetect;
/// [startInternalArguments] Information about the state of the MobileScanner widget
final MobileScannerCallback onDetect;
/// Function that gets called when the scanner is started.
///
/// [arguments] The start arguments of the scanner. This contains the size of
/// the scanner which can be used to draw a box over the scanner.
final MobileScannerArgumentsCallback? onStart;
/// Handles how the widget should fit the screen.
final BoxFit fit;
... ... @@ -29,10 +41,12 @@ class MobileScanner extends StatefulWidget {
const MobileScanner({
super.key,
required this.onDetect,
this.onStart,
this.controller,
this.autoResume = true,
this.fit = BoxFit.cover,
this.onPermissionSet,
@Deprecated('Use the [onPermissionSet] paremeter in the [MobileScannerController] instead.')
this.onPermissionSet,
});
@override
... ... @@ -49,7 +63,14 @@ class _MobileScannerState extends State<MobileScanner>
WidgetsBinding.instance.addObserver(this);
controller = widget.controller ??
MobileScannerController(onPermissionSet: widget.onPermissionSet);
if (!controller.isStarting) controller.start();
if (!controller.isStarting) {
_startScanner();
}
}
Future<void> _startScanner() async {
final arguments = await controller.start();
widget.onStart?.call(arguments);
}
bool resumeFromBackground = false;
... ... @@ -64,7 +85,7 @@ class _MobileScannerState extends State<MobileScanner>
switch (state) {
case AppLifecycleState.resumed:
resumeFromBackground = false;
controller.start();
_startScanner();
break;
case AppLifecycleState.paused:
resumeFromBackground = true;
... ... @@ -87,7 +108,7 @@ class _MobileScannerState extends State<MobileScanner>
return const ColoredBox(color: Colors.black);
} else {
controller.barcodes.listen((barcode) {
widget.onDetect(barcode, value! as MobileScannerArguments);
widget.onDetect(barcode);
});
return ClipRect(
child: SizedBox(
... ...
... ... @@ -30,7 +30,8 @@ class MobileScannerController {
.listen((data) => _handleEvent(data as Map));
}
//Must be static to keep the same value on new instances
/// The hashcode of the controller to check if the correct object is mounted.
/// Must be static to keep the same value on new instances
static int? controllerHashcode;
/// Select which camera should be used.
... ... @@ -56,6 +57,11 @@ class MobileScannerController {
/// WARNING: DetectionSpeed.unrestricted can cause memory issues on some devices
final DetectionSpeed detectionSpeed;
/// Sets the timeout of scanner.
/// The timeout is set in miliseconds.
///
/// NOTE: The timeout only works if the [detectionSpeed] is set to
/// [DetectionSpeed.normal] (which is the default value).
final int detectionTimeoutMs;
/// Sets the barcode stream
... ... @@ -85,6 +91,7 @@ class MobileScannerController {
ValueNotifier(facing);
bool isStarting = false;
bool? _hasTorch;
/// Set the starting arguments for the camera
... ... @@ -113,9 +120,9 @@ class MobileScannerController {
Future<MobileScannerArguments?> start({
CameraFacing? cameraFacingOverride,
}) async {
debugPrint('Hashcode controller: $hashCode');
if (isStarting) {
debugPrint("Called start() while starting.");
return null;
}
isStarting = true;
... ... @@ -152,10 +159,10 @@ class MobileScannerController {
);
} on PlatformException catch (error) {
debugPrint('${error.code}: ${error.message}');
isStarting = false;
if (error.code == "MobileScannerWeb") {
onPermissionSet?.call(false);
}
isStarting = false;
return null;
}
... ... @@ -172,27 +179,24 @@ class MobileScannerController {
}
if (kIsWeb) {
// If we reach this line, it means camera permission has been granted
onPermissionSet?.call(
true,
); // If we reach this line, it means camera permission has been granted
startArguments.value = MobileScannerArguments(
webId: startResult['ViewID'] as String?,
size: Size(
startResult['videoWidth'] as double? ?? 0,
startResult['videoHeight'] as double? ?? 0,
),
hasTorch: _hasTorch!,
);
} else {
startArguments.value = MobileScannerArguments(
textureId: startResult['textureId'] as int?,
size: toSize(startResult['size'] as Map? ?? {}),
hasTorch: _hasTorch!,
);
}
isStarting = false;
return startArguments.value!;
return startArguments.value = MobileScannerArguments(
size: kIsWeb
? Size(
startResult['videoWidth'] as double? ?? 0,
startResult['videoHeight'] as double? ?? 0,
)
: toSize(startResult['size'] as Map? ?? {}),
hasTorch: _hasTorch!,
textureId: kIsWeb ? null : startResult['textureId'] as int?,
webId: kIsWeb ? startResult['ViewID'] as String? : null,
);
}
/// Stops the camera, but does not dispose this controller.
... ...
... ... @@ -12,11 +12,6 @@ class Barcode {
/// Returns null if the corner points can not be determined.
final List<Offset>? corners;
/// Returns raw bytes of the image buffer
///
/// Returns null if the image was not returned
final Uint8List? image;
/// Returns barcode format
final BarcodeFormat format;
... ... @@ -79,7 +74,6 @@ class Barcode {
Barcode({
this.corners,
this.image,
this.format = BarcodeFormat.ean13,
this.rawBytes,
this.type = BarcodeType.text,
... ... @@ -97,7 +91,7 @@ class Barcode {
});
/// Create a [Barcode] from native data.
Barcode.fromNative(Map data, {this.image})
Barcode.fromNative(Map data)
: 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';
import 'package:mobile_scanner/src/objects/barcode.dart';
/// The return object after a frame is scanned.
///
/// [barcodes] A list with barcodes. A scanned frame can contain multiple
/// barcodes.
/// [image] If enabled, an image of the scanned frame.
class BarcodeCapture {
List<Barcode> barcodes;
Uint8List? image;
final List<Barcode> barcodes;
final Uint8List? image;
BarcodeCapture({
required this.barcodes,
... ...
import 'package:flutter/material.dart';
/// Camera args for [CameraView].
/// The start arguments of the scanner.
class MobileScannerArguments {
/// The texture id.
final int? textureId;
/// Size of the texture.
/// The output size of the camera.
/// This value can be used to draw a box in the image.
final Size size;
/// A bool which is true if the device has a torch.
final bool hasTorch;
/// The texture id of the capture used internally.
final int? textureId;
/// The texture id of the capture used internally if device is web.
final String? webId;
/// Create a [MobileScannerArguments].
MobileScannerArguments({
this.textureId,
required this.size,
required this.hasTorch,
this.textureId,
this.webId,
});
}
... ...
name: mobile_scanner
description: A universal barcode and QR code scanner for Flutter based on MLKit. Uses CameraX on Android, AVFoundation on iOS and Apple Vision & AVFoundation on macOS.
version: 3.0.0-beta.1
version: 3.0.0-beta.2
repository: https://github.com/juliansteenbakker/mobile_scanner
environment:
... ...