diff --git a/example/lib/main.dart b/example/lib/main.dart index a933806..53ad8b3 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -32,8 +32,7 @@ class ExampleHomePage extends StatefulWidget { } class _ExampleHomePageState extends State { - final StreamController _verificationNotifier = - StreamController.broadcast(); + final StreamController _verificationNotifier = StreamController.broadcast(); bool isAuthenticated = false; @@ -79,12 +78,8 @@ class _ExampleHomePageState extends State { onPressed: () { _showLockScreen(context, opaque: false, - circleUIConfig: CircleUIConfig( - borderColor: Colors.blue, - fillColor: Colors.blue, - circleSize: 30), - keyboardUIConfig: KeyboardUIConfig( - digitBorderWidth: 2, primaryColor: Colors.blue), + circleUIConfig: CircleUIConfig(borderColor: Colors.blue, fillColor: Colors.blue, circleSize: 30), + keyboardUIConfig: KeyboardUIConfig(digitBorderWidth: 2, primaryColor: Colors.blue), cancelButton: Icon( Icons.arrow_back, color: Colors.blue, @@ -102,17 +97,29 @@ class _ExampleHomePageState extends State { required Widget cancelButton, List? digits, }) { + final screenSize = MediaQuery.of(context).size; + Navigator.push( context, PageRouteBuilder( opaque: opaque, - pageBuilder: (context, animation, secondaryAnimation) => - PasscodeScreen( - title: Text( - 'Enter App Passcode', + pageBuilder: (context, animation, secondaryAnimation) => PasscodeScreen( + title: Container( + margin: EdgeInsets.only(top: screenSize.height * 0.08), + child: Text( + 'Enter App Passcode', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white, fontSize: 28), + ), + ), + subtitle: Container( + margin: EdgeInsets.only(top: 12, right: 16, left: 16), + child: Text( + "And subtitle here", + style: Theme.of(context).textTheme.subtitle2?.copyWith(color: Colors.white), textAlign: TextAlign.center, - style: TextStyle(color: Colors.white, fontSize: 28), ), + ), circleUIConfig: circleUIConfig, keyboardUIConfig: keyboardUIConfig, passwordEnteredCallback: _onPasscodeEntered, @@ -160,10 +167,7 @@ class _ExampleHomePageState extends State { child: Text( "Reset passcode", textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 16, - color: Colors.white, - fontWeight: FontWeight.w300), + style: const TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.w300), ), onPressed: _resetAppPassword, // splashColor: Colors.white.withOpacity(0.4), diff --git a/lib/circle.dart b/lib/circle.dart index 3c9b5d5..d74a2d2 100644 --- a/lib/circle.dart +++ b/lib/circle.dart @@ -7,25 +7,27 @@ class CircleUIConfig { final double borderWidth; final double circleSize; + //IndicatorWidget allows you to define your own style instead of using the [Cicle] + final IndicatorWidget Function(bool, double)? indicatorBuilder; + const CircleUIConfig({ this.borderColor = Colors.white, this.borderWidth = 1, this.fillColor = Colors.white, this.circleSize = 20, + this.indicatorBuilder, }); } -class Circle extends StatelessWidget { - final bool filled; +class Circle extends IndicatorWidget { final CircleUIConfig circleUIConfig; - final double extraSize; Circle({ Key? key, - this.filled = false, + bool filled = false, required this.circleUIConfig, - this.extraSize = 0, - }) : super(key: key); + double extraSize = 0, + }) : super(key: key, filled: filled, extraSize: extraSize); @override Widget build(BuildContext context) { @@ -44,3 +46,10 @@ class Circle extends StatelessWidget { ); } } + +abstract class IndicatorWidget extends StatelessWidget { + final bool filled; + final double extraSize; + + const IndicatorWidget({Key? key, required this.filled, this.extraSize = 0}) : super(key: key); +} diff --git a/lib/keyboard.dart b/lib/keyboard.dart index 3d4933f..dbae2a3 100644 --- a/lib/keyboard.dart +++ b/lib/keyboard.dart @@ -13,21 +13,28 @@ class KeyboardUIConfig { final Color digitFillColor; final EdgeInsetsGeometry keyboardRowMargin; final EdgeInsetsGeometry digitInnerMargin; + final EdgeInsetsGeometry keyboardRootMargin; + final BoxDecoration? digitItemDecoration; //Size for the keyboard can be define and provided from the app. //If it will not be provided the size will be adjusted to a screen size. final Size? keyboardSize; + //KeyboardItemWidget allows you to define your own style instead of using the [KeyboardDigit] + final KeyboardItemWidget Function(String, KeyboardUIConfig, KeyboardTapCallback)? keyboardItemBuilder; + const KeyboardUIConfig({ this.digitBorderWidth = 1, this.keyboardRowMargin = const EdgeInsets.only(top: 15, left: 4, right: 4), + this.keyboardRootMargin = const EdgeInsets.only(top: 16), this.digitInnerMargin = const EdgeInsets.all(24), this.primaryColor = Colors.white, this.digitFillColor = Colors.transparent, this.digitTextStyle = const TextStyle(fontSize: 30, color: Colors.white), - this.deleteButtonTextStyle = - const TextStyle(fontSize: 16, color: Colors.white), + this.deleteButtonTextStyle = const TextStyle(fontSize: 16, color: Colors.white), + this.digitItemDecoration, this.keyboardSize, + this.keyboardItemBuilder, }); } @@ -58,43 +65,57 @@ class Keyboard extends StatelessWidget { keyboardItems = digits!; } final screenSize = MediaQuery.of(context).size; - final keyboardHeight = screenSize.height > screenSize.width - ? screenSize.height / 2 - : screenSize.height - 80; + final keyboardHeight = screenSize.height > screenSize.width ? screenSize.height / 2 : screenSize.height - 80; final keyboardWidth = keyboardHeight * 3 / 4; - final keyboardSize = this.keyboardUIConfig.keyboardSize != null - ? this.keyboardUIConfig.keyboardSize! - : Size(keyboardWidth, keyboardHeight); + final keyboardSize = this.keyboardUIConfig.keyboardSize != null ? this.keyboardUIConfig.keyboardSize! : Size(keyboardWidth, keyboardHeight); return Container( - width: keyboardSize.width, - height: keyboardSize.height, - margin: EdgeInsets.only(top: 16), - child: RawKeyboardListener( - focusNode: _focusNode, - autofocus: true, - onKey: (event) { - if (event is RawKeyUpEvent) { - if (keyboardItems.contains(event.data.keyLabel)) { - onKeyboardTap(event.logicalKey.keyLabel); - return; - } - if (event.logicalKey.keyLabel== 'Backspace' || event.logicalKey.keyLabel == 'Delete') { - onKeyboardTap(Keyboard.deleteButton); - return; + margin: this.keyboardUIConfig.keyboardRootMargin, + child: SizedBox( + width: keyboardSize.width + 16, + height: keyboardSize.height + 16, + child: RawKeyboardListener( + focusNode: _focusNode, + autofocus: true, + onKey: (event) { + if (event is RawKeyUpEvent) { + if (keyboardItems.contains(event.data.keyLabel)) { + onKeyboardTap(event.logicalKey.keyLabel); + return; + } + if (event.logicalKey.keyLabel == 'Backspace' || event.logicalKey.keyLabel == 'Delete') { + onKeyboardTap(Keyboard.deleteButton); + return; + } } - } - }, - child: AlignedGrid( - keyboardSize: keyboardSize, - children: List.generate(10, (index) { - return _buildKeyboardDigit(keyboardItems[index]); - }), + }, + child: AlignedGrid( + keyboardSize: keyboardSize, + children: List.generate(keyboardItems.length, (index) { + if (keyboardUIConfig.keyboardItemBuilder != null) { + return keyboardUIConfig.keyboardItemBuilder!(keyboardItems[index], keyboardUIConfig, onKeyboardTap); + } else { + return KeyboardDigit( + text: keyboardItems[index], + keyboardUIConfig: keyboardUIConfig, + onKeyboardTap: onKeyboardTap, + ); + } + }), + ), ), ), ); } +} + +class KeyboardDigit extends KeyboardItemWidget { + final KeyboardUIConfig keyboardUIConfig; - Widget _buildKeyboardDigit(String text) { + const KeyboardDigit({Key? key, required String text, required this.keyboardUIConfig, required Function(String) onKeyboardTap}) + : super(key: key, text: text, onKeyboardTap: onKeyboardTap); + + @override + Widget build(BuildContext context) { return Container( margin: EdgeInsets.all(4), child: ClipOval( @@ -106,13 +127,15 @@ class Keyboard extends StatelessWidget { onKeyboardTap(text); }, child: Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.transparent, - border: Border.all( - color: keyboardUIConfig.primaryColor, - width: keyboardUIConfig.digitBorderWidth), - ), + decoration: keyboardUIConfig.digitItemDecoration ?? + BoxDecoration( + shape: BoxShape.circle, + color: Colors.transparent, + border: Border.all( + color: keyboardUIConfig.primaryColor, + width: keyboardUIConfig.digitBorderWidth, + ), + ), child: Container( decoration: BoxDecoration( shape: BoxShape.circle, @@ -134,36 +157,65 @@ class Keyboard extends StatelessWidget { } } +abstract class KeyboardItemWidget extends StatelessWidget { + final String text; + final Function(String) onKeyboardTap; + + const KeyboardItemWidget({Key? key, required this.text, required this.onKeyboardTap}) : super(key: key); +} + class AlignedGrid extends StatelessWidget { - final double runSpacing = 4; - final double spacing = 4; + final double runSpacing = 2; + final double spacing = 2.0; final int listSize; final columns = 3; + final rows = 4; final List children; final Size keyboardSize; - const AlignedGrid( - {Key? key, required this.children, required this.keyboardSize}) + const AlignedGrid({Key? key, required this.children, required this.keyboardSize}) : listSize = children.length, super(key: key); @override Widget build(BuildContext context) { - final primarySize = keyboardSize.width > keyboardSize.height - ? keyboardSize.height - : keyboardSize.width; - final itemSize = (primarySize - runSpacing * (columns - 1)) / columns; - return Wrap( - runSpacing: runSpacing, - spacing: spacing, - alignment: WrapAlignment.center, - children: children - .map((item) => Container( - width: itemSize, - height: itemSize, - child: item, - )) - .toList(growable: false), + final primarySize; + final itemSize; + if (keyboardSize.width > keyboardSize.height) { + primarySize = keyboardSize.height; + itemSize = primarySize / rows; + } else { + primarySize = keyboardSize.width; + itemSize = primarySize / rows; + } + + final numToItem = (item) => Container( + margin: EdgeInsets.all(2), + width: itemSize, + height: itemSize, + child: item, + ); + + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: children.sublist(0, 3).map(numToItem).toList(), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: children.sublist(3, 6).map(numToItem).toList(), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: children.sublist(6, 9).map(numToItem).toList(), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: children.sublist(9).map(numToItem).toList(), + ) + ], ); } } diff --git a/lib/passcode_screen.dart b/lib/passcode_screen.dart index 41f51a9..10c9ddd 100644 --- a/lib/passcode_screen.dart +++ b/lib/passcode_screen.dart @@ -13,12 +13,13 @@ typedef IsValidCallback = void Function(); typedef CancelCallback = void Function(); class PasscodeScreen extends StatefulWidget { - final Widget title; + final Widget? title; + final Widget? subtitle; final int passwordDigits; final PasswordEnteredCallback passwordEnteredCallback; // Cancel button and delete button will be switched based on the screen state - final Widget cancelButton; - final Widget deleteButton; + final Widget? cancelButton; + final Widget? deleteButton; final Stream shouldTriggerVerification; final CircleUIConfig circleUIConfig; final KeyboardUIConfig keyboardUIConfig; @@ -33,11 +34,12 @@ class PasscodeScreen extends StatefulWidget { PasscodeScreen({ Key? key, - required this.title, + this.title, + this.subtitle, this.passwordDigits = 6, required this.passwordEnteredCallback, - required this.cancelButton, - required this.deleteButton, + this.cancelButton, + this.deleteButton, required this.shouldTriggerVerification, this.isValidCallback, CircleUIConfig? circleUIConfig, @@ -54,8 +56,7 @@ class PasscodeScreen extends StatefulWidget { State createState() => _PasscodeScreenState(); } -class _PasscodeScreenState extends State - with SingleTickerProviderStateMixin { +class _PasscodeScreenState extends State with SingleTickerProviderStateMixin { late StreamSubscription streamSubscription; String enteredPasscode = ''; late AnimationController controller; @@ -64,12 +65,9 @@ class _PasscodeScreenState extends State @override initState() { super.initState(); - streamSubscription = widget.shouldTriggerVerification - .listen((isValid) => _showValidation(isValid)); - controller = AnimationController( - duration: const Duration(milliseconds: 500), vsync: this); - final Animation curve = - CurvedAnimation(parent: controller, curve: ShakeCurve()); + streamSubscription = widget.shouldTriggerVerification.listen((isValid) => _showValidation(isValid)); + controller = AnimationController(duration: const Duration(milliseconds: 500), vsync: this); + final Animation curve = CurvedAnimation(parent: controller, curve: ShakeCurve()); animation = Tween(begin: 0.0, end: 10.0).animate(curve as Animation) ..addStatusListener((status) { if (status == AnimationStatus.completed) { @@ -88,42 +86,36 @@ class _PasscodeScreenState extends State @override Widget build(BuildContext context) { + final isPortrait = MediaQuery.of(context).orientation == Orientation.portrait; return Scaffold( backgroundColor: widget.backgroundColor ?? Colors.black.withOpacity(0.8), body: SafeArea( - child: OrientationBuilder( - builder: (context, orientation) { - return orientation == Orientation.portrait - ? _buildPortraitPasscodeScreen() - : _buildLandscapePasscodeScreen(); - }, - ), + child: isPortrait ? _buildPortraitPasscodeScreen() : _buildLandscapePasscodeScreen(), ), ); } _buildPortraitPasscodeScreen() => Stack( children: [ - Positioned( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - widget.title, - Container( - margin: const EdgeInsets.only(top: 20), - height: 40, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: _buildCircles(), - ), - ), - _buildKeyboard(), - widget.bottomWidget ?? Container() - ], + Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + widget.title ?? Container(), + Container( + margin: const EdgeInsets.only(top: 20), + height: 40, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: _buildCircles(), + ), ), - ), + widget.subtitle ?? Container(), + _buildKeyboardPortrait(), + ], ), + Positioned(child: widget.bottomWidget ?? Container()), Positioned( child: Align( alignment: Alignment.bottomRight, @@ -140,41 +132,40 @@ class _PasscodeScreenState extends State child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Container( - child: Stack( - children: [ - Positioned( - child: Align( - alignment: Alignment.center, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - widget.title, - Container( - margin: const EdgeInsets.only(top: 20), - height: 40, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: _buildCircles(), + Expanded( + child: Container( + child: Stack( + children: [ + Positioned( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + widget.title ?? Container(), + Container( + margin: const EdgeInsets.only(top: 20), + height: 40, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: _buildCircles(), + ), ), - ), - ], + widget.subtitle ?? Container(), + ], + ), ), ), - ), - widget.bottomWidget != null - ? Positioned( - child: Align( - alignment: Alignment.topCenter, - child: widget.bottomWidget), - ) - : Container() - ], + widget.bottomWidget != null ? widget.bottomWidget! : Container() + ], + ), ), ), - _buildKeyboard(), + Expanded( + child: _buildKeyboardLandscape(), + ), ], ), ), @@ -188,7 +179,16 @@ class _PasscodeScreenState extends State ], ); - _buildKeyboard() => Container( + _buildKeyboardPortrait() => Expanded( + flex: 1, + child: Keyboard( + onKeyboardTap: _onKeyboardButtonPressed, + keyboardUIConfig: widget.keyboardUIConfig, + digits: widget.digits, + ), + ); + + _buildKeyboardLandscape() => Container( child: Keyboard( onKeyboardTap: _onKeyboardButtonPressed, keyboardUIConfig: widget.keyboardUIConfig, @@ -201,14 +201,17 @@ class _PasscodeScreenState extends State var config = widget.circleUIConfig; var extraSize = animation.value; for (int i = 0; i < widget.passwordDigits; i++) { + final isFilled = i < enteredPasscode.length; list.add( Container( margin: EdgeInsets.all(8), - child: Circle( - filled: i < enteredPasscode.length, - circleUIConfig: config, - extraSize: extraSize, - ), + child: widget.circleUIConfig.indicatorBuilder != null + ? widget.circleUIConfig.indicatorBuilder!(isFilled, extraSize) + : Circle( + filled: isFilled, + circleUIConfig: config, + extraSize: extraSize, + ), ), ); } @@ -218,16 +221,24 @@ class _PasscodeScreenState extends State _onDeleteCancelButtonPressed() { if (enteredPasscode.length > 0) { setState(() { - enteredPasscode = - enteredPasscode.substring(0, enteredPasscode.length - 1); + enteredPasscode = enteredPasscode.substring(0, enteredPasscode.length - 1); }); } else { - if (widget.cancelCallback != null) { + if (widget.cancelCallback != null && widget.deleteButton != null && widget.cancelButton != null) { widget.cancelCallback!(); } } } + _onCancelButtonPressed() { + setState(() { + enteredPasscode = ''; + }); + if (widget.cancelCallback != null) { + widget.cancelCallback!(); + } + } + _onKeyboardButtonPressed(String text) { if (text == Keyboard.deleteButton) { _onDeleteCancelButtonPressed(); @@ -249,8 +260,7 @@ class _PasscodeScreenState extends State // in case the stream instance changed, subscribe to the new one if (widget.shouldTriggerVerification != old.shouldTriggerVerification) { streamSubscription.cancel(); - streamSubscription = widget.shouldTriggerVerification - .listen((isValid) => _showValidation(isValid)); + streamSubscription = widget.shouldTriggerVerification.listen((isValid) => _showValidation(isValid)); } } @@ -273,22 +283,42 @@ class _PasscodeScreenState extends State if (widget.isValidCallback != null) { widget.isValidCallback!(); } else { - print( - "You didn't implement validation callback. Please handle a state by yourself then."); + print("You didn't implement validation callback. Please handle a state by yourself then."); } } Widget _buildDeleteButton() { - return Container( - child: CupertinoButton( - onPressed: _onDeleteCancelButtonPressed, - child: Container( - margin: widget.keyboardUIConfig.digitInnerMargin, - child: enteredPasscode.length == 0 - ? widget.cancelButton - : widget.deleteButton, + if (widget.deleteButton != null && widget.cancelButton != null) { + return Container( + child: CupertinoButton( + onPressed: _onDeleteCancelButtonPressed, + child: Container( + margin: widget.keyboardUIConfig.digitInnerMargin, + child: enteredPasscode.length == 0 ? widget.cancelButton : widget.deleteButton, + ), ), - ), - ); + ); + } else if (widget.deleteButton != null) { + return Container( + child: CupertinoButton( + onPressed: _onDeleteCancelButtonPressed, + child: Container( + margin: widget.keyboardUIConfig.digitInnerMargin, + child: widget.deleteButton, + ), + ), + ); + } else if (widget.cancelButton != null) { + return Container( + child: CupertinoButton( + onPressed: _onCancelButtonPressed, + child: Container( + margin: widget.keyboardUIConfig.digitInnerMargin, + child: widget.cancelButton, + ), + ), + ); + } + return Container(); } }