Julian Steenbakker

ci: fixed scanner on ios and android

... ... @@ -47,5 +47,16 @@ android {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.google.mlkit:barcode-scanning:17.0.2'
implementation 'com.google.mlkit:camera:16.0.0-beta3'
implementation "androidx.camera:camera-camera2:1.1.0-beta01"
implementation 'androidx.camera:camera-lifecycle:1.1.0-beta01'
// // The following line is optional, as the core library is included indirectly by camera-camera2
// implementation "androidx.camera:camera-core:1.1.0-alpha11"
// implementation "androidx.camera:camera-camera2:1.1.0-alpha11"
// // If you want to additionally use the CameraX Lifecycle library
// implementation "androidx.camera:camera-lifecycle:1.1.0-alpha11"
// // If you want to additionally use the CameraX View class
// implementation "androidx.camera:camera-view:1.0.0-alpha31"
// // If you want to additionally use the CameraX Extensions library
// implementation "androidx.camera:camera-extensions:1.0.0-alpha31"
}
... ...
package dev.steenbakker.mobile_scanner
import android.Manifest
import android.R.attr.height
import android.R.attr.width
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.ImageFormat
import android.graphics.SurfaceTexture
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.hardware.camera2.params.StreamConfigurationMap
import android.util.Log
import android.util.Rational
import android.util.Size
import android.view.Surface
import android.view.Surface.ROTATION_0
import android.view.Surface.ROTATION_180
import androidx.annotation.IntDef
import androidx.annotation.NonNull
import androidx.camera.core.*
import androidx.camera.core.impl.PreviewConfig
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.common.InputImage
import io.flutter.plugin.common.*
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.PluginRegistry
import io.flutter.view.TextureRegistry
class MobileScanner(private val activity: Activity, private val textureRegistry: TextureRegistry)
: MethodChannel.MethodCallHandler, EventChannel.StreamHandler, PluginRegistry.RequestPermissionsResultListener {
companion object {
... ... @@ -34,6 +51,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
@AnalyzeMode
private var analyzeMode: Int = AnalyzeMode.NONE
@ExperimentalGetImage
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: MethodChannel.Result) {
when (call.method) {
"state" -> stateNative(result)
... ... @@ -81,8 +99,23 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
ActivityCompat.requestPermissions(activity, permissions, REQUEST_CODE)
}
private var sensorOrientation = 0
@ExperimentalGetImage
// @androidx.camera.camera2.interop.ExperimentalCamera2Interop
private fun startNative(call: MethodCall, result: MethodChannel.Result) {
val targetWidth: Int? = call.argument<Int>("targetWidth")
val targetHeight: Int? = call.argument<Int>("targetHeight")
val facing: Int? = call.argument<Int>("facing")
if (targetWidth == null || targetHeight == null) {
result.error("INVALID_ARGUMENT", "Missing a required argument", "Expecting targetWidth, targetHeight")
return
}
Log.i("LOG", "Target resolution : $targetWidth, $targetHeight")
val future = ProcessCameraProvider.getInstance(activity)
val executor = ContextCompat.getMainExecutor(activity)
future.addListener({
... ... @@ -91,13 +124,23 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
val textureId = textureEntry!!.id()
// Preview
val surfaceProvider = Preview.SurfaceProvider { request ->
val resolution = request.resolution
val texture = textureEntry!!.surfaceTexture()
val resolution = request.resolution
texture.setDefaultBufferSize(resolution.width, resolution.height)
Log.i("LOG", "Image resolution : ${request.resolution}")
val surface = Surface(texture)
request.provideSurface(surface, executor, { })
request.provideSurface(surface, executor) { }
}
val preview = Preview.Builder().build().apply { setSurfaceProvider(surfaceProvider) }
// PreviewConfig().apply { }
// val previewConfig = PreviewConfig.Builder().apply {
// setTargetAspectRatio(SQUARE_ASPECT_RATIO)
// setTargetRotation(viewFinder.display.rotation)
// }.build()
val preview = Preview.Builder()
.setTargetResolution(Size(targetWidth, targetHeight))
.build().apply { setSurfaceProvider(surfaceProvider) }
// Analyzer
val analyzer = ImageAnalysis.Analyzer { imageProxy -> // YUV_420_888 format
when (analyzeMode) {
... ... @@ -120,6 +163,7 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
}
val analysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.setTargetResolution(Size(targetWidth, targetHeight))
.build().apply { setAnalyzer(executor, analyzer) }
// Bind to lifecycle.
val owner = activity as LifecycleOwner
... ... @@ -127,17 +171,23 @@ class MobileScanner(private val activity: Activity, private val textureRegistry:
if (call.arguments == 0) CameraSelector.DEFAULT_FRONT_CAMERA
else CameraSelector.DEFAULT_BACK_CAMERA
camera = cameraProvider!!.bindToLifecycle(owner, selector, preview, analysis)
camera!!.cameraInfo.torchState.observe(owner, { state ->
val analysisSize = analysis.resolutionInfo?.resolution ?: Size(0, 0)
val previewSize = preview.resolutionInfo?.resolution ?: Size(0, 0)
Log.i("LOG", "Analyzer: $analysisSize")
Log.i("LOG", "Preview: $previewSize")
camera!!.cameraInfo.torchState.observe(owner) { state ->
// TorchState.OFF = 0; TorchState.ON = 1
val event = mapOf("name" to "torchState", "data" to state)
sink?.success(event)
})
// TODO: seems there's not a better way to get the final resolution
@SuppressLint("RestrictedApi")
val resolution = preview.attachedSurfaceResolution!!
}
val resolution = preview.resolutionInfo!!.resolution
val portrait = camera!!.cameraInfo.sensorRotationDegrees % 180 == 0
val width = resolution.width.toDouble()
val height = resolution.height.toDouble()
// val size = mapOf("width" to 1920.0, "height" to 1080.0)
val size = if (portrait) mapOf("width" to width, "height" to height) else mapOf("width" to height, "height" to width)
val answer = mapOf("textureId" to textureId, "size" to size, "torchable" to camera!!.torchable)
result.success(answer)
... ...
... ... @@ -18,7 +18,7 @@ class _AnalyzeViewState extends State<AnalyzeView>
with SingleTickerProviderStateMixin {
List<Offset> points = [];
CameraController cameraController = CameraController();
// CameraController cameraController = CameraController(context, width: 320, height: 150);
String? barcode = null;
... ... @@ -29,25 +29,34 @@ class _AnalyzeViewState extends State<AnalyzeView>
body: Builder(builder: (context) {
return Stack(
children: [
CameraView(
controller: cameraController,
MobileScanner(
// fitScreen: false,
// controller: cameraController,
onDetect: (barcode, args) {
if (this.barcode != barcode.rawValue) {
this.barcode = barcode.rawValue;
if (barcode.corners != null) {
debugPrint('Size: ${MediaQuery.of(context).size}');
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('${barcode.rawValue}'),
duration: Duration(milliseconds: 200),
duration: const Duration(milliseconds: 200),
animation: null,
));
setState(() {
final List<Offset> points = [];
double factorWidth = args.size.width / 520;
double factorHeight = args.size.height / 640;
// double factorHeight = wanted / args.size.height;
final size = MediaQuery.of(context).devicePixelRatio;
debugPrint('Size: ${barcode.corners}');
for (var point in barcode.corners!) {
points.add(Offset(point.dx * factorWidth,
point.dy * factorHeight));
final adjustedWith = point.dx ;
final adjustedHeight= point.dy ;
points.add(Offset(adjustedWith / size, adjustedHeight / size));
// points.add(Offset((point.dx ) / size,
// (point.dy) / size));
// final differenceWidth = (args.wantedSize!.width - args.size.width) / 2;
// final differenceHeight = (args.wantedSize!.height - args.size.height) / 2;
// points.add(Offset((point.dx + differenceWidth) / size,
// (point.dy + differenceHeight) / size));
}
this.points = points;
});
... ... @@ -55,29 +64,25 @@ class _AnalyzeViewState extends State<AnalyzeView>
}
// Default 640 x480
}),
Container(
// width: 400,
// height: 400,
child: CustomPaint(
CustomPaint(
painter: OpenPainter(points),
),
),
Container(
alignment: Alignment.bottomCenter,
margin: EdgeInsets.only(bottom: 80.0),
child: IconButton(
icon: ValueListenableBuilder(
valueListenable: cameraController.torchState,
builder: (context, state, child) {
final color =
state == TorchState.off ? Colors.grey : Colors.white;
return Icon(Icons.bolt, color: color);
},
),
iconSize: 32.0,
onPressed: () => cameraController.torch(),
),
),
// Container(
// alignment: Alignment.bottomCenter,
// margin: EdgeInsets.only(bottom: 80.0),
// child: IconButton(
// icon: ValueListenableBuilder(
// valueListenable: cameraController.torchState,
// builder: (context, state, child) {
// final color =
// state == TorchState.off ? Colors.grey : Colors.white;
// return Icon(Icons.bolt, color: color);
// },
// ),
// iconSize: 32.0,
// onPressed: () => cameraController.torch(),
// ),
// ),
],
);
}),
... ... @@ -87,7 +92,7 @@ class _AnalyzeViewState extends State<AnalyzeView>
@override
void dispose() {
cameraController.dispose();
// cameraController.dispose();
super.dispose();
}
... ...
... ... @@ -80,6 +80,11 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
analyzing = true
let buffer = CMSampleBufferGetImageBuffer(sampleBuffer)
let image = VisionImage(image: buffer!.image)
image.orientation = imageOrientation(
deviceOrientation: UIDevice.current.orientation,
defaultOrientation: .portrait
)
let scanner = BarcodeScanner.barcodeScanner()
scanner.process(image) { [self] barcodes, error in
if error == nil && barcodes != nil {
... ... @@ -95,6 +100,26 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
}
}
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 stateNative(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status {
... ... @@ -111,10 +136,22 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
AVCaptureDevice.requestAccess(for: .video, completionHandler: { result($0) })
}
var position = AVCaptureDevice.Position.back
func startNative(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
textureId = registry.register(self)
captureSession = AVCaptureSession()
let position = call.arguments as! Int == 0 ? AVCaptureDevice.Position.front : .back
let argReader = MapArgumentReader(call.arguments as? [String: Any])
guard let targetWidth = argReader.int(key: "targetWidth"),
let targetHeight = argReader.int(key: "targetHeight"),
let facing = argReader.int(key: "facing") else {
result(FlutterError(code: "INVALID_ARGUMENT", message: "Missing a required argument", details: "Expecting targetWidth, targetHeight, formats, and optionally heartbeatTimeout"))
return
}
position = facing == 0 ? AVCaptureDevice.Position.front : .back
if #available(iOS 10.0, *) {
device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: position).devices.first
} else {
... ... @@ -129,10 +166,13 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
} 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 {
... ... @@ -199,3 +239,25 @@ public class SwiftMobileScannerPlugin: NSObject, FlutterPlugin, FlutterStreamHan
}
}
}
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 stringArray(key: String) -> [String]? {
return args?[key] as? [String]
}
}
... ...
library mobile_scanner;
export 'src/mobile_scanner.dart';
export 'src/camera_controller.dart';
export 'src/camera_view.dart';
export 'src/torch_state.dart';
export 'src/mobile_scanner_controller.dart';
export 'src/objects/barcode.dart';
\ No newline at end of file
... ...
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'camera_args.dart';
/// A widget showing a live camera preview.
class CameraView extends StatefulWidget {
/// The controller of the camera.
final CameraController? controller;
final Function(Barcode barcode, CameraArgs args)? onDetect;
/// Create a [CameraView] with a [controller], the [controller] must has been initialized.
const CameraView({Key? key, this.onDetect, this.controller}) : super(key: key);
@override
State<CameraView> createState() => _CameraViewState();
}
class _CameraViewState extends State<CameraView> {
late CameraController controller;
@override
initState() {
super.initState();
controller = widget.controller ?? CameraController();
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: controller.args,
builder: (context, value, child) {
value = value as CameraArgs?;
if (value == null) {
return Container(color: Colors.black);
} else {
controller.barcodes
.listen((a) => widget.onDetect!(a, value as CameraArgs));
return ClipRect(
child: Transform.scale(
scale: value.size.fill(MediaQuery.of(context).size),
child: Center(
child: AspectRatio(
aspectRatio: value.size.aspectRatio,
child: Texture(textureId: value.textureId),
),
),
),
);
}
});
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
extension on Size {
double fill(Size targetSize) {
if (targetSize.aspectRatio < aspectRatio) {
return targetSize.height * aspectRatio / targetSize.width;
} else {
return targetSize.width / aspectRatio / targetSize.height;
}
}
}
// import 'dart:async';
// import 'package:flutter/material.dart';
// import 'package:mobile_scanner/src/mobile_scanner_handler.dart';
// import 'package:mobile_scanner/src/objects/preview_details.dart';
//
// import 'mobile_scanner_preview.dart';
// import 'objects/barcode_formats.dart';
//
// typedef ErrorCallback = Widget Function(BuildContext context, Object? error);
//
// Text _defaultNotStartedBuilder(context) => const Text("Camera Loading ...");
// Text _defaultOffscreenBuilder(context) => const Text("Camera Paused.");
// Text _defaultOnError(BuildContext context, Object? error) {
// debugPrint("Error reading from camera: $error");
// return const Text("Error reading from camera...");
// }
//
// class MobileScanner extends StatefulWidget {
// const MobileScanner(
// {Key? key,
// required this.qrCodeCallback,
// this.child,
// this.fit = BoxFit.cover,
// WidgetBuilder? notStartedBuilder,
// WidgetBuilder? offscreenBuilder,
// ErrorCallback? onError,
// this.formats,
// this.rearLens = true,
// this.manualFocus = false})
// : notStartedBuilder = notStartedBuilder ?? _defaultNotStartedBuilder,
// offscreenBuilder =
// offscreenBuilder ?? notStartedBuilder ?? _defaultOffscreenBuilder,
// onError = onError ?? _defaultOnError,
// super(key: key);
//
// final BoxFit fit;
// final ValueChanged<String?> qrCodeCallback;
// final Widget? child;
// final WidgetBuilder notStartedBuilder;
// final WidgetBuilder offscreenBuilder;
// final ErrorCallback onError;
// final List<BarcodeFormats>? formats;
// final bool rearLens;
// final bool manualFocus;
//
// static void toggleFlash() {
// MobileScannerHandler.toggleFlash();
// }
//
// static void flipCamera() {
// MobileScannerHandler.switchCamera();
// }
//
// @override
// _MobileScannerState createState() => _MobileScannerState();
// }
//
// class _MobileScannerState extends State<MobileScanner>
// with WidgetsBindingObserver {
//
// bool onScreen = true;
// Future<PreviewDetails>? _previewDetails;
//
// @override
// void initState() {
// super.initState();
// WidgetsBinding.instance!.addObserver(this);
// }
//
// @override
// void dispose() {
// WidgetsBinding.instance!.removeObserver(this);
// super.dispose();
// }
//
// @override
// void didChangeAppLifecycleState(AppLifecycleState state) {
// if (state == AppLifecycleState.resumed) {
// setState(() => onScreen = true);
// } else {
// if (_previewDetails != null && onScreen) {
// MobileScannerHandler.stop();
// }
// setState(() {
// onScreen = false;
// _previewDetails = null;
// });
// }
// }
//
// Future<PreviewDetails> _initPreview(num width, num height) async {
// final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
// return await MobileScannerHandler.start(
// width: (devicePixelRatio * width.toInt()).ceil(),
// height: (devicePixelRatio * height.toInt()).ceil(),
// qrCodeHandler: widget.qrCodeCallback,
// formats: widget.formats,
// );
// }
//
// void switchCamera() {
// MobileScannerHandler.rearLens = !MobileScannerHandler.rearLens;
// restart();
// }
//
//
// void switchFocus() {
// MobileScannerHandler.manualFocus = !MobileScannerHandler.manualFocus;
// restart();
// }
//
// /// This method can be used to restart scanning
// /// the event that it was paused.
// Future<void> restart() async {
// await MobileScannerHandler.stop();
// setState(() {
// _previewDetails = null;
// });
// }
//
// /// This method can be used to manually stop the
// /// camera.
// Future<void> stop() async {
// await MobileScannerHandler.stop();
// }
//
// @override
// deactivate() {
// super.deactivate();
// MobileScannerHandler.stop();
// }
//
// @override
// Widget build(BuildContext context) {
// return LayoutBuilder(
// builder: (BuildContext context, BoxConstraints constraints) {
// if (_previewDetails == null && onScreen) {
// _previewDetails =
// _initPreview(constraints.maxWidth, constraints.maxHeight);
// } else if (!onScreen) {
// return widget.offscreenBuilder(context);
// }
//
// return FutureBuilder(
// future: _previewDetails,
// builder: (BuildContext context, AsyncSnapshot<PreviewDetails> details) {
// switch (details.connectionState) {
// case ConnectionState.none:
// case ConnectionState.waiting:
// return widget.notStartedBuilder(context);
// case ConnectionState.done:
// if (details.hasError) {
// debugPrint(details.error.toString());
// return widget.onError(context, details.error);
// }
// Widget preview = SizedBox(
// width: constraints.maxWidth,
// height: constraints.maxHeight,
// child: Preview(
// previewDetails: details.data!,
// targetWidth: constraints.maxWidth,
// targetHeight: constraints.maxHeight,
// fit: widget.fit,
// ),
// );
//
// if (widget.child != null) {
// return Stack(
// children: [
// preview,
// widget.child!,
// ],
// );
// }
// return preview;
//
// default:
// throw AssertionError("${details.connectionState} not supported.");
// }
// },
// );
// });
// }
// }
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'mobile_scanner_arguments.dart';
/// A widget showing a live camera preview.
class MobileScanner extends StatefulWidget {
/// The controller of the camera.
final MobileScannerController? controller;
final Function(Barcode barcode, MobileScannerArguments args)? onDetect;
final bool fitScreen;
final bool fitWidth;
/// Create a [MobileScanner] with a [controller], the [controller] must has been initialized.
const MobileScanner(
{Key? key, this.onDetect, this.controller, this.fitScreen = true, this.fitWidth = true})
: super(key: key);
@override
State<MobileScanner> createState() => _MobileScannerState();
}
class _MobileScannerState extends State<MobileScanner>
with WidgetsBindingObserver {
bool onScreen = true;
MobileScannerController? controller;
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
setState(() => onScreen = true);
} else {
if (controller != null && onScreen) {
controller!.stop();
}
setState(() {
onScreen = false;
controller = null;
});
}
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, BoxConstraints constraints) {
final media = MediaQuery.of(context);
controller ??= MobileScannerController(context,
width: constraints.maxWidth, height: constraints.maxHeight);
if (!onScreen) return const Text("Camera Paused.");
return ValueListenableBuilder(
valueListenable: controller!.args,
builder: (context, value, child) {
value = value as MobileScannerArguments?;
if (value == null) {
return Container(color: Colors.black);
} else {
controller!.barcodes.listen(
(a) => widget.onDetect!(a, value as MobileScannerArguments));
// Texture(textureId: value.textureId)
return ClipRect(
child: FittedBox(
fit: BoxFit.cover,
child: SizedBox(
width: value.size.width,
height: value.size.height,
child: Texture(textureId: value.textureId),
),
),
);
}
});
});
}
@override
void dispose() {
controller?.dispose();
super.dispose();
}
}
extension on Size {
double fill(Size targetSize) {
if (targetSize.aspectRatio < aspectRatio) {
return targetSize.height * aspectRatio / targetSize.width;
} else {
return targetSize.width / aspectRatio / targetSize.height;
}
}
}
... ...
import 'package:flutter/material.dart';
/// Camera args for [CameraView].
class CameraArgs {
class MobileScannerArguments {
/// The texture id.
final int textureId;
/// Size of the texture.
final Size size;
/// Create a [CameraArgs].
CameraArgs(this.textureId, this.size);
/// Size of the texture.
final Size? wantedSize;
/// Create a [MobileScannerArguments].
MobileScannerArguments({required this.textureId,required this.size, this.wantedSize});
}
... ...
... ... @@ -3,10 +3,9 @@ import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'camera_args.dart';
import 'mobile_scanner_arguments.dart';
import 'objects/barcode.dart';
import 'torch_state.dart';
import 'util.dart';
import 'objects/barcode_utility.dart';
/// The facing of a camera.
enum CameraFacing {
... ... @@ -17,45 +16,60 @@ enum CameraFacing {
back,
}
enum MobileScannerState {
undetermined,
authorized,
denied
}
/// A camera controller.
abstract class CameraController {
/// Arguments for [CameraView].
ValueNotifier<CameraArgs?> get args;
/// Torch state of the camera.
ValueNotifier<TorchState> get torchState;
/// A stream of barcodes.
Stream<Barcode> get barcodes;
/// The state of torch.
enum TorchState {
/// Torch is off.
off,
/// Create a [CameraController].
///
/// [facing] target facing used to select camera.
///
/// [formats] the barcode formats for image analyzer.
factory CameraController([CameraFacing facing = CameraFacing.back]) =>
_CameraController(facing);
/// Torch is on.
on,
}
/// Start the camera asynchronously.
Future<void> startAsync();
/// Switch the torch's state.
void torch();
/// Release the resources of the camera.
void dispose();
}
// /// A camera controller.
// abstract class CameraController {
// /// Arguments for [CameraView].
// ValueNotifier<CameraArgs?> get args;
//
// /// Torch state of the camera.
// ValueNotifier<TorchState> get torchState;
//
// /// A stream of barcodes.
// Stream<Barcode> get barcodes;
//
// /// Create a [CameraController].
// ///
// /// [facing] target facing used to select camera.
// ///
// /// [formats] the barcode formats for image analyzer.
// factory CameraController([CameraFacing facing = CameraFacing.back] ) =>
// _CameraController(facing);
//
// /// Start the camera asynchronously.
// Future<void> start();
//
// /// Switch the torch's state.
// void torch();
//
// /// Release the resources of the camera.
// void dispose();
// }
class MobileScannerController {
class _CameraController implements CameraController {
static const MethodChannel method =
MethodChannel('dev.steenbakker.mobile_scanner/scanner/method');
static const EventChannel event =
EventChannel('dev.steenbakker.mobile_scanner/scanner/event');
static const undetermined = 0;
static const authorized = 1;
static const denied = 2;
static const analyze_none = 0;
static const analyze_barcode = 1;
... ... @@ -64,18 +78,15 @@ class _CameraController implements CameraController {
static StreamSubscription? subscription;
final CameraFacing facing;
@override
final ValueNotifier<CameraArgs?> args;
@override
final ValueNotifier<MobileScannerArguments?> args;
final ValueNotifier<TorchState> torchState;
bool torchable;
late StreamController<Barcode> barcodesController;
@override
Stream<Barcode> get barcodes => barcodesController.stream;
_CameraController(this.facing)
MobileScannerController(BuildContext context, {required num width, required num height, this.facing = CameraFacing.back})
: args = ValueNotifier(null),
torchState = ValueNotifier(TorchState.off),
torchable = false {
... ... @@ -89,7 +100,13 @@ class _CameraController implements CameraController {
onListen: () => tryAnalyze(analyze_barcode),
onCancel: () => tryAnalyze(analyze_none),
);
startAsync();
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
start(
width: (devicePixelRatio * width.toInt()).ceil(),
height: (devicePixelRatio * height.toInt()).ceil());
// Listen event handler.
subscription =
event.receiveBroadcastStream().listen((data) => handleEvent(data));
... ... @@ -119,39 +136,53 @@ class _CameraController implements CameraController {
method.invokeMethod('analyze', mode);
}
@override
Future<void> startAsync() async {
Future<void> start({
int? width,
int? height,
// List<BarcodeFormats>? formats = _defaultBarcodeFormats,
}) async {
ensure('startAsync');
// Check authorization state.
var state = await method.invokeMethod('state');
if (state == undetermined) {
final result = await method.invokeMethod('request');
state = result ? authorized : denied;
}
if (state != authorized) {
MobileScannerState state = MobileScannerState.values[await method.invokeMethod('state')];
switch (state) {
case MobileScannerState.undetermined:
final bool result = await method.invokeMethod('request');
state = result ? MobileScannerState.authorized : MobileScannerState.denied;
break;
case MobileScannerState.authorized:
break;
case MobileScannerState.denied:
throw PlatformException(code: 'NO ACCESS');
}
debugPrint('TARGET RESOLUTION $width, $height');
// Start camera.
final answer =
await method.invokeMapMethod<String, dynamic>('start', facing.index);
await method.invokeMapMethod<String, dynamic>('start', {
'targetWidth': width,
'targetHeight': height,
'facing': facing.index
});
final textureId = answer?['textureId'];
final size = toSize(answer?['size']);
args.value = CameraArgs(textureId, size);
final Size size = toSize(answer?['size']);
debugPrint('RECEIVED SIZE: ${size.width} ${size.height}');
if (width != null && height != null) {
args.value = MobileScannerArguments(textureId: textureId, size: size, wantedSize: Size(width.toDouble(), height.toDouble()));
} else {
args.value = MobileScannerArguments(textureId: textureId, size: size);
}
torchable = answer?['torchable'];
}
@override
void torch() {
ensure('torch');
if (!torchable) {
return;
}
if (!torchable) return;
var state =
torchState.value == TorchState.off ? TorchState.on : TorchState.off;
method.invokeMethod('torch', state.index);
}
@override
void dispose() {
if (hashCode == id) {
stop();
... ...
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mobile_scanner/src/objects/preview_details.dart';
import 'objects/barcode_formats.dart';
enum FrameRotation { none, ninetyCC, oneeighty, twoseventyCC }
const _defaultBarcodeFormats = [
BarcodeFormats.ALL_FORMATS,
];
typedef QRCodeHandler = void Function(String? qr);
class MobileScannerHandler {
static const MethodChannel _channel =
MethodChannel('dev.steenbakker.mobile_scanner/scanner');
static Future<String?> get platformVersion async {
final String? version = await _channel.invokeMethod('getPlatformVersion');
return version;
}
static bool rearLens = true;
static bool manualFocus = false;
//Set target size before starting
static Future<PreviewDetails> start({
required int width,
required int height,
required QRCodeHandler qrCodeHandler,
List<BarcodeFormats>? formats = _defaultBarcodeFormats,
}) async {
final _formats = formats ?? _defaultBarcodeFormats;
assert(_formats.isNotEmpty);
List<String> formatStrings = _formats
.map((format) => format.toString().split('.')[1])
.toList(growable: false);
_channel.setMethodCallHandler((MethodCall call) async {
switch (call.method) {
case 'qrRead':
assert(call.arguments is String);
qrCodeHandler(call.arguments);
break;
default:
debugPrint("QrChannelHandler: unknown method call received at "
"${call.method}");
}
});
var details = await _channel.invokeMethod('start', {
'targetWidth': width,
'targetHeight': height,
'heartbeatTimeout': 0,
'formats': formatStrings,
'rearLens': rearLens,
'manualFocus': manualFocus
});
assert(details is Map<dynamic, dynamic>);
int? textureId = details["textureId"];
num? orientation = details["surfaceOrientation"];
num? surfaceHeight = details["surfaceHeight"];
num? surfaceWidth = details["surfaceWidth"];
return PreviewDetails(surfaceWidth, surfaceHeight, orientation, textureId);
}
static Future switchCamera() {
return _channel.invokeMethod('switch').catchError(print);
}
static Future toggleFlash() {
return _channel.invokeMethod('toggleFlash').catchError(print);
}
static Future stop() {
_channel.setMethodCallHandler(null);
return _channel.invokeMethod('stop').catchError(print);
}
static Future heartbeat() {
return _channel.invokeMethod('heartbeat').catchError(print);
}
static Future<List<List<int>>?> getSupportedSizes() {
return _channel.invokeMethod('getSupportedSizes').catchError(print)
as Future<List<List<int>>?>;
}
}
// import 'dart:async';
//
// import 'package:flutter/material.dart';
// import 'package:mobile_scanner/src/objects/preview_details.dart';
//
// class Preview extends StatefulWidget {
// final double width, height;
// final double targetWidth, targetHeight;
// final int? textureId;
// final int? sensorOrientation;
// final BoxFit fit;
//
// Preview({
// Key? key,
// required PreviewDetails previewDetails,
// required this.targetWidth,
// required this.targetHeight,
// required this.fit,
// }) : textureId = previewDetails.textureId,
// width = previewDetails.width!.toDouble(),
// height = previewDetails.height!.toDouble(),
// sensorOrientation = previewDetails.sensorOrientation as int?,
// super(key: key);
//
// @override
// State<Preview> createState() => _PreviewState();
// }
//
// class _PreviewState extends State<Preview> {
//
// final _streamSubscriptions = <StreamSubscription<dynamic>>[];
// bool landscapeLeft = false;
//
// @override
// void initState() {
// super.initState();
// _streamSubscriptions.add(
// magnetometerEvents.listen(
// (MagnetometerEvent event) {
// if (event.x <= 0) {
// landscapeLeft = true;
// } else {
// landscapeLeft = false;
// }
// },
// ),
// );
// }
//
// @override
// void dispose() {
// super.dispose();
// for (final subscription in _streamSubscriptions) {
// subscription.cancel();
// }
// }
//
//
// int _getRotationCompensation(NativeDeviceOrientation nativeOrientation) {
// int nativeRotation = 0;
// switch (nativeOrientation) {
// case NativeDeviceOrientation.portraitUp:
// nativeRotation = 0;
// break;
// case NativeDeviceOrientation.landscapeRight:
// nativeRotation = 90;
// break;
// case NativeDeviceOrientation.portraitDown:
// nativeRotation = 180;
// break;
// case NativeDeviceOrientation.landscapeLeft:
// nativeRotation = 270;
// break;
// case NativeDeviceOrientation.unknown:
// default:
// break;
// }
//
// return ((nativeRotation - widget.sensorOrientation! + 450) % 360) ~/ 90;
// }
//
// @override
// Widget build(BuildContext context) {
// final orientation = MediaQuery.of(context).orientation;
// double frameHeight = widget.width;
// double frameWidth = widget.height;
//
// return ClipRect(
// child: FittedBox(
// fit: widget.fit,
// child: RotatedBox(
// quarterTurns: orientation == Orientation.landscape ? landscapeLeft ? 1 : 3 : 0,
// child: SizedBox(
// width: frameWidth,
// height: frameHeight,
// child: Texture(textureId: widget.textureId!),
// ),
// ),
// ),
// );
//
// return NativeDeviceOrientationReader(
// builder: (context) {
// var nativeOrientation =
// NativeDeviceOrientationReader.orientation(context);
//
// double frameHeight = widget.width;
// double frameWidth = widget.height;
//
// return ClipRect(
// child: FittedBox(
// fit: widget.fit,
// child: RotatedBox(
// quarterTurns: _getRotationCompensation(nativeOrientation),
// child: SizedBox(
// width: frameWidth,
// height: frameHeight,
// child: Texture(textureId: widget.textureId!),
// ),
// ),
// ),
// );
// },
// );
// }
// }
import 'dart:typed_data';
import 'dart:ui';
import '../util.dart';
import 'barcode_utility.dart';
/// Represents a single recognized barcode and its value.
class Barcode {
... ...
enum BarcodeFormats {
ALL_FORMATS,
AZTEC,
CODE_128,
CODE_39,
CODE_93,
CODABAR,
DATA_MATRIX,
EAN_13,
EAN_8,
ITF,
PDF417,
QR_CODE,
UPC_A,
UPC_E,
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'objects/barcode.dart';
import 'barcode.dart';
Size toSize(Map<dynamic, dynamic> data) {
final width = data['width'];
... ...
class PreviewDetails {
num? width;
num? height;
num? sensorOrientation;
int? textureId;
PreviewDetails(this.width, this.height, this.sensorOrientation, this.textureId);
}
/// The state of torch.
enum TorchState {
/// Torch is off.
off,
/// Torch is on.
on,
}