From 237a2c8bde60baac03aec6e857cee9bdfaa70184 Mon Sep 17 00:00:00 2001 From: Luis Thein Date: Sun, 28 Jul 2019 23:09:02 +0200 Subject: [PATCH] Scanner overlay - Added optional parameter overlay to QrCodeScanner. - If overlay != null, a shapeBorder gets added on top of the scanner view. - Added class QrScannerOverlayShape, which is a customizable overlay. - updated example to use an overlay. --- example/lib/main.dart | 154 ++++++++++++++------------- lib/qr_code_scanner.dart | 76 +++++++++----- lib/qr_scanner_overlay_shape.dart | 168 ++++++++++++++++++++++++++++++ 3 files changed, 299 insertions(+), 99 deletions(-) create mode 100644 lib/qr_scanner_overlay_shape.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 2e01987..866346b 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:qr_code_scanner/qr_code_scanner.dart'; +import 'package:qr_code_scanner/qr_scanner_overlay_shape.dart'; void main() => runApp(MaterialApp(home: QRViewExample())); @@ -33,83 +34,94 @@ class _QRViewExampleState extends State { child: QRView( key: qrKey, onQRViewCreated: _onQRViewCreated, + overlay: QrScannerOverlayShape( + borderColor: Colors.red, + borderRadius: 10, + borderLength: 30, + borderWidth: 10, + cutOutSize: 300, + ), ), flex: 4, ), Expanded( - child: Column( - children: [ - Text("This is the result of scan: $qrText"), - Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - margin: EdgeInsets.all(8.0), - child: RaisedButton( - onPressed: () { - if (controller != null) { - controller.toggleFlash(); - if (_isFlashOn(flashState)) - setState(() { - flashState = flash_off; - }); - else - setState(() { - flashState = flash_on; - }); - } - }, - child: Text(flashState, style: TextStyle(fontSize: 20)), + child: FittedBox( + fit: BoxFit.contain, + child: Column( + children: [ + Text("This is the result of scan: $qrText"), + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + margin: EdgeInsets.all(8.0), + child: RaisedButton( + onPressed: () { + if (controller != null) { + controller.toggleFlash(); + if (_isFlashOn(flashState)) + setState(() { + flashState = flash_off; + }); + else + setState(() { + flashState = flash_on; + }); + } + }, + child: + Text(flashState, style: TextStyle(fontSize: 20)), + ), ), - ), - Container( - margin: EdgeInsets.all(8.0), - child: RaisedButton( - onPressed: () { - if (controller != null) { - controller.flipCamera(); - if (_isBackCamera(cameraState)) - setState(() { - cameraState = front_camera; - }); - else - setState(() { - cameraState = back_camera; - }); - } - }, - child: - Text(cameraState, style: TextStyle(fontSize: 20)), + Container( + margin: EdgeInsets.all(8.0), + child: RaisedButton( + onPressed: () { + if (controller != null) { + controller.flipCamera(); + if (_isBackCamera(cameraState)) + setState(() { + cameraState = front_camera; + }); + else + setState(() { + cameraState = back_camera; + }); + } + }, + child: + Text(cameraState, style: TextStyle(fontSize: 20)), + ), + ) + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + margin: EdgeInsets.only(bottom: 8.0), + child: RaisedButton( + onPressed: () { + controller?.pauseCamera(); + }, + child: Text('pause', style: TextStyle(fontSize: 20)), + ), ), - ) - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - margin: EdgeInsets.only(bottom: 8.0), - child: RaisedButton( - onPressed: () { - controller?.pauseCamera(); - }, - child: Text('pause', style: TextStyle(fontSize: 20)), - ), - ), - Container( - margin: EdgeInsets.only(bottom: 8.0), - child: RaisedButton( - onPressed: () { - controller.resumeCamera(); - }, - child: Text('resume', style: TextStyle(fontSize: 20)), - ), - ) - ], - ), - ], + Container( + margin: EdgeInsets.only(bottom: 8.0), + child: RaisedButton( + onPressed: () { + controller.resumeCamera(); + }, + child: Text('resume', style: TextStyle(fontSize: 20)), + ), + ) + ], + ), + ], + ), ), flex: 1, ) diff --git a/lib/qr_code_scanner.dart b/lib/qr_code_scanner.dart index e707de1..e7b5559 100644 --- a/lib/qr_code_scanner.dart +++ b/lib/qr_code_scanner.dart @@ -10,10 +10,14 @@ class QRView extends StatefulWidget { const QRView({ @required Key key, @required this.onQRViewCreated, - }) : super(key: key); + this.overlay, + }) : assert(key != null), + super(key: key); final QRViewCreatedCallback onQRViewCreated; + final ShapeBorder overlay; + @override State createState() => _QRViewState(); } @@ -21,26 +25,42 @@ class QRView extends StatefulWidget { class _QRViewState extends State { @override Widget build(BuildContext context) { - var androidView = AndroidView( - viewType: 'net.touchcapture.qr.flutterqr/qrview', - onPlatformViewCreated: _onPlatformViewCreated, + return Stack( + children: [ + _getPlatformQrView(), + widget.overlay != null + ? Container( + decoration: ShapeDecoration( + shape: widget.overlay, + ), + ) + : Container(), + ], ); + } - if (defaultTargetPlatform == TargetPlatform.android) { - return androidView; - } - - if (defaultTargetPlatform == TargetPlatform.iOS) { - return UiKitView( - viewType: 'net.touchcapture.qr.flutterqr/qrview', - onPlatformViewCreated: _onPlatformViewCreated, - creationParams: _CreationParams.fromWidget(0, 0).toMap(), - creationParamsCodec: StandardMessageCodec(), - ); + Widget _getPlatformQrView() { + Widget _platformQrView; + switch (defaultTargetPlatform) { + case TargetPlatform.android: + _platformQrView = AndroidView( + viewType: 'net.touchcapture.qr.flutterqr/qrview', + onPlatformViewCreated: _onPlatformViewCreated, + ); + break; + case TargetPlatform.iOS: + _platformQrView = UiKitView( + viewType: 'net.touchcapture.qr.flutterqr/qrview', + onPlatformViewCreated: _onPlatformViewCreated, + creationParams: _CreationParams.fromWidget(0, 0).toMap(), + creationParamsCodec: StandardMessageCodec(), + ); + break; + default: + throw UnsupportedError( + "Trying to use the default webview implementation for $defaultTargetPlatform but there isn't a default one"); } - - return Text( - '$defaultTargetPlatform is not yet supported by the text_view plugin'); + return _platformQrView; } void _onPlatformViewCreated(int id) { @@ -79,14 +99,16 @@ class QRViewController { Stream get scannedData => _scanUpdateController.stream; - QRViewController._(int id, GlobalKey qrKey) - : channel = MethodChannel('net.touchcapture.qr.flutterqr/qrview_$id') { + MethodChannel _channel; + + QRViewController._(int id, GlobalKey qrKey) { + _channel = MethodChannel('net.touchcapture.qr.flutterqr/qrview_$id'); if (defaultTargetPlatform == TargetPlatform.iOS) { final RenderBox renderBox = qrKey.currentContext.findRenderObject(); - channel.invokeMethod("setDimensions", + _channel.invokeMethod("setDimensions", {"width": renderBox.size.width, "height": renderBox.size.height}); } - channel.setMethodCallHandler( + _channel.setMethodCallHandler( (MethodCall call) async { switch (call.method) { case scanMethodCall: @@ -98,22 +120,20 @@ class QRViewController { ); } - final MethodChannel channel; - void flipCamera() { - channel.invokeMethod("flipCamera"); + _channel.invokeMethod("flipCamera"); } void toggleFlash() { - channel.invokeMethod("toggleFlash"); + _channel.invokeMethod("toggleFlash"); } void pauseCamera() { - channel.invokeMethod("pauseCamera"); + _channel.invokeMethod("pauseCamera"); } void resumeCamera() { - channel.invokeMethod("resumeCamera"); + _channel.invokeMethod("resumeCamera"); } void dispose() { diff --git a/lib/qr_scanner_overlay_shape.dart b/lib/qr_scanner_overlay_shape.dart new file mode 100644 index 0000000..db42605 --- /dev/null +++ b/lib/qr_scanner_overlay_shape.dart @@ -0,0 +1,168 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +class QrScannerOverlayShape extends ShapeBorder { + final Color borderColor; + final double borderWidth; + final Color overlayColor; + final double borderRadius; + final double borderLength; + final double cutOutSize; + + QrScannerOverlayShape({ + this.borderColor = Colors.red, + this.borderWidth = 3.0, + this.overlayColor = const Color.fromRGBO(0, 0, 0, 80), + this.borderRadius = 0, + this.borderLength = 40, + this.cutOutSize = 250, + }) : assert( + cutOutSize != null + ? cutOutSize != null + ? borderLength <= cutOutSize / 2 + borderWidth * 2 + : true + : true, + "Border can't be larger than ${cutOutSize / 2 + borderWidth * 2}"); + + @override + EdgeInsetsGeometry get dimensions => const EdgeInsets.all(10.0); + + @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 != null && 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( + width / 2 - _cutOutSize / 2 + borderOffset, + height / 2 - _cutOutSize / 2 + borderOffset, + _cutOutSize - borderOffset * 2, + _cutOutSize - borderOffset * 2, + ); + + canvas.saveLayer( + rect, + backgroundPaint, + ); + + canvas + ..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 QrScannerOverlayShape( + borderColor: borderColor, + borderWidth: borderWidth, + overlayColor: overlayColor, + ); + } +}