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]); } } // Updated: Accept context to access MediaQuery double _width(BuildContext context) { // For tablets, use full screen width if (MediaQuery.of(context).size.shortestSide >= 600) { return MediaQuery.of(context).size.width; } // For phones, use original fixed 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: [ _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), ); } // Updated: Accept context for width calculation Widget _otpTextInput(BuildContext context) { var transparentBorder = OutlineInputBorder( borderSide: BorderSide(color: Colors.transparent, width: 0), ); return Container( width: _width(context), // Use dynamic 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(); // Determine if it's a tablet bool isTablet = MediaQuery.of(context).size.shortestSide >= 600; double boxWidth = widget.pinBoxWidth; // Calculate dynamic width for tablets if (isTablet) { double screenWidth = MediaQuery.of(context).size.width; double totalMargin = (widget.maxLength - 1) * (widget.pinBoxOuterPadding.left + widget.pinBoxOuterPadding.right); double availableWidth = screenWidth - totalMargin; boxWidth = (availableWidth / widget.maxLength).clamp(100.0, 180.0); // Reasonable limits } List pinCodes = List.generate(widget.maxLength, (int i) { return _buildPinCode(i, context, boxWidth); // Pass dynamic width }); return SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: pinCodes, mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, ), ); } // Updated: Accept dynamic width Widget _buildPinCode(int i, BuildContext context, double boxWidth) { final bool isFilled = i < text.length && strList[i].isNotEmpty; // Check both conditions final bool isCurrent = i == currentIndex; final bool isFocused = hasFocus && isCurrent; Color bgColor = Colors.white; Color borderColor = Colors.white; 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.black; // Changed from white to black for better visibility } else if (isFilled) { borderColor = Colors.green; bgColor = Colors.green; textColor = Colors.white; } // When cleared/empty, colors remain as default initialized above 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), ), width: boxWidth, // Use dynamic width height: widget.pinBoxHeight, child: _animatedTextBox( strList[i], i, textColor, ), ); } 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), // ); // } // }}