import 'dart:math'; import 'package:flutter/material.dart'; class HighlightedText extends StatelessWidget { final String text; final List highlights; final Color? color; final TextStyle? style; final bool caseSensitive; final TextAlign textAlign; final TextDirection? textDirection; final TextOverflow overflow; final double textScaleFactor; final int? maxLines; final StrutStyle? strutStyle; final TextWidthBasis textWidthBasis; final TextHeightBehavior? textHeightBehavior; const HighlightedText({ super.key, required this.text, required this.highlights, this.style, this.color = Colors.yellowAccent, this.caseSensitive = true, this.textAlign = TextAlign.start, this.textDirection = TextDirection.ltr, this.overflow = TextOverflow.clip, this.textScaleFactor = 1.0, this.maxLines, this.strutStyle, this.textWidthBasis = TextWidthBasis.parent, this.textHeightBehavior, }); @override Widget build(BuildContext context) { if (text.isEmpty || highlights.isEmpty || highlights.contains('')) { return SelectableText.rich( _normalSpan(text, context), key: key, textAlign: textAlign, textDirection: textDirection, textScaleFactor: textScaleFactor, maxLines: maxLines, strutStyle: strutStyle, textWidthBasis: textWidthBasis, textHeightBehavior: textHeightBehavior, style: TextStyle(overflow: overflow), ); } return SelectableText.rich( TextSpan(children: _buildChildren(context)), key: key, textAlign: textAlign, textDirection: textDirection, textScaleFactor: textScaleFactor, maxLines: maxLines, strutStyle: strutStyle, textWidthBasis: textWidthBasis, textHeightBehavior: textHeightBehavior, style: TextStyle(overflow: overflow), ); } List _buildChildren(BuildContext context) { List _spans = []; int _start = 0; String _text = caseSensitive ? text : text.toLowerCase(); List _highlights = caseSensitive ? highlights : highlights.map((e) => e.toLowerCase()).toList(); while (true) { Map _highlightsMap = {}; //key (index), value (highlight). for (final h in _highlights) { final idx = _text.indexOf(h, _start); if (idx >= 0) { _highlightsMap.putIfAbsent(_text.indexOf(h, _start), () => h); } } if (_highlightsMap.isNotEmpty) { int _currentIndex = _highlightsMap.keys.reduce(min); String _currentHighlight = text.substring( _currentIndex, _currentIndex + _highlightsMap[_currentIndex]!.length, ); if (_currentIndex == _start) { _spans.add(_highlightSpan(_currentHighlight)); _start += _currentHighlight.length; } else { _spans .add(_normalSpan(text.substring(_start, _currentIndex), context)); _spans.add(_highlightSpan(_currentHighlight)); _start = _currentIndex + _currentHighlight.length; } } else { _spans.add(_normalSpan(text.substring(_start, text.length), context)); break; } } return _spans; } TextSpan _highlightSpan(String value) { return TextSpan( text: value, style: style?.copyWith( backgroundColor: color, ), ); } TextSpan _normalSpan(String value, BuildContext context) { return TextSpan( text: value, style: style ?? Theme.of(context).textTheme.bodyMedium, ); } }