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'; import 'package:hmg_patient_app/uitl/utils.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; 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 createState() { return OTPWidgetState(); } } class OTPWidgetState extends State with SingleTickerProviderStateMixin { late AnimationController _highlightAnimationController; late 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(); _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( fit: StackFit.loose, 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), ); return Container( width: _width, height: widget.pinBoxHeight, child: TextField( autofocus: !kIsWeb ? widget.autoFocus : false, enableInteractiveSelection: false, focusNode: focusNode, controller: widget.controller, textAlign: TextAlign.center, keyboardType: widget.keyboardType, inputFormatters: widget.keyboardType == TextInputType.number ? [FilteringTextInputFormatter.digitsOnly] : null, style: TextStyle( height: 0.5, 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, isDense: true), 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 SingleChildScrollView( // Add this scrollDirection: Axis.horizontal, // For horizontal scrolling child: Row( children: pinCodes, mainAxisSize: MainAxisSize.min, // Important when inside a scroll view mainAxisAlignment: MainAxisAlignment.start, // Or another suitable alignment crossAxisAlignment: CrossAxisAlignment.center, ), ); } // // Widget _pinBoxRow(BuildContext context) { // _calculateStrList(); // List 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) { final bool isFilled = i < text.length; final bool isCurrent = i == currentIndex; final bool isFocused = hasFocus && isCurrent; // Colors based on state Color bgColor = Colors.white; Color borderColor = widget.defaultBorderColor; Color textColor = Colors.black; if (widget.hasError) { borderColor = widget.errorBorderColor; bgColor = Colors.red.shade50; } else if (isFocused) { borderColor = Colors.transparent; bgColor = Colors.white; textColor = Colors.white; } else if (isFilled) { borderColor = Colors.green; bgColor = Colors.green; textColor = Colors.white; } return Container( key: ValueKey("container$i"), alignment: Alignment.center, margin: widget.pinBoxOuterPadding, padding: EdgeInsets.zero, decoration: BoxDecoration( color: bgColor, border: Border.all(color: borderColor, width: 1), borderRadius: BorderRadius.circular(16), // Rounded edges ), width: widget.pinBoxWidth, height: widget.pinBoxHeight, child: _animatedTextBox( strList[i], i, textColor, ), ); } // 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, // ); // } // } Widget _animatedTextBox(String text, int i, Color textColor) { final bool isFilled = text.isNotEmpty; final double fontSize = isFilled ? 50 : 50; final FontWeight fontWeight = isFilled ? FontWeight.w600 : FontWeight.normal; if (widget.pinTextAnimatedSwitcherTransition != null) { return AnimatedSwitcher( duration: widget.pinTextAnimatedSwitcherDuration, transitionBuilder: widget.pinTextAnimatedSwitcherTransition!, child: Text( text, softWrap: true, key: ValueKey("$text$i"), style: widget.pinTextStyle?.copyWith(color: textColor, fontSize: fontSize, fontWeight: fontWeight, fontFamily: context.fontFamily) ?? TextStyle(color: textColor, fontSize: fontSize, fontWeight: fontWeight, fontFamily: context.fontFamily, ), ), ); } else { return Text( text, softWrap: true, key: ValueKey("${strList[i]}$i"), style: widget.pinTextStyle?.copyWith(color: textColor, fontSize: fontSize, fontWeight: fontWeight, fontFamily: context.fontFamily) ?? TextStyle(color: textColor, fontSize: fontSize, fontWeight: fontWeight, fontFamily: context.fontFamily), ); } } // // Widget _animatedTextBox(String text, int i, Color textColor) { // if (widget.pinTextAnimatedSwitcherTransition != null) { // return AnimatedSwitcher( // duration: widget.pinTextAnimatedSwitcherDuration, // transitionBuilder: widget.pinTextAnimatedSwitcherTransition!, // child: Text( // text, // key: ValueKey("$text$i"), // style: widget.pinTextStyle?.copyWith(color: textColor) ?? // TextStyle(color: textColor, fontSize: 24, fontWeight: FontWeight.bold), // ), // ); // } else { // return Text( // text, // key: ValueKey("${strList[i]}$i"), // style: widget.pinTextStyle?.copyWith(color: textColor) ?? // TextStyle(color: textColor, fontSize: 24, fontWeight: FontWeight.bold), // ); // } // } }