-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
336 additions
and
101 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
export 'cubit/qr_code_scan_cubit.dart'; | ||
export 'model/siopv2_param.dart'; | ||
export 'view/qr_camera_view.dart'; | ||
export 'view/qr_code_scan_page.dart'; | ||
export 'view/qr_scanner_page.dart'; |
242 changes: 242 additions & 0 deletions
242
lib/dashboard/qr_code/qr_code_scan/view/qr_camera_view.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,242 @@ | ||
import 'dart:io'; | ||
|
||
import 'package:camera/camera.dart'; | ||
import 'package:flutter/material.dart'; | ||
import 'package:flutter/services.dart'; | ||
import 'package:google_mlkit_commons/google_mlkit_commons.dart'; | ||
|
||
class QrCameraView extends StatefulWidget { | ||
const QrCameraView( | ||
{super.key, | ||
required this.onImage, | ||
this.onCameraFeedReady, | ||
this.onDetectorViewModeChanged, | ||
this.onCameraLensDirectionChanged, | ||
this.initialCameraLensDirection = CameraLensDirection.back}); | ||
|
||
final Function(InputImage inputImage) onImage; | ||
final VoidCallback? onCameraFeedReady; | ||
final VoidCallback? onDetectorViewModeChanged; | ||
final Function(CameraLensDirection direction)? onCameraLensDirectionChanged; | ||
final CameraLensDirection initialCameraLensDirection; | ||
|
||
@override | ||
State<QrCameraView> createState() => _QrCameraViewState(); | ||
} | ||
|
||
class _QrCameraViewState extends State<QrCameraView> { | ||
static List<CameraDescription> _cameras = []; | ||
CameraController? _controller; | ||
int _cameraIndex = -1; | ||
double _currentZoomLevel = 1.0; | ||
double _minAvailableZoom = 1.0; | ||
double _maxAvailableZoom = 1.0; | ||
double _minAvailableExposureOffset = 0.0; | ||
double _maxAvailableExposureOffset = 0.0; | ||
double _currentExposureOffset = 0.0; | ||
bool _changingCameraLens = false; | ||
|
||
@override | ||
void initState() { | ||
super.initState(); | ||
|
||
_initialize(); | ||
} | ||
|
||
void _initialize() async { | ||
if (_cameras.isEmpty) { | ||
_cameras = await availableCameras(); | ||
} | ||
for (var i = 0; i < _cameras.length; i++) { | ||
if (_cameras[i].lensDirection == widget.initialCameraLensDirection) { | ||
_cameraIndex = i; | ||
break; | ||
} | ||
} | ||
if (_cameraIndex != -1) { | ||
_startLiveFeed(); | ||
} | ||
} | ||
|
||
@override | ||
void dispose() { | ||
_stopLiveFeed(); | ||
super.dispose(); | ||
} | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return Scaffold(body: _liveFeedBody()); | ||
} | ||
|
||
Widget _liveFeedBody() { | ||
if (_cameras.isEmpty) return Container(); | ||
if (_controller == null) return Container(); | ||
if (_controller?.value.isInitialized == false) return Container(); | ||
return ColoredBox( | ||
color: Colors.black, | ||
child: Stack( | ||
fit: StackFit.expand, | ||
children: <Widget>[ | ||
Center( | ||
child: _changingCameraLens | ||
? Container() | ||
: CameraPreview( | ||
_controller!, | ||
child: null, | ||
), | ||
) | ||
], | ||
), | ||
); | ||
} | ||
|
||
Future<void> _startLiveFeed() async { | ||
final camera = _cameras[_cameraIndex]; | ||
_controller = CameraController( | ||
camera, | ||
// Set to ResolutionPreset.high. Do NOT set it to ResolutionPreset.max because for some phones does NOT work. | ||
ResolutionPreset.high, | ||
enableAudio: false, | ||
imageFormatGroup: Platform.isAndroid | ||
? ImageFormatGroup.nv21 | ||
: ImageFormatGroup.bgra8888, | ||
); | ||
await _controller?.initialize().then((_) { | ||
if (!mounted) { | ||
return; | ||
} | ||
_controller?.getMinZoomLevel().then((value) { | ||
_currentZoomLevel = value; | ||
_minAvailableZoom = value; | ||
}); | ||
_controller?.getMaxZoomLevel().then((value) { | ||
_maxAvailableZoom = value; | ||
}); | ||
_currentExposureOffset = 0.0; | ||
_controller?.getMinExposureOffset().then((value) { | ||
_minAvailableExposureOffset = value; | ||
}); | ||
_controller?.getMaxExposureOffset().then((value) { | ||
_maxAvailableExposureOffset = value; | ||
}); | ||
_controller?.startImageStream(_processCameraImage).then((value) { | ||
if (widget.onCameraFeedReady != null) { | ||
widget.onCameraFeedReady!(); | ||
} | ||
if (widget.onCameraLensDirectionChanged != null) { | ||
widget.onCameraLensDirectionChanged!(camera.lensDirection); | ||
} | ||
}); | ||
setState(() {}); | ||
}); | ||
} | ||
|
||
Future<void> _stopLiveFeed() async { | ||
await _controller?.stopImageStream(); | ||
await _controller?.dispose(); | ||
_controller = null; | ||
} | ||
|
||
void _processCameraImage(CameraImage image) { | ||
final inputImage = _inputImageFromCameraImage(image); | ||
if (inputImage == null) return; | ||
widget.onImage(inputImage); | ||
} | ||
|
||
final _orientations = { | ||
DeviceOrientation.portraitUp: 0, | ||
DeviceOrientation.landscapeLeft: 90, | ||
DeviceOrientation.portraitDown: 180, | ||
DeviceOrientation.landscapeRight: 270, | ||
}; | ||
|
||
InputImage? _inputImageFromCameraImage(CameraImage image) { | ||
if (_controller == null) return null; | ||
|
||
// get image rotation | ||
// it is used in android to convert the InputImage from Dart to Java: https://github.com/flutter-ml/google_ml_kit_flutter/blob/master/packages/google_mlkit_commons/android/src/main/java/com/google_mlkit_commons/InputImageConverter.java | ||
// `rotation` is not used in iOS to convert the InputImage from Dart to Obj-C: https://github.com/flutter-ml/google_ml_kit_flutter/blob/master/packages/google_mlkit_commons/ios/Classes/MLKVisionImage%2BFlutterPlugin.m | ||
// in both platforms `rotation` and `camera.lensDirection` can be used to compensate `x` and `y` coordinates on a canvas: https://github.com/flutter-ml/google_ml_kit_flutter/blob/master/packages/example/lib/vision_detector_views/painters/coordinates_translator.dart | ||
final camera = _cameras[_cameraIndex]; | ||
final sensorOrientation = camera.sensorOrientation; | ||
// print( | ||
// 'lensDirection: ${camera.lensDirection}, sensorOrientation: $sensorOrientation, ${_controller?.value.deviceOrientation} ${_controller?.value.lockedCaptureOrientation} ${_controller?.value.isCaptureOrientationLocked}'); | ||
InputImageRotation? rotation; | ||
if (Platform.isIOS) { | ||
rotation = InputImageRotationValue.fromRawValue(sensorOrientation); | ||
} else if (Platform.isAndroid) { | ||
var rotationCompensation = | ||
_orientations[_controller!.value.deviceOrientation]; | ||
if (rotationCompensation == null) return null; | ||
if (camera.lensDirection == CameraLensDirection.front) { | ||
// front-facing | ||
rotationCompensation = (sensorOrientation + rotationCompensation) % 360; | ||
} else { | ||
// back-facing | ||
rotationCompensation = | ||
(sensorOrientation - rotationCompensation + 360) % 360; | ||
} | ||
rotation = InputImageRotationValue.fromRawValue(rotationCompensation); | ||
// print('rotationCompensation: $rotationCompensation'); | ||
} | ||
if (rotation == null) return null; | ||
// print('final rotation: $rotation'); | ||
|
||
// get image format | ||
final format = InputImageFormatValue.fromRawValue(image.format.raw as int); | ||
// validate format depending on platform | ||
// only supported formats: | ||
// * nv21 for Android | ||
// * bgra8888 for iOS | ||
if (format == null || | ||
(Platform.isAndroid && format != InputImageFormat.nv21) || | ||
(Platform.isIOS && format != InputImageFormat.bgra8888)) return null; | ||
|
||
// since format is constraint to nv21 or bgra8888, both only have one plane | ||
if (image.planes.length != 1) return null; | ||
final plane = image.planes.first; | ||
|
||
// compose InputImage using bytes | ||
return InputImage.fromBytes( | ||
bytes: plane.bytes, | ||
metadata: InputImageMetadata( | ||
size: Size(image.width.toDouble(), image.height.toDouble()), | ||
rotation: rotation, // used only in Android | ||
format: format, // used only in iOS | ||
bytesPerRow: plane.bytesPerRow, // used only in iOS | ||
), | ||
); | ||
} | ||
} | ||
|
||
class SquarePainter extends CustomPainter { | ||
@override | ||
void paint(Canvas canvas, Size size) { | ||
final double squareLength = 100; | ||
final double borderWidth = 3; | ||
final double borderRadius = 5; | ||
|
||
final Offset center = size.center(Offset.zero); | ||
final Rect squareRect = Rect.fromCenter( | ||
center: center, | ||
width: squareLength, | ||
height: squareLength, | ||
); | ||
|
||
final Paint borderPaint = Paint() | ||
..color = Colors.black | ||
..style = PaintingStyle.stroke | ||
..strokeWidth = borderWidth; | ||
|
||
final RRect squareRRect = | ||
RRect.fromRectAndRadius(squareRect, Radius.circular(borderRadius)); | ||
|
||
canvas.drawRRect(squareRRect, borderPaint); | ||
} | ||
|
||
@override | ||
bool shouldRepaint(covariant CustomPainter oldDelegate) { | ||
return false; | ||
} | ||
} |
Oops, something went wrong.