- 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: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<QRViewExample> { | |||
| 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: <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, | |||
| ) | |||
| @@ -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<StatefulWidget> createState() => _QRViewState(); | |||
| } | |||
| @@ -21,26 +25,42 @@ class QRView extends StatefulWidget { | |||
| class _QRViewState extends State<QRView> { | |||
| @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<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) { | |||
| 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() { | |||
| @@ -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, | |||
| ); | |||
| } | |||
| } | |||