The Adapter code below for the RecyclerView successfully highlights user entered text (using an EditText line linked to a SearchView) to the color Green.
public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
String cardNumsHighlight = String.valueOf(card.getId());
String todoHighlight = card.getTodo().toLowerCase(Locale.getDefault());
String note1Highlight = card.getNote1().toLowerCase(Locale.getDefault());
// Set up the code for the highlighted text of successful search results
int cardNumsStartPos = cardNumsHighlight.indexOf(searchString);
int cardNumsEndPos = cardNumsStartPos + searchString.length();
int todoStartPos = todoHighlight.indexOf(searchString);
int todoEndPos = todoStartPos + searchString.length();
int note1StartPos = note1Highlight.indexOf(searchString);
int note1EndPos = note1StartPos + searchString.length();
Spannable spanString1 = Spannable.Factory.getInstance().newSpannable(
itemHolder.cardBlankTextNumstotal.getText());
Spannable spanString2 = Spannable.Factory.getInstance().newSpannable(
itemHolder.cardBlankText2.getText());
Spannable spanString4 = Spannable.Factory.getInstance().newSpannable(
itemHolder.cardBlankText4.getText());
if (cardNumsStartPos != -1 && searchString.length() > 0 && cardNumsHighlight.contains(searchString)) {
spanString1.setSpan(new ForegroundColorSpan(Color.GREEN), cardNumsStartPos, cardNumsEndPos,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
itemHolder.cardBlankTextNumstotal.setText(spanString1);
}
if (todoStartPos != -1 && searchString.length() > 0 && todoHighlight.contains(searchString)) {
spanString2.setSpan(new ForegroundColorSpan(Color.GREEN), todoStartPos, todoEndPos,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
itemHolder.cardBlankText2.setText(spanString2);
}
if (note1StartPos != -1 && searchString.length() > 0 && note1Highlight.contains(searchString)) {
spanString4.setSpan(new ForegroundColorSpan(Color.GREEN), note1StartPos, note1EndPos,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
itemHolder.cardBlankText4.setText(spanString4);
}
I tried the following code addition using removeSpan() in the Answer here: Android Spannable: how to clear color? to clear the color when the query does not return any items, but with no luck. What am I missing here?
Activity
...
#Override
public void afterTextChanged(Editable s) {
if (s.toString().length() >0) {
String queryText = "%" + s.toString() + "%";
mQuickcardViewModel.searchQuery(queryText).observe(MainActivity.this, searchQuickcards -> {
searchList = searchQuickcards;
if (!mSearchView.isIconified() && searchList.size() == 0) {
mQuickcardViewModel.loadFullList();
}
...
ViewModel
...
public LiveData<List<Quickcard>> getAllCards() {
if (quickcards == null) {
quickcards = new MutableLiveData<>();
loadFullList();
}
return quickcards;
}
public void loadFullList(){
quickcards.postValue(repository.getAllCards());
}
// Repository gets data from Dao, which then gets data from Room database.
I'm facing this exception on Android N : java.lang.StringIndexOutOfBoundsException line :
res.add(new SpannableStringBuilder(in.subSequence(lastImageSpanPosition, spanStart)));
I don't know why fromHtml medthod in android N returning different value
These are my code :
ArrayList<Object> res = new ArrayList<Object>();
Spanned in;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
in = Html.fromHtml(html,Html.FROM_HTML_MODE_LEGACY);
} else {
in = Html.fromHtml(html);
}
Object[] spans = in.getSpans(0, Integer.MAX_VALUE, Object.class); // get all spans
int lastImageSpanPosition = 0; // it's end position of image span
for (int i = 0; i < spans.length; i++) { // itarete searching ImageSpan
Object span = spans[i];
if (span instanceof ImageSpan) {
int spanStart = in.getSpanStart(span); // If you;ve found one
res.add(new SpannableStringBuilder(in.subSequence(lastImageSpanPosition, spanStart))); // add all previous spans as a single Spannable object
ImageSpan imageSpan = (ImageSpan) span;
String imageUrl = imageSpan.getSource();
if (imageUrl != null && !imageUrl.startsWith("http"))
imageUrl = "http:" + imageUrl;
if (imageUrl != null) {
res.add(new ImageSpan(null, imageUrl)); // add separate span for image
}
lastImageSpanPosition = in.getSpanEnd(span);
}
}
I have here code where it highlights all the words inside the TextView which is equal to the typed string. By pressing the buttons next/previous, It highlights the current word found.
What I'm trying to do is to go to the position of that word currently being highlighted.
I've added this code inside my onClick
myScroll.scrollBy(0, +20);
But it doesn't seem to be what im looking for
Also I've tried something like this
myScroll.scrollTo(0, selected); // getting the index of the highlighted word
// but it's not working
onClickListener to highlight next word
findViewById(R.id.next).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
selected++;
String text = ((EditText) findViewById(R.id.editText)).getText().toString();
if(text.equals("") {}
else
selected = changeTextView(details, text, selected);
}
});
method to highlight the text
private int changeTextView(TextView tv, String target, int selected) {
if(selected < 0 ) {
selected = 0;
}
String bString = (String) tv.getText().toString();
int startSpan = 0, endSPan = 0;
Spannable spanRange = new SpannableString(bString);
int currentIndex = 0;
while(true) {
startSpan = bString.indexOf(target, endSpan);
endSpan = startSpan + target.lenght();
boolean isLast = bString.indexOf(target, endSpan) < 0;
if(startSpan < 0)
break;
ParcelableSpan span;
if(currentIndex == selected || isLast && selected > currentIndex) {
span = new BackgroundColorSpan(Color.LTGRAY);
if(isLast && selected > currentIndex) {
selected = currentIndex;
}
} else {
span = new BackgroundColorSpan(Color.YELLOW);
}
currentIndex++;
spanRange.setSpan(
span,
startSpan,
endSpan,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
tv.setText(spanRange);
if(currentIndex == 0) {
return -1;
} else {
return selected;
}
}
I have implemented SearchView Widget in my app. Its working fine. Now i need to do is, whenever i type a word in my SearchView Bar , the filtered result should show the searched word highlighted. like:
I am using this SearchView widget as :
#Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.myMenu , menu);
MenuItem searchItem = menu.findItem(R.id.action_search);
SearchView sv = new SearchView(getActivity());
// Changing the color of Searchview widget text field to white.
int searchSrcTextId = getResources().getIdentifier("android:id/search_src_text", null, null);
EditText searchEditText = (EditText) sv.findViewById(searchSrcTextId);
searchEditText.setTextColor(Color.WHITE);
sv.setOnQueryTextListener(this);
searchItem.setActionView(sv);
super.onCreateOptionsMenu(menu, inflater);
}
you can use Spannable TextView for this.
hope so this Method will help you
Method:
public static CharSequence highlightText(String search, String originalText) {
if (search != null && !search.equalsIgnoreCase("")) {
String normalizedText = Normalizer.normalize(originalText, Normalizer.Form.NFD).replaceAll("\\p{InCombiningDiacriticalMarks}+", "").toLowerCase();
int start = normalizedText.indexOf(search);
if (start < 0) {
return originalText;
} else {
Spannable highlighted = new SpannableString(originalText);
while (start >= 0) {
int spanStart = Math.min(start, originalText.length());
int spanEnd = Math.min(start + search.length(), originalText.length());
highlighted.setSpan(new ForegroundColorSpan(Color.BLUE), spanStart, spanEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
start = normalizedText.indexOf(search, spanEnd);
}
return highlighted;
}
}
return originalText;
}
and return originalText will highlight text.
You Should do this in onBindViewHolder() method (using RecyclerView)
class YourAdapter
String searchString="";
#Override
public void onBindViewHolder(AllMessageAdapter.DataObjectHolder holder, final int position) {
holder.message.setText(mDataset.get(position).Message);
AllMessageList.Message message=mDataset.get(position);
String name = message.Message.toLowerCase(Locale.getDefault());
if (name.contains(searchString)) {
int startPos = name.indexOf(searchString);
int endPos = startPos + searchString.length();
Spannable spanString = Spannable.Factory.getInstance().newSpannable(holder.message.getText());
spanString.setSpan(new ForegroundColorSpan(Color.RED), startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
holder.message.setText(spanString);
}
}
Your Filter(in Adapter)
public void setFilter(ArrayList<AllMessageList.Message> countryModels,String searchString) {
this.searchString=searchString;
mDataset = new ArrayList<>();
mDataset.addAll(countryModels);
notifyDataSetChanged();
}
You can use this to highlight all the keywords.
button.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
String ett = edittext.getText().toString();
String tvt = textview.getText().toString();
int ofe = tvt.indexOf(ett, 0);
Spannable spannable = new SpannableString(tvt);
for (int ofs = 0; ofs < tvt.length() && ofe != -1; ofs = ofe + 1) {
ofe = tvt.indexOf(ett, ofs);
if (ofe == -1)
break;
else {
ColorStateList blueColor = new ColorStateList(new nt[][] { new int[] {} }, new int[] { Color.BLUE });
TextAppearanceSpan highlightSpan = new TextAppearanceSpan(null, Typeface.BOLD, -1, blueColor, null);
spannable.setSpan(highlightSpan, ofe, ofe+edittext.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
textview.setText(spannable);
}
}
}
});
I have a ListView with Strings. With the below code I can highlight search results, but the user must type the words to search case sensitive. How can I implement a none - case sensitive highlighting of search results for example like the native Android Contact search?
Here is my code for Highlighting. I extend the ArrayAdapter and implement customized filter to get the string to search. In the getView method I check if my String in ListView contains the prefixString and highlight it.
public class HighlightListAdapter extends ArrayAdapter {
ArrayList<String> objects;
final Object mLock =new Object();
private ArrayList<String> mOriginalValues;
private ArrayFilter filter;
private String prefixString;
public AuthorsListAdapter(Context context, int textViewResourceId, ArrayList<String> objects) {
super(context, textViewResourceId, objects);
this.objects = objects;
}
class ViewHolder{
TextView author;
}
public View getView(final int position, View convertView, ViewGroup parent){
// assign the view we are converting to a local variable
View v = convertView;
ViewHolder holder = null;
// first check to see if the view is null. if so, we have to inflate it.
// to inflate it basically means to render, or show, the view.
LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (v == null) {
holder = new ViewHolder();
v = inflater.inflate(R.layout.author_list_item, null);
holder.author =(TextView) v.findViewById(R.id.author_list_item_text);
v.setTag(holder);
}else{
holder = (ViewHolder) v.getTag();
}
final String author = objects.get(position);
if (author != null) {
holder.author.setText(author);
if(prefixString !=null && prefixString.length()>1){
String s = author;
**if(s.contains(prefixString)){
String rep = s.replace(prefixString, "<b><font color=#2825A6>"+ prefixString+ "</font></b>");
holder.author.setText(Html.fromHtml(rep));
}** // higlight
}
}
return v;
}
#Override
public int getCount() {
// TODO Auto-generated method stub
return objects.size();
}
#Override
public Filter getFilter() {
// TODO Auto-generated method stub
if(filter == null){
filter =new ArrayFilter();
}
return filter;
}
#Override
public Object getItem(int position) {
// TODO Auto-generated method stub
return this.objects.get(position);
}
private class ArrayFilter extends Filter {
#Override
protected FilterResults performFiltering(CharSequence prefix) {
FilterResults results = new FilterResults();
if (mOriginalValues == null) {
synchronized (mLock) {
mOriginalValues = new ArrayList<String>(objects);
}
}
if (prefix == null || prefix.length() == 0) {
ArrayList<String> list;
synchronized (mLock) {
list = new ArrayList<String>(mOriginalValues);
}
results.values = list;
results.count = list.size();
} else {
**prefixString = prefix.toString();** // get string to search
ArrayList<String> values;
synchronized (mLock) {
values = new ArrayList<String>(mOriginalValues);
}
final int count = values.size();
final ArrayList<String> newValues = new ArrayList<String>();
for (int i = 0; i < count; i++) {
final String value = values.get(i);
final String valueText = value.toString().toLowerCase();
// First match against the whole, non-splitted value
if (valueText.startsWith(prefixString)) {
newValues.add(value);
} else {
final String[] words = valueText.split(" ");
final int wordCount = words.length;
// Start at index 0, in case valueText starts with space(s)
for (int k = 0; k < wordCount; k++) {
if (words[k].startsWith(prefixString)) {
newValues.add(value);
break;
}
}
}
}
results.values = newValues;
results.count = newValues.size();
}
return results;
}
#SuppressWarnings("unchecked")
#Override
protected void publishResults(CharSequence constraint, FilterResults results) {
objects = (ArrayList<String>) results.values;
if (results.count > 0) {
notifyDataSetChanged();
} else {
notifyDataSetInvalidated();
}
}
};
}
This what I use :
Every occurence is replaced (not only prefix)
Case and accent are ignored while searching but retained in the result.
It uses directly SpannableString, which you can use in setText(). I believe it's more efficient than using an intermediate html step.
.
public static CharSequence highlight(String search, String originalText) {
// ignore case and accents
// the same thing should have been done for the search text
String normalizedText = Normalizer.normalize(originalText, Normalizer.Form.NFD).replaceAll("\\p{InCombiningDiacriticalMarks}+", "").toLowerCase();
int start = normalizedText.indexOf(search);
if (start < 0) {
// not found, nothing to to
return originalText;
} else {
// highlight each appearance in the original text
// while searching in normalized text
Spannable highlighted = new SpannableString(originalText);
while (start >= 0) {
int spanStart = Math.min(start, originalText.length());
int spanEnd = Math.min(start + search.length(), originalText.length());
highlighted.setSpan(new BackgroundColorSpan(<background_color>), spanStart, spanEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
start = normalizedText.indexOf(search, spanEnd);
}
return highlighted;
}
}
The accepted answer is nice. But you can do it by a single line of code. What I've done in my case to avoid the case sensitive issue is:
Spannable sb = new SpannableString(originalText);
sb.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), originalText.toLowerCase().indexOf(query.toLowerCase()),
originalText.toLowerCase().indexOf(query.toLowerCase()) + query.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
result.setText(sb);
Hope it might help!
Note: Here 'query' is the part of the string that you want to highlight.
Simple & Advanced Search Highlighting Example [Case Insensitive Order]
1. Simple Search (Html):
public static void setSearchTextHighlightSimpleHtml(TextView textView, String fullText, String searchText) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
fullText = fullText.replaceAll("(?i)(" + searchText + ")", "<span style=\"background-color:#FCFF48;\"><b><big><font color='#a10901'>$1</font></big></b></span>");
textView.setText(Html.fromHtml(fullText, Html.FROM_HTML_MODE_LEGACY), TextView.BufferType.SPANNABLE);
} else {
fullText = fullText.replaceAll("(?i)(" + searchText + ")", "<b><big><font color='red'>$1</font></big></b>");
textView.setText(Html.fromHtml(fullText), TextView.BufferType.SPANNABLE);
}
} catch (Exception e) {
textView.setText(fullText);
}
}
2. Simple Search (Spannable):
public static void setSearchTextHighlightSimpleSpannable(TextView textView, String fullText, String searchText) {
// highlight search text
if (null != searchText && !searchText.isEmpty()) {
SpannableStringBuilder wordSpan = new SpannableStringBuilder(fullText);
Pattern p = Pattern.compile(searchText, Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher(fullText);
while (m.find()) {
int wordStart = m.start();
int wordEnd = m.end();
// Now highlight based on the word boundaries
ColorStateList redColor = new ColorStateList(new int[][]{new int[]{}}, new int[]{0xffa10901});
TextAppearanceSpan highlightSpan = new TextAppearanceSpan(null, Typeface.BOLD, -1, redColor, null);
wordSpan.setSpan(highlightSpan, wordStart, wordEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
wordSpan.setSpan(new BackgroundColorSpan(0xFFFCFF48), wordStart, wordEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
wordSpan.setSpan(new RelativeSizeSpan(1.25f), wordStart, wordEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
textView.setText(wordSpan, TextView.BufferType.SPANNABLE);
} else {
textView.setText(fullText);
}
}
3. Advanced Search (Spannable):
public static void setAdvancedSearch(TextView textView, String fullText, String searchText) {
if (searchText.length() == 0) {
textView.setText(fullText);
return;
}
final String searchBoundary = " \n()ред.,;?-+!";
char[] boundaries = searchBoundary.toCharArray();
// highlight search text
if (isNotEquals(searchText, boundaries)) {
SpannableStringBuilder wordSpan = new SpannableStringBuilder(fullText);
Pattern p = Pattern.compile(searchText, Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher(fullText);
while (m.find()) {
int wordStart = m.start();
while (wordStart >= 0 && isNotEquals(fullText.charAt(wordStart), boundaries)) {
--wordStart;
}
wordStart = wordStart + 1;
int wordEnd = m.end();
while (wordEnd < fullText.length() && isNotEquals(fullText.charAt(wordEnd), boundaries)) {
++wordEnd;
}
setWordSpan(wordSpan, wordStart, wordEnd);
}
textView.setText(wordSpan, TextView.BufferType.SPANNABLE);
} else {
textView.setText(fullText);
}
}
private static boolean isNotEquals(char charAt, char[] boundaries) {
return isNotEquals(String.valueOf(charAt), boundaries);
}
private static boolean isNotEquals(String searchText, char[] boundaries) {
for (char boundary : boundaries) {
boolean equals = searchText.equals(String.valueOf(boundary));
if (equals) return false;
}
return true;
}
private static void setWordSpan(SpannableStringBuilder wordSpan, int wordStart, int wordEnd) {
// Now highlight based on the word boundaries
ColorStateList redColor = new ColorStateList(new int[][]{new int[]{}}, new int[]{0xffa10901});
TextAppearanceSpan highlightSpan = new TextAppearanceSpan(null, Typeface.BOLD, -1, redColor, null);
wordSpan.setSpan(highlightSpan, wordStart, wordEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
wordSpan.setSpan(new BackgroundColorSpan(0xFFFCFF48), wordStart, wordEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
wordSpan.setSpan(new RelativeSizeSpan(1.25f), wordStart, wordEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
First, your code
if(s.contains(prefixString)){
String rep = s.replace(prefixString, "<b><font color=#2825A6>"+ prefixString+ "</font></b>");
holder.author.setText(Html.fromHtml(rep));
}
is not good. You should use String.startsWith to check if the start of s equals to prefixString. Your actual code works, but it checks presence of prefixString in s, but doesn't care about its position.
For having case insensitive search, you can use String.toLowerCase or String.toUpperCase on both strings when checking presence of prefixString. Case will be ignored.
if(s.toLowerCase().startsWith(prefixString.toLowerCase())){
String rep = "<b><font color=#2825A6>" + prefixString + "</font></b>" + s.substring(prefixString.length());
holder.author.setText(Html.fromHtml(rep));
}