|
|
@ -4,13 +4,12 @@ import 'dart:math'; |
|
|
|
|
|
|
|
|
|
|
|
import 'package:camera/camera.dart'; |
|
|
|
import 'package:camera/camera.dart'; |
|
|
|
import 'package:flutter/material.dart'; |
|
|
|
import 'package:flutter/material.dart'; |
|
|
|
import 'package:flutter/services.dart'; |
|
|
|
|
|
|
|
// import 'package:flutter_beep/flutter_beep.dart'; |
|
|
|
// import 'package:flutter_beep/flutter_beep.dart'; |
|
|
|
|
|
|
|
|
|
|
|
import '../../generated_bindings.dart'; |
|
|
|
import '../../generated_bindings.dart'; |
|
|
|
import '../logic/zxing.dart'; |
|
|
|
import '../logic/zxing.dart'; |
|
|
|
import '../utils/extentions.dart'; |
|
|
|
import '../utils/extentions.dart'; |
|
|
|
import '../utils/isolate_utils.dart'; |
|
|
|
|
|
|
|
import 'scanner_overlay.dart'; |
|
|
|
import 'scanner_overlay.dart'; |
|
|
|
|
|
|
|
|
|
|
|
class ReaderWidget extends StatefulWidget { |
|
|
|
class ReaderWidget extends StatefulWidget { |
|
|
@ -24,8 +23,10 @@ class ReaderWidget extends StatefulWidget { |
|
|
|
this.showFlashlight = true, |
|
|
|
this.showFlashlight = true, |
|
|
|
this.allowPinchZoom = true, |
|
|
|
this.allowPinchZoom = true, |
|
|
|
this.scanDelay = const Duration(milliseconds: 1000), // 1000ms delay |
|
|
|
this.scanDelay = const Duration(milliseconds: 1000), // 1000ms delay |
|
|
|
|
|
|
|
this.scanDelaySuccess = const Duration(milliseconds: 1000), // 1000ms delay |
|
|
|
this.cropPercent = 0.5, // 50% of the screen |
|
|
|
this.cropPercent = 0.5, // 50% of the screen |
|
|
|
this.resolution = ResolutionPreset.high, |
|
|
|
this.resolution = ResolutionPreset.high, |
|
|
|
|
|
|
|
this.loading = const DecoratedBox(decoration: BoxDecoration(color: Colors.black)), |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
final Function(CodeResult) onScan; |
|
|
|
final Function(CodeResult) onScan; |
|
|
@ -38,14 +39,15 @@ class ReaderWidget extends StatefulWidget { |
|
|
|
final Duration scanDelay; |
|
|
|
final Duration scanDelay; |
|
|
|
final double cropPercent; |
|
|
|
final double cropPercent; |
|
|
|
final ResolutionPreset resolution; |
|
|
|
final ResolutionPreset resolution; |
|
|
|
|
|
|
|
final Duration scanDelaySuccess; |
|
|
|
|
|
|
|
final Widget loading; |
|
|
|
|
|
|
|
|
|
|
|
@override |
|
|
|
@override |
|
|
|
State<ReaderWidget> createState() => _ReaderWidgetState(); |
|
|
|
State<ReaderWidget> createState() => _ReaderWidgetState(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
class _ReaderWidgetState extends State<ReaderWidget> |
|
|
|
class _ReaderWidgetState extends State<ReaderWidget> with TickerProviderStateMixin, WidgetsBindingObserver { |
|
|
|
with TickerProviderStateMixin { |
|
|
|
List<CameraDescription> cameras = <CameraDescription>[]; |
|
|
|
List<CameraDescription>? cameras; |
|
|
|
|
|
|
|
CameraController? controller; |
|
|
|
CameraController? controller; |
|
|
|
bool _cameraOn = false; |
|
|
|
bool _cameraOn = false; |
|
|
|
|
|
|
|
|
|
|
@ -59,13 +61,10 @@ class _ReaderWidgetState extends State<ReaderWidget> |
|
|
|
// true when code detecting is ongoing |
|
|
|
// true when code detecting is ongoing |
|
|
|
bool _isProcessing = false; |
|
|
|
bool _isProcessing = false; |
|
|
|
|
|
|
|
|
|
|
|
/// Instance of [IsolateUtils] |
|
|
|
|
|
|
|
IsolateUtils? isolateUtils; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@override |
|
|
|
@override |
|
|
|
void initState() { |
|
|
|
void initState() { |
|
|
|
super.initState(); |
|
|
|
super.initState(); |
|
|
|
|
|
|
|
WidgetsBinding.instance.addObserver(this); |
|
|
|
initStateAsync(); |
|
|
|
initStateAsync(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -81,38 +80,50 @@ class _ReaderWidgetState extends State<ReaderWidget> |
|
|
|
} |
|
|
|
} |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
SystemChannels.lifecycle.setMessageHandler((String? message) async { |
|
|
|
@override |
|
|
|
debugPrint(message); |
|
|
|
void didChangeAppLifecycleState(AppLifecycleState state) { |
|
|
|
final CameraController? cameraController = controller; |
|
|
|
switch (state) { |
|
|
|
if (cameraController == null || !cameraController.value.isInitialized) { |
|
|
|
case AppLifecycleState.resumed: |
|
|
|
return; |
|
|
|
if (cameras.isNotEmpty && !_cameraOn) { |
|
|
|
|
|
|
|
onNewCameraSelected(cameras.first); |
|
|
|
} |
|
|
|
} |
|
|
|
if (mounted) { |
|
|
|
|
|
|
|
if (message == AppLifecycleState.paused.toString()) { |
|
|
|
break; |
|
|
|
await cameraController.stopImageStream(); |
|
|
|
case AppLifecycleState.inactive: |
|
|
|
await cameraController.dispose(); |
|
|
|
break; |
|
|
|
|
|
|
|
case AppLifecycleState.paused: |
|
|
|
|
|
|
|
controller?.stopImageStream().then((_) => controller?.dispose()); |
|
|
|
|
|
|
|
setState(() { |
|
|
|
_cameraOn = false; |
|
|
|
_cameraOn = false; |
|
|
|
setState(() {}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (message == AppLifecycleState.resumed.toString()) { |
|
|
|
|
|
|
|
_cameraOn = true; |
|
|
|
|
|
|
|
onNewCameraSelected(cameraController.description); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return null; |
|
|
|
|
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
case AppLifecycleState.detached: |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@override |
|
|
|
@override |
|
|
|
void dispose() { |
|
|
|
void dispose() { |
|
|
|
stopCameraProcessing(); |
|
|
|
stopCameraProcessing(); |
|
|
|
|
|
|
|
controller?.removeListener(rebuildOnMount); |
|
|
|
controller?.dispose(); |
|
|
|
controller?.dispose(); |
|
|
|
|
|
|
|
WidgetsBinding.instance.removeObserver(this); |
|
|
|
super.dispose(); |
|
|
|
super.dispose(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void rebuildOnMount() { |
|
|
|
|
|
|
|
if (mounted) { |
|
|
|
|
|
|
|
setState(() { |
|
|
|
|
|
|
|
_cameraOn = true; |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Future<void> onNewCameraSelected(CameraDescription cameraDescription) async { |
|
|
|
Future<void> onNewCameraSelected(CameraDescription cameraDescription) async { |
|
|
|
if (controller != null) { |
|
|
|
if (controller != null) { |
|
|
|
|
|
|
|
controller?.removeListener(rebuildOnMount); |
|
|
|
await controller!.dispose(); |
|
|
|
await controller!.dispose(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -120,35 +131,24 @@ class _ReaderWidgetState extends State<ReaderWidget> |
|
|
|
cameraDescription, |
|
|
|
cameraDescription, |
|
|
|
widget.resolution, |
|
|
|
widget.resolution, |
|
|
|
enableAudio: false, |
|
|
|
enableAudio: false, |
|
|
|
imageFormatGroup: |
|
|
|
imageFormatGroup: isAndroid() ? ImageFormatGroup.yuv420 : ImageFormatGroup.bgra8888, |
|
|
|
isAndroid() ? ImageFormatGroup.yuv420 : ImageFormatGroup.bgra8888, |
|
|
|
|
|
|
|
); |
|
|
|
); |
|
|
|
final CameraController? cameraController = controller; |
|
|
|
if (controller == null) { |
|
|
|
if (cameraController == null) { |
|
|
|
|
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
try { |
|
|
|
try { |
|
|
|
await cameraController.initialize(); |
|
|
|
await controller!.initialize(); |
|
|
|
await cameraController.setFlashMode(FlashMode.off); |
|
|
|
await controller!.setFlashMode(FlashMode.off); |
|
|
|
_maxZoomLevel = await cameraController.getMaxZoomLevel(); |
|
|
|
_maxZoomLevel = await controller!.getMaxZoomLevel(); |
|
|
|
_minZoomLevel = await cameraController.getMinZoomLevel(); |
|
|
|
_minZoomLevel = await controller!.getMinZoomLevel(); |
|
|
|
cameraController.startImageStream(processImageStream); |
|
|
|
controller!.startImageStream(processImageStream); |
|
|
|
} on CameraException catch (e) { |
|
|
|
} on CameraException catch (e) { |
|
|
|
debugPrint('${e.code}: ${e.description}'); |
|
|
|
debugPrint('${e.code}: ${e.description}'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
cameraController.addListener(() { |
|
|
|
controller!.addListener(rebuildOnMount); |
|
|
|
if (mounted) { |
|
|
|
rebuildOnMount(); |
|
|
|
setState(() {}); |
|
|
|
widget.onControllerCreated?.call(controller); |
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (mounted) { |
|
|
|
|
|
|
|
_cameraOn = true; |
|
|
|
|
|
|
|
setState(() {}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
widget.onControllerCreated?.call(cameraController); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Future<void> processImageStream(CameraImage image) async { |
|
|
|
Future<void> processImageStream(CameraImage image) async { |
|
|
@ -163,7 +163,7 @@ class _ReaderWidgetState extends State<ReaderWidget> |
|
|
|
if (result.isValidBool) { |
|
|
|
if (result.isValidBool) { |
|
|
|
widget.onScan(result); |
|
|
|
widget.onScan(result); |
|
|
|
setState(() {}); |
|
|
|
setState(() {}); |
|
|
|
await Future<void>.delayed(const Duration(seconds: 1)); |
|
|
|
await Future<void>.delayed(widget.scanDelaySuccess); |
|
|
|
} |
|
|
|
} |
|
|
|
} on FileSystemException catch (e) { |
|
|
|
} on FileSystemException catch (e) { |
|
|
|
debugPrint(e.message); |
|
|
|
debugPrint(e.message); |
|
|
@ -179,30 +179,13 @@ class _ReaderWidgetState extends State<ReaderWidget> |
|
|
|
|
|
|
|
|
|
|
|
@override |
|
|
|
@override |
|
|
|
Widget build(BuildContext context) { |
|
|
|
Widget build(BuildContext context) { |
|
|
|
final Size size = MediaQuery.of(context).size; |
|
|
|
final bool isCameraReady = cameras.isNotEmpty && _cameraOn && controller != null && controller!.value.isInitialized; |
|
|
|
final double cropSize = min(size.width, size.height) * widget.cropPercent; |
|
|
|
|
|
|
|
return Stack( |
|
|
|
|
|
|
|
children: <Widget>[ |
|
|
|
|
|
|
|
// Camera preview |
|
|
|
|
|
|
|
Center( |
|
|
|
|
|
|
|
child: _cameraPreviewWidget(cropSize), |
|
|
|
|
|
|
|
), |
|
|
|
|
|
|
|
], |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Display the preview from the camera. |
|
|
|
|
|
|
|
Widget _cameraPreviewWidget(double cropSize) { |
|
|
|
|
|
|
|
final CameraController? cameraController = controller; |
|
|
|
|
|
|
|
final bool isCameraReady = cameras != null && |
|
|
|
|
|
|
|
(cameras?.isNotEmpty ?? false) && |
|
|
|
|
|
|
|
_cameraOn && |
|
|
|
|
|
|
|
!(cameraController == null || !cameraController.value.isInitialized); |
|
|
|
|
|
|
|
final Size size = MediaQuery.of(context).size; |
|
|
|
final Size size = MediaQuery.of(context).size; |
|
|
|
final double cameraMaxSize = max(size.width, size.height); |
|
|
|
final double cameraMaxSize = max(size.width, size.height); |
|
|
|
|
|
|
|
final double cropSize = min(size.width, size.height) * widget.cropPercent; |
|
|
|
return Stack( |
|
|
|
return Stack( |
|
|
|
children: <Widget>[ |
|
|
|
children: <Widget>[ |
|
|
|
if (!isCameraReady) Container(color: Colors.black), |
|
|
|
if (!isCameraReady) widget.loading, |
|
|
|
if (isCameraReady) |
|
|
|
if (isCameraReady) |
|
|
|
SizedBox( |
|
|
|
SizedBox( |
|
|
|
width: cameraMaxSize, |
|
|
|
width: cameraMaxSize, |
|
|
@ -214,7 +197,7 @@ class _ReaderWidgetState extends State<ReaderWidget> |
|
|
|
child: SizedBox( |
|
|
|
child: SizedBox( |
|
|
|
width: cameraMaxSize, |
|
|
|
width: cameraMaxSize, |
|
|
|
child: CameraPreview( |
|
|
|
child: CameraPreview( |
|
|
|
cameraController, |
|
|
|
controller!, |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
@ -241,9 +224,8 @@ class _ReaderWidgetState extends State<ReaderWidget> |
|
|
|
_zoom = _scaleFactor; |
|
|
|
_zoom = _scaleFactor; |
|
|
|
}, |
|
|
|
}, |
|
|
|
onScaleUpdate: (ScaleUpdateDetails details) { |
|
|
|
onScaleUpdate: (ScaleUpdateDetails details) { |
|
|
|
_scaleFactor = |
|
|
|
_scaleFactor = (_zoom * details.scale).clamp(_minZoomLevel, _maxZoomLevel); |
|
|
|
(_zoom * details.scale).clamp(_minZoomLevel, _maxZoomLevel); |
|
|
|
controller?.setZoomLevel(_scaleFactor); |
|
|
|
cameraController?.setZoomLevel(_scaleFactor); |
|
|
|
|
|
|
|
}, |
|
|
|
}, |
|
|
|
), |
|
|
|
), |
|
|
|
if (widget.showFlashlight) |
|
|
|
if (widget.showFlashlight) |
|
|
@ -252,36 +234,40 @@ class _ReaderWidgetState extends State<ReaderWidget> |
|
|
|
left: 20, |
|
|
|
left: 20, |
|
|
|
child: FloatingActionButton( |
|
|
|
child: FloatingActionButton( |
|
|
|
onPressed: () { |
|
|
|
onPressed: () { |
|
|
|
if (cameraController != null) { |
|
|
|
if (controller != null) { |
|
|
|
FlashMode mode = cameraController.value.flashMode; |
|
|
|
FlashMode mode = controller!.value.flashMode; |
|
|
|
if (mode == FlashMode.torch) { |
|
|
|
if (mode == FlashMode.torch) { |
|
|
|
mode = FlashMode.off; |
|
|
|
mode = FlashMode.off; |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
mode = FlashMode.torch; |
|
|
|
mode = FlashMode.torch; |
|
|
|
} |
|
|
|
} |
|
|
|
cameraController.setFlashMode(mode); |
|
|
|
controller!.setFlashMode(mode); |
|
|
|
setState(() {}); |
|
|
|
setState(() {}); |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
}, |
|
|
|
backgroundColor: Colors.black26, |
|
|
|
backgroundColor: Colors.black26, |
|
|
|
child: Icon(_flashIcon(cameraController)), |
|
|
|
child: _FlashIcon(flashMode: controller!.value.flashMode)), |
|
|
|
), |
|
|
|
), |
|
|
|
) |
|
|
|
|
|
|
|
], |
|
|
|
], |
|
|
|
); |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
IconData _flashIcon(CameraController? cameraController) { |
|
|
|
class _FlashIcon extends StatelessWidget { |
|
|
|
final FlashMode mode = cameraController?.value.flashMode ?? FlashMode.torch; |
|
|
|
const _FlashIcon({required this.flashMode}); |
|
|
|
switch (mode) { |
|
|
|
final FlashMode flashMode; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
|
|
|
Widget build(BuildContext context) { |
|
|
|
|
|
|
|
switch (flashMode) { |
|
|
|
case FlashMode.torch: |
|
|
|
case FlashMode.torch: |
|
|
|
return Icons.flash_on; |
|
|
|
return const Icon(Icons.flash_on); |
|
|
|
case FlashMode.off: |
|
|
|
case FlashMode.off: |
|
|
|
return Icons.flash_off; |
|
|
|
return const Icon(Icons.flash_off); |
|
|
|
case FlashMode.always: |
|
|
|
case FlashMode.always: |
|
|
|
return Icons.flash_on; |
|
|
|
return const Icon(Icons.flash_on); |
|
|
|
case FlashMode.auto: |
|
|
|
case FlashMode.auto: |
|
|
|
return Icons.flash_auto; |
|
|
|
return const Icon(Icons.flash_auto); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|