2022-10-04 13:12:54 +03:00
|
|
|
/*
|
2023-09-21 12:40:41 +03:00
|
|
|
* Copyright (C) 2022-2023 Yubico.
|
2022-10-04 13:12:54 +03:00
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2023-09-21 12:40:41 +03:00
|
|
|
import 'dart:math';
|
|
|
|
|
2022-08-05 11:40:36 +03:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
|
|
|
import 'qr_scanner_scan_status.dart';
|
2023-09-21 12:40:41 +03:00
|
|
|
|
|
|
|
class QRScannerOverlay extends StatelessWidget {
|
|
|
|
final ScanStatus status;
|
|
|
|
final Size screenSize;
|
|
|
|
final GlobalKey overlayWidgetKey;
|
|
|
|
|
|
|
|
const QRScannerOverlay(
|
|
|
|
{super.key,
|
|
|
|
required this.status,
|
|
|
|
required this.screenSize,
|
|
|
|
required this.overlayWidgetKey});
|
|
|
|
|
|
|
|
RRect getOverlayRRect(Size size) {
|
|
|
|
final renderBox =
|
|
|
|
overlayWidgetKey.currentContext?.findRenderObject() as RenderBox;
|
|
|
|
final renderObjectSize = renderBox.size;
|
|
|
|
final renderObjectOffset = renderBox.globalToLocal(Offset.zero);
|
|
|
|
|
|
|
|
final double shorterEdge =
|
|
|
|
min(renderObjectSize.width, renderObjectSize.height);
|
|
|
|
|
|
|
|
var top = (size.height - shorterEdge) / 2 - 32;
|
|
|
|
|
|
|
|
if (top + renderObjectOffset.dy < 0) {
|
|
|
|
top = -renderObjectOffset.dy;
|
|
|
|
}
|
|
|
|
|
|
|
|
return RRect.fromRectAndRadius(
|
|
|
|
Rect.fromLTWH(
|
|
|
|
(size.width - shorterEdge) / 2, top, shorterEdge, shorterEdge),
|
|
|
|
const Radius.circular(10));
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
overlayRectProvider(Size size) {
|
|
|
|
return getOverlayRRect(size);
|
|
|
|
}
|
|
|
|
|
|
|
|
return Stack(fit: StackFit.expand, children: [
|
|
|
|
/// clip scanner area "hole" into a darkened background
|
|
|
|
ClipPath(
|
|
|
|
clipper: _OverlayClipper(overlayRectProvider),
|
|
|
|
child: const Opacity(
|
|
|
|
opacity: 0.6,
|
|
|
|
child: ColoredBox(
|
|
|
|
color: Colors.black,
|
|
|
|
child: Column(
|
|
|
|
mainAxisSize: MainAxisSize.max,
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
|
|
children: [Spacer()],
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
|
|
|
|
/// draw a stroke around the scanner area
|
|
|
|
CustomPaint(
|
|
|
|
painter: _OverlayPainter(status, overlayRectProvider),
|
|
|
|
),
|
|
|
|
]);
|
|
|
|
}
|
2022-08-05 11:40:36 +03:00
|
|
|
}
|
|
|
|
|
2023-09-21 12:40:41 +03:00
|
|
|
/// Paints a colored stroke and status icon.
|
|
|
|
/// The stroke area is acquired through passed in rectangle provider.
|
|
|
|
/// The color is computed from the scan status.
|
|
|
|
class _OverlayPainter extends CustomPainter {
|
|
|
|
final ScanStatus _status;
|
|
|
|
final Function(Size) _rectProvider;
|
2022-08-05 11:40:36 +03:00
|
|
|
|
2023-09-21 12:40:41 +03:00
|
|
|
_OverlayPainter(this._status, this._rectProvider) : super();
|
2022-08-05 11:40:36 +03:00
|
|
|
|
|
|
|
@override
|
|
|
|
void paint(Canvas canvas, Size size) {
|
2023-09-21 12:40:41 +03:00
|
|
|
final color = _status == ScanStatus.error
|
|
|
|
? Colors.red.shade400
|
|
|
|
: Colors.green.shade400;
|
2022-08-05 11:40:36 +03:00
|
|
|
Paint paint = Paint()
|
2023-09-21 12:40:41 +03:00
|
|
|
..color = color
|
2022-08-05 11:40:36 +03:00
|
|
|
..style = PaintingStyle.stroke
|
|
|
|
..strokeWidth = 3.0;
|
|
|
|
|
2023-09-21 12:40:41 +03:00
|
|
|
final RRect overlayRRect = _rectProvider(size);
|
|
|
|
|
|
|
|
Path path = Path()..addRRect(overlayRRect);
|
2022-08-05 11:40:36 +03:00
|
|
|
canvas.drawPath(path, paint);
|
2023-09-21 12:40:41 +03:00
|
|
|
|
|
|
|
if (_status == ScanStatus.success) {
|
|
|
|
const icon = Icons.check_circle;
|
|
|
|
final iconSize =
|
|
|
|
overlayRRect.width < 150 ? overlayRRect.width - 5.0 : 150.0;
|
|
|
|
TextPainter iconPainter = TextPainter(
|
|
|
|
textDirection: TextDirection.rtl,
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
);
|
|
|
|
iconPainter.text = TextSpan(
|
|
|
|
text: String.fromCharCode(icon.codePoint),
|
|
|
|
style: TextStyle(
|
|
|
|
fontSize: iconSize,
|
|
|
|
fontFamily: icon.fontFamily,
|
|
|
|
color: color.withAlpha(240),
|
|
|
|
));
|
|
|
|
iconPainter.layout();
|
|
|
|
iconPainter.paint(
|
|
|
|
canvas,
|
|
|
|
overlayRRect.center.translate(-iconSize / 2, -iconSize / 2),
|
|
|
|
);
|
|
|
|
}
|
2022-08-05 11:40:36 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
|
|
|
}
|
|
|
|
|
2023-09-21 12:40:41 +03:00
|
|
|
/// Clips a hole into the background.
|
|
|
|
/// The clipped area is acquired through passed in rectangle provider.
|
|
|
|
class _OverlayClipper extends CustomClipper<Path> {
|
|
|
|
final Function(Size) _rectProvider;
|
|
|
|
|
|
|
|
_OverlayClipper(this._rectProvider);
|
|
|
|
|
2022-08-05 11:40:36 +03:00
|
|
|
@override
|
|
|
|
Path getClip(Size size) {
|
|
|
|
return Path()
|
|
|
|
..addRect(Rect.fromLTWH(0, 0, size.width, size.height))
|
2023-09-21 12:40:41 +03:00
|
|
|
..addRRect(_rectProvider(size))
|
2022-08-05 11:40:36 +03:00
|
|
|
..fillType = PathFillType.evenOdd;
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
2023-09-21 12:40:41 +03:00
|
|
|
bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false;
|
2022-08-05 11:40:36 +03:00
|
|
|
}
|