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
		
	
|   
											2 months ago
										 | 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, | ||
|  |       ); | ||
|  |     } | ||
|  |   } | ||
|  | } |