Flutter 验证码输入框

前言:

验证码输入框很常见:处理不好 bug也会比较多 想实现方法很多,这里列举一种完美方式,完美兼容 软键盘粘贴方式

效果如下:

之前使用 uniapp 的方式实现过一次 两种方式(原理相同):

input 验证码 密码 输入框_input密码输入框-****博客文章浏览阅读3.9k次,点赞3次,收藏6次。前言:uniapp 在做需求的时候,经常会遇到;验证码输入框 或者 密码输框 自定义样式输入框 或者 格式化显示 银行卡 手机号码等等:这里总结了两种 常用的实现方式;从这两种实现方式 其实也能延伸出其他的显示 方式;先看样式: 自己实现 光标闪烁动画第一种:可以识别 获得焦点 失去焦点第一种实现的思路: 实际上就是,下层的真实 input 负责响应系统的输入,上面一层负责显示 应为输入框在手机端会 出现长按 学着 复制等等 输入框自带属..._input密码输入框https://blog.****.net/nicepainkiller/article/details/124384995?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522171723341916800226511048%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=171723341916800226511048&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-1-124384995-null-null.nonecase&utm_term=input&spm=1018.2226.3001.4450

实现原理拆解:

输入框区域我们分割成两层:

  • 6个黄色的区域 仅仅做展示,中间的黑色是一个动画 模拟光标闪烁 或者 展示 输入的数字
  • 最上层盖一个 输入框控件 接收输入事件,设置透明度 0.00001,设置不支持长按 选取复制,仅仅支持数字

这样一来就很明了, 逻辑也很简单

 具体实现:

  • 要实现 软键盘的 填充事件,所以我们需要动态监听 输入事件
    
    @override
    void initState() {
      // TODO: implement initState
      super.initState();
      // 自动弹出软键盘
      Future.delayed(Duration.zero, () {
        FocusScope.of(context).requestFocus(_focusNode);
      });
      // 监听粘贴事件
      _textEditingController.addListener(() {
        if (Clipboard.getData('text/plain') != null) {
          Clipboard.getData('text/plain').then((value) {
            if (value != null && value.text != null) {
              if (value.text!.isNotEmpty && value.text!.length == 6) {
                if (RegExp(AppRegular.numberAll).firstMatch(value.text!) !=
                    null) {
                  _textEditingController.text = value!.text!;
                  //取完值 置为 null
                  Clipboard.setData(const ClipboardData(text: ''));
                  //设置输入框光标到末尾 防止某些情况下 光标跑到前面,键盘无法删除输入字符
                  _textEditingController.selection = TextSelection.fromPosition(
                    TextPosition(offset: _textEditingController.text.length),
                  );
                }
              }
            }
          });
        }
        setState(() {
          _arrayCode = List<String>.filled(widget.length, '');
          for (int i = 0; i < _textEditingController.value.text.length; i++) {
            _arrayCode[i] = _textEditingController.value.text.substring(i, i + 1);
          }
        });
        if (_textEditingController.value.text.length == 6) {
          //防止重复触发 回调事件
          if (!_triggerState) {
            _triggerState = true;
            AppScreen.showToast('输入完成:${_textEditingController.value.text}');
            widget.onComplete(_textEditingController.value.text);
          }
        } else {
          _triggerState = false;
        }
      });
    }
  • 输入框的设置,禁止长按

    child: TextField(
      enableInteractiveSelection: false, // 禁用长按复制功
      maxLength: widget.length,
      focusNode: _focusNode,
      maxLines: 1,
      controller: _textEditingController,
      style: AppTextStyle.textStyle_32_333333,
      inputFormatters: [InputFormatter(AppRegular.numberAll)],
      decoration: const InputDecoration(
        focusedBorder: OutlineInputBorder(
            borderSide:
                BorderSide(width: 0, color: Colors.transparent)),
        disabledBorder: OutlineInputBorder(
            borderSide:
                BorderSide(width: 0, color: Colors.transparent)),
        enabledBorder: OutlineInputBorder(
            borderSide:
                BorderSide(width: 0, color: Colors.transparent)),
        border: OutlineInputBorder(
            borderSide:
                BorderSide(width: 0, color: Colors.transparent)),
        counterText: '', //取消文字计数器
      ),
    )
  • 页面动画的展示,FadeTransition 为了性能优化到我们动画缩小到最小范围

    class InputFocusWidget extends StatefulWidget {
      const InputFocusWidget({Key? key}) : super(key: key);
      @override
      State<InputFocusWidget> createState() => _InputFocusWidgetState();
    }
    
    class _InputFocusWidgetState extends State<InputFocusWidget>
        with TickerProviderStateMixin {
      late AnimationController controller;
      late Animation<double> animation;
    
      @override
      void initState() {
        // TODO: implement initState
        super.initState();
        controller = AnimationController(
            duration: const Duration(milliseconds: 600), vsync: this);
        animation = CurvedAnimation(parent: controller, curve: Curves.easeIn);
        controller.repeat(min: 0, max: 1, reverse: true);
      }
    
      @override
      void dispose() {
        controller.dispose();
        // TODO: implement dispose
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return FadeTransition(
          opacity: animation,
          child: Container(
            color: Colors.green,
            width: double.infinity,
            height: double.infinity,
          ),
        );
      }
    }

完整代码:

 因为里面使用到我自己封装的一些工具,用的时候需要你转成自己的

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:game/utils/app_screen.dart';
import 'package:game/wrap/extension/extension.dart';
import 'package:game/wrap/overlay/app_overlay.dart';

import '../const/app_regular.dart';
import '../const/app_textStyle.dart';
import 'input_formatter.dart';

class InputWithCode extends StatefulWidget {
  final int length;
  final ValueChanged<String> onComplete;
  const InputWithCode(
      {required this.length, required this.onComplete, Key? key})
      : super(key: key);

  @override
  State<InputWithCode> createState() => _InputWithCodeState();
}

class _InputWithCodeState extends State<InputWithCode> {
  final TextEditingController _textEditingController = TextEditingController();
  bool _triggerState = false;
  late List<String> _arrayCode = List<String>.filled(widget.length, '');
  final FocusNode _focusNode = FocusNode();

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    // 自动弹出软键盘
    Future.delayed(Duration.zero, () {
      FocusScope.of(context).requestFocus(_focusNode);
    });
    // 监听粘贴事件
    _textEditingController.addListener(() {
      if (Clipboard.getData('text/plain') != null) {
        Clipboard.getData('text/plain').then((value) {
          if (value != null && value.text != null) {
            if (value.text!.isNotEmpty && value.text!.length == 6) {
              if (RegExp(AppRegular.numberAll).firstMatch(value.text!) !=
                  null) {
                _textEditingController.text = value!.text!;
                Clipboard.setData(const ClipboardData(text: ''));
                _textEditingController.selection = TextSelection.fromPosition(
                  TextPosition(offset: _textEditingController.text.length),
                );
              }
            }
          }
        });
      }
      setState(() {
        _arrayCode = List<String>.filled(widget.length, '');
        for (int i = 0; i < _textEditingController.value.text.length; i++) {
          _arrayCode[i] = _textEditingController.value.text.substring(i, i + 1);
        }
      });
      if (_textEditingController.value.text.length == 6) {
        if (!_triggerState) {
          _triggerState = true;
          AppScreen.showToast('输入完成:${_textEditingController.value.text}');
          widget.onComplete(_textEditingController.value.text);
        }
      } else {
        _triggerState = false;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      height: double.infinity,
      child: Stack(
        children: [
          Center(
            child: Row(
              children: _arrayCode
                  .asMap()
                  .map(
                    (index, value) => MapEntry(
                      index,
                      Container(
                        width: 80.cale,
                        height: 80.cale,
                        margin: EdgeInsets.symmetric(horizontal: 10.cale),
                        decoration: BoxDecoration(
                          border: Border(
                            bottom: BorderSide(
                              width: 3.cale,
                              color: value != ''
                                  ? Colors.amberAccent
                                  : Colors.amberAccent.withOpacity(0.5),
                            ),
                          ),
                        ),
                        child: index != _textEditingController.value.text.length
                            ? Center(
                                child: Text(
                                  value,
                                  style: AppTextStyle.textStyle_40_1A1A1A_Bold,
                                ),
                              )
                            : Center(
                                child: SizedBox(
                                  width: 3.cale,
                                  height: 40.cale,
                                  child: const InputFocusWidget(),
                                ),
                              ),
                      ),
                    ),
                  )
                  .values
                  .toList(),
            ),
          ),
          Opacity(
            opacity: 0.0001,
            child: SizedBox(
              height: double.infinity,
              width: double.infinity,
              child: TextField(
                enableInteractiveSelection: false, // 禁用长按复制功
                maxLength: widget.length,
                focusNode: _focusNode,
                maxLines: 1,
                controller: _textEditingController,
                style: AppTextStyle.textStyle_32_333333,
                inputFormatters: [InputFormatter(AppRegular.numberAll)],
                decoration: const InputDecoration(
                  focusedBorder: OutlineInputBorder(
                      borderSide:
                          BorderSide(width: 0, color: Colors.transparent)),
                  disabledBorder: OutlineInputBorder(
                      borderSide:
                          BorderSide(width: 0, color: Colors.transparent)),
                  enabledBorder: OutlineInputBorder(
                      borderSide:
                          BorderSide(width: 0, color: Colors.transparent)),
                  border: OutlineInputBorder(
                      borderSide:
                          BorderSide(width: 0, color: Colors.transparent)),
                  counterText: '', //取消文字计数器
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class InputFocusWidget extends StatefulWidget {
  const InputFocusWidget({Key? key}) : super(key: key);
  @override
  State<InputFocusWidget> createState() => _InputFocusWidgetState();
}

class _InputFocusWidgetState extends State<InputFocusWidget>
    with TickerProviderStateMixin {
  late AnimationController controller;
  late Animation<double> animation;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    controller = AnimationController(
        duration: const Duration(milliseconds: 600), vsync: this);
    animation = CurvedAnimation(parent: controller, curve: Curves.easeIn);
    controller.repeat(min: 0, max: 1, reverse: true);
  }

  @override
  void dispose() {
    controller.dispose();
    // TODO: implement dispose
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return FadeTransition(
      opacity: animation,
      child: Container(
        color: Colors.green,
        width: double.infinity,
        height: double.infinity,
      ),
    );
  }
}
使用:
  •  控件名称:InputWithCode
  •  length:验证码长度
  • onComplete: 输入完成回调
Container(
  child: InputWithCode(
    length: 6,
    onComplete: (code) => {
      print('InputWithCode:$code'),
    },
  ),
  width: double.infinity,
  height: 200.cale,
),
上一篇:Spark SQL数据源 - Parquet文件


下一篇:c语言多进程编程实例:深度探索与实用技巧