You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			378 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Dart
		
	
			
		
		
	
	
			378 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Dart
		
	
| import 'dart:async';
 | |
| 
 | |
| import 'package:flutter/animation.dart';
 | |
| import 'package:flutter/foundation.dart';
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:flutter/rendering.dart';
 | |
| import 'package:flutter/services.dart';
 | |
| 
 | |
| typedef OnDone = void Function(String text);
 | |
| 
 | |
| class ProvidedPinBoxTextAnimation {
 | |
|   static AnimatedSwitcherTransitionBuilder scalingTransition = (child, animation) {
 | |
|     return ScaleTransition(
 | |
|       child: child,
 | |
|       scale: animation,
 | |
|     );
 | |
|   };
 | |
| 
 | |
|   static AnimatedSwitcherTransitionBuilder defaultNoTransition = (Widget child, Animation<double> animation) {
 | |
|     return child;
 | |
|   };
 | |
| }
 | |
| 
 | |
| class OTPWidget extends StatefulWidget {
 | |
|   final int maxLength;
 | |
|   late TextEditingController? controller;
 | |
| 
 | |
|   final Color defaultBorderColor;
 | |
|   final Color pinBoxColor;
 | |
|   final double pinBoxBorderWidth;
 | |
|   final double pinBoxRadius;
 | |
|   final bool hideDefaultKeyboard;
 | |
| 
 | |
|   final TextStyle? pinTextStyle;
 | |
|   final double pinBoxHeight;
 | |
|   final double pinBoxWidth;
 | |
|   final OnDone? onDone;
 | |
|   final bool hasError;
 | |
|   final Color errorBorderColor;
 | |
|   final Color textBorderColor;
 | |
|   final Function(String)? onTextChanged;
 | |
|   final bool autoFocus;
 | |
|   final FocusNode? focusNode;
 | |
|   final AnimatedSwitcherTransitionBuilder? pinTextAnimatedSwitcherTransition;
 | |
|   final Duration pinTextAnimatedSwitcherDuration;
 | |
|   final TextDirection textDirection;
 | |
|   final TextInputType keyboardType;
 | |
|   final EdgeInsets pinBoxOuterPadding;
 | |
| 
 | |
|   OTPWidget({
 | |
|     Key? key,
 | |
|     this.maxLength = 4,
 | |
|     this.controller,
 | |
|     this.pinBoxWidth = 70.0,
 | |
|     this.pinBoxHeight = 70.0,
 | |
|     this.pinTextStyle,
 | |
|     this.onDone,
 | |
|     this.defaultBorderColor = Colors.black,
 | |
|     this.textBorderColor = Colors.black,
 | |
|     this.pinTextAnimatedSwitcherTransition,
 | |
|     this.pinTextAnimatedSwitcherDuration = const Duration(),
 | |
|     this.hasError = false,
 | |
|     this.errorBorderColor = Colors.red,
 | |
|     this.onTextChanged,
 | |
|     this.autoFocus = false,
 | |
|     this.focusNode,
 | |
|     this.textDirection = TextDirection.ltr,
 | |
|     this.keyboardType = TextInputType.number,
 | |
|     this.pinBoxOuterPadding = const EdgeInsets.symmetric(horizontal: 4.0),
 | |
|     this.pinBoxColor = Colors.white,
 | |
|     this.pinBoxBorderWidth = 2.0,
 | |
|     this.pinBoxRadius = 0,
 | |
|     this.hideDefaultKeyboard = false,
 | |
|   }) : super(key: key);
 | |
| 
 | |
|   @override
 | |
|   State<StatefulWidget> createState() {
 | |
|     return OTPWidgetState();
 | |
|   }
 | |
| }
 | |
| 
 | |
| class OTPWidgetState extends State<OTPWidget> with SingleTickerProviderStateMixin {
 | |
|  late  AnimationController _highlightAnimationController;
 | |
|   late FocusNode focusNode;
 | |
|   String text = "";
 | |
|   int currentIndex = 0;
 | |
|   List<String> strList = [];
 | |
|   bool hasFocus = false;
 | |
| 
 | |
|   @override
 | |
|   void didUpdateWidget(OTPWidget oldWidget) {
 | |
|     super.didUpdateWidget(oldWidget);
 | |
|     focusNode = widget.focusNode ?? focusNode;
 | |
| 
 | |
|     if (oldWidget.maxLength < widget.maxLength) {
 | |
|       setState(() {
 | |
|         currentIndex = text.length;
 | |
|       });
 | |
|       widget.controller?.text = text;
 | |
|       widget.controller?.selection = TextSelection.collapsed(offset: text.length);
 | |
|     } else if (oldWidget.maxLength > widget.maxLength && widget.maxLength > 0 && text.length > 0 && text.length > widget.maxLength) {
 | |
|       setState(() {
 | |
|         text = text.substring(0, widget.maxLength);
 | |
|         currentIndex = text.length;
 | |
|       });
 | |
|       widget.controller?.text = text;
 | |
|       widget.controller?.selection = TextSelection.collapsed(offset: text.length);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _calculateStrList() {
 | |
|     if (strList.length > widget.maxLength) {
 | |
|       strList.length = widget.maxLength;
 | |
|     }
 | |
|     while (strList.length < widget.maxLength) {
 | |
|       strList.add("");
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void initState() {
 | |
|     super.initState();
 | |
|     focusNode = widget.focusNode ?? FocusNode();
 | |
|     _highlightAnimationController = AnimationController(vsync: this);
 | |
|     _initTextController();
 | |
|     _calculateStrList();
 | |
|     widget.controller!.addListener(_controllerListener);
 | |
|     focusNode.addListener(_focusListener);
 | |
|   }
 | |
| 
 | |
|   void _controllerListener() {
 | |
|     if (mounted == true) {
 | |
|       setState(() {
 | |
|         _initTextController();
 | |
|       });
 | |
|       var onTextChanged = widget.onTextChanged;
 | |
|       if (onTextChanged != null) {
 | |
|         onTextChanged(widget.controller?.text ?? "");
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   void _focusListener() {
 | |
|     if (mounted == true) {
 | |
|       setState(() {
 | |
|         hasFocus = focusNode?.hasFocus ?? false;
 | |
|       });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   void _initTextController() {
 | |
|     if (widget.controller == null) {
 | |
|       return;
 | |
|     }
 | |
|     strList.clear();
 | |
|     var text = widget.controller?.text ?? "";
 | |
|     if (text.isNotEmpty) {
 | |
|       if (text.length > widget.maxLength) {
 | |
|         throw Exception("TextEditingController length exceeded maxLength!");
 | |
|       }
 | |
|     }
 | |
|     for (var i = 0; i < text.length; i++) {
 | |
|       strList.add(text[i]);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   double get _width {
 | |
|     var width = 0.0;
 | |
|     for (var i = 0; i < widget.maxLength; i++) {
 | |
|       width += widget.pinBoxWidth;
 | |
|       if (i == 0) {
 | |
|         width += widget.pinBoxOuterPadding.left;
 | |
|       } else if (i + 1 == widget.maxLength) {
 | |
|         width += widget.pinBoxOuterPadding.right;
 | |
|       } else {
 | |
|         width += widget.pinBoxOuterPadding.left;
 | |
|       }
 | |
|     }
 | |
|     return width;
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   void dispose() {
 | |
|     if (widget.focusNode == null) {
 | |
|       focusNode.dispose();
 | |
|     } else {
 | |
|       focusNode.removeListener(_focusListener);
 | |
|     }
 | |
|     _highlightAnimationController.dispose();
 | |
|     widget.controller?.removeListener(_controllerListener);
 | |
| 
 | |
|     super.dispose();
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     return Stack(
 | |
|       children: <Widget>[
 | |
|         _otpTextInput(),
 | |
|         _touchPinBoxRow(),
 | |
|       ],
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget _touchPinBoxRow() {
 | |
|     return widget.hideDefaultKeyboard
 | |
|         ? _pinBoxRow(context)
 | |
|         : GestureDetector(
 | |
|             behavior: HitTestBehavior.opaque,
 | |
|             onTap: () {
 | |
|               if (hasFocus) {
 | |
|                 FocusScope.of(context).requestFocus(FocusNode());
 | |
|                 Future.delayed(Duration(milliseconds: 100), () {
 | |
|                   FocusScope.of(context).requestFocus(focusNode);
 | |
|                 });
 | |
|               } else {
 | |
|                 FocusScope.of(context).requestFocus(focusNode);
 | |
|               }
 | |
|             },
 | |
|             child: _pinBoxRow(context),
 | |
|           );
 | |
|   }
 | |
| 
 | |
|   Widget _otpTextInput() {
 | |
|     var transparentBorder = OutlineInputBorder(
 | |
|       borderSide: BorderSide(
 | |
|         color: Colors.transparent,
 | |
|         width: 0.0,
 | |
|       ),
 | |
|     );
 | |
|     return Container(
 | |
|       width: _width,
 | |
|       height: widget.pinBoxHeight,
 | |
|       child: TextField(
 | |
|         autofocus: !kIsWeb ? widget.autoFocus : false,
 | |
|         enableInteractiveSelection: false,
 | |
|         focusNode: focusNode,
 | |
|         controller: widget.controller,
 | |
|         keyboardType: widget.keyboardType,
 | |
|         inputFormatters: widget.keyboardType == TextInputType.number ? <TextInputFormatter>[FilteringTextInputFormatter.digitsOnly] : null,
 | |
|         style: TextStyle(
 | |
|           height: 0.1,
 | |
|           color: Colors.transparent,
 | |
|         ),
 | |
|         decoration: InputDecoration(
 | |
|           contentPadding: EdgeInsets.all(0),
 | |
|           focusedErrorBorder: transparentBorder,
 | |
|           errorBorder: transparentBorder,
 | |
|           disabledBorder: transparentBorder,
 | |
|           enabledBorder: transparentBorder,
 | |
|           focusedBorder: transparentBorder,
 | |
|           counterText: null,
 | |
|           counterStyle: null,
 | |
|           helperStyle: TextStyle(
 | |
|             height: 0.0,
 | |
|             color: Colors.transparent,
 | |
|           ),
 | |
|           labelStyle: TextStyle(height: 0.1),
 | |
|           fillColor: Colors.transparent,
 | |
|           border: InputBorder.none,
 | |
|         ),
 | |
|         cursorColor: Colors.transparent,
 | |
|         showCursor: false,
 | |
|         maxLength: widget.maxLength,
 | |
|         onChanged: _onTextChanged,
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   void _onTextChanged(text) {
 | |
|     var onTextChanged = widget.onTextChanged;
 | |
|     if (onTextChanged != null) {
 | |
|       onTextChanged(text);
 | |
|     }
 | |
|     setState(() {
 | |
|       this.text = text;
 | |
|       if (text.length >= currentIndex) {
 | |
|         for (int i = currentIndex; i < text.length; i++) {
 | |
|           strList[i] = text[i];
 | |
|         }
 | |
|       }
 | |
|       currentIndex = text.length;
 | |
|     });
 | |
|     if (text.length == widget.maxLength) {
 | |
|       FocusScope.of(context).requestFocus(FocusNode());
 | |
|       var onDone = widget.onDone;
 | |
|       if (onDone != null) {
 | |
|         onDone(text);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   Widget _pinBoxRow(BuildContext context) {
 | |
|     _calculateStrList();
 | |
|     List<Widget> pinCodes = List.generate(widget.maxLength, (int i) {
 | |
|       return _buildPinCode(i, context);
 | |
|     });
 | |
|     return Row(
 | |
|       children: pinCodes,
 | |
|       mainAxisSize: MainAxisSize.min,
 | |
|       mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget _buildPinCode(int i, BuildContext context) {
 | |
|     Color borderColor;
 | |
|     Color pinBoxColor = widget.pinBoxColor;
 | |
| 
 | |
|     if (widget.hasError) {
 | |
|       borderColor = widget.errorBorderColor;
 | |
|     } else if (i < text.length) {
 | |
|       borderColor = widget.textBorderColor;
 | |
|     } else {
 | |
|       borderColor = widget.defaultBorderColor;
 | |
|       pinBoxColor = widget.pinBoxColor;
 | |
|     }
 | |
| 
 | |
|     EdgeInsets insets;
 | |
|     if (i == 0) {
 | |
|       insets = EdgeInsets.only(
 | |
|         left: 0,
 | |
|         top: widget.pinBoxOuterPadding.top,
 | |
|         right: widget.pinBoxOuterPadding.right,
 | |
|         bottom: widget.pinBoxOuterPadding.bottom,
 | |
|       );
 | |
|     } else if (i == strList.length - 1) {
 | |
|       insets = EdgeInsets.only(
 | |
|         left: widget.pinBoxOuterPadding.left,
 | |
|         top: widget.pinBoxOuterPadding.top,
 | |
|         right: 0,
 | |
|         bottom: widget.pinBoxOuterPadding.bottom,
 | |
|       );
 | |
|     } else {
 | |
|       insets = widget.pinBoxOuterPadding;
 | |
|     }
 | |
|     return Container(
 | |
|       key: ValueKey<String>("container$i"),
 | |
|       alignment: Alignment.center,
 | |
|       padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 1.0),
 | |
|       margin: insets,
 | |
|       child: _animatedTextBox(strList[i], i),
 | |
|       decoration: BoxDecoration(
 | |
|         border: Border.all(
 | |
|           color: borderColor,
 | |
|           width: widget.pinBoxBorderWidth,
 | |
|         ),
 | |
|         color: pinBoxColor,
 | |
|         borderRadius: BorderRadius.circular(widget.pinBoxRadius),
 | |
|       ),
 | |
|       width: widget.pinBoxWidth,
 | |
|       height: widget.pinBoxHeight,
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   Widget _animatedTextBox(String text, int i) {
 | |
|     if (widget.pinTextAnimatedSwitcherTransition != null) {
 | |
|       return AnimatedSwitcher(
 | |
|         duration: widget.pinTextAnimatedSwitcherDuration,
 | |
|         transitionBuilder: widget.pinTextAnimatedSwitcherTransition ??
 | |
|             (Widget child, Animation<double> animation) {
 | |
|               return child;
 | |
|             },
 | |
|         child: Text(
 | |
|           text,
 | |
|           key: ValueKey<String>("$text$i"),
 | |
|           style: widget.pinTextStyle,
 | |
|         ),
 | |
|       );
 | |
|     } else {
 | |
|       return Text(
 | |
|         text,
 | |
|         key: ValueKey<String>("${strList[i]}$i"),
 | |
|         style: widget.pinTextStyle,
 | |
|       );
 | |
|     }
 | |
|   }
 | |
| }
 |