Skip to content

Instantly share code, notes, and snippets.

@nhancv
Last active January 10, 2023 12:36
Show Gist options
  • Save nhancv/83bb7792d18a4da9cae22ec47256b9f4 to your computer and use it in GitHub Desktop.
Save nhancv/83bb7792d18a4da9cae22ec47256b9f4 to your computer and use it in GitHub Desktop.
Flutter highlight text in search
// For display friends
List<Friend> _friendsDto = <Friend>[];
// Save search value
String _searchValue;
/// On search input change listener to validate form
void onSearchValueChange(final String search) {
searchValue = search;
// Filter on _friends, and copy to display data
friendsDto = _friends
.where((Friend f) =>
(f.name?.toLowerCase()?.contains(search?.toLowerCase() ?? '') ??
true) ||
(f.phone?.toLowerCase()?.contains(search?.toLowerCase() ?? '') ??
true))
.toList();
}
// bold search text
// Usage:
// RichText(
// text: TextSpan(
// children: highlightOccurrences(source, query),
// style: const TextStyle(color: Colors.black),
// ),
// ),
List<TextSpan> highlightOccurrences(String source, String query) {
if (query == null || query.isEmpty) {
return <TextSpan>[TextSpan(text: source)];
}
final List<Match> matches = <Match>[];
for (final String token in query.trim().toLowerCase().split(' ')) {
matches.addAll(token.allMatches(source.toLowerCase()));
}
if (matches.isEmpty) {
return <TextSpan>[TextSpan(text: source)];
}
matches.sort((Match a, Match b) => a.start.compareTo(b.start));
int lastMatchEnd = 0;
final List<TextSpan> children = <TextSpan>[];
const Color matchColor = Color(0xFF602885);
for (final Match 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:
const TextStyle(fontWeight: FontWeight.bold, color: matchColor),
));
} else {
children.add(TextSpan(
text: source.substring(lastMatchEnd, match.start),
));
children.add(TextSpan(
text: source.substring(match.start, match.end),
style:
const TextStyle(fontWeight: FontWeight.bold, color: matchColor),
));
}
if (lastMatchEnd < match.end) {
lastMatchEnd = match.end;
}
}
if (lastMatchEnd < source.length) {
children.add(TextSpan(
text: source.substring(lastMatchEnd, source.length),
));
}
return children;
}
// list
return ListView.separated(
physics: const AlwaysScrollableScrollPhysics(),
itemCount: friends.length,
itemBuilder: (BuildContext context, int index) {
final Friend item = friends[index];
return _buildItem(context, index, item, provider);
},
separatorBuilder: (_, int index) => const Divider(),
);
// Build friend view
Widget _buildItem(BuildContext context, int index, Friend item, ContactProvider provider) {
return ListTile(
leading: item.avatarUrl == null
? CircleAvatar(
child: Text(item.name?.substring(0, 1)?.toUpperCase() ?? ''))
: CircleAvatar(
backgroundColor: Colors.white,
backgroundImage: NetworkImage(item.avatarUrl)),
title: RichText(
text: TextSpan(
children: provider.highlightOccurrences(item.name, provider.searchValue),
style: const TextStyle(color: Colors.black),
),
),
subtitle: RichText(
text: TextSpan(
children: provider.highlightOccurrences(item.phone, provider.searchValue),
style: const TextStyle(color: Colors.black),
),
),
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment