You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何修复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,
                      ),
                    ),
                  ),
                ],
              ),
            )
        ],
      ),
    );
  }
}

问题原因分析

  1. build方法强制重置光标位置:每次组件重建时,都会将光标强制设置到文本末尾,覆盖用户手动调整的光标位置
  2. onChanged中重复设置文本:onChanged回调里再次给控制器赋值text,触发文本控制器更新,进而重置光标位置
  3. 初始值与控制器联动逻辑混乱:外部控制器传入时,初始值处理未与外部控制器做正确联动,导致状态冲突
  4. 资源管理不规范:未区分内部创建和外部传入的控制器,存在潜在内存泄漏风险

修复后的代码

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 13:37:10