美文网首页Flutter
Flutter 验证码输入框

Flutter 验证码输入框

作者: Cheney2006 | 来源:发表于2020-03-15 17:17 被阅读0次

      在 Flutter 做的一个项目中,要用到一个验证码输入框,在原生应用中很常见,但 Flutter 中资料比较少,就自己简单写个。

      UI 设计效果如下:

    设计规范.png

      分析一下,这个需要自定义一个输入框,输入框自动获焦,并且输入一位密码的时候,输入框就填入一位,且光标自动移到下一位框中,这就需要单独绘制了,系统默认的输入框没办法直接实现。

    实现效果

    device-2020-03-12-140112.gif

    实现思路比较简单,直接看代码就会懂了。

    支持属性

    属性名 作用
    autoFocus 是否获焦
    codeLength 验证码长度
    decoration 下划线样式
    inputFormatter 输入文本校验
    keyboardType 键盘类型
    focusNode 焦点
    textInputAction 用于控制键盘动作

    主要源码

    import 'package:flutter/foundation.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart';
    import 'package:flutter_common_utils/lcfarm_size.dart';
    import 'package:kappa_app/utils/lcfarm_color.dart';
    
    /// @desc 短信验证码输入框
    /// @time 2019-05-14 16:16
    /// @author Cheney
    class LcfarmCodeInput extends StatefulWidget {
      /// The max length of pin.
      final int codeLength;
    
      /// The callback will execute when user click done.
      final ValueChanged<String> onSubmit;
    
      /// Decorate the pin.
      final CodeDecoration decoration;
    
      /// Just like [TextField]'s inputFormatter.
      final List<TextInputFormatter> inputFormatters;
    
      /// Just like [TextField]'s keyboardType.
      final TextInputType keyboardType;
    
      /// Same as [TextField]'s autoFocus.
      final bool autoFocus;
    
      /// Same as [TextField]'s focusNode.
      final FocusNode focusNode;
    
      /// Same as [TextField]'s textInputAction.
      final TextInputAction textInputAction;
    
      LcfarmCodeInput({
        GlobalKey<LcfarmCodeInputState> key,
        this.codeLength = 6,
        this.onSubmit,
        this.decoration = const UnderlineDecoration(),
        List<TextInputFormatter> inputFormatter,
        this.keyboardType = TextInputType.number,
        this.focusNode,
        this.autoFocus = false,
        this.textInputAction = TextInputAction.done,
      })  : inputFormatters = inputFormatter ??
                <TextInputFormatter>[WhitelistingTextInputFormatter.digitsOnly],
            super(key: key);
    
      @override
      State createState() {
        return LcfarmCodeInputState();
      }
    }
    
    class LcfarmCodeInputState extends State<LcfarmCodeInput>
        with SingleTickerProviderStateMixin {
      ///输入监听器
      TextEditingController _controller = TextEditingController();
    
      /// The display text to the user.
      String _text;
    
      AnimationController _animationController;
      Animation<double> _animation;
    
      FocusNode _focusNode;
    
      @override
      void initState() {
        _focusNode = FocusNode();
        _controller.addListener(() {
          setState(() {
            _text = _controller.text;
          });
          submit(_controller.text);
        });
    
        _animationController =
            AnimationController(duration: Duration(milliseconds: 500), vsync: this);
    
        _animation = Tween(begin: 0.0, end: 255.0).animate(_animationController)
          ..addStatusListener((status) {
            if (status == AnimationStatus.completed) {
              //动画执行结束时反向执行动画
              _animationController.reverse();
            } else if (status == AnimationStatus.dismissed) {
              //动画恢复到初始状态时执行动画(正向)
              _animationController.forward();
            }
          })
          ..addListener(() {
            setState(() {});
          });
    
        ///启动动画
        _animationController.forward();
    
        super.initState();
      }
    
      void submit(String text) {
        if (text.length >= widget.codeLength) {
          widget.onSubmit(text.substring(0, widget.codeLength));
          _controller.text = "";
          //外部有传focusNode就直接使用外部的,没有则使用内部定义的
          widget.focusNode == null
              ? _focusNode.unfocus()
              : widget.focusNode.unfocus();
        }
      }
    
      @override
      void dispose() {
        /// Only execute when the controller is autoDispose.
        _controller.dispose();
        _animationController.dispose();
        _focusNode.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return CustomPaint(
          /// The foreground paint to display pin.
          foregroundPainter: _CodePaint(
            text: _text,
            codeLength: widget.codeLength,
            decoration: widget.decoration,
            alpha: _animation.value.toInt(),
          ),
          child: RepaintBoundary(
            child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
              TextField(
                /// Actual textEditingController.
                controller: _controller,
    
                /// Fake the text style.
                style: TextStyle(
                  /// Hide the editing text.
                  color: Colors.transparent,
                ),
    
                /// Hide the Cursor.
                cursorColor: Colors.transparent,
    
                /// Hide the cursor.
                cursorWidth: 0.0,
    
                /// No need to correct the user input.
                autocorrect: false,
    
                /// Center the input to make more natrual.
                textAlign: TextAlign.center,
    
                /// Disable the actual textField selection.
                enableInteractiveSelection: false,
    
                /// The maxLength of the pin input, the default value is 6.
                maxLength: widget.codeLength,
    
                /// If use system keyboard and user click done, it will execute callback
                /// Note!!! Custom keyboard in Android will not execute, see the related issue [https://github.com/flutter/flutter/issues/19027]
                onSubmitted: submit,
    
                /// Default text input type is number.
                keyboardType: widget.keyboardType,
    
                /// only accept digits.
                inputFormatters: widget.inputFormatters,
    
                /// Defines the keyboard focus for this widget.
                focusNode: widget.focusNode == null ? _focusNode : widget.focusNode,
    
                /// {@macro flutter.widgets.editableText.autofocus}
                autofocus: widget.autoFocus,
    
                /// The type of action button to use for the keyboard.
                ///
                /// Defaults to [TextInputAction.done]
                textInputAction: widget.textInputAction,
    
                /// {@macro flutter.widgets.editableText.obscureText}
                /// Default value of the obscureText is false. Make
                obscureText: true,
    
                /// Clear default text decoration.
                decoration: InputDecoration(
                  /// Hide the counterText
                  counterText: '',
                  contentPadding: EdgeInsets.symmetric(vertical: LcfarmSize.dp(24)),
    
                  /// Hide the outline border.
                  border: OutlineInputBorder(
                    borderSide: BorderSide.none,
                  ),
                ),
              ),
            ]),
          ),
        );
      }
    }
    
    class _CodePaint extends CustomPainter {
      String text;
      final int codeLength;
      final double space;
      final CodeDecoration decoration;
      final int alpha;
    
      _CodePaint({
        @required String text,
        @required this.codeLength,
        this.decoration,
        this.space = 4.0,
        this.alpha,
      }) {
        text ??= "";
        this.text = text.trim();
      }
    
      @override
      bool shouldRepaint(CustomPainter oldDelegate) =>
          !(oldDelegate is _CodePaint && oldDelegate.text == this.text);
    
      _drawUnderLine(Canvas canvas, Size size) {
        /// Force convert to [UnderlineDecoration].
        var dr = decoration as UnderlineDecoration;
        Paint underlinePaint = Paint()
          ..color = dr.color
          ..strokeWidth = dr.lineHeight
          ..style = PaintingStyle.stroke
          ..isAntiAlias = true;
    
        var startX = 0.0;
        var startY = size.height;
    
        /// 画下划线
        double singleWidth =
            (size.width - (codeLength - 1) * dr.gapSpace) / codeLength;
    
        for (int i = 0; i < codeLength; i++) {
          if (i == text.length && dr.enteredColor != null) {
            underlinePaint.color = dr.enteredColor;
            underlinePaint.strokeWidth = LcfarmSize.dp(1);
          } else {
            underlinePaint.color = dr.color;
            underlinePaint.strokeWidth = LcfarmSize.dp(0.5);
          }
          canvas.drawLine(Offset(startX, startY),
              Offset(startX + singleWidth, startY), underlinePaint);
          startX += singleWidth + dr.gapSpace;
        }
    
        /// 画文本
        var index = 0;
        startX = 0.0;
        startY = LcfarmSize.dp(28);
    
        /// Determine whether display obscureText.
        bool obscureOn;
        obscureOn = decoration.obscureStyle != null &&
            decoration.obscureStyle.isTextObscure;
    
        /// The text style of pin.
        TextStyle textStyle;
        if (decoration.textStyle == null) {
          textStyle = defaultStyle;
        } else {
          textStyle = decoration.textStyle;
        }
    
        text.runes.forEach((rune) {
          String code;
          if (obscureOn) {
            code = decoration.obscureStyle.obscureText;
          } else {
            code = String.fromCharCode(rune);
          }
          TextPainter textPainter = TextPainter(
            text: TextSpan(
              style: textStyle,
              text: code,
            ),
            textAlign: TextAlign.center,
            textDirection: TextDirection.ltr,
          );
    
          /// Layout the text.
          textPainter.layout();
    
          startX = singleWidth * index +
              singleWidth / 2 -
              textPainter.width / 2 +
              dr.gapSpace * index;
          textPainter.paint(canvas, Offset(startX, startY));
          index++;
        });
    
        ///画光标  如果外部有传,则直接使用外部
        Color cursorColor =
            dr.enteredColor != null ? dr.enteredColor : LcfarmColor.color3776E9;
        cursorColor = cursorColor.withAlpha(alpha);
    
        double cursorWidth = LcfarmSize.dp(1);
        double cursorHeight = LcfarmSize.dp(24);
    
        //LogUtil.v("animation.value=$alpha");
    
        Paint cursorPaint = Paint()
          ..color = cursorColor
          ..strokeWidth = cursorWidth
          ..style = PaintingStyle.stroke
          ..isAntiAlias = true;
    
        startX = text.length * (singleWidth + dr.gapSpace) + singleWidth / 2;
    
        var endX = startX + cursorWidth;
        var endY = startY + cursorHeight;
    //    var endY = size.height - 28.0 - 12;
    //    canvas.drawLine(Offset(startX, startY), Offset(startX, endY), cursorPaint);
        //绘制圆角光标
        Rect rect = Rect.fromLTRB(startX, startY, endX, endY);
        RRect rrect = RRect.fromRectAndRadius(rect, Radius.circular(cursorWidth));
        canvas.drawRRect(rrect, cursorPaint);
      }
    
      @override
      void paint(Canvas canvas, Size size) {
        _drawUnderLine(canvas, size);
      }
    }
    
    /// 默认的样式
    const TextStyle defaultStyle = TextStyle(
      /// Default text color.
      color: LcfarmColor.color80000000,
    
      /// Default text size.
      fontSize: 24.0,
    );
    
    abstract class CodeDecoration {
      /// The style of painting text.
      final TextStyle textStyle;
    
      final ObscureStyle obscureStyle;
    
      const CodeDecoration({
        this.textStyle,
        this.obscureStyle,
      });
    }
    
    /// The object determine the obscure display
    class ObscureStyle {
      /// Determine whether replace [obscureText] with number.
      final bool isTextObscure;
    
      /// The display text when [isTextObscure] is true
      final String obscureText;
    
      const ObscureStyle({
        this.isTextObscure = false,
        this.obscureText = '*',
      }) : assert(obscureText.length == 1);
    }
    
    /// The object determine the underline color etc.
    class UnderlineDecoration extends CodeDecoration {
      /// The space between text and underline.
      final double gapSpace;
    
      /// The color of the underline.
      final Color color;
    
      /// The height of the underline.
      final double lineHeight;
    
      /// The underline changed color when user enter pin.
      final Color enteredColor;
    
      const UnderlineDecoration({
        TextStyle textStyle,
        ObscureStyle obscureStyle,
        this.enteredColor = LcfarmColor.color3776E9,
        this.gapSpace = 15.0,
        this.color = LcfarmColor.color24000000,
        this.lineHeight = 0.5,
      }) : super(
              textStyle: textStyle,
              obscureStyle: obscureStyle,
            );
    }
    
    

    最后

      如果在使用过程遇到问题,欢迎下方留言交流。
      代码地址

    学习资料

    请大家不吝点赞!因为您的点赞是对我最大的鼓励,谢谢!

    相关文章

      网友评论

        本文标题:Flutter 验证码输入框

        本文链接:https://www.haomeiwen.com/subject/hdjyehtx.html