前言:
验证码输入框很常见:处理不好 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, ),