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

Flutter自定义TextEditingController选中文本触发范围断言错误

问题描述

我有一个金额输入TextField,使用自定义的AmountBigController,该控制器会在视觉上显示金额符号(例如200 $)。当尝试选中全部文本(如|2 000 $|,|为选中标注)时,抛出断言错误。曾尝试通过输入格式化器处理,但金额符号需要使用不同的文本样式,因此无法解决。

报错信息

════════ 手势捕获到异常 ═══════════════════════════════════════════
Range end 12 is out of text of length 7
'package:flutter/src/services/text_input.dart':
Failed assertion: line 968 pos 12: 'range.end >= 0 && range.end <= text.length'
═══════════════════════════════════════════════════════════════════════════════

[AppMetricaPlugin.MainFacade] error caught by handler Range end 10 is out of text of length 7
[AppMetricaPlugin.MainFacade] _AssertionError ('package:flutter/src/services/text_input.dart': Failed assertion: line 968 pos 12: 'range.end >= 0 && range.end <= text.length': Range end 10 is out of text of length 7)
[AppMetricaPlugin.MainFacade] #0      _AssertionError._doThrowNew (dart:core-patch/errors_patch.dart:51:61)
                              #1      _AssertionError._throwNew (dart:core-patch/errors_patch.dart:40:5)
                              #2      TextEditingValue._textRangeIsValid (package:flutter/src/services/text_input.dart:968:12)
                              #3      TextEditingValue.toJSON (package:flutter/src/services/text_input.dart:921:12)
                              #4      _PlatformTextInputControl.setEditingState (package:flutter/src/services/text_input.dart:2276:13)
                              #5      TextInput._setEditingState (package:flutter/src/services/text_input.dart:1957:15)
                              #6      TextInputConnection.setEditingState (package:flutter/src/services/text_input.dart:1381:25)
                              #7      EditableTextState._updateRemoteEditingValueIfNeeded (package:flutter/src/widgets/editable_text.dart:3379:27)
                              #8      EditableTextState.endBatchEdit (package:flutter/src/widgets/editable_text.dart:3368:5)
                              #9      EditableTextState._formatAndSetValue (package:flutter/src/widgets/editable_text.dart:3919:5)
                              #10     EditableTextState.userUpdateTextEditingValue (package:flutter/src/widgets/editable_text.dart:4271:5)
                              #11     TextSelectionOverlay._handleSelectionHandleChanged (package:flutter/src/widgets/text_selection.dart:900:23)
                              #12     TextSelectionOverlay._handleSelectionEndHandleDragUpdate (package:flutter/src/widgets/text_selection.dart:776:5)
                              #13     SelectionOverlay._handleEndHandleDragUpdate (package:flutter/src/widgets/text_selection.dart:1212:28)
                              #14     DragGestureRecognizer._checkUpdate.<anonymous closure> (package:flutter/src/gestures/monodrag.dart:581:55)
                              #15     GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:315:24)
                              #16     DragGestureRecognizer._checkUpdate (package:flutter/src/gestures/monodrag.dart:581:7)
                              #17     DragGestureRecognizer.handleEvent (package:flutter/src/gestures/monodrag.dart:422:9)
                              #18     PointerRouter._dispatch (package:flutter/src/gestures/pointer_router.dart:98:12)
                              #19     PointerRouter._dispatchEventToRoutes.<anonymous closure> (package:flutter/src/gestures/pointer_router.dart:143:9)
                              #20     _LinkedHashMapMixin.forEach (dart:collection-patch/compact_hash.dart:633:13)
                              #21     PointerRouter._dispatchEventToRoutes (package:flutter/src/gestures/pointer_router.dart:141:18)
                              #22     PointerRouter.route (package:flutter/src/gestures/pointer_router.dart:127:7)
                              #23     GestureBinding.handleEvent (package:flutter/src/gestures/binding.dart:495:19)
                              #24     GestureBinding.dispatchEvent (package:flutter/src/gestures/binding.dart:475:22)
                              #25     RendererBinding.dispatchEvent (package:flutter/src/rendering/binding.dart:430:11)
                              #26     GestureBinding._handlePointerEventImmediately (package:flutter/src/gestures/binding.dart:420:7)
                              #27     GestureBinding.handlePointerEvent (package:flutter/src/gestures/binding.dart:383:5)
                              #28     GestureBinding._flushPointerEventQueue (package:flutter/src/gestures/binding.dart:330:7)
                              #29     GestureBinding._handlePointerDataPacket (package:flutter/src/gestures/binding.dart:299:9)
                              #30     _rootRunUnary (dart:async/zone.dart:1415:13)
                              #31     _CustomZone.runUnary (dart:async/zone.dart:1308:19)
                              #32     _CustomZone.runUnaryGuarded (dart:async/zone.dart:1217:7)
                              #33     _invoke1 (dart:ui/hooks.dart:330:10)
                              #34     PlatformDispatcher._dispatchPointerDataPacket (dart:ui/platform_dispatcher.dart:429:7)
                              #35     _dispatchPointerDataPacket (dart:ui/hooks.dart:262:31)

自定义TextEditingController代码

class AmountBigController extends TextEditingController {
  AmountBigController({
    required this.symbol,
    required this.focusNode,
    this.includeSymbolInHint = true,
    String? hintText,
    super.text = '',
  }) : _hintText = hintText;

  final String symbol;
  final FocusNode focusNode;
  final bool includeSymbolInHint;
  String? _hintText;

  void setHintText(String text) {
    _hintText = text;
    notifyListeners();
  }

  @override
  void clear() {
    value = const TextEditingValue(
      text: '',
      selection: TextSelection.collapsed(offset: 0),
    );
  }

  @override
  TextSpan buildTextSpan({
    required BuildContext context,
    required bool withComposing,
    TextStyle? style,
  }) {
    final List<TextSpan> children = [];
    final isHintShown = focusNode.hasFocus && value.text.isEmpty;

    final TextStyle? textStyle =
        isHintShown ? Typographies.textMediumHelper : style;

    late final TextSpan symbolTextSpan;
    if (isHintShown) {
      if (includeSymbolInHint) {
        symbolTextSpan = TextSpan(
          text: symbol,
          style: textStyle,
        );
      } else {
        symbolTextSpan = TextSpan(
          text: '',
          style: textStyle,
        );
      }
    } else {
      symbolTextSpan = TextSpan(
        text: symbol,
        style: textStyle,
      );
    }

    if (text.isEmpty) {
      if (focusNode.hasFocus) {
        children.insert(
          0,
          TextSpan(text: _hintText, style: Typographies.textMediumHelper),
        );
      } else {
        children.insert(
          0,
          TextSpan(
            text: '0',
            style: style,
          ),
        );
      }
    } else {
      children.add(TextSpan(style: style, text: text));
    }

    children.insert(children.length, symbolTextSpan);
    return TextSpan(children: children);
  }
}

问题原因与解决方案

问题根源

控制器内部的text属性仅存储用户输入的金额数字,而buildTextSpan中额外添加了金额符号,导致视觉上的文本长度(数字+符号)大于实际存储的文本长度(仅数字)。当用户尝试选中视觉上的全部文本时,生成的选择范围会包含符号的长度,超出了实际text的长度限制,触发断言错误。

修复方案

重写控制器的value setter,确保选择范围始终不超过实际text的长度;同时在buildTextSpan中处理选择样式,让符号部分看起来也被选中,但实际选择范围仅作用于有效数字部分。

修改后的控制器代码:

class AmountBigController extends TextEditingController {
  AmountBigController({
    required this.symbol,
    required this.focusNode,
    this.includeSymbolInHint = true,
    String? hintText,
    super.text = '',
  }) : _hintText = hintText;

  final String symbol;
  final FocusNode focusNode;
  final bool includeSymbolInHint;
  String? _hintText;

  void setHintText(String text) {
    _hintText = text;
    notifyListeners();
  }

  @override
  void clear() {
    value = const TextEditingValue(
      text: '',
      selection: TextSelection.collapsed(offset: 0),
    );
  }

  // 修正选择范围,确保不会超出实际文本长度
  @override
  set value(TextEditingValue newValue) {
    final correctedSelection = newValue.selection.copyWith(
      baseOffset: newValue.selection.baseOffset.clamp(0, newValue.text.length),
      extentOffset: newValue.selection.extentOffset.clamp(0, newValue.text.length),
    );
    super.value = newValue.copyWith(selection: correctedSelection);
  }

  @override
  TextSpan buildTextSpan({
    required BuildContext context,
    required bool withComposing,
    TextStyle? style,
  }) {
    final List<TextSpan> children = [];
    final isHintShown = focusNode.hasFocus && value.text.isEmpty;
    final TextStyle? textStyle = isHintShown ? Typographies.textMediumHelper : style;

    // 处理选中状态:如果有选择范围,符号部分也应用选中样式
    final selection = value.selection;
    final isSelected = !selection.isCollapsed;
    final TextStyle? selectedStyle = style?.merge(const TextStyle(backgroundColor: Colors.blue.withOpacity(0.3)));

    late final TextSpan symbolTextSpan;
    if (isHintShown) {
      symbolTextSpan = includeSymbolInHint
          ? TextSpan(text: symbol, style: textStyle)
          : TextSpan(text: '', style: textStyle);
    } else {
      // 符号部分的样式:如果文本被选中,同步应用选中样式
      symbolTextSpan = TextSpan(
        text: symbol,
        style: isSelected ? selectedStyle : textStyle,
      );
    }

    if (text.isEmpty) {
      if (focusNode.hasFocus) {
        children.add(TextSpan(text: _hintText, style: Typographies.textMediumHelper));
      } else {
        children.add(TextSpan(text: '0', style: style));
      }
    } else {
      // 数字部分的文本段,处理选择范围
      final textSpan = TextSpan(
        text: text,
        style: isSelected ? selectedStyle : style,
        recognizer: null,
      );
      children.add(textSpan);
    }

    children.add(symbolTextSpan);
    return TextSpan(children: children);
  }
}

额外优化

如果需要支持双击选中全部文本(包括视觉上的符号),可以添加对selectAll方法的重写,确保选中范围仅覆盖实际数字:

@override
void selectAll() {
  value = value.copyWith(
    selection: TextSelection(baseOffset: 0, extentOffset: text.length),
  );
}

内容的提问来源于stack exchange,提问作者CaypoH23

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 21:59:53