- 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.flutter-beta
| @@ -1,5 +1,6 @@ | |||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||
| import 'package:qr_code_scanner/qr_code_scanner.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())); | void main() => runApp(MaterialApp(home: QRViewExample())); | ||||
| @@ -33,83 +34,94 @@ class _QRViewExampleState extends State<QRViewExample> { | |||||
| child: QRView( | child: QRView( | ||||
| key: qrKey, | key: qrKey, | ||||
| onQRViewCreated: _onQRViewCreated, | onQRViewCreated: _onQRViewCreated, | ||||
| overlay: QrScannerOverlayShape( | |||||
| borderColor: Colors.red, | |||||
| borderRadius: 10, | |||||
| borderLength: 30, | |||||
| borderWidth: 10, | |||||
| cutOutSize: 300, | |||||
| ), | |||||
| ), | ), | ||||
| flex: 4, | flex: 4, | ||||
| ), | ), | ||||
| Expanded( | Expanded( | ||||
| child: Column( | |||||
| children: <Widget>[ | |||||
| Text("This is the result of scan: $qrText"), | |||||
| Row( | |||||
| mainAxisAlignment: MainAxisAlignment.center, | |||||
| crossAxisAlignment: CrossAxisAlignment.center, | |||||
| children: <Widget>[ | |||||
| 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: <Widget>[ | |||||
| Text("This is the result of scan: $qrText"), | |||||
| Row( | |||||
| mainAxisAlignment: MainAxisAlignment.center, | |||||
| crossAxisAlignment: CrossAxisAlignment.center, | |||||
| children: <Widget>[ | |||||
| 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: <Widget>[ | |||||
| 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: <Widget>[ | |||||
| 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, | flex: 1, | ||||
| ) | ) | ||||
| @@ -10,10 +10,14 @@ class QRView extends StatefulWidget { | |||||
| const QRView({ | const QRView({ | ||||
| @required Key key, | @required Key key, | ||||
| @required this.onQRViewCreated, | @required this.onQRViewCreated, | ||||
| }) : super(key: key); | |||||
| this.overlay, | |||||
| }) : assert(key != null), | |||||
| super(key: key); | |||||
| final QRViewCreatedCallback onQRViewCreated; | final QRViewCreatedCallback onQRViewCreated; | ||||
| final ShapeBorder overlay; | |||||
| @override | @override | ||||
| State<StatefulWidget> createState() => _QRViewState(); | State<StatefulWidget> createState() => _QRViewState(); | ||||
| } | } | ||||
| @@ -21,26 +25,42 @@ class QRView extends StatefulWidget { | |||||
| class _QRViewState extends State<QRView> { | class _QRViewState extends State<QRView> { | ||||
| @override | @override | ||||
| Widget build(BuildContext context) { | 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) { | void _onPlatformViewCreated(int id) { | ||||
| @@ -79,14 +99,16 @@ class QRViewController { | |||||
| Stream<String> get scannedData => _scanUpdateController.stream; | Stream<String> 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) { | if (defaultTargetPlatform == TargetPlatform.iOS) { | ||||
| final RenderBox renderBox = qrKey.currentContext.findRenderObject(); | final RenderBox renderBox = qrKey.currentContext.findRenderObject(); | ||||
| channel.invokeMethod("setDimensions", | |||||
| _channel.invokeMethod("setDimensions", | |||||
| {"width": renderBox.size.width, "height": renderBox.size.height}); | {"width": renderBox.size.width, "height": renderBox.size.height}); | ||||
| } | } | ||||
| channel.setMethodCallHandler( | |||||
| _channel.setMethodCallHandler( | |||||
| (MethodCall call) async { | (MethodCall call) async { | ||||
| switch (call.method) { | switch (call.method) { | ||||
| case scanMethodCall: | case scanMethodCall: | ||||
| @@ -98,22 +120,20 @@ class QRViewController { | |||||
| ); | ); | ||||
| } | } | ||||
| final MethodChannel channel; | |||||
| void flipCamera() { | void flipCamera() { | ||||
| channel.invokeMethod("flipCamera"); | |||||
| _channel.invokeMethod("flipCamera"); | |||||
| } | } | ||||
| void toggleFlash() { | void toggleFlash() { | ||||
| channel.invokeMethod("toggleFlash"); | |||||
| _channel.invokeMethod("toggleFlash"); | |||||
| } | } | ||||
| void pauseCamera() { | void pauseCamera() { | ||||
| channel.invokeMethod("pauseCamera"); | |||||
| _channel.invokeMethod("pauseCamera"); | |||||
| } | } | ||||
| void resumeCamera() { | void resumeCamera() { | ||||
| channel.invokeMethod("resumeCamera"); | |||||
| _channel.invokeMethod("resumeCamera"); | |||||
| } | } | ||||
| void dispose() { | void dispose() { | ||||
| @@ -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, | |||||
| ); | |||||
| } | |||||
| } | |||||