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.
		
		
		
		
		
			
		
			
				
	
	
		
			374 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Dart
		
	
			
		
		
	
	
			374 lines
		
	
	
		
			10 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';
 | 
						|
 | 
						|
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;
 | 
						|
  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<StatefulWidget> createState() {
 | 
						|
    return OTPWidgetState();
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class OTPWidgetState extends State<OTPWidget> with SingleTickerProviderStateMixin {
 | 
						|
  AnimationController? _highlightAnimationController;
 | 
						|
  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);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  void 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: <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);
 | 
						|
  }
 | 
						|
 | 
						|
  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,
 | 
						|
      );
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 |