14

I highlighted the word, but not the correct word.

In my BuilderSuggection, I added like this code,

check my searchDelegate

 title: RichText(
          text: TextSpan(
              text: suggestList[index].d.substring(0, query.length),
              style: TextStyle(
                  color: Colors.black, fontWeight: FontWeight.bold),
              children: [
            TextSpan(
                text: suggestList[index].d.substring(query.length),
                style: TextStyle(color: Colors.grey))
          ])),
user9139407
  • 964
  • 1
  • 12
  • 25
  • Can't you understand this question? Because I added screenshot you can understand better. – user9139407 Jun 14 '19 at 08:40
  • Here you are showing a UI problem. But a photo of a homework assignment is pointless. See https://idownvotedbecau.se/imageofcode for example. Anything here that is *text* and that can be shown in a question as (formatted) text should be text. – GhostCat Jun 14 '19 at 10:02
  • Seriously: stop making *assumptions* what is appropriate here and what not. Turn to the [help] and start reading there. – GhostCat Jun 14 '19 at 10:03

5 Answers5

44

I wrote a quick function that returns a List of TextSpan.

Function matches the query string against the source string, enumerating the matches one by one, cutting the source string into pieces: before the match, after the match, and the match itself - making it bold.

It is intended to be used in a RichText widget.

List<TextSpan> highlightOccurrences(String source, String query) {
  if (query.isEmpty || !source.toLowerCase().contains(query.toLowerCase())) {
    return [ TextSpan(text: source) ];
  }
  final matches = query.toLowerCase().allMatches(source.toLowerCase());

  int lastMatchEnd = 0;

  final List<TextSpan> children = [];
  for (var i = 0; i < matches.length; i++) {
    final match = matches.elementAt(i);

    if (match.start != lastMatchEnd) {
      children.add(TextSpan(
        text: source.substring(lastMatchEnd, match.start),
      ));
    }

    children.add(TextSpan(
      text: source.substring(match.start, match.end),
      style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black),
    ));

    if (i == matches.length - 1 && match.end != source.length) {
      children.add(TextSpan(
        text: source.substring(match.end, source.length),
      ));
    }

    lastMatchEnd = match.end;
  }
  return children;
}

Example based on your code:

Text.rich(
  TextSpan(
    children: highlightOccurrences(suggestList[index].d, query),
    style: TextStyle(color: Colors.grey),
  ),
),

Let me know if this helped.

Shereef Marzouk
  • 3,282
  • 7
  • 41
  • 64
George Zvonov
  • 9,401
  • 5
  • 33
  • 37
5

Based on @George's answer there is a similar function with the only difference that the query is first split by spaces and each separate word is then highlighted. It took me a while to make it work properly so why not to share:

List<TextSpan> highlightOccurrences(String source, String query) {
  if (query == null || query.isEmpty) {
    return [TextSpan(text: source)];
  }

  var matches = <Match>[];
  for (final token in query.trim().toLowerCase().split(' ')) {
    matches.addAll(token.allMatches(source.toLowerCase()));
  }

  if (matches.isEmpty) {
    return [TextSpan(text: source)];
  }
  matches.sort((a, b) => a.start.compareTo(b.start));

  int lastMatchEnd = 0;
  final List<TextSpan> children = [];
  for (final match in matches) {
    if (match.end <= lastMatchEnd) {
      // already matched -> ignore
    } else if (match.start <= lastMatchEnd) {
      children.add(TextSpan(
        text: source.substring(lastMatchEnd, match.end),
        style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black),
      ));
    } else if (match.start > lastMatchEnd) {
      children.add(TextSpan(
        text: source.substring(lastMatchEnd, match.start),
      ));

      children.add(TextSpan(
        text: source.substring(match.start, match.end),
        style: TextStyle(fontWeight: FontWeight.bold, color: Colors.black),
      ));
    }

    if (lastMatchEnd < match.end) {
      lastMatchEnd = match.end;
    }
  }

  if (lastMatchEnd < source.length) {
    children.add(TextSpan(
      text: source.substring(lastMatchEnd, source.length),
    ));
  }

  return children;
}

The usage is the same as with @George's answer:

RichText(
  text: TextSpan(
    children: highlightOccurrences(suggestList[index].d, query),
    style: TextStyle(color: Colors.grey),
  ),
),
Ikar Pohorský
  • 4,617
  • 6
  • 39
  • 56
1

Sorry for my very late answer, but I wanted to give also my support for this kind of "problem".

I wanted to find a different way, and I figured it out without using if statements, which is pretty nice looking, and maybe even easier to manage it; I considered the "suggestion string" as divided in the worst case scenario of 3 substrings: 2 strings on the side, and one in the center. The center one, as you can imagine is the "bold" one. That's it! If there's no correspondence, obviously there will be no result shown in suggestion box. I directly copy&pasted the same code I used.

return ListView.builder(
        itemCount: _posts.length,
        itemBuilder: (context, index) {
          int startIndex = _posts[index].title.toLowerCase().indexOf(query.toLowerCase());
          return ListTile(
            title: query.isEmpty
                ? Text(_posts[index].title)
                : RichText(
                    text: TextSpan(
                    text: _posts[index].title.substring(0, startIndex),
                    style: TextStyle(color: Colors.grey),
                    children: [
                      TextSpan(
                        text: _posts[index]
                            .title
                            .substring(startIndex, startIndex + query.length),
                        style: TextStyle(
                            fontWeight: FontWeight.bold, color: Colors.black),
                      ),
                      TextSpan(
                        text: _posts[index]
                            .title
                            .substring(startIndex + query.length),
                        style: TextStyle(color: Colors.grey),
                      )
                    ],
                  )),
            subtitle: Text(_posts[index].date),
          );
sharkpowah
  • 11
  • 1
0
        //hight light occurrentces
    List<TextSpan> _highlightOccurrences(String text, String query) {
      final List<TextSpan> spans = [];
      final String lowercaseText = text.toLowerCase();
      final String lowercaseQuery = query.toLowerCase();
    
      int lastIndex = 0;
      int index = lowercaseText.indexOf(lowercaseQuery);
    
      while (index != -1) {
        spans.add(TextSpan(text: text.substring(lastIndex, index)));
        spans.add(TextSpan(text: text.substring(index, index + query.length), style: const TextStyle(fontWeight: FontWeight.bold)));
        lastIndex = index + query.length;
        index = lowercaseText.indexOf(lowercaseQuery, lastIndex);
      }
    
      spans.add(TextSpan(text: text.substring(lastIndex, text.length)));
    
      return spans;
    }



using:

    @override
      Widget buildSuggestions(BuildContext context) {
        final suggestions = lstString.where((name) {
          return name.toLowerCase().contains(query.toLowerCase());
        }).toList();
    
        //limit suggest
        const int limitSuggest = 5;
        if (suggestions.length > limitSuggest) {
          suggestions.removeRange(limitSuggest, suggestions.length);
        }
    
        return ListView.builder(
          itemCount: suggestions.length,
          itemBuilder: (BuildContext context, int index) {
            return ListTile(
              title: query.isEmpty
                  ? Text(
                      suggestions.elementAt(index),
                    )
                  : RichText(
                      text: TextSpan(
                          children: _highlightOccurrences(suggestions[index], query), style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
                    ),
              onTap: () => query = suggestions.elementAt(index),
            );
          },
        );

  }
Eric Aya
  • 69,473
  • 35
  • 181
  • 253
wwwwww3q
  • 1
  • 1
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community May 13 '22 at 08:38
0

Wrote another implementation based on @ikar-pohorsky's approach, but instead of using spans and matches, I used a boolean array (bitarray).

TextSpan _buildHighlightedText({
    TextStyle? normalStyle,
    TextStyle? highlightedStyle,
    required String source,
    required String query,
}) {
    if (query.isEmpty || source.isEmpty) {
        return TextSpan(text: source, style: normalStyle);
    }

    final mask = List.filled(source.length, false);
    for (final token in query.trim().toLowerCase().split(RegExp(r'\s'))) {
        for (final match in token.allMatches(source.toLowerCase())) {
            for (int i = match.start; i < match.end; i++) {
                mask[i] = true;
            }
        }
    }

    // Add query substring matching or other custom matching support here
    // e.g. if query = "pizza", match "pizz", "piz", "za" too

    // Basically run-length encoding
    final List<TextSpan> children = [];
    for (int i = 0; i < source.length; i++) {
        final start = i;
        while (i < source.length - 1 && mask[i] == mask[i + 1]) {
            i++;
        }
        final end = i + 1;

        children.add(TextSpan(
            text: source.substring(start, end),
            style: mask[start] ? highlightedStyle : normalStyle,
        ));
    }

    return TextSpan(children: children);
}
iBlueDust
  • 51
  • 7