230 lines
6.7 KiB
Dart
230 lines
6.7 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:mobile_scanner/mobile_scanner.dart';
|
|
|
|
class ScanQRScreen extends StatefulWidget {
|
|
@override
|
|
State<ScanQRScreen> createState() => _ScanQRScreenState();
|
|
}
|
|
|
|
class _ScanQRScreenState extends State<ScanQRScreen> {
|
|
final MobileScannerController cameraController = MobileScannerController(
|
|
autoStart: true,
|
|
formats: [BarcodeFormat.qrCode],
|
|
);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final scanWindow = Rect.fromCenter(
|
|
center: MediaQuery.sizeOf(context).center(Offset.zero),
|
|
width: 250,
|
|
height: 250,
|
|
);
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(title: const Text('Scan QR')),
|
|
backgroundColor: Colors.black,
|
|
body: Stack(fit: StackFit.expand, children: [
|
|
Center(
|
|
child: MobileScanner(
|
|
fit: BoxFit.contain,
|
|
controller: cameraController,
|
|
scanWindow: scanWindow,
|
|
onDetect: (BarcodeCapture barcodes) {
|
|
var barcode = barcodes.barcodes.firstOrNull;
|
|
if (barcode != null && mounted) {
|
|
cameraController.stop().then((_) {
|
|
Navigator.pop(context, barcode.rawValue);
|
|
});
|
|
}
|
|
}),
|
|
),
|
|
ValueListenableBuilder(
|
|
valueListenable: cameraController,
|
|
builder: (context, value, child) {
|
|
if (!value.isInitialized || !value.isRunning || value.error != null) {
|
|
return const SizedBox();
|
|
}
|
|
|
|
return CustomPaint(
|
|
painter: ScannerOverlay(scanWindow: scanWindow),
|
|
);
|
|
},
|
|
),
|
|
Align(
|
|
alignment: Alignment.bottomCenter,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
ToggleFlashlightButton(controller: cameraController),
|
|
SwitchCameraButton(controller: cameraController),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
]));
|
|
}
|
|
}
|
|
|
|
class ScannerOverlay extends CustomPainter {
|
|
const ScannerOverlay({
|
|
required this.scanWindow,
|
|
this.borderRadius = 12.0,
|
|
});
|
|
|
|
final Rect scanWindow;
|
|
final double borderRadius;
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
// we need to pass the size to the custom paint widget
|
|
final backgroundPath = Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height));
|
|
|
|
final cutoutPath = Path()
|
|
..addRRect(
|
|
RRect.fromRectAndCorners(
|
|
scanWindow,
|
|
topLeft: Radius.circular(borderRadius),
|
|
topRight: Radius.circular(borderRadius),
|
|
bottomLeft: Radius.circular(borderRadius),
|
|
bottomRight: Radius.circular(borderRadius),
|
|
),
|
|
);
|
|
|
|
final backgroundPaint = Paint()
|
|
..color = Colors.black.withOpacity(0.5)
|
|
..style = PaintingStyle.fill
|
|
..blendMode = BlendMode.srcOver;
|
|
|
|
final backgroundWithCutout = Path.combine(
|
|
PathOperation.difference,
|
|
backgroundPath,
|
|
cutoutPath,
|
|
);
|
|
|
|
final borderPaint = Paint()
|
|
..color = Colors.white
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = 4.0;
|
|
|
|
final borderRect = RRect.fromRectAndCorners(
|
|
scanWindow,
|
|
topLeft: Radius.circular(borderRadius),
|
|
topRight: Radius.circular(borderRadius),
|
|
bottomLeft: Radius.circular(borderRadius),
|
|
bottomRight: Radius.circular(borderRadius),
|
|
);
|
|
|
|
// First, draw the background,
|
|
// with a cutout area that is a bit larger than the scan window.
|
|
// Finally, draw the scan window itself.
|
|
canvas.drawPath(backgroundWithCutout, backgroundPaint);
|
|
canvas.drawRRect(borderRect, borderPaint);
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(ScannerOverlay oldDelegate) {
|
|
return scanWindow != oldDelegate.scanWindow || borderRadius != oldDelegate.borderRadius;
|
|
}
|
|
}
|
|
|
|
class SwitchCameraButton extends StatelessWidget {
|
|
const SwitchCameraButton({required this.controller, super.key});
|
|
|
|
final MobileScannerController controller;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ValueListenableBuilder(
|
|
valueListenable: controller,
|
|
builder: (context, state, child) {
|
|
if (!state.isInitialized || !state.isRunning) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
|
|
final int? availableCameras = state.availableCameras;
|
|
|
|
if (availableCameras != null && availableCameras < 2) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
|
|
final Widget icon;
|
|
|
|
switch (state.cameraDirection) {
|
|
case CameraFacing.front:
|
|
icon = const Icon(Icons.camera_front);
|
|
case CameraFacing.back:
|
|
icon = const Icon(Icons.camera_rear);
|
|
}
|
|
|
|
return IconButton(
|
|
iconSize: 32.0,
|
|
color: Colors.white,
|
|
icon: icon,
|
|
onPressed: () async {
|
|
await controller.switchCamera();
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class ToggleFlashlightButton extends StatelessWidget {
|
|
const ToggleFlashlightButton({required this.controller, super.key});
|
|
|
|
final MobileScannerController controller;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ValueListenableBuilder(
|
|
valueListenable: controller,
|
|
builder: (context, controllerState, child) {
|
|
if (!controllerState.isInitialized || !controllerState.isRunning) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
|
|
switch (controllerState.torchState) {
|
|
case TorchState.auto:
|
|
return IconButton(
|
|
color: Colors.white,
|
|
iconSize: 32.0,
|
|
icon: const Icon(Icons.flash_auto),
|
|
onPressed: () async {
|
|
await controller.toggleTorch();
|
|
},
|
|
);
|
|
case TorchState.off:
|
|
return IconButton(
|
|
color: Colors.white,
|
|
iconSize: 32.0,
|
|
icon: const Icon(Icons.flash_off),
|
|
onPressed: () async {
|
|
await controller.toggleTorch();
|
|
},
|
|
);
|
|
case TorchState.on:
|
|
return IconButton(
|
|
color: Colors.white,
|
|
iconSize: 32.0,
|
|
icon: const Icon(Icons.flash_on),
|
|
onPressed: () async {
|
|
await controller.toggleTorch();
|
|
},
|
|
);
|
|
case TorchState.unavailable:
|
|
return const SizedBox.square(
|
|
dimension: 48.0,
|
|
child: Icon(
|
|
Icons.no_flash,
|
|
size: 32.0,
|
|
color: Colors.grey,
|
|
),
|
|
);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
}
|