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 animation) { return child; }; } class OTPWidget extends StatefulWidget { final int maxLength; final 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; const 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 createState() { return OTPWidgetState(); } } class OTPWidgetState extends State with SingleTickerProviderStateMixin { AnimationController? _highlightAnimationController; FocusNode? focusNode; String text = ""; int currentIndex = 0; List 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(); _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: [ _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 ? [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 pinCodes = List.generate(widget.maxLength, (int i) { return _buildPinCode(i, context); }); return Row(children: pinCodes, mainAxisSize: MainAxisSize.min); } 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("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 animation) { return child; }, child: Text( text, key: ValueKey("$text$i"), style: widget.pinTextStyle, ), ); } else { return Text( text, key: ValueKey("${strList[i]}$i"), style: widget.pinTextStyle, ); } } }