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.
		
		
		
		
		
			
		
			
				
	
	
		
			421 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Dart
		
	
			
		
		
	
	
			421 lines
		
	
	
		
			12 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';
 | 
						|
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<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]);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  // Updated: Always use full screen width for responsive design
 | 
						|
  double _width(BuildContext context) {
 | 
						|
    return MediaQuery.of(context).size.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: <Widget>[
 | 
						|
        _otpTextInput(context),
 | 
						|
        _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 ? <TextInputFormatter>[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();
 | 
						|
 | 
						|
    // Calculate dynamic width for all screen sizes to prevent scrolling
 | 
						|
    double screenWidth = MediaQuery.of(context).size.width;
 | 
						|
    double totalHorizontalPadding = widget.pinBoxOuterPadding.left + widget.pinBoxOuterPadding.right;
 | 
						|
    double totalMargin = widget.maxLength * totalHorizontalPadding;
 | 
						|
    double availableWidth = screenWidth - totalMargin - 32; // 32 for additional safe padding
 | 
						|
    double boxWidth = (availableWidth / widget.maxLength).clamp(60.0, 200.0); // Reasonable limits
 | 
						|
 | 
						|
    List<Widget> pinCodes = List.generate(widget.maxLength, (int i) {
 | 
						|
      return Expanded(
 | 
						|
        child: Container(
 | 
						|
          margin: widget.pinBoxOuterPadding,
 | 
						|
          child: _buildPinCode(i, context, boxWidth),
 | 
						|
        ),
 | 
						|
      );
 | 
						|
    });
 | 
						|
 | 
						|
    return Row(
 | 
						|
      children: pinCodes,
 | 
						|
      mainAxisSize: MainAxisSize.max,
 | 
						|
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
 | 
						|
      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<String>("container$i"),
 | 
						|
      alignment: Alignment.center,
 | 
						|
      padding: EdgeInsets.zero,
 | 
						|
      decoration: BoxDecoration(
 | 
						|
        color: bgColor,
 | 
						|
        border: Border.all(color: borderColor, width: 1),
 | 
						|
        borderRadius: BorderRadius.circular(16),
 | 
						|
      ),
 | 
						|
      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<String>("$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<String>("${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<String>("$text$i"),
 | 
						|
//         style: widget.pinTextStyle?.copyWith(color: textColor) ??
 | 
						|
//             TextStyle(color: textColor, fontSize: 24, fontWeight: FontWeight.bold),
 | 
						|
//       ),
 | 
						|
//     );
 | 
						|
//   } else {
 | 
						|
//     return Text(
 | 
						|
//       text,
 | 
						|
//       key: ValueKey<String>("${strList[i]}$i"),
 | 
						|
//       style: widget.pinTextStyle?.copyWith(color: textColor) ??
 | 
						|
//           TextStyle(color: textColor, fontSize: 24, fontWeight: FontWeight.bold),
 | 
						|
//     );
 | 
						|
//   }
 | 
						|
// }}
 |