Flutter实现文本框选中内容变色及多段样式保留方案
解决方案
你当前代码使用SelectableText.rich,它仅支持文本选择,不支持编辑。要实现可编辑+富文本样式修改,需要改用TextField配合自定义的TextEditingController来维护富文本内容和样式。
核心思路
- 用
TextEditingController管理文本内容,通过自定义子类维护富文本样式列表 - 监听文本选择变化,实时记录选中范围
- 修改样式时,重构富文本的
TextSpan列表,保留未选中区域的原有样式,更新选中区域的样式 - 确保文本编辑时,样式能跟随内容同步更新
修改后的完整代码
import 'dart:math'; import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) => const MaterialApp(home: MyHomePage()); } class MyHomePage extends StatefulWidget { const MyHomePage({super.key}); @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { late _RichTextEditingController _controller; (int startIndex, int endIndex) _selection = (0, 0); @override void initState() { super.initState(); const initialText = 'Lorem ipsum dolor sit amet, ' 'consectetur adipiscing elit, ' 'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ' 'Ut enim ad minim veniam, ' 'quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ' 'Duis aute irure dolor in reprehenderit ' 'in voluptate velit esse cillum dolore eu fugiat nulla pariatur. ' 'Excepteur sint occaecat cupidatat non proident, ' 'sunt in culpa qui officia deserunt mollit anim id est laborum.'; // 初始化富文本控制器,默认所有文本使用空样式 _controller = _RichTextEditingController( text: initialText, spans: [TextSpan(text: initialText, style: const TextStyle())], ); _controller.addListener(_updateSelection); } void _updateSelection() { final selection = _controller.selection; if (!selection.isValid) return; setState(() { _selection = (min(selection.start, selection.end), max(selection.start, selection.end)); }); } void highlight(Color color) { final (start, end) = _selection; if (start == end) return; final currentText = _controller.text; final List<TextSpan> newSpans = []; int currentPos = 0; // 拆分现有span,找到选中区域对应的位置,重构样式 for (final span in _controller.spans) { final spanLength = span.text?.length ?? 0; final spanEnd = currentPos + spanLength; // 情况1:当前span在选中区域之前 if (spanEnd <= start) { newSpans.add(span); } // 情况2:当前span与选中区域重叠 else if (currentPos < end && spanEnd > start) { // 重叠前的部分 if (currentPos < start) { newSpans.add(TextSpan( text: span.text?.substring(0, start - currentPos), style: span.style, )); } // 重叠部分(应用新样式) final overlapStart = max(currentPos, start); final overlapEnd = min(spanEnd, end); newSpans.add(TextSpan( text: span.text?.substring(overlapStart - currentPos, overlapEnd - currentPos), style: TextStyle( color: color.computeLuminance() > 0.25 ? Colors.black : Colors.white, backgroundColor: color, ), )); // 重叠后的部分 if (spanEnd > end) { newSpans.add(TextSpan( text: span.text?.substring(end - currentPos), style: span.style, )); } } // 情况3:当前span在选中区域之后 else if (currentPos >= end) { newSpans.add(span); } currentPos = spanEnd; } setState(() { // 更新控制器的span列表 _controller.spans = newSpans; // 通知UI刷新 _controller.notifyListeners(); // 恢复选中状态 _controller.selection = TextSelection(baseOffset: start, extentOffset: end); }); } late final Widget highlightButtons = Padding( padding: const EdgeInsets.only(top: 50, bottom: 25), child: Row(children: [ for (final color in [ Colors.red, Colors.orange, Colors.amber, Colors.yellow, Colors.lime, Colors.green, Colors.cyan, Colors.blue, Colors.indigo, Colors.purple, ]) _HighlightButton(color: color, onPressed: () => highlight(color)) ]), ); late final Widget resetButton = OutlinedButton( style: OutlinedButton.styleFrom(foregroundColor: Colors.grey), onPressed: () { const initialText = 'Lorem ipsum dolor sit amet, ' 'consectetur adipiscing elit, ' 'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ' 'Ut enim ad minim veniam, ' 'quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ' 'Duis aute irure dolor in reprehenderit ' 'in voluptate velit esse cillum dolore eu fugiat nulla pariatur. ' 'Excepteur sint occaecat cupidatat non proident, ' 'sunt in culpa qui officia deserunt mollit anim id est laborum.'; setState(() { _controller = _RichTextEditingController( text: initialText, spans: [TextSpan(text: initialText, style: const TextStyle())], ); _controller.addListener(_updateSelection); _selection = (0, 0); }); }, child: const Text('reset'), ); @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: Container( margin: EdgeInsets.symmetric(horizontal: MediaQuery.of(context).size.width / 16), alignment: Alignment.center, child: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: _controller, maxLines: null, decoration: const InputDecoration( border: OutlineInputBorder(), hintText: '输入或编辑文本...', ), ), highlightButtons, resetButton, ], ), ), ); } } // 自定义富文本编辑控制器,维护TextSpan样式列表 class _RichTextEditingController extends TextEditingController { _RichTextEditingController({required String text, required this.spans}) : super(text: text); List<TextSpan> spans; @override TextSpan buildTextSpan({required BuildContext context, TextStyle? style, required bool withComposing}) { // 合并全局样式和span样式 return TextSpan( style: style, children: spans.map((span) => TextSpan( text: span.text, style: style?.merge(span.style) ?? span.style, )).toList(), ); } } class _HighlightButton extends StatelessWidget { const _HighlightButton({required this.color, required this.onPressed}); final Color color; final void Function() onPressed; @override Widget build(BuildContext context) { return Expanded( child: Padding( padding: const EdgeInsets.all(4), child: ElevatedButton( style: OutlinedButton.styleFrom(backgroundColor: color), onPressed: onPressed, child: const SizedBox.shrink(), ), ), ); } }
关键修改说明
- 替换
SelectableText为TextField:TextField支持文本编辑,配合自定义控制器实现富文本渲染 - 自定义
_RichTextEditingController:重写buildTextSpan方法,通过维护spans列表来管理不同区域的文本样式 - 精细化样式拆分:高亮时拆分现有样式区间,确保未选中区域的样式被完整保留,仅修改选中部分的样式
- 实时选择监听:通过控制器的
addListener方法实时获取当前文本选中范围
内容的提问来源于stack exchange,提问作者Mohammad Hosseini
相关产品推荐
相关产品推荐

