mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-22 16:32:01 +03:00
improve QRScanner views for responsiveness
This commit is contained in:
parent
1f72862c0b
commit
d25a1fddb1
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Yubico.
|
||||
* Copyright (C) 2022-2023 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -14,114 +14,142 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'qr_scanner_scan_status.dart';
|
||||
import 'qr_scanner_util.dart';
|
||||
|
||||
/// Return the rounded rect which represents the scanner area for the background
|
||||
/// overlay and the stroke
|
||||
RRect _getScannerAreaRRect(Size size) {
|
||||
double scannerAreaWidth = getScannerAreaWidth(size);
|
||||
var scannerAreaRect = Rect.fromCenter(
|
||||
center: Offset(size.width / 2, size.height / 2),
|
||||
width: scannerAreaWidth,
|
||||
height: scannerAreaWidth);
|
||||
class QRScannerOverlay extends StatelessWidget {
|
||||
final ScanStatus status;
|
||||
final Size screenSize;
|
||||
final GlobalKey overlayWidgetKey;
|
||||
|
||||
return RRect.fromRectAndRadius(
|
||||
scannerAreaRect, const Radius.circular(scannerAreaRadius));
|
||||
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),
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// CustomPainter which strokes the scannerArea
|
||||
class _ScannerAreaStrokePainter extends CustomPainter {
|
||||
final Color _strokeColor;
|
||||
/// 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;
|
||||
|
||||
_ScannerAreaStrokePainter(this._strokeColor) : super();
|
||||
_OverlayPainter(this._status, this._rectProvider) : super();
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final color = _status == ScanStatus.error
|
||||
? Colors.red.shade400
|
||||
: Colors.green.shade400;
|
||||
Paint paint = Paint()
|
||||
..color = _strokeColor
|
||||
..color = color
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 3.0;
|
||||
|
||||
Path path = Path()..addRRect(_getScannerAreaRRect(size));
|
||||
final RRect overlayRRect = _rectProvider(size);
|
||||
|
||||
Path path = Path()..addRRect(overlayRRect);
|
||||
canvas.drawPath(path, paint);
|
||||
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
/// clips the scanner area rounded rect of specific Size
|
||||
class _ScannerAreaClipper extends CustomClipper<Path> {
|
||||
/// 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);
|
||||
|
||||
@override
|
||||
Path getClip(Size size) {
|
||||
return Path()
|
||||
..addRect(Rect.fromLTWH(0, 0, size.width, size.height))
|
||||
..addRRect(_getScannerAreaRRect(size))
|
||||
..addRRect(_rectProvider(size))
|
||||
..fillType = PathFillType.evenOdd;
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReclip(covariant CustomClipper<Path> oldClipper) => true;
|
||||
}
|
||||
|
||||
class QRScannerOverlay extends StatelessWidget {
|
||||
final ScanStatus status;
|
||||
final Size screenSize;
|
||||
|
||||
const QRScannerOverlay({
|
||||
super.key,
|
||||
required this.status,
|
||||
required this.screenSize,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var size = screenSize;
|
||||
|
||||
return Stack(children: [
|
||||
/// clip scanner area "hole" into a darkened background
|
||||
ClipPath(
|
||||
clipper: _ScannerAreaClipper(),
|
||||
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
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
CustomPaint(
|
||||
painter: _ScannerAreaStrokePainter(status == ScanStatus.error
|
||||
? Colors.red.shade400
|
||||
: Colors.green.shade400),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
/// extra icon when successful scan occurred
|
||||
if (status == ScanStatus.success)
|
||||
Positioned.fromRect(
|
||||
rect: Rect.fromCenter(
|
||||
center: Offset(size.width / 2, size.height / 2),
|
||||
width: size.width,
|
||||
height: size.height),
|
||||
child: Icon(
|
||||
Icons.check_circle,
|
||||
size: 200,
|
||||
color: Colors.green.shade400,
|
||||
)),
|
||||
]);
|
||||
}
|
||||
bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Yubico.
|
||||
* Copyright (C) 2022-2023 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -18,7 +18,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
import 'qr_scanner_scan_status.dart';
|
||||
import 'qr_scanner_util.dart';
|
||||
|
||||
class QRScannerPermissionsUI extends StatelessWidget {
|
||||
final ScanStatus status;
|
||||
@ -34,71 +33,62 @@ class QRScannerPermissionsUI extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final scannerAreaWidth = getScannerAreaWidth(screenSize);
|
||||
|
||||
return Stack(children: [
|
||||
/// instruction text under the scanner area
|
||||
Positioned.fromRect(
|
||||
rect: Rect.fromCenter(
|
||||
center: Offset(screenSize.width / 2,
|
||||
screenSize.height - scannerAreaWidth / 2.0 + 8.0),
|
||||
width: screenSize.width,
|
||||
height: screenSize.height),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 36),
|
||||
child: Text(
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Text(
|
||||
l10n.p_need_camera_permission,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
)),
|
||||
|
||||
/// button for manual entry
|
||||
Positioned.fromRect(
|
||||
rect: Rect.fromCenter(
|
||||
center: Offset(screenSize.width / 2, screenSize.height),
|
||||
width: screenSize.width,
|
||||
height: screenSize.height),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Column(
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Text(
|
||||
l10n.q_have_account_info,
|
||||
textScaleFactor: 0.7,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop('');
|
||||
},
|
||||
child: Text(
|
||||
l10n.s_enter_manually,
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
l10n.q_have_account_info,
|
||||
textScaleFactor: 0.7,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
)),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
l10n.q_want_to_scan,
|
||||
textScaleFactor: 0.7,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop('');
|
||||
},
|
||||
child: Text(
|
||||
l10n.s_enter_manually,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
)),
|
||||
],
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
onPermissionRequest();
|
||||
},
|
||||
child: Text(
|
||||
l10n.s_review_permissions,
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
l10n.q_want_to_scan,
|
||||
textScaleFactor: 0.7,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
)),
|
||||
],
|
||||
)
|
||||
]),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
onPermissionRequest();
|
||||
},
|
||||
child: Text(
|
||||
l10n.s_review_permissions,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
)),
|
||||
],
|
||||
)
|
||||
])
|
||||
],
|
||||
),
|
||||
),
|
||||
]);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Yubico.
|
||||
* Copyright (C) 2022-2023 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -19,69 +19,76 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
import '../keys.dart' as keys;
|
||||
import 'qr_scanner_scan_status.dart';
|
||||
import 'qr_scanner_util.dart';
|
||||
|
||||
class QRScannerUI extends StatelessWidget {
|
||||
final ScanStatus status;
|
||||
final Size screenSize;
|
||||
final GlobalKey overlayWidgetKey;
|
||||
|
||||
const QRScannerUI({
|
||||
super.key,
|
||||
required this.status,
|
||||
required this.screenSize,
|
||||
});
|
||||
const QRScannerUI(
|
||||
{super.key,
|
||||
required this.status,
|
||||
required this.screenSize,
|
||||
required this.overlayWidgetKey});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final scannerAreaWidth = getScannerAreaWidth(screenSize);
|
||||
|
||||
return Stack(children: [
|
||||
/// instruction text under the scanner area
|
||||
Positioned.fromRect(
|
||||
rect: Rect.fromCenter(
|
||||
center: Offset(screenSize.width / 2,
|
||||
screenSize.height + scannerAreaWidth / 2.0 + 8.0),
|
||||
width: screenSize.width,
|
||||
height: screenSize.height),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Text(
|
||||
status != ScanStatus.error
|
||||
? l10n.l_point_camera_scan
|
||||
: l10n.l_invalid_qr,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
/// button for manual entry
|
||||
Positioned.fromRect(
|
||||
rect: Rect.fromCenter(
|
||||
center: Offset(screenSize.width / 2,
|
||||
screenSize.height + scannerAreaWidth / 2.0 + 80.0),
|
||||
width: screenSize.width,
|
||||
height: screenSize.height),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
l10n.q_no_qr,
|
||||
textScaleFactor: 0.7,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop('');
|
||||
},
|
||||
key: keys.manualEntryButton,
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
SafeArea(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 16, right: 16, top: 0, bottom: 0),
|
||||
child: SizedBox(
|
||||
// other widgets can find the RenderObject of this
|
||||
// widget by its key value and query its size and offset.
|
||||
key: overlayWidgetKey,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 0.0),
|
||||
child: Text(
|
||||
l10n.s_enter_manually,
|
||||
status != ScanStatus.error
|
||||
? l10n.l_point_camera_scan
|
||||
: l10n.l_invalid_qr,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
]);
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
l10n.q_no_qr,
|
||||
textScaleFactor: 0.7,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop('');
|
||||
},
|
||||
key: keys.manualEntryButton,
|
||||
child: Text(
|
||||
l10n.s_enter_manually,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Yubico.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import 'dart:ui';
|
||||
|
||||
const double scannerAreaRadius = 40.0;
|
||||
|
||||
double getScannerAreaWidth(Size size) => size.width - scannerAreaRadius;
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Yubico.
|
||||
* Copyright (C) 2022-2023 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -103,6 +103,7 @@ class _QrScannerViewState extends State<QrScannerView> {
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
final overlayWidgetKey = GlobalKey();
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
extendBodyBehindAppBar: true,
|
||||
@ -153,12 +154,14 @@ class _QrScannerViewState extends State<QrScannerView> {
|
||||
child: QRScannerOverlay(
|
||||
status: _status,
|
||||
screenSize: screenSize,
|
||||
overlayWidgetKey: overlayWidgetKey,
|
||||
)),
|
||||
Visibility(
|
||||
visible: _permissionsGranted,
|
||||
child: QRScannerUI(
|
||||
status: _status,
|
||||
screenSize: screenSize,
|
||||
overlayWidgetKey: overlayWidgetKey,
|
||||
)),
|
||||
Visibility(
|
||||
visible: _previewInitialized && !_permissionsGranted,
|
||||
|
Loading…
Reference in New Issue
Block a user