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
相关产品推荐
相关产品推荐

