Flutter Form表单切换输入框后所有输入框光标持续闪烁异常
Flutter Form表单切换后所有输入框光标持续闪烁问题修复
问题现象
应用内常规Form表单页面初始加载时,无输入框聚焦、无光标闪烁异常。但依次切换输入框(无论是否填写内容)后,所有输入框都会出现光标持续闪烁的问题,预期效果为仅当前聚焦的输入框显示闪烁光标,异常效果如下:
异常表单实现代码如下:
class RegisterView extends StatefulWidget { const RegisterView({Key? key}) : super(key: key); @override State<RegisterView> createState() => _RegisterViewState(); } class _RegisterViewState extends State<RegisterView> { final _formKey = GlobalKey<FormState>(); final FocusScopeNode _node = FocusScopeNode(); String name = ''; String email = ''; String nif = ''; String password = ''; String _errorMessage = ''; TextEditingController nameCtrl = TextEditingController(); TextEditingController emailCtrl = TextEditingController(); TextEditingController nifCtrl = TextEditingController(); TextEditingController passwordCtrl = TextEditingController(); @override void dispose() { _node.dispose(); super.dispose(); } @override Widget build(BuildContext context) { UserProvider userProvider = Provider.of<UserProvider>(context, listen: false); return Scaffold( appBar: AppBar( leading: const CustomBackButton(), title: const Text('Register', style: TextStyle( color: kPrimaryColor, fontSize: 16.0, fontWeight: FontWeight.bold)), centerTitle: true, ), body: GestureDetector( onTap: () { FocusScope.of(context).requestFocus(FocusNode()); }, child: SingleChildScrollView( child: Form( key: _formKey, child: FocusScope( node: _node, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 10.0), child: Column( children: [ const Padding( padding: EdgeInsets.symmetric(vertical: 15.0), child: Text( 'Please set your personal data:', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), ), Padding( padding: const EdgeInsets.only(bottom: 10.0), child: Row( children: [ Text(_errorMessage, style: const TextStyle(color: Colors.red)), ], ), ), TextFormField( controller: nameCtrl, keyboardType: TextInputType.number, decoration: InputDecoration( filled: true, fillColor: Colors.grey[250], contentPadding: const EdgeInsets.all(10.0), hintText: 'Name', border: OutlineInputBorder( borderRadius: BorderRadius.circular(15), borderSide: const BorderSide( color: kPrimaryColor, width: 2, ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(15), borderSide: const BorderSide( color: kPrimaryColor, width: 2, ), ), ), onChanged: (String data) { setState(() { name = data; }); }, onEditingComplete: _node.nextFocus, ), const SizedBox(height: 15), TextFormField( controller: emailCtrl, keyboardType: TextInputType.number, decoration: InputDecoration( filled: true, fillColor: Colors.grey[250], contentPadding: const EdgeInsets.all(10.0), hintText: 'Email', border: OutlineInputBorder( borderRadius: BorderRadius.circular(15), borderSide: const BorderSide( color: kPrimaryColor, width: 2, ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(15), borderSide: const BorderSide( color: kPrimaryColor, width: 2, ), ), ), onChanged: (String data) { setState(() { email = data; }); }, validator: (String? value) { if (value!.isEmpty) { return 'Required'; } else if (!emailRegex.hasMatch(value)) { return 'Please set a valid email'; } }, onEditingComplete: _node.nextFocus, ), const SizedBox(height: 15), TextFormField( controller: nifCtrl, keyboardType: TextInputType.number, decoration: InputDecoration( filled: true, fillColor: Colors.grey[250], contentPadding: const EdgeInsets.all(10.0), hintText: 'NIF', border: OutlineInputBorder( borderRadius: BorderRadius.circular(15), borderSide: const BorderSide( color: kPrimaryColor, width: 2, ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(15), borderSide: const BorderSide( color: kPrimaryColor, width: 2, ), ), ), onChanged: (String data) { setState(() { nif = data; }); }, onEditingComplete: _node.nextFocus, ), const SizedBox(height: 15), TextFormField( controller: passwordCtrl, keyboardType: TextInputType.number, decoration: InputDecoration( filled: true, fillColor: Colors.grey[250], contentPadding: const EdgeInsets.all(10.0), hintText: 'Password', border: OutlineInputBorder( borderRadius: BorderRadius.circular(15), borderSide: const BorderSide( color: kPrimaryColor, width: 2, ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(15), borderSide: const BorderSide( color: kPrimaryColor, width: 2, ), ), ), onChanged: (String data) { setState(() { password = data; }); }, validator: (String? value) { if (value!.isEmpty) { return 'Campo obrigatório'; } else if (value.length < 6) { return 'Password deve conter pelo menos 6 caracteres'; } }, onEditingComplete: _node.nextFocus, ), const SizedBox(height: 15), RoundedButton( bgColor: kPrimaryColor, padding: const EdgeInsets.symmetric( vertical: 10.0, horizontal: 10), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ userProvider.isLoading ? const SpinKitFadingCircle( color: Colors.white, size: 25.0, ) : const Text( 'Save', style: TextStyle( fontSize: 16, color: Colors.white, fontWeight: FontWeight.w700, ), ), ], ), onPressed: () { if (_formKey.currentState!.validate()) { print('Ok to register'); } }, ) ], ), ), ), ), ), ), ); } }
问题原因
手动创建FocusScopeNode管理焦点时,点击空白区域失焦调用的是根FocusScope的requestFocus方法,没有正确解绑自定义FocusScope下所有输入框的焦点状态,导致Flutter框架错误判定多个输入框同时处于可编辑激活状态,最终出现多光标同时闪烁的渲染异常。
修复方案
- 方案1:统一使用自定义的FocusScopeNode处理失焦
把空白区域点击的失焦逻辑改为调用持有的_node对象解绑焦点,修改GestureDetector的onTap回调:onTap: () { _node.unfocus(); // 若unfocus后仍有异常,可追加这行强制取消焦点 // _node.requestFocus(FocusNode()); }, - 方案2:移除手动创建的FocusScopeNode,直接使用Flutter自动注入的FocusScope
无特殊焦点管理需求时,可删掉手动实例化的_node相关代码,所有焦点操作统一用FocusScope.of(context)完成:- 删除
final FocusScopeNode _node = FocusScopeNode();声明 - 删除
dispose方法里的_node.dispose(); - 移除包裹表单内容的
FocusScope组件 - 把所有
onEditingComplete: _node.nextFocus改为onEditingComplete: () => FocusScope.of(context).nextFocus() - 空白区域点击失焦逻辑保留原有写法即可正常工作
- 删除
额外优化点
代码里所有输入框的keyboardType都设置成了TextInputType.number,但姓名、邮箱、密码字段都不是纯数字输入场景,需要对应修改为正确的键盘类型:
- 姓名字段:
TextInputType.text - 邮箱字段:
TextInputType.emailAddress - 密码字段:
TextInputType.visiblePassword配合obscureText: true
内容的提问来源于stack exchange,提问作者JS_LnMstr
相关产品推荐
相关产品推荐

