Khoren Markosyan
3 years ago
14 changed files with 1175 additions and 56 deletions
@ -0,0 +1,199 @@ |
|||||||
|
import 'dart:io'; |
||||||
|
import 'dart:typed_data'; |
||||||
|
|
||||||
|
import 'package:camera/camera.dart'; |
||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:flutter/services.dart'; |
||||||
|
import 'package:flutter_zxing/flutter_zxing.dart'; |
||||||
|
import 'package:flutter_zxing/generated_bindings.dart'; |
||||||
|
import 'package:flutter_zxing/zxing_reader_widget.dart'; |
||||||
|
import 'package:flutter_zxing/zxing_writer_widget.dart'; |
||||||
|
import 'package:path_provider/path_provider.dart'; |
||||||
|
import 'package:share_plus/share_plus.dart'; |
||||||
|
|
||||||
|
late Directory tempDir; |
||||||
|
String get tempPath => '${tempDir.path}/zxing.jpg'; |
||||||
|
|
||||||
|
class ZxingPage extends StatefulWidget { |
||||||
|
const ZxingPage({ |
||||||
|
Key? key, |
||||||
|
}) : super(key: key); |
||||||
|
|
||||||
|
@override |
||||||
|
State<ZxingPage> createState() => _ZxingPageState(); |
||||||
|
} |
||||||
|
|
||||||
|
class _ZxingPageState extends State<ZxingPage> with TickerProviderStateMixin { |
||||||
|
CameraController? controller; |
||||||
|
TabController? _tabController; |
||||||
|
|
||||||
|
bool isAndroid() => Theme.of(context).platform == TargetPlatform.android; |
||||||
|
|
||||||
|
// Scan result queue |
||||||
|
final _resultQueue = <CodeResult>[]; |
||||||
|
|
||||||
|
// Write result |
||||||
|
Uint8List? writeResult; |
||||||
|
|
||||||
|
// true when the camera is active |
||||||
|
bool _isScanning = true; |
||||||
|
|
||||||
|
@override |
||||||
|
void initState() { |
||||||
|
super.initState(); |
||||||
|
|
||||||
|
initStateAsync(); |
||||||
|
} |
||||||
|
|
||||||
|
void initStateAsync() async { |
||||||
|
_tabController = TabController(length: 3, vsync: this); |
||||||
|
_tabController?.addListener(() { |
||||||
|
_isScanning = _tabController?.index == 0; |
||||||
|
if (_isScanning) { |
||||||
|
controller?.resumePreview(); |
||||||
|
} else { |
||||||
|
controller?.pausePreview(); |
||||||
|
} |
||||||
|
}); |
||||||
|
getTemporaryDirectory().then((value) { |
||||||
|
tempDir = value; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
void dispose() { |
||||||
|
super.dispose(); |
||||||
|
controller?.dispose(); |
||||||
|
} |
||||||
|
|
||||||
|
void showInSnackBar(String message) {} |
||||||
|
|
||||||
|
void logError(String code, String? message) { |
||||||
|
if (message != null) { |
||||||
|
debugPrint('Error: $code\nError Message: $message'); |
||||||
|
} else { |
||||||
|
debugPrint('Error: $code'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
return Scaffold( |
||||||
|
appBar: AppBar( |
||||||
|
title: const Text('Flutter Scanner'), |
||||||
|
bottom: TabBar( |
||||||
|
controller: _tabController, |
||||||
|
tabs: const [ |
||||||
|
Tab(text: 'Scanner'), |
||||||
|
Tab(text: 'Result'), |
||||||
|
Tab(text: 'Writer'), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
body: TabBarView( |
||||||
|
controller: _tabController, |
||||||
|
children: [ |
||||||
|
// Scanner |
||||||
|
ZxingReaderWidget(onScan: (result) async { |
||||||
|
_resultQueue.insert(0, result); |
||||||
|
_tabController?.index = 1; |
||||||
|
await Future.delayed(const Duration(milliseconds: 500)); |
||||||
|
setState(() {}); |
||||||
|
}), |
||||||
|
// Result |
||||||
|
_buildResultList(), |
||||||
|
// Writer |
||||||
|
SingleChildScrollView( |
||||||
|
child: Column( |
||||||
|
children: [ |
||||||
|
ZxingWriterWidget( |
||||||
|
onSuccess: (result) { |
||||||
|
setState(() { |
||||||
|
writeResult = result; |
||||||
|
}); |
||||||
|
}, |
||||||
|
onError: (error) { |
||||||
|
setState(() { |
||||||
|
writeResult = null; |
||||||
|
}); |
||||||
|
ScaffoldMessenger.of(context).hideCurrentSnackBar(); |
||||||
|
ScaffoldMessenger.of(context).showSnackBar( |
||||||
|
SnackBar( |
||||||
|
content: Text( |
||||||
|
error, |
||||||
|
textAlign: TextAlign.center, |
||||||
|
), |
||||||
|
), |
||||||
|
); |
||||||
|
}, |
||||||
|
), |
||||||
|
if (writeResult != null) buildWriteResult(), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
Column buildWriteResult() { |
||||||
|
return Column( |
||||||
|
children: [ |
||||||
|
// Barcode image |
||||||
|
Image.memory(writeResult ?? Uint8List(0)), |
||||||
|
// Share button |
||||||
|
ElevatedButton( |
||||||
|
onPressed: () { |
||||||
|
// Save image to device |
||||||
|
final file = File(tempPath); |
||||||
|
file.writeAsBytesSync(writeResult ?? Uint8List(0)); |
||||||
|
final path = file.path; |
||||||
|
// Share image |
||||||
|
Share.shareFiles([path]); |
||||||
|
}, |
||||||
|
child: const Text('Share'), |
||||||
|
), |
||||||
|
], |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
_buildResultList() { |
||||||
|
return _resultQueue.isEmpty |
||||||
|
? const Center( |
||||||
|
child: Text( |
||||||
|
'No Results', |
||||||
|
style: TextStyle(fontSize: 24), |
||||||
|
)) |
||||||
|
: ListView.builder( |
||||||
|
itemCount: _resultQueue.length, |
||||||
|
itemBuilder: (context, index) { |
||||||
|
final result = _resultQueue[index]; |
||||||
|
return ListTile( |
||||||
|
title: Text(result.textString), |
||||||
|
subtitle: Text(result.formatString), |
||||||
|
trailing: ButtonBar( |
||||||
|
mainAxisSize: MainAxisSize.min, |
||||||
|
children: [ |
||||||
|
// Copy button |
||||||
|
TextButton( |
||||||
|
child: const Text('Copy'), |
||||||
|
onPressed: () { |
||||||
|
Clipboard.setData( |
||||||
|
ClipboardData(text: result.textString)); |
||||||
|
}, |
||||||
|
), |
||||||
|
// Remove button |
||||||
|
IconButton( |
||||||
|
icon: const Icon(Icons.delete, color: Colors.red), |
||||||
|
onPressed: () { |
||||||
|
_resultQueue.removeAt(index); |
||||||
|
setState(() {}); |
||||||
|
}, |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
); |
||||||
|
}, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,57 @@ |
|||||||
|
import 'dart:typed_data'; |
||||||
|
|
||||||
|
import 'package:camera/camera.dart'; |
||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:image/image.dart' as imglib; |
||||||
|
|
||||||
|
// https://gist.github.com/Alby-o/fe87e35bc21d534c8220aed7df028e03 |
||||||
|
|
||||||
|
Future<Uint8List> convertImage(CameraImage image) async { |
||||||
|
try { |
||||||
|
late imglib.Image img; |
||||||
|
if (image.format.group == ImageFormatGroup.yuv420) { |
||||||
|
// img = convertYUV420(image); |
||||||
|
return image.planes.first.bytes; |
||||||
|
} else if (image.format.group == ImageFormatGroup.bgra8888) { |
||||||
|
img = convertBGRA8888(image); |
||||||
|
} |
||||||
|
return img.getBytes(format: imglib.Format.luminance); |
||||||
|
} catch (e) { |
||||||
|
debugPrint(">>>>>>>>>>>> ERROR:" + e.toString()); |
||||||
|
} |
||||||
|
return Uint8List(0); |
||||||
|
} |
||||||
|
|
||||||
|
imglib.Image convertBGRA8888(CameraImage image) { |
||||||
|
return imglib.Image.fromBytes( |
||||||
|
image.width, |
||||||
|
image.height, |
||||||
|
image.planes[0].bytes, |
||||||
|
format: imglib.Format.bgra, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
// ignore: unused_element |
||||||
|
imglib.Image convertYUV420(CameraImage image) { |
||||||
|
var img = imglib.Image(image.width, image.height); // Create Image buffer |
||||||
|
|
||||||
|
Plane plane = image.planes[0]; |
||||||
|
const int shift = (0xFF << 24); |
||||||
|
|
||||||
|
// Fill image buffer with plane[0] from YUV420_888 |
||||||
|
for (int x = 0; x < image.width; x++) { |
||||||
|
for (int planeOffset = 0; |
||||||
|
planeOffset < image.height * image.width; |
||||||
|
planeOffset += image.width) { |
||||||
|
final pixelColor = plane.bytes[planeOffset + x]; |
||||||
|
// color: 0x FF FF FF FF |
||||||
|
// A B G R |
||||||
|
// Calculate pixel color |
||||||
|
var newVal = shift | (pixelColor << 16) | (pixelColor << 8) | pixelColor; |
||||||
|
|
||||||
|
img.data[planeOffset + x] = newVal; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return img; |
||||||
|
} |
@ -0,0 +1,69 @@ |
|||||||
|
import 'dart:isolate'; |
||||||
|
import 'dart:math'; |
||||||
|
|
||||||
|
import 'package:camera/camera.dart'; |
||||||
|
|
||||||
|
import 'flutter_zxing.dart'; |
||||||
|
import 'image_converter.dart'; |
||||||
|
|
||||||
|
// Inspired from https://github.com/am15h/object_detection_flutter |
||||||
|
|
||||||
|
/// Bundles data to pass between Isolate |
||||||
|
class IsolateData { |
||||||
|
CameraImage cameraImage; |
||||||
|
double cropPercent; |
||||||
|
|
||||||
|
SendPort? responsePort; |
||||||
|
|
||||||
|
IsolateData( |
||||||
|
this.cameraImage, |
||||||
|
this.cropPercent, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/// Manages separate Isolate instance for inference |
||||||
|
class IsolateUtils { |
||||||
|
static const String kDebugName = "ZxingIsolate"; |
||||||
|
|
||||||
|
// ignore: unused_field |
||||||
|
Isolate? _isolate; |
||||||
|
final _receivePort = ReceivePort(); |
||||||
|
SendPort? _sendPort; |
||||||
|
|
||||||
|
SendPort? get sendPort => _sendPort; |
||||||
|
|
||||||
|
Future<void> start() async { |
||||||
|
_isolate = await Isolate.spawn<SendPort>( |
||||||
|
entryPoint, |
||||||
|
_receivePort.sendPort, |
||||||
|
debugName: kDebugName, |
||||||
|
); |
||||||
|
|
||||||
|
_sendPort = await _receivePort.first; |
||||||
|
} |
||||||
|
|
||||||
|
void stop() { |
||||||
|
_isolate?.kill(priority: Isolate.immediate); |
||||||
|
_isolate = null; |
||||||
|
_sendPort = null; |
||||||
|
} |
||||||
|
|
||||||
|
static void entryPoint(SendPort sendPort) async { |
||||||
|
final port = ReceivePort(); |
||||||
|
sendPort.send(port.sendPort); |
||||||
|
|
||||||
|
await for (final IsolateData? isolateData in port) { |
||||||
|
if (isolateData != null) { |
||||||
|
final image = isolateData.cameraImage; |
||||||
|
final cropPercent = isolateData.cropPercent; |
||||||
|
final bytes = await convertImage(image); |
||||||
|
final cropSize = (min(image.width, image.height) * cropPercent).round(); |
||||||
|
|
||||||
|
final result = |
||||||
|
FlutterZxing.zxingRead(bytes, image.width, image.height, cropSize); |
||||||
|
|
||||||
|
isolateData.responsePort?.send(result); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,158 @@ |
|||||||
|
import 'package:flutter/material.dart'; |
||||||
|
|
||||||
|
class ScannerOverlay extends ShapeBorder { |
||||||
|
const ScannerOverlay({ |
||||||
|
this.borderColor = Colors.red, |
||||||
|
this.borderWidth = 3.0, |
||||||
|
this.overlayColor = const Color.fromRGBO(0, 0, 0, 40), |
||||||
|
this.borderRadius = 0, |
||||||
|
this.borderLength = 40, |
||||||
|
this.cutOutSize = 250, |
||||||
|
}) : assert(borderLength <= cutOutSize / 2 + borderWidth * 2, |
||||||
|
"Border can't be larger than ${cutOutSize / 2 + borderWidth * 2}"); |
||||||
|
|
||||||
|
final Color borderColor; |
||||||
|
final double borderWidth; |
||||||
|
final Color overlayColor; |
||||||
|
final double borderRadius; |
||||||
|
final double borderLength; |
||||||
|
final double cutOutSize; |
||||||
|
|
||||||
|
@override |
||||||
|
EdgeInsetsGeometry get dimensions => const EdgeInsets.all(10); |
||||||
|
|
||||||
|
@override |
||||||
|
Path getInnerPath(Rect rect, {TextDirection? textDirection}) { |
||||||
|
return Path() |
||||||
|
..fillType = PathFillType.evenOdd |
||||||
|
..addPath(getOuterPath(rect), Offset.zero); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
Path getOuterPath(Rect rect, {TextDirection? textDirection}) { |
||||||
|
Path _getLeftTopPath(Rect rect) { |
||||||
|
return Path() |
||||||
|
..moveTo(rect.left, rect.bottom) |
||||||
|
..lineTo(rect.left, rect.top) |
||||||
|
..lineTo(rect.right, rect.top); |
||||||
|
} |
||||||
|
|
||||||
|
return _getLeftTopPath(rect) |
||||||
|
..lineTo( |
||||||
|
rect.right, |
||||||
|
rect.bottom, |
||||||
|
) |
||||||
|
..lineTo( |
||||||
|
rect.left, |
||||||
|
rect.bottom, |
||||||
|
) |
||||||
|
..lineTo( |
||||||
|
rect.left, |
||||||
|
rect.top, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) { |
||||||
|
final width = rect.width; |
||||||
|
final borderWidthSize = width / 2; |
||||||
|
final height = rect.height; |
||||||
|
final borderOffset = borderWidth / 2; |
||||||
|
final _borderLength = borderLength > cutOutSize / 2 + borderWidth * 2 |
||||||
|
? borderWidthSize / 2 |
||||||
|
: borderLength; |
||||||
|
final _cutOutSize = cutOutSize < width ? cutOutSize : width - borderOffset; |
||||||
|
|
||||||
|
final backgroundPaint = Paint() |
||||||
|
..color = overlayColor |
||||||
|
..style = PaintingStyle.fill; |
||||||
|
|
||||||
|
final borderPaint = Paint() |
||||||
|
..color = borderColor |
||||||
|
..style = PaintingStyle.stroke |
||||||
|
..strokeWidth = borderWidth; |
||||||
|
|
||||||
|
final boxPaint = Paint() |
||||||
|
..color = borderColor |
||||||
|
..style = PaintingStyle.fill |
||||||
|
..blendMode = BlendMode.dstOut; |
||||||
|
|
||||||
|
final cutOutRect = Rect.fromLTWH( |
||||||
|
rect.left + width / 2 - _cutOutSize / 2 + borderOffset, |
||||||
|
rect.top + height / 2 - _cutOutSize / 2 + borderOffset, |
||||||
|
_cutOutSize - borderOffset * 2, |
||||||
|
_cutOutSize - borderOffset * 2, |
||||||
|
); |
||||||
|
|
||||||
|
canvas |
||||||
|
..saveLayer( |
||||||
|
rect, |
||||||
|
backgroundPaint, |
||||||
|
) |
||||||
|
..drawRect( |
||||||
|
rect, |
||||||
|
backgroundPaint, |
||||||
|
) |
||||||
|
// Draw top right corner |
||||||
|
..drawRRect( |
||||||
|
RRect.fromLTRBAndCorners( |
||||||
|
cutOutRect.right - _borderLength, |
||||||
|
cutOutRect.top, |
||||||
|
cutOutRect.right, |
||||||
|
cutOutRect.top + _borderLength, |
||||||
|
topRight: Radius.circular(borderRadius), |
||||||
|
), |
||||||
|
borderPaint, |
||||||
|
) |
||||||
|
// Draw top left corner |
||||||
|
..drawRRect( |
||||||
|
RRect.fromLTRBAndCorners( |
||||||
|
cutOutRect.left, |
||||||
|
cutOutRect.top, |
||||||
|
cutOutRect.left + _borderLength, |
||||||
|
cutOutRect.top + _borderLength, |
||||||
|
topLeft: Radius.circular(borderRadius), |
||||||
|
), |
||||||
|
borderPaint, |
||||||
|
) |
||||||
|
// Draw bottom right corner |
||||||
|
..drawRRect( |
||||||
|
RRect.fromLTRBAndCorners( |
||||||
|
cutOutRect.right - _borderLength, |
||||||
|
cutOutRect.bottom - _borderLength, |
||||||
|
cutOutRect.right, |
||||||
|
cutOutRect.bottom, |
||||||
|
bottomRight: Radius.circular(borderRadius), |
||||||
|
), |
||||||
|
borderPaint, |
||||||
|
) |
||||||
|
// Draw bottom left corner |
||||||
|
..drawRRect( |
||||||
|
RRect.fromLTRBAndCorners( |
||||||
|
cutOutRect.left, |
||||||
|
cutOutRect.bottom - _borderLength, |
||||||
|
cutOutRect.left + _borderLength, |
||||||
|
cutOutRect.bottom, |
||||||
|
bottomLeft: Radius.circular(borderRadius), |
||||||
|
), |
||||||
|
borderPaint, |
||||||
|
) |
||||||
|
..drawRRect( |
||||||
|
RRect.fromRectAndRadius( |
||||||
|
cutOutRect, |
||||||
|
Radius.circular(borderRadius), |
||||||
|
), |
||||||
|
boxPaint, |
||||||
|
) |
||||||
|
..restore(); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
ShapeBorder scale(double t) { |
||||||
|
return ScannerOverlay( |
||||||
|
borderColor: borderColor, |
||||||
|
borderWidth: borderWidth, |
||||||
|
overlayColor: overlayColor, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,238 @@ |
|||||||
|
import 'dart:async'; |
||||||
|
import 'dart:io'; |
||||||
|
import 'dart:isolate'; |
||||||
|
import 'dart:math'; |
||||||
|
|
||||||
|
import 'package:camera/camera.dart'; |
||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:flutter/services.dart'; |
||||||
|
import 'package:flutter_beep/flutter_beep.dart'; |
||||||
|
import 'flutter_zxing.dart'; |
||||||
|
import 'generated_bindings.dart'; |
||||||
|
|
||||||
|
import 'isolate_utils.dart'; |
||||||
|
import 'scanner_overlay.dart'; |
||||||
|
|
||||||
|
class ZxingReaderWidget extends StatefulWidget { |
||||||
|
const ZxingReaderWidget({ |
||||||
|
Key? key, |
||||||
|
required this.onScan, |
||||||
|
this.onControllerCreated, |
||||||
|
this.beep = true, |
||||||
|
this.showCroppingRect = true, |
||||||
|
this.scanDelay = const Duration(milliseconds: 500), // 500ms delay |
||||||
|
this.cropPercent = 0.5, // 50% of the screen |
||||||
|
this.resolution = ResolutionPreset.high, |
||||||
|
}) : super(key: key); |
||||||
|
|
||||||
|
final Function(CodeResult) onScan; |
||||||
|
final Function(CameraController?)? onControllerCreated; |
||||||
|
final bool beep; |
||||||
|
final bool showCroppingRect; |
||||||
|
final Duration scanDelay; |
||||||
|
final double cropPercent; |
||||||
|
final ResolutionPreset resolution; |
||||||
|
|
||||||
|
@override |
||||||
|
State<ZxingReaderWidget> createState() => _ZxingReaderWidgetState(); |
||||||
|
} |
||||||
|
|
||||||
|
class _ZxingReaderWidgetState extends State<ZxingReaderWidget> |
||||||
|
with TickerProviderStateMixin { |
||||||
|
late List<CameraDescription> cameras; |
||||||
|
CameraController? controller; |
||||||
|
|
||||||
|
bool isAndroid() => Theme.of(context).platform == TargetPlatform.android; |
||||||
|
|
||||||
|
// true when code detecting is ongoing |
||||||
|
bool _isProcessing = false; |
||||||
|
|
||||||
|
/// Instance of [IsolateUtils] |
||||||
|
IsolateUtils? isolateUtils; |
||||||
|
|
||||||
|
@override |
||||||
|
void initState() { |
||||||
|
super.initState(); |
||||||
|
|
||||||
|
initStateAsync(); |
||||||
|
} |
||||||
|
|
||||||
|
void initStateAsync() async { |
||||||
|
// Spawn a new isolate |
||||||
|
isolateUtils = IsolateUtils(); |
||||||
|
await isolateUtils?.start(); |
||||||
|
|
||||||
|
availableCameras().then((cameras) { |
||||||
|
setState(() { |
||||||
|
this.cameras = cameras; |
||||||
|
onNewCameraSelected(cameras.first); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
SystemChannels.lifecycle.setMessageHandler((message) async { |
||||||
|
debugPrint(message); |
||||||
|
final CameraController? cameraController = controller; |
||||||
|
if (cameraController == null || !cameraController.value.isInitialized) { |
||||||
|
return; |
||||||
|
} |
||||||
|
if (mounted) { |
||||||
|
if (message == AppLifecycleState.paused.toString()) { |
||||||
|
cameraController.dispose(); |
||||||
|
} |
||||||
|
if (message == AppLifecycleState.resumed.toString()) { |
||||||
|
onNewCameraSelected(cameraController.description); |
||||||
|
} |
||||||
|
} |
||||||
|
return null; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
void dispose() { |
||||||
|
controller?.dispose(); |
||||||
|
isolateUtils?.stop(); |
||||||
|
super.dispose(); |
||||||
|
} |
||||||
|
|
||||||
|
void onNewCameraSelected(CameraDescription cameraDescription) async { |
||||||
|
if (controller != null) { |
||||||
|
await controller!.dispose(); |
||||||
|
} |
||||||
|
|
||||||
|
controller = CameraController( |
||||||
|
cameraDescription, |
||||||
|
widget.resolution, |
||||||
|
enableAudio: false, |
||||||
|
imageFormatGroup: |
||||||
|
isAndroid() ? ImageFormatGroup.yuv420 : ImageFormatGroup.bgra8888, |
||||||
|
); |
||||||
|
|
||||||
|
try { |
||||||
|
await controller?.initialize(); |
||||||
|
controller?.startImageStream(processCameraImage); |
||||||
|
} on CameraException catch (e) { |
||||||
|
_showCameraException(e); |
||||||
|
} |
||||||
|
|
||||||
|
controller?.addListener(() { |
||||||
|
if (mounted) setState(() {}); |
||||||
|
}); |
||||||
|
|
||||||
|
if (mounted) { |
||||||
|
setState(() {}); |
||||||
|
} |
||||||
|
|
||||||
|
widget.onControllerCreated?.call(controller); |
||||||
|
} |
||||||
|
|
||||||
|
void _showCameraException(CameraException e) { |
||||||
|
logError(e.code, e.description); |
||||||
|
showInSnackBar('Error: ${e.code}\n${e.description}'); |
||||||
|
} |
||||||
|
|
||||||
|
void showInSnackBar(String message) {} |
||||||
|
|
||||||
|
void logError(String code, String? message) { |
||||||
|
if (message != null) { |
||||||
|
debugPrint('Error: $code\nError Message: $message'); |
||||||
|
} else { |
||||||
|
debugPrint('Error: $code'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
processCameraImage(CameraImage image) async { |
||||||
|
if (!_isProcessing) { |
||||||
|
_isProcessing = true; |
||||||
|
try { |
||||||
|
var isolateData = IsolateData(image, widget.cropPercent); |
||||||
|
|
||||||
|
/// perform inference in separate isolate |
||||||
|
CodeResult result = await inference(isolateData); |
||||||
|
if (result.isValidBool) { |
||||||
|
if (widget.beep) { |
||||||
|
FlutterBeep.beep(); |
||||||
|
} |
||||||
|
widget.onScan(result); |
||||||
|
setState(() {}); |
||||||
|
await Future.delayed(const Duration(seconds: 1)); |
||||||
|
} |
||||||
|
} on FileSystemException catch (e) { |
||||||
|
debugPrint(e.message); |
||||||
|
} catch (e) { |
||||||
|
debugPrint(e.toString()); |
||||||
|
} |
||||||
|
await Future.delayed(widget.scanDelay); |
||||||
|
_isProcessing = false; |
||||||
|
} |
||||||
|
|
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
/// Runs inference in another isolate |
||||||
|
Future<CodeResult> inference(IsolateData isolateData) async { |
||||||
|
ReceivePort responsePort = ReceivePort(); |
||||||
|
isolateUtils?.sendPort |
||||||
|
?.send(isolateData..responsePort = responsePort.sendPort); |
||||||
|
var results = await responsePort.first; |
||||||
|
return results; |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
final size = MediaQuery.of(context).size; |
||||||
|
final cropSize = min(size.width, size.height) * widget.cropPercent; |
||||||
|
return Stack( |
||||||
|
children: [ |
||||||
|
// Camera preview |
||||||
|
Center(child: _cameraPreviewWidget(cropSize)), |
||||||
|
], |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
// Display the preview from the camera. |
||||||
|
Widget _cameraPreviewWidget(double cropSize) { |
||||||
|
final CameraController? cameraController = controller; |
||||||
|
if (cameraController == null || !cameraController.value.isInitialized) { |
||||||
|
return const CircularProgressIndicator(); |
||||||
|
} else { |
||||||
|
final size = MediaQuery.of(context).size; |
||||||
|
var cameraMaxSize = max(size.width, size.height); |
||||||
|
return Stack( |
||||||
|
children: [ |
||||||
|
SizedBox( |
||||||
|
width: cameraMaxSize, |
||||||
|
height: cameraMaxSize, |
||||||
|
child: ClipRRect( |
||||||
|
child: OverflowBox( |
||||||
|
alignment: Alignment.center, |
||||||
|
child: FittedBox( |
||||||
|
fit: BoxFit.cover, |
||||||
|
child: SizedBox( |
||||||
|
width: cameraMaxSize, |
||||||
|
child: CameraPreview( |
||||||
|
cameraController, |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
widget.showCroppingRect |
||||||
|
? Container( |
||||||
|
decoration: ShapeDecoration( |
||||||
|
shape: ScannerOverlay( |
||||||
|
borderColor: Theme.of(context).primaryColor, |
||||||
|
overlayColor: const Color.fromRGBO(0, 0, 0, 0.5), |
||||||
|
borderRadius: 1, |
||||||
|
borderLength: 16, |
||||||
|
borderWidth: 8, |
||||||
|
cutOutSize: cropSize, |
||||||
|
), |
||||||
|
), |
||||||
|
) |
||||||
|
: Container() |
||||||
|
], |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,119 @@ |
|||||||
|
import 'dart:typed_data'; |
||||||
|
|
||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:image/image.dart' as imglib; |
||||||
|
|
||||||
|
import 'flutter_zxing.dart'; |
||||||
|
import 'generated_bindings.dart'; |
||||||
|
|
||||||
|
class ZxingWriterWidget extends StatefulWidget { |
||||||
|
const ZxingWriterWidget({ |
||||||
|
Key? key, |
||||||
|
this.onSuccess, |
||||||
|
this.onError, |
||||||
|
}) : super(key: key); |
||||||
|
|
||||||
|
final Function(Uint8List)? onSuccess; |
||||||
|
final Function(String)? onError; |
||||||
|
|
||||||
|
@override |
||||||
|
State<ZxingWriterWidget> createState() => _ZxingWriterWidgetState(); |
||||||
|
} |
||||||
|
|
||||||
|
class _ZxingWriterWidgetState extends State<ZxingWriterWidget> |
||||||
|
with TickerProviderStateMixin { |
||||||
|
final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); |
||||||
|
final TextEditingController _textController = TextEditingController(); |
||||||
|
|
||||||
|
bool isAndroid() => Theme.of(context).platform == TargetPlatform.android; |
||||||
|
|
||||||
|
final _maxTextLength = 2000; |
||||||
|
final _supportedFormats = CodeFormat.writerFormats; |
||||||
|
var _codeFormat = Format.QRCode; |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
return SingleChildScrollView( |
||||||
|
child: Form( |
||||||
|
key: _formKey, |
||||||
|
child: Padding( |
||||||
|
padding: const EdgeInsets.all(20.0), |
||||||
|
child: Column( |
||||||
|
mainAxisSize: MainAxisSize.min, |
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly, |
||||||
|
children: [ |
||||||
|
const SizedBox(height: 20), |
||||||
|
// Format DropDown button |
||||||
|
DropdownButtonFormField<int>( |
||||||
|
value: _codeFormat, |
||||||
|
items: _supportedFormats |
||||||
|
.map((format) => DropdownMenuItem( |
||||||
|
value: format, |
||||||
|
child: Text(CodeFormat.formatName(format)), |
||||||
|
)) |
||||||
|
.toList(), |
||||||
|
onChanged: (format) { |
||||||
|
setState(() { |
||||||
|
_codeFormat = format ?? Format.QRCode; |
||||||
|
}); |
||||||
|
}, |
||||||
|
), |
||||||
|
const SizedBox(height: 20), |
||||||
|
// Input multiline text |
||||||
|
TextFormField( |
||||||
|
controller: _textController, |
||||||
|
keyboardType: TextInputType.multiline, |
||||||
|
maxLines: null, |
||||||
|
maxLength: _maxTextLength, |
||||||
|
onChanged: (value) { |
||||||
|
setState(() {}); |
||||||
|
}, |
||||||
|
decoration: InputDecoration( |
||||||
|
border: const OutlineInputBorder(), |
||||||
|
filled: true, |
||||||
|
hintText: 'Enter text to encode', |
||||||
|
counterText: |
||||||
|
'${_textController.value.text.length} / $_maxTextLength', |
||||||
|
), |
||||||
|
), |
||||||
|
// Write button |
||||||
|
ElevatedButton( |
||||||
|
onPressed: () { |
||||||
|
if (_formKey.currentState?.validate() ?? false) { |
||||||
|
_formKey.currentState?.save(); |
||||||
|
FocusScope.of(context).unfocus(); |
||||||
|
final text = _textController.value.text; |
||||||
|
const width = 300; |
||||||
|
const height = 300; |
||||||
|
final result = FlutterZxing.zxingEncode( |
||||||
|
text, width, height, _codeFormat, 5, 0); |
||||||
|
String? error; |
||||||
|
if (result.isValidBool) { |
||||||
|
try { |
||||||
|
final img = |
||||||
|
imglib.Image.fromBytes(width, height, result.bytes); |
||||||
|
final resultBytes = |
||||||
|
Uint8List.fromList(imglib.encodeJpg(img)); |
||||||
|
widget.onSuccess?.call(resultBytes); |
||||||
|
} on Exception catch (e) { |
||||||
|
error = e.toString(); |
||||||
|
} |
||||||
|
} else { |
||||||
|
error = result.errorMessage; |
||||||
|
} |
||||||
|
if (error != null) { |
||||||
|
debugPrint(error); |
||||||
|
widget.onError?.call(error); |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
child: const Text('Encode'), |
||||||
|
), |
||||||
|
const SizedBox(height: 20), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue