Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added ability to style eye and eyeball #133

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions lib/src/qr_image_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand All @@ -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;

Expand Down Expand Up @@ -222,6 +230,7 @@ class _QrImageViewState extends State<QrImageView> {
gapless: widget.gapless,
embeddedImageStyle: widget.embeddedImageStyle,
embeddedImage: image,
blendEmbeddedImage: widget.blendEmbeddedImage,
eyeStyle: widget.eyeStyle,
dataModuleStyle: widget.dataModuleStyle,
);
Expand Down
103 changes: 103 additions & 0 deletions lib/src/qr_painter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 &&
Expand Down Expand Up @@ -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));
Expand All @@ -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(
Expand Down
64 changes: 63 additions & 1 deletion lib/src/types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ enum QrEyeShape {

/// Use circular eye frame.
circle,

/// Use custom frame
custom
}

/// Enumeration representing the shape of Data modules inside QR.
Expand All @@ -56,18 +59,77 @@ 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;

/// 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;

Expand Down