Skip to content

Commit

Permalink
feat: add basic ui elements for barcode overlay and scanner overlay, …
Browse files Browse the repository at this point in the history
…update scanWindow via MobileScanner widget
  • Loading branch information
juliansteenbakker committed Oct 29, 2024
1 parent be47302 commit b7d6599
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 176 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## NEXT
- Fixed an issue which caused the scanWindow to always be present if provided, even when scanWindow is updated to null
- Integrated basic barcode overlay and scanner overlay into the package.
- Made updateScanWindow private, because logic within the MobileScanner widget is needed in order to pass a correct scanWindow.
The scanWindow can be updated by directly changing the scanWindow in the MobileScanner widget.

## 7.0.0-beta.3
Fix build issues on macOS

Expand Down
180 changes: 5 additions & 175 deletions example/lib/barcode_scanner_window.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,77 +21,6 @@ class _BarcodeScannerWithScanWindowState
// TODO: Fix BoxFit.fill & BoxFit.fitHeight
final boxFit = BoxFit.contain;

Widget _buildBarcodeOverlay() {
return ValueListenableBuilder(
valueListenable: controller,
builder: (context, value, child) {
// Not ready.
if (!value.isInitialized || !value.isRunning || value.error != null) {
return const SizedBox();
}

return StreamBuilder<BarcodeCapture>(
stream: controller.barcodes,
builder: (context, snapshot) {
final BarcodeCapture? barcodeCapture = snapshot.data;

// No barcode.
if (barcodeCapture == null || barcodeCapture.barcodes.isEmpty) {
return const SizedBox();
}

final overlays = <Widget>[];

for (final scannedBarcode in barcodeCapture.barcodes) {
// No barcode corners, or size, or no camera preview size.
if (value.size.isEmpty ||
scannedBarcode.size.isEmpty ||
scannedBarcode.corners.isEmpty) {
continue;
}

overlays.add(
CustomPaint(
painter: BarcodeOverlay(
barcodeCorners: scannedBarcode.corners,
barcodeSize: scannedBarcode.size,
boxFit: boxFit,
cameraPreviewSize: barcodeCapture.size,
),
),
);
}

return Stack(
fit: StackFit.expand,
children: overlays,
);
},
);
},
);
}

Widget _buildScanWindow(Rect scanWindowRect) {
return ValueListenableBuilder(
valueListenable: controller,
builder: (context, value, child) {
// Not ready.
if (!value.isInitialized ||
!value.isRunning ||
value.error != null ||
value.size.isEmpty) {
return const SizedBox();
}

return CustomPaint(
size: value.size,
painter: ScannerOverlay(scanWindowRect),
);
},
);
}

@override
Widget build(BuildContext context) {
final scanWindow = Rect.fromCenter(
Expand All @@ -114,8 +43,11 @@ class _BarcodeScannerWithScanWindowState
return ScannerErrorWidget(error: error);
},
),
_buildBarcodeOverlay(),
_buildScanWindow(scanWindow),
BarcodeOverlay(controller: controller, boxFit: boxFit),
ScannerOverlay(
scanWindow: scanWindow,
controller: controller,
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
Expand All @@ -137,105 +69,3 @@ class _BarcodeScannerWithScanWindowState
await controller.dispose();
}
}

class ScannerOverlay extends CustomPainter {
ScannerOverlay(this.scanWindow);

final Rect scanWindow;

@override
void paint(Canvas canvas, Size size) {
// Define the main overlay path covering the entire screen
final backgroundPath = Path()
..addRect(Rect.fromLTWH(0, 0, size.width, size.height));

// Define the cutout path in the center
final cutoutPath = Path()..addRect(scanWindow);

// Combine the two paths: overlay minus the cutout area
final overlayWithCutoutPath =
Path.combine(PathOperation.difference, backgroundPath, cutoutPath);

// Paint the overlay with the cutout
final paint = Paint()
..color = Colors.black.withOpacity(0.5); // Semi-transparent black
canvas.drawPath(overlayWithCutoutPath, paint);
}

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}

class BarcodeOverlay extends CustomPainter {
BarcodeOverlay({
required this.barcodeCorners,
required this.barcodeSize,
required this.boxFit,
required this.cameraPreviewSize,
});

final List<Offset> barcodeCorners;
final Size barcodeSize;
final BoxFit boxFit;
final Size cameraPreviewSize;

@override
void paint(Canvas canvas, Size size) {
if (barcodeCorners.isEmpty ||
barcodeSize.isEmpty ||
cameraPreviewSize.isEmpty) {
return;
}

final adjustedSize = applyBoxFit(boxFit, cameraPreviewSize, size);

double verticalPadding = size.height - adjustedSize.destination.height;
double horizontalPadding = size.width - adjustedSize.destination.width;
if (verticalPadding > 0) {
verticalPadding = verticalPadding / 2;
} else {
verticalPadding = 0;
}

if (horizontalPadding > 0) {
horizontalPadding = horizontalPadding / 2;
} else {
horizontalPadding = 0;
}

final double ratioWidth;
final double ratioHeight;

// if (!kIsWeb && defaultTargetPlatform == TargetPlatform.iOS) {
// ratioWidth = barcodeSize.width / adjustedSize.destination.width;
// ratioHeight = barcodeSize.height / adjustedSize.destination.height;
// } else
// {
ratioWidth = cameraPreviewSize.width / adjustedSize.destination.width;
ratioHeight = cameraPreviewSize.height / adjustedSize.destination.height;
// }

final List<Offset> adjustedOffset = [
for (final offset in barcodeCorners)
Offset(
offset.dx / ratioWidth + horizontalPadding,
offset.dy / ratioHeight + verticalPadding,
),
];

final cutoutPath = Path()..addPolygon(adjustedOffset, true);

final backgroundPaint = Paint()
..color = Colors.red.withOpacity(0.3)
..style = PaintingStyle.fill;

canvas.drawPath(cutoutPath, backgroundPaint);
}

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
4 changes: 4 additions & 0 deletions lib/mobile_scanner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@ export 'src/objects/phone.dart';
export 'src/objects/sms.dart';
export 'src/objects/url_bookmark.dart';
export 'src/objects/wifi.dart';
export 'src/overlay/barcode_overlay.dart';
export 'src/overlay/barcode_painter.dart';
export 'src/overlay/scanner_overlay.dart';
export 'src/overlay/scanner_painter.dart';
7 changes: 6 additions & 1 deletion lib/src/mobile_scanner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,12 @@ class _MobileScannerState extends State<MobileScanner>
MobileScannerState scannerState,
BoxConstraints constraints,
) {
if (widget.scanWindow == null) {
if (widget.scanWindow == null && scanWindow == null) {
return;
} else if (widget.scanWindow == null) {
scanWindow = null;

unawaited(controller.updateScanWindow(null));
return;
}

Expand Down
65 changes: 65 additions & 0 deletions lib/src/overlay/barcode_overlay.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';

class BarcodeOverlay extends StatelessWidget {
const BarcodeOverlay({
super.key,
required this.controller,
required this.boxFit,
});

final MobileScannerController controller;
final BoxFit boxFit;

@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: controller,
builder: (context, value, child) {
// Not ready.
if (!value.isInitialized || !value.isRunning || value.error != null) {
return const SizedBox();
}

return StreamBuilder<BarcodeCapture>(
stream: controller.barcodes,
builder: (context, snapshot) {
final BarcodeCapture? barcodeCapture = snapshot.data;

// No barcode.
if (barcodeCapture == null || barcodeCapture.barcodes.isEmpty) {
return const SizedBox();
}

final overlays = <Widget>[];

for (final scannedBarcode in barcodeCapture.barcodes) {
// No barcode corners, or size, or no camera preview size.
if (value.size.isEmpty ||
scannedBarcode.size.isEmpty ||
scannedBarcode.corners.isEmpty) {
continue;
}

overlays.add(
CustomPaint(
painter: BarcodePainter(
barcodeCorners: scannedBarcode.corners,
barcodeSize: scannedBarcode.size,
boxFit: boxFit,
cameraPreviewSize: barcodeCapture.size,
),
),
);
}

return Stack(
fit: StackFit.expand,
children: overlays,
);
},
);
},
);
}
}
67 changes: 67 additions & 0 deletions lib/src/overlay/barcode_painter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import 'package:flutter/material.dart';

class BarcodePainter extends CustomPainter {
BarcodePainter({
required this.barcodeCorners,
required this.barcodeSize,
required this.boxFit,
required this.cameraPreviewSize,
});

final List<Offset> barcodeCorners;
final Size barcodeSize;
final BoxFit boxFit;
final Size cameraPreviewSize;

@override
void paint(Canvas canvas, Size size) {
if (barcodeCorners.isEmpty ||
barcodeSize.isEmpty ||
cameraPreviewSize.isEmpty) {
return;
}

final adjustedSize = applyBoxFit(boxFit, cameraPreviewSize, size);

double verticalPadding = size.height - adjustedSize.destination.height;
double horizontalPadding = size.width - adjustedSize.destination.width;
if (verticalPadding > 0) {
verticalPadding = verticalPadding / 2;
} else {
verticalPadding = 0;
}

if (horizontalPadding > 0) {
horizontalPadding = horizontalPadding / 2;
} else {
horizontalPadding = 0;
}

final double ratioWidth;
final double ratioHeight;

ratioWidth = cameraPreviewSize.width / adjustedSize.destination.width;
ratioHeight = cameraPreviewSize.height / adjustedSize.destination.height;

final List<Offset> adjustedOffset = [
for (final offset in barcodeCorners)
Offset(
offset.dx / ratioWidth + horizontalPadding,
offset.dy / ratioHeight + verticalPadding,
),
];

final cutoutPath = Path()..addPolygon(adjustedOffset, true);

final backgroundPaint = Paint()
..color = Colors.red.withOpacity(0.3)
..style = PaintingStyle.fill;

canvas.drawPath(cutoutPath, backgroundPaint);
}

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
35 changes: 35 additions & 0 deletions lib/src/overlay/scanner_overlay.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:mobile_scanner/src/mobile_scanner_controller.dart';
import 'package:mobile_scanner/src/overlay/scanner_painter.dart';

class ScannerOverlay extends StatelessWidget {
final MobileScannerController controller;
final Rect scanWindow;

const ScannerOverlay({
super.key,
required this.controller,
required this.scanWindow,
});

@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: controller,
builder: (context, value, child) {
// Not ready.
if (!value.isInitialized ||
!value.isRunning ||
value.error != null ||
value.size.isEmpty) {
return const SizedBox();
}

return CustomPaint(
size: value.size,
painter: ScannerPainter(scanWindow),
);
},
);
}
}
Loading

0 comments on commit b7d6599

Please sign in to comment.