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