diff --git a/lib/src/qr_image_view.dart b/lib/src/qr_image_view.dart index e803f5c..8f90f67 100644 --- a/lib/src/qr_image_view.dart +++ b/lib/src/qr_image_view.dart @@ -42,6 +42,7 @@ class QrImageView extends StatefulWidget { color: Colors.black, ), this.embeddedImageEmitsError = false, + this.blendEmbeddedImage=false, @Deprecated('use colors in eyeStyle and dataModuleStyle instead') this.foregroundColor, }) : assert( @@ -75,6 +76,7 @@ class QrImageView extends StatefulWidget { dataModuleShape: QrDataModuleShape.square, color: Colors.black, ), + this.blendEmbeddedImage=false, this.embeddedImageEmitsError = false, @Deprecated('use colors in eyeStyle and dataModuleStyle instead') this.foregroundColor, @@ -100,6 +102,12 @@ class QrImageView extends StatefulWidget { /// The QR code error correction level to use. final int errorCorrectionLevel; + /// Embedded image will be blended into the qr code + /// + /// In layman terms, qr code will not be drawn behind embedded image so it + /// looks like the image is part of the qr code. + final bool blendEmbeddedImage; + /// The external padding between the edge of the widget and the content. final EdgeInsets padding; @@ -222,6 +230,7 @@ class _QrImageViewState extends State { gapless: widget.gapless, embeddedImageStyle: widget.embeddedImageStyle, embeddedImage: image, + blendEmbeddedImage: widget.blendEmbeddedImage, eyeStyle: widget.eyeStyle, dataModuleStyle: widget.dataModuleStyle, ); diff --git a/lib/src/qr_painter.dart b/lib/src/qr_painter.dart index f2f3ba2..90eb2f7 100644 --- a/lib/src/qr_painter.dart +++ b/lib/src/qr_painter.dart @@ -34,6 +34,7 @@ class QrPainter extends CustomPainter { this.gapless = false, this.embeddedImage, this.embeddedImageStyle, + this.blendEmbeddedImage = false, this.eyeStyle = const QrEyeStyle( eyeShape: QrEyeShape.square, color: Color(0xFF000000), @@ -63,6 +64,7 @@ class QrPainter extends CustomPainter { this.gapless = false, this.embeddedImage, this.embeddedImageStyle, + this.blendEmbeddedImage = false, this.eyeStyle = const QrEyeStyle( eyeShape: QrEyeShape.square, color: Color(0xFF000000), @@ -98,6 +100,12 @@ class QrPainter extends CustomPainter { /// be added to the center of the QR code. final ui.Image? embeddedImage; + /// Embedded image will be blended into the qr code + /// + /// In layman terms, qr code will not be drawn behind embedded image so it + /// looks like the image is part of the qr code. + final bool blendEmbeddedImage; + /// Styling options for the image overlay. final QrEmbeddedImageStyle? embeddedImageStyle; @@ -251,6 +259,7 @@ class QrPainter extends CustomPainter { if (_isFinderPatternPosition(x, y)) { continue; } + if (blendEmbeddedImage && _isLogoArea(x, y)) continue; final paint = _qrImage.isDark(y, x) ? pixelPaint : emptyPixelPaint; if (paint == null) { @@ -317,6 +326,18 @@ class QrPainter extends CustomPainter { return _qrImage.isDark(y, x + 1); } + bool _isLogoArea(int x, int y) { + //Find center of module count and portion to cut out of QR + var center = _qr!.moduleCount / 2; + var canvasPortion = _qr!.moduleCount * 0.15; + + if (x > center - canvasPortion && + x < center + canvasPortion && + y > center - canvasPortion && + y < center + canvasPortion) return true; + return false; + } + bool _isFinderPatternPosition(int x, int y) { final isTopLeft = y < _finderPatternLimit && x < _finderPatternLimit; final isBottomLeft = y < _finderPatternLimit && @@ -396,6 +417,10 @@ class QrPainter extends CustomPainter { canvas.drawRect(outerRect, outerPaint); canvas.drawRect(innerRect, innerPaint); canvas.drawRect(dotRect, dotPaint); + } + if (eyeStyle.eyeShape == QrEyeShape.custom) { + _drawCustomEyeStyle( + position, outerRect, outerPaint, dotRect, dotPaint, canvas); } else { final roundedOuterStrokeRect = RRect.fromRectAndRadius(outerRect, Radius.circular(radius)); @@ -411,6 +436,84 @@ class QrPainter extends CustomPainter { } } + void _drawCustomEyeStyle( + FinderPatternPosition position, + ui.Rect outerRect, + ui.Paint outerPaint, + ui.Rect dotRect, + ui.Paint dotPaint, + ui.Canvas canvas) { + BorderRadius eyeBorderRadius; + BorderRadius eyeballBorderRadius; + var dashedBorder = false; + switch (position) { + case FinderPatternPosition.topLeft: + eyeBorderRadius = eyeStyle.shape.topLeft.eyeShape; + eyeballBorderRadius = eyeStyle.shape.topLeft.eyeballShape; + dashedBorder = eyeStyle.shape.topLeft.dashedBorder; + break; + case FinderPatternPosition.topRight: + eyeBorderRadius = eyeStyle.shape.topRight.eyeShape; + eyeballBorderRadius = eyeStyle.shape.topRight.eyeballShape; + dashedBorder = eyeStyle.shape.topRight.dashedBorder; + break; + case FinderPatternPosition.bottomLeft: + eyeBorderRadius = eyeStyle.shape.bottomLeft.eyeShape; + eyeballBorderRadius = eyeStyle.shape.bottomLeft.eyeballShape; + dashedBorder = eyeStyle.shape.bottomLeft.dashedBorder; + break; + } + var eyePath = drawRRect(outerRect.left, outerRect.top, outerRect.width, + outerRect.height, eyeBorderRadius, outerPaint.strokeWidth); + var eyeBallPath = drawRRect(dotRect.left, dotRect.top, dotRect.width, + dotRect.height, eyeballBorderRadius, dotPaint.strokeWidth); + + if (dashedBorder) { + var eyeDashPath = Path(); + + var dashWidth = 10.0; + var dashSpace = 5.0; + var distance = 0.0; + eyePath.close(); + for (var pathMetric in eyePath.computeMetrics()) { + while (distance < pathMetric.length) { + eyeDashPath.addPath( + pathMetric.extractPath(distance, distance + dashWidth), + Offset.zero, + ); + distance += dashWidth; + distance += dashSpace; + } + } + canvas.drawPath(eyeDashPath..close(), outerPaint); + } else { + canvas.drawPath(eyePath..close(), outerPaint); + } + canvas.drawPath(eyeBallPath..close(), dotPaint); + } + + /// Draws a rounded rectangle using the current state of the canvas. + /// + /// [Canvas.drawRRect] wasn't used because there was a bug causing the + /// rect to not close properly if radius is 0 + Path drawRRect(double left, double top, double width, double height, + BorderRadius radius, double strokeWidth) { + var ctx = Path(); + ctx.moveTo(left + radius.topLeft.x, top); + ctx.lineTo(left + width - radius.topRight.x, top); + ctx.quadraticBezierTo( + left + width, top, left + width, top + radius.topRight.y); + ctx.lineTo(left + width, top + height - radius.bottomRight.y); + ctx.quadraticBezierTo(left + width, top + height, + left + width - radius.bottomRight.x, top + height); + ctx.lineTo(left + radius.bottomLeft.x, top + height); + ctx.quadraticBezierTo( + left, top + height, left, top + height - radius.bottomLeft.y); + ctx.lineTo(left, top + radius.topLeft.y); + ctx.quadraticBezierTo(left, top, left + radius.topLeft.x, top); + return ctx; + } + bool _hasOneNonZeroSide(Size size) => size.longestSide > 0; Size _scaledAspectSize( diff --git a/lib/src/types.dart b/lib/src/types.dart index b67b185..f6112c2 100644 --- a/lib/src/types.dart +++ b/lib/src/types.dart @@ -45,6 +45,9 @@ enum QrEyeShape { /// Use circular eye frame. circle, + + /// Use custom frame + custom } /// Enumeration representing the shape of Data modules inside QR. @@ -56,11 +59,67 @@ enum QrDataModuleShape { circle, } +/// A holder for customizing the eye of a QRCode +class QrEyeShapeStyle { + /// Set the border radius of the eye to give a shape + final BorderRadius eyeShape; + + /// Set the border radius of the eyeball to give it a custom shape + final BorderRadius eyeballShape; + + /// if to use a dashed eye + /// + /// This field is ignored for the eyeball + final bool dashedBorder; + + /// Create A new Shape style + const QrEyeShapeStyle( + {this.dashedBorder = false, + this.eyeShape = BorderRadius.zero, + this.eyeballShape = BorderRadius.zero}); + + /// Sets the style to none + static const none = QrEyeShapeStyle( + eyeShape: BorderRadius.zero, eyeballShape: BorderRadius.zero); +} + +/// Customize the shape of the finders on a QRCode +class QrEyeShapeStyles { + /// Customize the bottom left eye + final QrEyeShapeStyle bottomLeft; + + /// Customize the top right eye + final QrEyeShapeStyle topRight; + + /// Customize the top left eye + final QrEyeShapeStyle topLeft; + + /// Sets the style to none + static const none = QrEyeShapeStyles.all(QrEyeShapeStyle.none); + + const QrEyeShapeStyles._(this.bottomLeft, this.topLeft, this.topRight) + : assert(bottomLeft != null && topRight != null && topLeft != null); + + const QrEyeShapeStyles.only({ + QrEyeShapeStyle bottomLeft = QrEyeShapeStyle.none, + QrEyeShapeStyle topRight = QrEyeShapeStyle.none, + QrEyeShapeStyle topLeft = QrEyeShapeStyle.none, + }) : this._(bottomLeft, topLeft, topRight); + + /// Call this to create a uniform style + const QrEyeShapeStyles.all(QrEyeShapeStyle style) + : this._(style, style, style); +} + /// Styling options for finder pattern eye. @immutable class QrEyeStyle { /// Create a new set of styling options for QR Eye. - const QrEyeStyle({this.eyeShape, this.color}); + const QrEyeStyle({ + this.eyeShape, + this.color, + this.shape = QrEyeShapeStyles.none, + }); /// Eye shape. final QrEyeShape? eyeShape; @@ -68,6 +127,9 @@ class QrEyeStyle { /// Color to tint the eye. final Color? color; + /// The custom shape of the eye + final QrEyeShapeStyles shape; + @override int get hashCode => eyeShape.hashCode ^ color.hashCode;