Flutter中如何实现SelectableText多段用户选中文本高亮
Flutter 多段文本选中标注高亮实现方案
核心实现思路
你原有单段高亮代码是基于字符串内容匹配实现的,做多段文本标注存在明显缺陷:如果原文出现重复内容,会出现错标高亮的问题。正确的实现逻辑是基于用户选中文本的字符偏移量区间切分原文生成TextSpan,完全规避内容匹配的误差。
预期实现效果参考:
具体实现步骤
- 第一步:定义标注数据结构,存储每段高亮的起止偏移、关联标签、高亮样式
- 第二步:从SelectableText组件获取用户选中区域的准确字符偏移,而非仅获取选中的文本内容
- 第三步:改造原有高亮组件,接收标注列表,按偏移区间切分原文生成对应样式的TextSpan
- 第四步:补充交互逻辑:用户选中文本后弹出标签选择面板,选中标签后将标注数据加入列表,刷新组件即可渲染多段高亮
改造后可直接使用的多段高亮组件代码
import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; // 标注数据实体 class TextAnnotation { final int start; // 高亮起始字符偏移 final int end; // 高亮结束字符偏移 final String tag; // 关联的标签内容 final Color bgColor; // 高亮背景色 final TextStyle? textStyle; // 高亮文字自定义样式 final Function(TextAnnotation)? onTap; // 点击高亮段的回调 TextAnnotation({ required this.start, required this.end, required this.tag, required this.bgColor, this.textStyle, this.onTap, }) : assert(start < end, "起始偏移必须小于结束偏移"); } class MultiHighlightText extends StatelessWidget { final String fullText; final List<TextAnnotation> annotations; final TextStyle defaultTextStyle; const MultiHighlightText({ Key? key, required this.fullText, required this.annotations, this.defaultTextStyle = const TextStyle(color: Colors.black87, fontSize: 16), }) : super(key: key); @override Widget build(BuildContext context) { if (fullText.isEmpty || annotations.isEmpty) { return SelectableText(fullText, style: defaultTextStyle); } // 先对标注按起始位置排序,处理边界 final sortedAnnotations = [...annotations] ..sort((a, b) => a.start.compareTo(b.start)); // 过滤非法区间 final validAnnotations = sortedAnnotations.where((e) => e.start >=0 && e.end <= fullText.length ).toList(); List<TextSpan> spans = []; int currentIndex = 0; for (final annotation in validAnnotations) { // 先加当前位置到标注起点的普通文本 if (annotation.start > currentIndex) { spans.add(TextSpan( text: fullText.substring(currentIndex, annotation.start), style: defaultTextStyle, )); } // 加高亮文本段 final highlightStyle = annotation.textStyle ?? defaultTextStyle.copyWith( backgroundColor: annotation.bgColor, ); spans.add(TextSpan( text: fullText.substring(annotation.start, annotation.end), style: highlightStyle, recognizer: annotation.onTap != null ? (TapGestureRecognizer() ..onTap = () => annotation.onTap?.call(annotation) ) : null, )); currentIndex = annotation.end; } // 加最后一段剩余的普通文本 if (currentIndex < fullText.length) { spans.add(TextSpan( text: fullText.substring(currentIndex), style: defaultTextStyle, )); } // 用SelectableText.rich代替普通Text.rich,保留文本选择能力 return SelectableText.rich( TextSpan(children: spans, style: defaultTextStyle), ); } }
选中文本偏移获取方式
给SelectableText加onSelectionChanged回调即可拿到选中的起止偏移:
SelectableText( _fullText, onSelectionChanged: (selection, cause) { if (selection.isCollapsed || cause != SelectionChangedCause.drag) return; // 拿到选中的起止偏移 final selectStart = selection.start; final selectEnd = selection.end; final selectedContent = _fullText.substring(selectStart, selectEnd); // 此处弹出标签选择弹窗,用户选完标签后构造TextAnnotation加入列表,setState刷新即可 }, )
注意事项
- 所有高亮判断必须基于字符偏移,不要基于文本内容匹配,否则遇到重复文本会出现错标
- 标注列表渲染前必须做排序和合法性校验,避免出现偏移越界、区间顺序错乱导致的渲染异常
- 如果需要支持标注重叠场景,可以在排序时给高优先级标注(比如后添加的)设置更高权重,切分时优先处理高权重区间
- 给高亮TextSpan添加TapGestureRecognizer即可实现点击高亮段编辑标签、删除标注的交互
内容的提问来源于stack exchange,提问作者someone
相关产品推荐
相关产品推荐

