如何修复Flutter自定义TextField的光标行为异常问题
自定义TextField光标跳转问题修复
问题描述
需要实现一个满足以下需求的自定义TextField:
- 支持传入自定义
TextEditingController,用于条码扫描时更新文本 - 配置
FocusNode实现自定义关闭按钮 - 支持传入初始值
但当前实现存在光标总是跳至文本末尾、无法在中间编辑的问题,且移除自定义控制器和初始值后问题消失;同时如果不在build方法中设置selection,TextField会失去焦点无法输入。
原代码如下:
import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:keyboard_actions/keyboard_actions.dart'; import 'package:myapp/app/components/material/icon.view.dart'; import 'package:myapp/app/components/text/text.view.dart'; import 'package:myapp/app/localizations/app_localizations.dart'; import 'package:myapp/theme/colors.extensions.dart'; import 'package:myapp/theme/icons.dart'; import 'package:myapp/theme/styling.extensions.dart'; class CustomTextField extends StatefulWidget { CustomTextField({ super.key, this.initialValue, this.updateValue, this.textEditingController, required this.orientation, }); dynamic initialValue; Function? updateValue; TextEditingController? textEditingController; Orientation orientation; @override State<CustomTextField> createState() => _CustomTextFieldState(); } class _CustomTextFieldState extends State<CustomTextField> { TextEditingController controller = TextEditingController(); FocusNode focusNode = FocusNode(); @override void initState() { controller = TextEditingController(text: widget.initialValue == null ? null : '${widget.initialValue}'); super.initState(); } @override Widget build(BuildContext context) { (widget.textEditingController ?? controller).selection = TextSelection.fromPosition(TextPosition(offset: (widget.textEditingController ?? controller).text.length)); return SizedBox( width: MediaQuery.of(context).size.width, height: 55, child: Stack( children: [ KeyboardActions( tapOutsideBehavior: TapOutsideBehavior.opaqueDismiss, config: KeyboardActionsConfig( keyboardActionsPlatform: KeyboardActionsPlatform.ALL, nextFocus: false, actions: kIsWeb != true ? [ KeyboardActionsItem( focusNode: focusNode, displayArrows: false, toolbarButtons: [ (node) { return GestureDetector( onTap: () { node.unfocus(); }, child: Padding( padding: CustomPadding.p10(), child: CustomText( AppLocalizations.instance.translate('Close'), ), ), ); } ], ), ] : [], ), child: TextField( focusNode: focusNode, enableSuggestions: false, autocorrect: false, controller: widget.textEditingController ?? controller, cursorColor: CustomColors.blue, onChanged: (value) { setState(() { (widget.textEditingController ?? controller).text = value; if (widget.updateValue != null) { widget.updateValue!(value); } }); }, onSubmitted: (value) { FocusManager.instance.primaryFocus?.unfocus(); }, style: CustomTextStyle.custom(fontSize: 24, color: Colors.black), textAlignVertical: TextAlignVertical.top, decoration: InputDecoration( contentPadding: CustomPadding.ltrb(10, 8, 10 + ((widget.textEditingController ?? controller).text.isNotEmpty ? 28 : 0), 8), isDense: true, filled: true, hintStyle: CustomTextStyle.custom(color: CustomColors.darkGrey, fontSize: 20), fillColor: Colors.white, focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: CustomColors.blue, width: 2), borderRadius: BorderRadius.circular(10), ), enabledBorder: OutlineInputBorder( borderSide: BorderSide( color: CustomColors.darkGrey, width: 1.5, ), borderRadius: BorderRadius.circular(10), ), ), ), ), if ((widget.textEditingController ?? controller).text.isNotEmpty) SizedBox( height: 49, child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ Padding( padding: CustomPadding.only10(PaddingPosition.r), child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { HapticFeedback.heavyImpact(); if (widget.updateValue != null) { widget.updateValue!(''); } setState(() { (widget.textEditingController ?? controller).text = ''; }); }, child: CustomIcon( icon: SvgFont.xmark_solid, color: CustomColors.red, ), ), ), ], ), ) ], ), ); } }
问题原因分析
- build方法强制重置光标位置:每次组件重建时,都会将光标强制设置到文本末尾,覆盖用户手动调整的光标位置
- onChanged中重复设置文本:
onChanged回调里再次给控制器赋值text,触发文本控制器更新,进而重置光标位置 - 初始值与控制器联动逻辑混乱:外部控制器传入时,初始值处理未与外部控制器做正确联动,导致状态冲突
- 资源管理不规范:未区分内部创建和外部传入的控制器,存在潜在内存泄漏风险
修复后的代码
import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:keyboard_actions/keyboard_actions.dart'; import 'package:myapp/app/components/material/icon.view.dart'; import 'package:myapp/app/components/text/text.view.dart'; import 'package:myapp/app/localizations/app_localizations.dart'; import 'package:myapp/theme/colors.extensions.dart'; import 'package:myapp/theme/icons.dart'; import 'package:myapp/theme/styling.extensions.dart'; class CustomTextField extends StatefulWidget { CustomTextField({ super.key, this.initialValue, this.updateValue, this.textEditingController, required this.orientation, }); final dynamic initialValue; final Function(String)? updateValue; final TextEditingController? textEditingController; final Orientation orientation; @override State<CustomTextField> createState() => _CustomTextFieldState(); } class _CustomTextFieldState extends State<CustomTextField> { late TextEditingController _controller; final FocusNode _focusNode = FocusNode(); @override void initState() { super.initState(); // 优先使用外部传入的控制器,否则创建内部控制器 if (widget.textEditingController != null) { _controller = widget.textEditingController!; // 外部控制器为空时,设置初始值 if (_controller.text.isEmpty && widget.initialValue != null) { _controller.text = '${widget.initialValue}'; } } else { _controller = TextEditingController( text: widget.initialValue == null ? null : '${widget.initialValue}', ); } } @override void didUpdateWidget(covariant CustomTextField oldWidget) { super.didUpdateWidget(oldWidget); // 外部控制器变化时切换控制器并同步初始值 if (widget.textEditingController != oldWidget.textEditingController) { if (widget.textEditingController != null) { _controller = widget.textEditingController!; if (_controller.text.isEmpty && widget.initialValue != null) { _controller.text = '${widget.initialValue}'; } } else { _controller = TextEditingController( text: widget.initialValue == null ? null : '${widget.initialValue}', ); } } } @override Widget build(BuildContext context) { return SizedBox( width: MediaQuery.of(context).size.width, height: 55, child: Stack( children: [ KeyboardActions( tapOutsideBehavior: TapOutsideBehavior.opaqueDismiss, config: KeyboardActionsConfig( keyboardActionsPlatform: KeyboardActionsPlatform.ALL, nextFocus: false, actions: kIsWeb != true ? [ KeyboardActionsItem( focusNode: _focusNode, displayArrows: false, toolbarButtons: [ (node) { return GestureDetector( onTap: () => node.unfocus(), child: Padding( padding: CustomPadding.p10(), child: CustomText( AppLocalizations.instance.translate('Close'), ), ), ); } ], ), ] : [], ), child: TextField( focusNode: _focusNode, enableSuggestions: false, autocorrect: false, controller: _controller, cursorColor: CustomColors.blue, onChanged: (value) { // 仅通知外部更新,无需手动设置控制器文本 widget.updateValue?.call(value); }, onSubmitted: (value) => _focusNode.unfocus(), style: CustomTextStyle.custom(fontSize: 24, color: Colors.black), textAlignVertical: TextAlignVertical.top, decoration: InputDecoration( contentPadding: CustomPadding.ltrb( 10, 8, 10 + (_controller.text.isNotEmpty ? 28 : 0), 8, ), isDense: true, filled: true, hintStyle: CustomTextStyle.custom( color: CustomColors.darkGrey, fontSize: 20, ), fillColor: Colors.white, focusedBorder: OutlineInputBorder( borderSide: const BorderSide( color: CustomColors.blue, width: 2, ), borderRadius: BorderRadius.circular(10), ), enabledBorder: OutlineInputBorder( borderSide: BorderSide( color: CustomColors.darkGrey, width: 1.5, ), borderRadius: BorderRadius.circular(10), ), ), ), ), if (_controller.text.isNotEmpty) SizedBox( height: 49, child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ Padding( padding: CustomPadding.only10(PaddingPosition.r), child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { HapticFeedback.heavyImpact(); _controller.clear(); widget.updateValue?.call(''); }, child: CustomIcon( icon: SvgFont.xmark_solid, color: CustomColors.red, ), ), ), ], ), ) ], ), ); } @override void dispose() { // 仅销毁内部创建的控制器,外部传入的由调用方管理 if (widget.textEditingController == null) { _controller.dispose(); } _focusNode.dispose(); super.dispose(); } }
关键改动说明
- 移除build中的光标强制设置:彻底解决组件重建时覆盖光标位置的问题,让用户可以自由编辑文本
- 简化onChanged逻辑:
TextField会自动通过控制器同步文本,无需手动赋值,只保留外部回调通知,避免触发不必要的控制器更新 - 优化控制器初始化与更新:
- 优先使用外部传入的
TextEditingController,避免内部控制器和外部控制器冲突 - 在
initState和didUpdateWidget中处理初始值,确保外部控制器为空时正确设置初始文本 - 外部控制器变化时自动切换,保持状态同步
- 优先使用外部传入的
- 修复清空按钮逻辑:直接调用
_controller.clear(),无需setState,操作更高效且不会触发不必要的重建 - 完善资源释放:仅销毁内部创建的控制器,外部传入的由调用方负责销毁,避免内存泄漏
- 优化焦点操作:
onSubmitted中直接使用_focusNode.unfocus(),逻辑更清晰,避免依赖全局焦点管理
内容的提问来源于stack exchange,提问作者Pandruz
相关产品推荐
相关产品推荐

