如何在TextFormField中嵌入内联Widget?
在TextFormField中嵌入内联Widget的实现方案
要在TextFormField中嵌入任意内联Widget,核心思路是通过自定义TextEditingController结合WidgetSpan来实现,逻辑类似extended_text_field处理图片的方式,只是将图片替换为自定义Widget。以下是具体实现步骤:
1. 自定义带Widget占位的TextEditingController
用特殊标记(比如[widget_xxx])作为文本中的占位符,同时维护映射表记录占位符对应的Widget。重写控制器的buildTextSpan方法,将占位符替换为WidgetSpan:
class WidgetTextEditingController extends TextEditingController { // 占位符与Widget的映射表 final Map<String, Widget> widgetMap = {}; // 占位符前缀,避免与普通文本冲突 static const placeholderPrefix = '[widget_]'; // 添加Widget并返回对应的占位符 String addWidget(Widget widget) { final placeholder = '$placeholderPrefix${widgetMap.length}'; widgetMap[placeholder] = widget; return placeholder; } @override TextSpan buildTextSpan({ required BuildContext context, TextStyle? style, required bool withComposing, }) { final text = this.text; final spans = <InlineSpan>[]; int currentIndex = 0; // 遍历文本,替换占位符为WidgetSpan while (currentIndex < text.length) { final placeholderStart = text.indexOf(placeholderPrefix, currentIndex); if (placeholderStart == -1) { // 剩余文本无占位符,直接添加普通文本 spans.add(TextSpan(text: text.substring(currentIndex), style: style)); break; } // 添加占位符前的普通文本 if (placeholderStart > currentIndex) { spans.add(TextSpan(text: text.substring(currentIndex, placeholderStart), style: style)); } // 找到占位符结束位置 final placeholderEnd = text.indexOf(']', placeholderStart); if (placeholderEnd == -1) { // 占位符不完整,当作普通文本处理 spans.add(TextSpan(text: text.substring(placeholderStart), style: style)); break; } final placeholder = text.substring(placeholderStart, placeholderEnd + 1); // 替换为WidgetSpan final widget = widgetMap[placeholder]; if (widget != null) { spans.add(WidgetSpan( child: widget, alignment: PlaceholderAlignment.middle, )); } else { spans.add(TextSpan(text: placeholder, style: style)); } currentIndex = placeholderEnd + 1; } return TextSpan(children: spans, style: style); } }
2. 在TextFormField中使用自定义控制器
将自定义控制器传入TextFormField,同时添加交互按钮动态插入Widget:
class InlineWidgetTextFormField extends StatefulWidget { const InlineWidgetTextFormField({super.key}); @override State<InlineWidgetTextFormField> createState() => _InlineWidgetTextFormFieldState(); } class _InlineWidgetTextFormFieldState extends State<InlineWidgetTextFormField> { late final WidgetTextEditingController _controller; @override void initState() { super.initState(); _controller = WidgetTextEditingController(); } @override void dispose() { _controller.dispose(); super.dispose(); } // 插入示例Widget(带颜色的容器) void insertWidget() { final placeholder = _controller.addWidget( Container( width: 24, height: 24, decoration: BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.circular(4), ), alignment: Alignment.center, child: const Text('W', style: TextStyle(color: Colors.white)), ), ); // 在光标位置插入占位符 final selection = _controller.selection; final newText = _controller.text.replaceRange( selection.start, selection.end, placeholder, ); _controller.text = newText; // 调整光标到占位符之后 _controller.selection = selection.copyWith( baseOffset: selection.start + placeholder.length, extentOffset: selection.start + placeholder.length, ); } @override Widget build(BuildContext context) { return Column( children: [ TextFormField( controller: _controller, maxLines: null, decoration: const InputDecoration( border: OutlineInputBorder(), hintText: '输入文本或点击按钮插入Widget', ), ), const SizedBox(height: 16), ElevatedButton( onPressed: insertWidget, child: const Text('插入内联Widget'), ), ], ); } }
3. 关键注意事项
- 占位符唯一性:用自增数字生成占位符,避免映射冲突。
- 光标处理:插入Widget后调整光标位置,防止光标停在占位符中间。
- 文本过滤:可重写控制器的
textsetter,限制用户修改占位符的格式,保证占位符完整。 - Widget交互:嵌入的Widget可直接添加
GestureDetector处理点击等事件,不影响输入框正常交互。
内容的提问来源于stack exchange,提问作者user3808307
相关产品推荐
相关产品推荐

